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