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