001package org.dcm4che3.conf.core.olock;
002
003import org.dcm4che3.conf.core.DelegatingConfiguration;
004import org.dcm4che3.conf.core.api.Configuration;
005import org.dcm4che3.conf.core.api.ConfigurationException;
006import org.dcm4che3.conf.core.api.Path;
007import org.dcm4che3.conf.core.api.internal.ConfigProperty;
008import org.dcm4che3.conf.core.api.internal.ConfigReflection;
009import org.dcm4che3.conf.core.util.ConfigNodeTraverser;
010import org.dcm4che3.conf.core.util.ConfigNodeTraverser.ConfigNodeTypesafeFilter;
011import org.dcm4che3.conf.core.Nodes;
012import org.slf4j.Logger;
013import org.slf4j.LoggerFactory;
014
015import java.util.List;
016import java.util.Map;
017
018/**
019 *
020 *
021 * Caution: persistNode should be called in a transaction to ensure consistent comparison between data from backend/data being persisted,
022 * otherwise it's possible that another writer modifies something before this class obtains a lock and the changes by that writer will be lost
023 *
024 * Edge cases:
025 * <ul>
026 * <li>
027 *  If one deletes a node that is a root of olock, and another changes that node, it is NOT considered a conflict, regardless of operation order the node will be just deleted.
028 *  this holds for case of a subnode, a map entry, or a collection element that has a uuid.
029 *  </li><li>
030 *  For any collections whose elements don't have uuids, a collection is always fully overwritten without trying
031 *  to merge in any modified contents from the backend.
032 *  </li>
033 * </ul>
034 *
035 *
036 * @author Roman K
037 */
038@SuppressWarnings("unchecked")
039public class HashBasedOptimisticLockingConfiguration extends DelegatingConfiguration {
040
041    private static final Logger log = LoggerFactory.getLogger(HashBasedOptimisticLockingConfiguration.class);
042
043    public static final String NOT_CALCULATED_YET = "not-calculated-yet";
044    public static final String OLD_OLOCK_HASH_KEY = "#old_hash";
045
046    private List<Class> allExtensionClasses;
047
048    /**
049     * @param delegate
050     * @param allExtensionClasses
051     */
052    public HashBasedOptimisticLockingConfiguration(Configuration delegate, List<Class> allExtensionClasses) {
053        super(delegate);
054
055        this.allExtensionClasses = allExtensionClasses;
056    }
057
058    @Override
059    public void persistNode(final Path path, final Map<String, Object> configNode, final Class configurableClass) throws ConfigurationException {
060        Map<String, Object> nodeBeingPersisted = (Map<String, Object>) Nodes.deepCloneNode(configNode);
061
062        // get existing node from storage
063        Map<String, Object> nodeInStorage = (Map<String, Object>) getConfigurationNode(path, configurableClass);
064
065        // if there is nothing in storage - just persist and leave
066        if (nodeInStorage == null) {
067            if (nodeBeingPersisted != null) {
068                ConfigNodeTraverser.traverseMapNode(nodeBeingPersisted, new CleanupFilter(Configuration.OLOCK_HASH_KEY));
069            }
070            delegate.persistNode(path, nodeBeingPersisted, configurableClass);
071            return;
072        }
073
074        // save old hashes in node being persisted
075        ConfigNodeTraverser.traverseMapNode(nodeBeingPersisted, new OLockCopyFilter(OLD_OLOCK_HASH_KEY));
076
077        // calculate current hashes in node being persisted
078        ConfigNodeTraverser.traverseMapNode(nodeBeingPersisted, new OLockHashCalcFilter(OLD_OLOCK_HASH_KEY));
079
080        ////// merge the object /////
081        ConfigNodeTraverser.dualTraverseMapNodes(nodeInStorage, nodeBeingPersisted, new OLockMergeDualFilter());
082
083        // filter the #hash clutter out
084        ConfigNodeTraverser.traverseMapNode(nodeBeingPersisted, new CleanupFilter(OLD_OLOCK_HASH_KEY, Configuration.OLOCK_HASH_KEY));
085
086        delegate.persistNode(path, nodeBeingPersisted, configurableClass);
087    }
088
089
090    @Override
091    public Object getConfigurationNode(Path path, Class configurableClass) throws ConfigurationException {
092
093        Object configurationNode = super.getConfigurationNode(path, configurableClass);
094
095        //  calculate olock hashes if called with configurableClass
096        if (configurableClass != null && configurationNode != null) {
097
098            ConfigNodeTraverser.traverseNodeTypesafe(
099                    configurationNode,
100                    ConfigReflection.getDummyPropertyForClass(configurableClass),
101                    allExtensionClasses,
102                    new HashMarkingTypesafeNodeFilter()
103            );
104
105            ConfigNodeTraverser.traverseMapNode(configurationNode, new OLockHashCalcFilter());
106        }
107
108        return configurationNode;
109    }
110
111    /**
112     * Inserts #hash props into node according to @ConfigurableProperty annotations
113     */
114    public static class HashMarkingTypesafeNodeFilter implements ConfigNodeTypesafeFilter {
115
116        @Override
117        public boolean beforeNode(Map<String, Object> containerNode, Class containerNodeClass, ConfigProperty property) throws ConfigurationException {
118
119            if (property.isOlockHash()) {
120                containerNode.put(OLOCK_HASH_KEY, NOT_CALCULATED_YET);
121                return true;
122            }
123
124            return false;
125        }
126    }
127
128}