/* * *** BEGIN LICENSE BLOCK ***** * Version: MPL 1.1/GPL 2.0/LGPL 2.1 * * The contents of this file are subject to the Mozilla Public License Version * 1.1 (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * http://www.mozilla.org/MPL/ * * Software distributed under the License is distributed on an "AS IS" basis, * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License * for the specific language governing rights and limitations under the * License. * * The Original Code is part of dcm4che, an implementation of DICOM(TM) in * Java(TM), hosted at https://github.com/gunterze/dcm4che. * * The Initial Developer of the Original Code is * Agfa Healthcare. * Portions created by the Initial Developer are Copyright (C) 2015 * the Initial Developer. All Rights Reserved. * * Contributor(s): * See @authors listed below * * Alternatively, the contents of this file may be used under the terms of * either the GNU General Public License Version 2 or later (the "GPL"), or * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), * in which case the provisions of the GPL or the LGPL are applicable instead * of those above. If you wish to allow use of your version of this file only * under the terms of either the GPL or the LGPL, and not to allow others to * use your version of this file under the terms of the MPL, indicate your * decision by deleting the provisions above and replace them with the notice * and other provisions required by the GPL or the LGPL. If you do not delete * the provisions above, a recipient may use your version of this file under * the terms of any one of the MPL, the GPL or the LGPL. * * ***** END LICENSE BLOCK ***** */ package org.dcm4chee.conf.storage; import org.dcm4che3.conf.ConfigurationSettingsLoader; import org.dcm4che3.conf.core.DelegatingConfiguration; import org.dcm4che3.conf.core.ExtensionMergingConfiguration; import org.dcm4che3.conf.core.api.Configuration; import org.dcm4che3.conf.core.api.ConfigurationException; import org.dcm4che3.conf.core.api.Path; import org.dcm4che3.conf.core.normalization.DefaultsAndNullFilterDecorator; import org.dcm4che3.conf.core.olock.HashBasedOptimisticLockingConfiguration; import org.dcm4che3.conf.dicom.CommonDicomConfiguration; import org.dcm4chee.conf.ConfigurableExtensionsResolver; import org.dcm4chee.conf.notif.ConfigNotificationDecorator; import org.dcm4chee.conf.storage.ConfigurationStorage.ConfigStorageAnno; import org.dcm4chee.util.TransactionSynchronization; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.annotation.PostConstruct; import javax.ejb.*; import javax.enterprise.inject.Any; import javax.enterprise.inject.Instance; import javax.inject.Inject; import javax.transaction.*; import java.util.List; import java.util.Map; /** * A per-deployment configuration singleton that brings the config framework parts together: * <ul> * <li> Injects the proper config storage by looking up the system property * <li> Sets up infinispan cache, reference index * <li> Enforces global "one-writer-at-a-time" locking for modification ops and tx demarcation * <li> Enables hash-based optimistic locking</li> * <li> Enables defaults filtering</li> * <li> Triggers integrity checks on transaction pre-commit * <li> Triggers config notifications on post-commit * </ul> * <p>Manages tx demarcation as follows: * <ul> * <li> runBatch always executes the batch in a new transaction with a special marker * <li> if any of state-modifying methods is called outside of a batch tx - executes it in a new transaction * <li> if any of state-modifying methods is called within a marked batch tx - executes it as is, i.e. without any extra wrappers * </ul> * </p> */ @SuppressWarnings("unchecked") @Singleton @ConcurrencyManagement(ConcurrencyManagementType.BEAN) @Local(ConfigurationEJB.class) @TransactionAttribute(TransactionAttributeType.SUPPORTS) public class ConfigurationEJB extends DelegatingConfiguration { public static final Logger log = LoggerFactory.getLogger(ConfigurationEJB.class); private static final String DISABLE_OLOCK_PROP = "org.dcm4che.conf.olock.disabled"; private static final String ENABLE_MERGE_CONFIG = "org.dcm4che.conf.merge.enabled"; // components @Inject InfinispanCachingConfigurationDecorator infinispanCachingConfigurationDecorator; @Inject InfinispanDicomReferenceIndexingDecorator indexingDecorator; @Inject @Any private Instance<Configuration> availableConfigStorage; @Inject ConfigNotificationDecorator configNotificationDecorator; @Inject private ConfigurationIntegrityCheck integrityCheck; @EJB ConfigurationEJB self; // util @Inject TransactionSynchronization txSync; @Inject ConfigurableExtensionsResolver extensionsProvider; @PostConstruct @TransactionAttribute(TransactionAttributeType.REQUIRES_NEW) public void init() { // detect user setting (system property) for config backend type String storageType = ConfigurationSettingsLoader.getPropertyWithNotice( System.getProperties(), Configuration.CONF_STORAGE_SYSTEM_PROP, ConfigStorageType.JSON_FILE.name().toLowerCase() ); log.info("Creating dcm4che configuration Singleton EJB. Resolving underlying configuration storage '{}' ...", storageType); // resolve the corresponding implementation Configuration storage; try { storage = availableConfigStorage.select(new ConfigStorageAnno(storageType)).get(); } catch (Exception e) { throw new IllegalArgumentException("Unable to initialize dcm4che configuration storage '" + storageType + "'", e); } // decorate with config notifications configNotificationDecorator.setDelegate(storage); storage = configNotificationDecorator; // decorate with cache infinispanCachingConfigurationDecorator.setDelegate(storage); storage = infinispanCachingConfigurationDecorator; // decorate with reference indexing/resolution indexingDecorator.setDelegate(storage); storage = indexingDecorator; List<Class> allExtensionClasses = extensionsProvider.resolveExtensionsList(); // ExtensionMergingConfiguration if ((System.getProperty(ENABLE_MERGE_CONFIG) != null) && Boolean.valueOf(System.getProperty(ENABLE_MERGE_CONFIG))) { storage = new ExtensionMergingConfiguration(storage, allExtensionClasses); } // olocking if (System.getProperty(DISABLE_OLOCK_PROP) == null) { storage = new HashBasedOptimisticLockingConfiguration( storage, allExtensionClasses); } // defaults filtering storage = new DefaultsAndNullFilterDecorator(storage, allExtensionClasses, CommonDicomConfiguration.createDefaultDicomVitalizer()); delegate = storage; // bootstrap delegate.lock(); delegate.refreshNode(Path.ROOT); log.info("dcm4che configuration singleton EJB created"); } @Override public void persistNode(Path path, Map<String, Object> configNode, Class configurableClass) throws ConfigurationException { Runnable r = () -> delegate.persistNode(path, configNode, configurableClass); if (isBatchTx()) runInOngoingTx(r); else self.runInNewTx(r); } @Override public void removeNode(Path path) throws ConfigurationException { Runnable r = () -> delegate.removeNode(path); if (isBatchTx()) runInOngoingTx(r); else self.runInNewTx(r); } @Override public void refreshNode(Path path) throws ConfigurationException { Runnable r = () -> delegate.refreshNode(path); if (isBatchTx()) runInOngoingTx(r); else self.runInNewTx(r); } @Override public void lock() { log.warn("Unexpected call to lock(). Locking is handled automatically on a lower layer and should not be called explicitly", new IllegalStateException()); delegate.lock(); } @Override public void runBatch(Batch batch) { self.runInNewTx( () -> { markBatchTx(); // run directly here - we are inside a tx for sure - no need to delegate batch.run(); }); } /** * Ensures that integrity checking will be done before committing the transaction */ private void registerTxHooks() { // only register if there is no marker Object configTxMarker = txSync.getSynchronizationRegistry().getResource(ConfigurationEJB.class); if (configTxMarker == null) { // mark this tx as a writertx txSync.getSynchronizationRegistry().putResource(ConfigurationEJB.class, new Object()); // register pre-commit hook try { txSync.getTransactionManager().getTransaction().registerSynchronization(new Synchronization() { @Override public void beforeCompletion() { beforeCommit(); } @Override public void afterCompletion(int i) { } }); } catch (RollbackException | SystemException e) { throw new RuntimeException("Error when trying to register a pre-commit hook for config change transaction", e); } } } private void beforeCommit() { try { // perform referential integrity check integrityCheck.performCheck(super.getConfigurationRoot()); } catch (ConfigurationException e) { throw new IllegalArgumentException("Configuration integrity violated", e); } indexingDecorator.beforeCommit(); } private boolean isBatchTx() { return txSync.getStatus() != Status.STATUS_NO_TRANSACTION && txSync.getSynchronizationRegistry().getResource(Batch.class) != null; } private void markBatchTx() { txSync.getSynchronizationRegistry().putResource(Batch.class, new Object()); } @TransactionAttribute(TransactionAttributeType.REQUIRES_NEW) public void runInNewTx(Runnable r) { delegate.lock(); registerTxHooks(); r.run(); } /** * to make the stack trace easier to read */ private void runInOngoingTx(Runnable r) { r.run(); } }