]> git.argeo.org Git - lgpl/argeo-commons.git/blob - CmsState.java
64a9d170bed6b551a49ea6c9a3019a65f6ca93a9
[lgpl/argeo-commons.git] / CmsState.java
1 package org.argeo.cms.internal.kernel;
2
3 import static bitronix.tm.TransactionManagerServices.getTransactionManager;
4 import static bitronix.tm.TransactionManagerServices.getTransactionSynchronizationRegistry;
5 import static java.util.Locale.ENGLISH;
6 import static org.argeo.cms.internal.auth.LocaleChoice.asLocaleList;
7 import static org.argeo.cms.internal.kernel.KernelUtils.getFrameworkProp;
8
9 import java.io.File;
10 import java.io.IOException;
11 import java.net.InetAddress;
12 import java.net.URI;
13 import java.net.UnknownHostException;
14 import java.nio.file.DirectoryStream;
15 import java.nio.file.Files;
16 import java.nio.file.Path;
17 import java.util.ArrayList;
18 import java.util.Dictionary;
19 import java.util.Hashtable;
20 import java.util.List;
21 import java.util.Locale;
22 import java.util.UUID;
23
24 import javax.jcr.RepositoryFactory;
25 import javax.transaction.TransactionManager;
26 import javax.transaction.TransactionSynchronizationRegistry;
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.core.RepositoryContext;
32 import org.argeo.cms.CmsException;
33 import org.argeo.cms.maintenance.MaintenanceUi;
34 import org.argeo.jcr.ArgeoJcrConstants;
35 import org.argeo.node.NodeConstants;
36 import org.argeo.node.NodeDeployment;
37 import org.argeo.node.NodeState;
38 import org.argeo.node.RepoConf;
39 import org.argeo.util.LangUtils;
40 import org.eclipse.equinox.http.jetty.JettyConfigurator;
41 import org.eclipse.equinox.http.jetty.JettyConstants;
42 import org.eclipse.rap.rwt.application.ApplicationConfiguration;
43 import org.osgi.framework.Bundle;
44 import org.osgi.framework.BundleContext;
45 import org.osgi.framework.Constants;
46 import org.osgi.framework.FrameworkUtil;
47 import org.osgi.framework.ServiceReference;
48 import org.osgi.service.cm.Configuration;
49 import org.osgi.service.cm.ConfigurationAdmin;
50 import org.osgi.service.cm.ConfigurationException;
51 import org.osgi.service.cm.ManagedService;
52 import org.osgi.service.cm.ManagedServiceFactory;
53 import org.osgi.service.http.HttpService;
54 import org.osgi.service.metatype.MetaTypeProvider;
55 import org.osgi.service.useradmin.UserAdmin;
56 import org.osgi.util.tracker.ServiceTracker;
57 import org.osgi.util.tracker.ServiceTrackerCustomizer;
58
59 import bitronix.tm.BitronixTransactionManager;
60 import bitronix.tm.BitronixTransactionSynchronizationRegistry;
61 import bitronix.tm.TransactionManagerServices;
62
63 public class CmsState implements NodeState, ManagedService {
64 private final Log log = LogFactory.getLog(CmsState.class);
65 private final BundleContext bc = FrameworkUtil.getBundle(CmsState.class).getBundleContext();
66
67 // avoid dependency to RWT OSGi
68 private final static String PROPERTY_CONTEXT_NAME = "contextName";
69
70 // REFERENCES
71 private ConfigurationAdmin configurationAdmin;
72
73 // i18n
74 private Locale defaultLocale;
75 private List<Locale> locales = null;
76
77 // private BitronixTransactionManager transactionManager;
78 // private BitronixTransactionSynchronizationRegistry
79 // transactionSynchronizationRegistry;
80 // private NodeRepositoryFactory repositoryFactory;
81 // private NodeUserAdmin userAdmin;
82 // private RepositoryServiceFactory repositoryServiceFactory;
83 // private RepositoryService repositoryService;
84
85 // Deployment
86 private final CmsDeployment nodeDeployment = new CmsDeployment();
87
88 private boolean cleanState = false;
89 private URI nodeRepoUri = null;
90
91 private ThreadGroup threadGroup = new ThreadGroup("CMS");
92 private KernelThread kernelThread;
93 private List<Runnable> shutdownHooks = new ArrayList<>();
94
95 private String hostname;
96
97 public CmsState() {
98 try {
99 this.hostname = InetAddress.getLocalHost().getHostName();
100 } catch (UnknownHostException e) {
101 log.error("Cannot set hostname", e);
102 }
103 }
104
105 @Override
106 public void updated(Dictionary<String, ?> properties) throws ConfigurationException {
107 if (properties == null) {
108 // TODO this should not happen anymore
109 this.cleanState = true;
110 if (log.isTraceEnabled())
111 log.trace("Clean state");
112 return;
113 }
114 String stateUuid = properties.get(NodeConstants.CN).toString();
115 String frameworkUuid = KernelUtils.getFrameworkProp(Constants.FRAMEWORK_UUID);
116 this.cleanState = stateUuid.equals(frameworkUuid);
117
118 try {
119 if (log.isDebugEnabled())
120 log.debug("## CMS STARTED " + stateUuid + (cleanState ? " (clean state) " : " ")
121 + LangUtils.toJson(properties, true));
122 configurationAdmin = bc.getService(bc.getServiceReference(ConfigurationAdmin.class));
123
124 nodeRepoUri = KernelUtils.getOsgiInstanceUri("repos/node");
125
126 initI18n(properties);
127 initServices();
128 initDeployConfigs(properties);
129 initWebServer();
130 initNodeDeployment();
131
132 // kernel thread
133 kernelThread = new KernelThread(threadGroup, "Kernel Thread");
134 kernelThread.setContextClassLoader(getClass().getClassLoader());
135 kernelThread.start();
136 } catch (Exception e) {
137 throw new CmsException("Cannot get configuration", e);
138 }
139 }
140
141 private void initI18n(Dictionary<String, ?> stateProps) {
142 Object defaultLocaleValue = stateProps.get(NodeConstants.I18N_DEFAULT_LOCALE);
143 defaultLocale = defaultLocaleValue != null ? new Locale(defaultLocaleValue.toString())
144 : new Locale(ENGLISH.getLanguage());
145 locales = asLocaleList(stateProps.get(NodeConstants.I18N_LOCALES));
146 }
147
148 private void initServices() {
149 // trackers
150 new ServiceTracker<HttpService, HttpService>(bc, HttpService.class, new PrepareHttpStc()).open();
151 new ServiceTracker<>(bc, RepositoryContext.class, new RepositoryContextStc()).open();
152
153 initTransactionManager();
154
155 // JCR
156 RepositoryServiceFactory repositoryServiceFactory = new RepositoryServiceFactory();
157 shutdownHooks.add(() -> repositoryServiceFactory.shutdown());
158 bc.registerService(ManagedServiceFactory.class, repositoryServiceFactory,
159 LangUtils.init(Constants.SERVICE_PID, NodeConstants.JACKRABBIT_FACTORY_PID));
160
161 NodeRepositoryFactory repositoryFactory = new NodeRepositoryFactory();
162 bc.registerService(RepositoryFactory.class, repositoryFactory, null);
163
164 RepositoryService repositoryService = new RepositoryService();
165 shutdownHooks.add(() -> repositoryService.shutdown());
166 bc.registerService(LangUtils.names(ManagedService.class, MetaTypeProvider.class), repositoryService,
167 LangUtils.init(Constants.SERVICE_PID, NodeConstants.NODE_REPO_PID));
168
169 // Security
170 NodeUserAdmin userAdmin = new NodeUserAdmin();
171 shutdownHooks.add(() -> userAdmin.destroy());
172 Dictionary<String, Object> props = userAdmin.currentState();
173 props.put(Constants.SERVICE_PID, NodeConstants.NODE_USER_ADMIN_PID);
174 bc.registerService(UserAdmin.class, userAdmin, props);
175
176 // UI
177 bc.registerService(ApplicationConfiguration.class, new MaintenanceUi(),
178 LangUtils.init(PROPERTY_CONTEXT_NAME, "system"));
179 bc.registerService(ApplicationConfiguration.class, new UserUi(), LangUtils.init(PROPERTY_CONTEXT_NAME, "user"));
180 }
181 // private void initUserAdmin() {
182 // userAdmin = new NodeUserAdmin();
183 // // register
184 // Dictionary<String, Object> props = userAdmin.currentState();
185 // props.put(Constants.SERVICE_PID, NodeConstants.NODE_USER_ADMIN_PID);
186 // // TODO use ManagedService
187 // bc.registerService(UserAdmin.class, userAdmin, props);
188 // }
189
190 private void initTransactionManager() {
191 // TODO manage it in a managed service, as startup could be long
192 ServiceReference<TransactionManager> existingTm = bc.getServiceReference(TransactionManager.class);
193 if (existingTm != null) {
194 if (log.isDebugEnabled())
195 log.debug("Using provided transaction manager " + existingTm);
196 }
197 bitronix.tm.Configuration tmConf = TransactionManagerServices.getConfiguration();
198 tmConf.setServerId(UUID.randomUUID().toString());
199
200 Bundle bitronixBundle = FrameworkUtil.getBundle(bitronix.tm.Configuration.class);
201 File tmBaseDir = bitronixBundle.getDataFile(KernelConstants.DIR_TRANSACTIONS);
202 File tmDir1 = new File(tmBaseDir, "btm1");
203 tmDir1.mkdirs();
204 tmConf.setLogPart1Filename(new File(tmDir1, tmDir1.getName() + ".tlog").getAbsolutePath());
205 File tmDir2 = new File(tmBaseDir, "btm2");
206 tmDir2.mkdirs();
207 tmConf.setLogPart2Filename(new File(tmDir2, tmDir2.getName() + ".tlog").getAbsolutePath());
208
209 BitronixTransactionManager transactionManager = getTransactionManager();
210 shutdownHooks.add(() -> transactionManager.shutdown());
211 BitronixTransactionSynchronizationRegistry transactionSynchronizationRegistry = getTransactionSynchronizationRegistry();
212 // register
213 bc.registerService(TransactionManager.class, transactionManager, null);
214 bc.registerService(UserTransaction.class, transactionManager, null);
215 bc.registerService(TransactionSynchronizationRegistry.class, transactionSynchronizationRegistry, null);
216 if (log.isDebugEnabled())
217 log.debug("Initialised default Bitronix transaction manager");
218 }
219
220 // private void initRepositoryFactory() {
221 // // TODO rationalise RepositoryFactory
222 // repositoryFactory = new NodeRepositoryFactory();
223 // // register
224 // bc.registerService(RepositoryFactory.class, repositoryFactory, null);
225 // }
226
227 // private void initUi() {
228 // bc.registerService(ApplicationConfiguration.class, new MaintenanceUi(),
229 // LangUtils.init(PROPERTY_CONTEXT_NAME, "system"));
230 // bc.registerService(ApplicationConfiguration.class, new UserUi(),
231 // LangUtils.init(PROPERTY_CONTEXT_NAME, "user"));
232 // }
233
234 private void initDeployConfigs(Dictionary<String, ?> stateProps) throws IOException {
235 Path deployPath = KernelUtils.getOsgiInstancePath(KernelConstants.DIR_NODE + '/' + KernelConstants.DIR_DEPLOY);
236 Files.createDirectories(deployPath);
237
238 Path nodeConfigPath = deployPath.resolve(NodeConstants.NODE_REPO_PID + ".properties");
239 if (!Files.exists(nodeConfigPath)) {
240 Dictionary<String, Object> nodeConfig = getNodeConfig(stateProps);
241 nodeConfig.put(ArgeoJcrConstants.JCR_REPOSITORY_ALIAS, ArgeoJcrConstants.ALIAS_NODE);
242 nodeConfig.put(RepoConf.labeledUri.name(), nodeRepoUri.toString());
243 LangUtils.storeAsProperties(nodeConfig, nodeConfigPath);
244 }
245
246 if (cleanState) {
247 try (DirectoryStream<Path> ds = Files.newDirectoryStream(deployPath)) {
248 for (Path path : ds) {
249 if (Files.isDirectory(path)) {// managed factories
250 try (DirectoryStream<Path> factoryDs = Files.newDirectoryStream(path)) {
251 for (Path confPath : factoryDs) {
252 Configuration conf = configurationAdmin
253 .createFactoryConfiguration(path.getFileName().toString());
254 Dictionary<String, Object> props = LangUtils.loadFromProperties(confPath);
255 conf.update(props);
256 }
257 }
258 } else {// managed services
259 String pid = path.getFileName().toString();
260 pid = pid.substring(0, pid.length() - ".properties".length());
261 Configuration conf = configurationAdmin.getConfiguration(pid);
262 Dictionary<String, Object> props = LangUtils.loadFromProperties(path);
263 conf.update(props);
264 }
265 }
266 }
267 }
268 }
269
270 // private void initRepositories(Dictionary<String, ?> stateProps) throws
271 // IOException {
272 // // register
273 // repositoryServiceFactory = new RepositoryServiceFactory();
274 // bc.registerService(ManagedServiceFactory.class, repositoryServiceFactory,
275 // LangUtils.init(Constants.SERVICE_PID,
276 // NodeConstants.JACKRABBIT_FACTORY_PID));
277 //
278 // repositoryService = new RepositoryService();
279 // Dictionary<String, Object> regProps =
280 // LangUtils.init(Constants.SERVICE_PID, NodeConstants.NODE_REPO_PID);
281 // bc.registerService(LangUtils.names(ManagedService.class,
282 // MetaTypeProvider.class), repositoryService, regProps);
283 // }
284
285 private void initWebServer() {
286 String httpPort = getFrameworkProp("org.osgi.service.http.port");
287 String httpsPort = getFrameworkProp("org.osgi.service.http.port.secure");
288 try {
289 if (httpPort != null || httpsPort != null) {
290 final Hashtable<String, Object> jettyProps = new Hashtable<String, Object>();
291 if (httpPort != null) {
292 jettyProps.put(JettyConstants.HTTP_PORT, httpPort);
293 jettyProps.put(JettyConstants.HTTP_ENABLED, true);
294 }
295 if (httpsPort != null) {
296 jettyProps.put(JettyConstants.HTTPS_PORT, httpsPort);
297 jettyProps.put(JettyConstants.HTTPS_ENABLED, true);
298 jettyProps.put(JettyConstants.SSL_KEYSTORETYPE, "PKCS12");
299 // jettyProps.put(JettyConstants.SSL_KEYSTORE,
300 // nodeSecurity.getHttpServerKeyStore().getCanonicalPath());
301 jettyProps.put(JettyConstants.SSL_PASSWORD, "changeit");
302 jettyProps.put(JettyConstants.SSL_WANTCLIENTAUTH, true);
303 }
304 if (configurationAdmin != null) {
305 // TODO make filter more generic
306 String filter = "(" + JettyConstants.HTTP_PORT + "=" + httpPort + ")";
307 if (configurationAdmin.listConfigurations(filter) != null)
308 return;
309 Configuration jettyConf = configurationAdmin
310 .createFactoryConfiguration(KernelConstants.JETTY_FACTORY_PID, null);
311 jettyConf.update(jettyProps);
312
313 } else {
314 JettyConfigurator.startServer("default", jettyProps);
315 }
316 }
317 } catch (Exception e) {
318 throw new CmsException("Cannot initialize web server on " + httpPortsMsg(httpPort, httpsPort), e);
319 }
320 }
321
322 private void initNodeDeployment() throws IOException {
323 Configuration nodeDeploymentConf = configurationAdmin.getConfiguration(NodeConstants.NODE_DEPLOYMENT_PID);
324 nodeDeploymentConf.update(new Hashtable<>());
325 }
326
327 void shutdown() {
328 // if (transactionManager != null)
329 // transactionManager.shutdown();
330 // if (userAdmin != null)
331 // userAdmin.destroy();
332 // if (repositoryServiceFactory != null)
333 // repositoryServiceFactory.shutdown();
334
335 applyShutdownHooks();
336
337 if (kernelThread != null)
338 kernelThread.destroyAndJoin();
339
340 if (log.isDebugEnabled())
341 log.debug("## CMS STOPPED");
342 }
343
344 /** Apply shutdown hoos in reverse order. */
345 private void applyShutdownHooks() {
346 for (int i = shutdownHooks.size() - 1; i >= 0; i--) {
347 try {
348 // new Thread(shutdownHooks.get(i), "CMS Shutdown Hook #" +
349 // i).start();
350 shutdownHooks.get(i).run();
351 } catch (Exception e) {
352 log.error("Could not run shutdown hook #" + i);
353 }
354 }
355 // Clean hanging Gogo shell thread
356 new GogoShellKiller().start();
357 }
358
359 private Dictionary<String, Object> getNodeConfig(Dictionary<String, ?> properties) {
360 // Object repoType = properties.get(NodeConstants.NODE_REPO_PROP_PREFIX
361 // + RepoConf.type.name());
362 // if (repoType == null)
363 // return null;
364
365 Hashtable<String, Object> props = new Hashtable<String, Object>();
366 for (RepoConf repoConf : RepoConf.values()) {
367 Object value = properties.get(NodeConstants.NODE_REPO_PROP_PREFIX + repoConf.name());
368 if (value != null)
369 props.put(repoConf.name(), value);
370 }
371 return props;
372 }
373
374 private class RepositoryContextStc implements ServiceTrackerCustomizer<RepositoryContext, RepositoryContext> {
375
376 @Override
377 public RepositoryContext addingService(ServiceReference<RepositoryContext> reference) {
378 RepositoryContext nodeRepo = bc.getService(reference);
379 Object repoUri = reference.getProperty(ArgeoJcrConstants.JCR_REPOSITORY_URI);
380 if (repoUri != null && repoUri.equals(nodeRepoUri.toString())) {
381 nodeDeployment.setDeployedNodeRepository(nodeRepo.getRepository());
382 Dictionary<String, Object> props = LangUtils.init(Constants.SERVICE_PID,
383 NodeConstants.NODE_DEPLOYMENT_PID);
384 props.put(NodeConstants.CN, nodeRepo.getRootNodeId().toString());
385 // register
386 bc.registerService(LangUtils.names(NodeDeployment.class, ManagedService.class), nodeDeployment, props);
387 }
388
389 return nodeRepo;
390 }
391
392 @Override
393 public void modifiedService(ServiceReference<RepositoryContext> reference, RepositoryContext service) {
394 }
395
396 @Override
397 public void removedService(ServiceReference<RepositoryContext> reference, RepositoryContext service) {
398 }
399
400 }
401
402 private class PrepareHttpStc implements ServiceTrackerCustomizer<HttpService, HttpService> {
403 private DataHttp dataHttp;
404 private NodeHttp nodeHttp;
405
406 @Override
407 public HttpService addingService(ServiceReference<HttpService> reference) {
408 HttpService httpService = addHttpService(reference);
409 return httpService;
410 }
411
412 @Override
413 public void modifiedService(ServiceReference<HttpService> reference, HttpService service) {
414 }
415
416 @Override
417 public void removedService(ServiceReference<HttpService> reference, HttpService service) {
418 if (dataHttp != null)
419 dataHttp.destroy();
420 dataHttp = null;
421 if (nodeHttp != null)
422 nodeHttp.destroy();
423 nodeHttp = null;
424 }
425
426 private HttpService addHttpService(ServiceReference<HttpService> sr) {
427 HttpService httpService = bc.getService(sr);
428 // TODO find constants
429 Object httpPort = sr.getProperty("http.port");
430 Object httpsPort = sr.getProperty("https.port");
431 dataHttp = new DataHttp(httpService);
432 nodeHttp = new NodeHttp(httpService, bc);
433 if (log.isDebugEnabled())
434 log.debug(httpPortsMsg(httpPort, httpsPort));
435 return httpService;
436 }
437
438 }
439
440 /*
441 * ACCESSORS
442 */
443 public Locale getDefaultLocale() {
444 return defaultLocale;
445 }
446
447 public List<Locale> getLocales() {
448 return locales;
449 }
450
451 public String getHostname() {
452 return hostname;
453 }
454
455 /*
456 * STATIC
457 */
458 private static String httpPortsMsg(Object httpPort, Object httpsPort) {
459 return "HTTP " + httpPort + (httpsPort != null ? " - HTTPS " + httpsPort : "");
460 }
461
462 /** Workaround for blocking Gogo shell by system shutdown. */
463 private class GogoShellKiller extends Thread {
464
465 public GogoShellKiller() {
466 super("Gogo Shell Killer");
467 setDaemon(true);
468 }
469
470 @Override
471 public void run() {
472 ThreadGroup rootTg = getRootThreadGroup(null);
473 Thread gogoShellThread = findGogoShellThread(rootTg);
474 if (gogoShellThread == null)
475 return;
476 while (getNonDaemonCount(rootTg) > 2) {
477 try {
478 Thread.sleep(100);
479 } catch (InterruptedException e) {
480 // silent
481 }
482 }
483 gogoShellThread = findGogoShellThread(rootTg);
484 if (gogoShellThread == null)
485 return;
486 System.exit(0);
487 }
488 }
489
490 private static ThreadGroup getRootThreadGroup(ThreadGroup tg) {
491 if (tg == null)
492 tg = Thread.currentThread().getThreadGroup();
493 if (tg.getParent() == null)
494 return tg;
495 else
496 return getRootThreadGroup(tg.getParent());
497 }
498
499 private static int getNonDaemonCount(ThreadGroup rootThreadGroup) {
500 Thread[] threads = new Thread[rootThreadGroup.activeCount()];
501 rootThreadGroup.enumerate(threads);
502 int nonDameonCount = 0;
503 for (Thread t : threads)
504 if (t != null && !t.isDaemon())
505 nonDameonCount++;
506 return nonDameonCount;
507 }
508
509 private static Thread findGogoShellThread(ThreadGroup rootThreadGroup) {
510 Thread[] threads = new Thread[rootThreadGroup.activeCount()];
511 rootThreadGroup.enumerate(threads, true);
512 for (Thread thread : threads) {
513 if (thread.getName().equals("Gogo shell"))
514 return thread;
515 }
516 return null;
517 }
518
519 }