]> git.argeo.org Git - lgpl/argeo-commons.git/blob - internal/kernel/CmsDeployment.java
Prepare next development cycle
[lgpl/argeo-commons.git] / internal / kernel / CmsDeployment.java
1 package org.argeo.cms.internal.kernel;
2
3 import static org.argeo.api.DataModelNamespace.CMS_DATA_MODEL_NAMESPACE;
4
5 import java.io.File;
6 import java.io.InputStreamReader;
7 import java.io.Reader;
8 import java.lang.management.ManagementFactory;
9 import java.net.URL;
10 import java.util.ArrayList;
11 import java.util.Arrays;
12 import java.util.HashSet;
13 import java.util.Hashtable;
14 import java.util.List;
15 import java.util.Map;
16 import java.util.Set;
17
18 import javax.jcr.Repository;
19 import javax.jcr.RepositoryException;
20 import javax.jcr.Session;
21 import javax.security.auth.callback.CallbackHandler;
22 import javax.transaction.UserTransaction;
23
24 import org.apache.commons.logging.Log;
25 import org.apache.commons.logging.LogFactory;
26 import org.apache.jackrabbit.commons.cnd.CndImporter;
27 import org.apache.jackrabbit.core.RepositoryContext;
28 import org.apache.jackrabbit.core.RepositoryImpl;
29 import org.argeo.api.DataModelNamespace;
30 import org.argeo.api.NodeConstants;
31 import org.argeo.api.NodeDeployment;
32 import org.argeo.api.NodeState;
33 import org.argeo.api.security.CryptoKeyring;
34 import org.argeo.api.security.Keyring;
35 import org.argeo.cms.ArgeoNames;
36 import org.argeo.cms.CmsException;
37 import org.argeo.jcr.JcrUtils;
38 import org.argeo.osgi.useradmin.UserAdminConf;
39 import org.argeo.util.LangUtils;
40 import org.eclipse.equinox.http.jetty.JettyConfigurator;
41 import org.osgi.framework.Bundle;
42 import org.osgi.framework.BundleContext;
43 import org.osgi.framework.Constants;
44 import org.osgi.framework.FrameworkUtil;
45 import org.osgi.framework.InvalidSyntaxException;
46 import org.osgi.framework.ServiceReference;
47 import org.osgi.framework.wiring.BundleCapability;
48 import org.osgi.framework.wiring.BundleWire;
49 import org.osgi.framework.wiring.BundleWiring;
50 import org.osgi.service.cm.Configuration;
51 import org.osgi.service.cm.ConfigurationAdmin;
52 import org.osgi.service.cm.ManagedService;
53 import org.osgi.service.useradmin.Group;
54 import org.osgi.service.useradmin.Role;
55 import org.osgi.service.useradmin.UserAdmin;
56 import org.osgi.util.tracker.ServiceTracker;
57
58 /** Implementation of a CMS deployment. */
59 public class CmsDeployment implements NodeDeployment {
60 private final Log log = LogFactory.getLog(getClass());
61 private final BundleContext bc = FrameworkUtil.getBundle(getClass()).getBundleContext();
62
63 private DataModels dataModels;
64 private DeployConfig deployConfig;
65
66 private Long availableSince;
67
68 private final boolean cleanState;
69
70 private NodeHttp nodeHttp;
71
72 private boolean argeoDataModelExtensionsAvailable = false;
73
74 // Readiness
75 private boolean nodeAvailable = false;
76 private boolean userAdminAvailable = false;
77 private boolean httpExpected = false;
78 private boolean httpAvailable = false;
79
80 public CmsDeployment() {
81 ServiceReference<NodeState> nodeStateSr = bc.getServiceReference(NodeState.class);
82 if (nodeStateSr == null)
83 throw new CmsException("No node state available");
84
85 NodeState nodeState = bc.getService(nodeStateSr);
86 cleanState = nodeState.isClean();
87
88 nodeHttp = new NodeHttp(cleanState);
89 dataModels = new DataModels(bc);
90 initTrackers();
91 }
92
93 private void initTrackers() {
94 ServiceTracker<?, ?> httpSt = new ServiceTracker<NodeHttp, NodeHttp>(bc, NodeHttp.class, null) {
95
96 @Override
97 public NodeHttp addingService(ServiceReference<NodeHttp> reference) {
98 httpAvailable = true;
99 checkReadiness();
100 return super.addingService(reference);
101 }
102 };
103 // httpSt.open();
104 KernelUtils.asyncOpen(httpSt);
105
106 ServiceTracker<?, ?> repoContextSt = new RepositoryContextStc();
107 // repoContextSt.open();
108 KernelUtils.asyncOpen(repoContextSt);
109
110 ServiceTracker<?, ?> userAdminSt = new ServiceTracker<UserAdmin, UserAdmin>(bc, UserAdmin.class, null) {
111 @Override
112 public UserAdmin addingService(ServiceReference<UserAdmin> reference) {
113 UserAdmin userAdmin = super.addingService(reference);
114 addStandardSystemRoles(userAdmin);
115 userAdminAvailable = true;
116 checkReadiness();
117 return userAdmin;
118 }
119 };
120 // userAdminSt.open();
121 KernelUtils.asyncOpen(userAdminSt);
122
123 ServiceTracker<?, ?> confAdminSt = new ServiceTracker<ConfigurationAdmin, ConfigurationAdmin>(bc,
124 ConfigurationAdmin.class, null) {
125 @Override
126 public ConfigurationAdmin addingService(ServiceReference<ConfigurationAdmin> reference) {
127 ConfigurationAdmin configurationAdmin = bc.getService(reference);
128 deployConfig = new DeployConfig(configurationAdmin, dataModels, cleanState);
129 httpExpected = deployConfig.getProps(KernelConstants.JETTY_FACTORY_PID, "default") != null;
130 try {
131 // Configuration[] configs = configurationAdmin
132 // .listConfigurations("(service.factoryPid=" +
133 // NodeConstants.NODE_REPOS_FACTORY_PID + ")");
134 // for (Configuration config : configs) {
135 // Object cn = config.getProperties().get(NodeConstants.CN);
136 // if (log.isDebugEnabled())
137 // log.debug("Standalone repo cn: " + cn);
138 // }
139 Configuration[] configs = configurationAdmin
140 .listConfigurations("(service.factoryPid=" + NodeConstants.NODE_USER_ADMIN_PID + ")");
141
142 boolean hasDomain = false;
143 for (Configuration config : configs) {
144 Object realm = config.getProperties().get(UserAdminConf.realm.name());
145 if (realm != null) {
146 log.debug("Found realm: " + realm);
147 hasDomain = true;
148 }
149 }
150 if (hasDomain) {
151 loadIpaJaasConfiguration();
152 }
153 } catch (Exception e) {
154 throw new CmsException("Cannot initialize config", e);
155 }
156 return super.addingService(reference);
157 }
158 };
159 // confAdminSt.open();
160 KernelUtils.asyncOpen(confAdminSt);
161 }
162
163 private void addStandardSystemRoles(UserAdmin userAdmin) {
164 // we assume UserTransaction is already available (TODO make it more robust)
165 UserTransaction userTransaction = bc.getService(bc.getServiceReference(UserTransaction.class));
166 try {
167 userTransaction.begin();
168 Role adminRole = userAdmin.getRole(NodeConstants.ROLE_ADMIN);
169 if (adminRole == null) {
170 adminRole = userAdmin.createRole(NodeConstants.ROLE_ADMIN, Role.GROUP);
171 }
172 if (userAdmin.getRole(NodeConstants.ROLE_USER_ADMIN) == null) {
173 Group userAdminRole = (Group) userAdmin.createRole(NodeConstants.ROLE_USER_ADMIN, Role.GROUP);
174 userAdminRole.addMember(adminRole);
175 }
176 userTransaction.commit();
177 } catch (Exception e) {
178 try {
179 userTransaction.rollback();
180 } catch (Exception e1) {
181 // silent
182 }
183 throw new CmsException("Cannot add standard system roles", e);
184 }
185 }
186
187 private void loadIpaJaasConfiguration() {
188 if (System.getProperty(KernelConstants.JAAS_CONFIG_PROP) == null) {
189 String jaasConfig = KernelConstants.JAAS_CONFIG_IPA;
190 URL url = getClass().getClassLoader().getResource(jaasConfig);
191 KernelUtils.setJaasConfiguration(url);
192 log.debug("Set IPA JAAS configuration.");
193 }
194 }
195
196 public void shutdown() {
197 if (nodeHttp != null)
198 nodeHttp.destroy();
199
200 try {
201 for (ServiceReference<JackrabbitLocalRepository> sr : bc
202 .getServiceReferences(JackrabbitLocalRepository.class, null)) {
203 bc.getService(sr).destroy();
204 }
205 } catch (InvalidSyntaxException e1) {
206 log.error("Cannot sclean repsoitories", e1);
207 }
208
209 try {
210 JettyConfigurator.stopServer(KernelConstants.DEFAULT_JETTY_SERVER);
211 } catch (Exception e) {
212 log.error("Cannot stop default Jetty server.", e);
213 }
214
215 if (deployConfig != null) {
216 new Thread(() -> deployConfig.save(), "Save Argeo Deploy Config").start();
217 }
218 }
219
220 /**
221 * Checks whether the deployment is available according to expectations, and
222 * mark it as available.
223 */
224 private synchronized void checkReadiness() {
225 if (isAvailable())
226 return;
227 if (nodeAvailable && userAdminAvailable && (httpExpected ? httpAvailable : true)) {
228 String data = KernelUtils.getFrameworkProp(KernelUtils.OSGI_INSTANCE_AREA);
229 String state = KernelUtils.getFrameworkProp(KernelUtils.OSGI_CONFIGURATION_AREA);
230 availableSince = System.currentTimeMillis();
231 long jvmUptime = ManagementFactory.getRuntimeMXBean().getUptime();
232 String jvmUptimeStr = " in " + (jvmUptime / 1000) + "." + (jvmUptime % 1000) + "s";
233 log.info("## ARGEO NODE AVAILABLE" + (log.isDebugEnabled() ? jvmUptimeStr : "") + " ##");
234 if (log.isDebugEnabled()) {
235 log.debug("## state: " + state);
236 if (data != null)
237 log.debug("## data: " + data);
238 }
239 long begin = bc.getService(bc.getServiceReference(NodeState.class)).getAvailableSince();
240 long initDuration = System.currentTimeMillis() - begin;
241 if (log.isTraceEnabled())
242 log.trace("Kernel initialization took " + initDuration + "ms");
243 tributeToFreeSoftware(initDuration);
244 }
245 }
246
247 final private void tributeToFreeSoftware(long initDuration) {
248 if (log.isTraceEnabled()) {
249 long ms = initDuration / 100;
250 log.trace("Spend " + ms + "ms" + " reflecting on the progress brought to mankind" + " by Free Software...");
251 long beginNano = System.nanoTime();
252 try {
253 Thread.sleep(ms, 0);
254 } catch (InterruptedException e) {
255 // silent
256 }
257 long durationNano = System.nanoTime() - beginNano;
258 final double M = 1000d * 1000d;
259 double sleepAccuracy = ((double) durationNano) / (ms * M);
260 log.trace("Sleep accuracy: " + String.format("%.2f", 100 - (sleepAccuracy * 100 - 100)) + " %");
261 }
262 }
263
264 private void prepareNodeRepository(Repository deployedNodeRepository) {
265 if (availableSince != null) {
266 throw new CmsException("Deployment is already available");
267 }
268
269 // home
270 prepareDataModel(NodeConstants.NODE, deployedNodeRepository);
271 }
272
273 private void prepareHomeRepository(RepositoryImpl deployedRepository) {
274 Session adminSession = KernelUtils.openAdminSession(deployedRepository);
275 try {
276 argeoDataModelExtensionsAvailable = Arrays
277 .asList(adminSession.getWorkspace().getNamespaceRegistry().getURIs())
278 .contains(ArgeoNames.ARGEO_NAMESPACE);
279 } catch (RepositoryException e) {
280 log.warn("Cannot check whether Argeo namespace is registered assuming it isn't.", e);
281 argeoDataModelExtensionsAvailable = false;
282 } finally {
283 JcrUtils.logoutQuietly(adminSession);
284 }
285
286 // Publish home with the highest service ranking
287 Hashtable<String, Object> regProps = new Hashtable<>();
288 regProps.put(NodeConstants.CN, NodeConstants.EGO);
289 regProps.put(Constants.SERVICE_RANKING, Integer.MAX_VALUE);
290 Repository egoRepository = new EgoRepository(deployedRepository, false);
291 bc.registerService(Repository.class, egoRepository, regProps);
292
293 // Keyring only if Argeo extensions are available
294 if (argeoDataModelExtensionsAvailable) {
295 new ServiceTracker<CallbackHandler, CallbackHandler>(bc, CallbackHandler.class, null) {
296
297 @Override
298 public CallbackHandler addingService(ServiceReference<CallbackHandler> reference) {
299 NodeKeyRing nodeKeyring = new NodeKeyRing(egoRepository);
300 CallbackHandler callbackHandler = bc.getService(reference);
301 nodeKeyring.setDefaultCallbackHandler(callbackHandler);
302 bc.registerService(LangUtils.names(Keyring.class, CryptoKeyring.class, ManagedService.class),
303 nodeKeyring, LangUtils.dico(Constants.SERVICE_PID, NodeConstants.NODE_KEYRING_PID));
304 return callbackHandler;
305 }
306
307 }.open();
308 }
309 }
310
311 /** Session is logged out. */
312 private void prepareDataModel(String cn, Repository repository) {
313 Session adminSession = KernelUtils.openAdminSession(repository);
314 try {
315 Set<String> processed = new HashSet<String>();
316 bundles: for (Bundle bundle : bc.getBundles()) {
317 BundleWiring wiring = bundle.adapt(BundleWiring.class);
318 if (wiring == null)
319 continue bundles;
320 if (NodeConstants.NODE.equals(cn))// process all data models
321 processWiring(cn, adminSession, wiring, processed, false);
322 else {
323 List<BundleCapability> capabilities = wiring.getCapabilities(CMS_DATA_MODEL_NAMESPACE);
324 for (BundleCapability capability : capabilities) {
325 String dataModelName = (String) capability.getAttributes().get(DataModelNamespace.NAME);
326 if (dataModelName.equals(cn))// process only own data model
327 processWiring(cn, adminSession, wiring, processed, false);
328 }
329 }
330 }
331 } finally {
332 JcrUtils.logoutQuietly(adminSession);
333 }
334 }
335
336 private void processWiring(String cn, Session adminSession, BundleWiring wiring, Set<String> processed,
337 boolean importListedAbstractModels) {
338 // recursively process requirements first
339 List<BundleWire> requiredWires = wiring.getRequiredWires(CMS_DATA_MODEL_NAMESPACE);
340 for (BundleWire wire : requiredWires) {
341 processWiring(cn, adminSession, wire.getProviderWiring(), processed, true);
342 }
343
344 List<String> publishAsLocalRepo = new ArrayList<>();
345 List<BundleCapability> capabilities = wiring.getCapabilities(CMS_DATA_MODEL_NAMESPACE);
346 capabilities: for (BundleCapability capability : capabilities) {
347 if (!importListedAbstractModels
348 && KernelUtils.asBoolean((String) capability.getAttributes().get(DataModelNamespace.ABSTRACT))) {
349 continue capabilities;
350 }
351 boolean publish = registerDataModelCapability(cn, adminSession, capability, processed);
352 if (publish)
353 publishAsLocalRepo.add((String) capability.getAttributes().get(DataModelNamespace.NAME));
354 }
355 // Publish all at once, so that bundles with multiple CNDs are consistent
356 for (String dataModelName : publishAsLocalRepo)
357 publishLocalRepo(dataModelName, adminSession.getRepository());
358 }
359
360 private boolean registerDataModelCapability(String cn, Session adminSession, BundleCapability capability,
361 Set<String> processed) {
362 Map<String, Object> attrs = capability.getAttributes();
363 String name = (String) attrs.get(DataModelNamespace.NAME);
364 if (processed.contains(name)) {
365 if (log.isTraceEnabled())
366 log.trace("Data model " + name + " has already been processed");
367 return false;
368 }
369
370 // CND
371 String path = (String) attrs.get(DataModelNamespace.CND);
372 if (path != null) {
373 File dataModel = bc.getBundle().getDataFile("dataModels/" + path);
374 if (!dataModel.exists()) {
375 URL url = capability.getRevision().getBundle().getResource(path);
376 if (url == null)
377 throw new CmsException("No data model '" + name + "' found under path " + path);
378 try (Reader reader = new InputStreamReader(url.openStream())) {
379 CndImporter.registerNodeTypes(reader, adminSession, true);
380 processed.add(name);
381 dataModel.getParentFile().mkdirs();
382 dataModel.createNewFile();
383 if (log.isDebugEnabled())
384 log.debug("Registered CND " + url);
385 } catch (Exception e) {
386 throw new CmsException("Cannot import CND " + url, e);
387 }
388 }
389 }
390
391 if (KernelUtils.asBoolean((String) attrs.get(DataModelNamespace.ABSTRACT)))
392 return false;
393 // Non abstract
394 boolean isStandalone = deployConfig.isStandalone(name);
395 boolean publishLocalRepo;
396 if (isStandalone && name.equals(cn))// includes the node itself
397 publishLocalRepo = true;
398 else if (!isStandalone && cn.equals(NodeConstants.NODE))
399 publishLocalRepo = true;
400 else
401 publishLocalRepo = false;
402
403 return publishLocalRepo;
404 }
405
406 private void publishLocalRepo(String dataModelName, Repository repository) {
407 Hashtable<String, Object> properties = new Hashtable<>();
408 properties.put(NodeConstants.CN, dataModelName);
409 LocalRepository localRepository;
410 String[] classes;
411 if (repository instanceof RepositoryImpl) {
412 localRepository = new JackrabbitLocalRepository((RepositoryImpl) repository, dataModelName);
413 classes = new String[] { Repository.class.getName(), LocalRepository.class.getName(),
414 JackrabbitLocalRepository.class.getName() };
415 } else {
416 localRepository = new LocalRepository(repository, dataModelName);
417 classes = new String[] { Repository.class.getName(), LocalRepository.class.getName() };
418 }
419 bc.registerService(classes, localRepository, properties);
420 if (log.isTraceEnabled())
421 log.trace("Published data model " + dataModelName);
422 }
423
424 @Override
425 public synchronized Long getAvailableSince() {
426 return availableSince;
427 }
428
429 public synchronized boolean isAvailable() {
430 return availableSince != null;
431 }
432
433 private class RepositoryContextStc extends ServiceTracker<RepositoryContext, RepositoryContext> {
434
435 public RepositoryContextStc() {
436 super(bc, RepositoryContext.class, null);
437 }
438
439 @Override
440 public RepositoryContext addingService(ServiceReference<RepositoryContext> reference) {
441 RepositoryContext repoContext = bc.getService(reference);
442 String cn = (String) reference.getProperty(NodeConstants.CN);
443 if (cn != null) {
444 if (cn.equals(NodeConstants.NODE)) {
445 prepareNodeRepository(repoContext.getRepository());
446 // TODO separate home repository
447 prepareHomeRepository(repoContext.getRepository());
448 nodeAvailable = true;
449 checkReadiness();
450 } else {
451 prepareDataModel(cn, repoContext.getRepository());
452 }
453 }
454 return repoContext;
455 }
456
457 @Override
458 public void modifiedService(ServiceReference<RepositoryContext> reference, RepositoryContext service) {
459 }
460
461 @Override
462 public void removedService(ServiceReference<RepositoryContext> reference, RepositoryContext service) {
463 }
464
465 }
466
467 }