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