]> gerrit.simantics Code Review - simantics/platform.git/blobdiff - bundles/org.simantics.modeling/src/org/simantics/modeling/services/CaseInsensitiveComponentNamingStrategy2.java
Migrated source code from Simantics SVN
[simantics/platform.git] / bundles / org.simantics.modeling / src / org / simantics / modeling / services / CaseInsensitiveComponentNamingStrategy2.java
diff --git a/bundles/org.simantics.modeling/src/org/simantics/modeling/services/CaseInsensitiveComponentNamingStrategy2.java b/bundles/org.simantics.modeling/src/org/simantics/modeling/services/CaseInsensitiveComponentNamingStrategy2.java
new file mode 100644 (file)
index 0000000..b2d4e2e
--- /dev/null
@@ -0,0 +1,534 @@
+/*******************************************************************************\r
+ * Copyright (c) 2007, 2010 Association for Decentralized Information Management\r
+ * in Industry THTH ry.\r
+ * All rights reserved. This program and the accompanying materials\r
+ * are made available under the terms of the Eclipse Public License v1.0\r
+ * which accompanies this distribution, and is available at\r
+ * http://www.eclipse.org/legal/epl-v10.html\r
+ *\r
+ * Contributors:\r
+ *     VTT Technical Research Centre of Finland - initial API and implementation\r
+ *******************************************************************************/\r
+package org.simantics.modeling.services;\r
+\r
+import gnu.trove.map.hash.THashMap;\r
+\r
+import java.lang.ref.SoftReference;\r
+import java.util.ArrayList;\r
+import java.util.Collection;\r
+import java.util.Collections;\r
+import java.util.Formatter;\r
+import java.util.HashSet;\r
+import java.util.IdentityHashMap;\r
+import java.util.Map;\r
+import java.util.Set;\r
+import java.util.concurrent.ConcurrentMap;\r
+import java.util.concurrent.ConcurrentSkipListMap;\r
+import java.util.concurrent.ConcurrentSkipListSet;\r
+\r
+import org.simantics.Simantics;\r
+import org.simantics.databoard.Bindings;\r
+import org.simantics.db.AsyncReadGraph;\r
+import org.simantics.db.ReadGraph;\r
+import org.simantics.db.Resource;\r
+import org.simantics.db.Session;\r
+import org.simantics.db.common.request.AsyncReadRequest;\r
+import org.simantics.db.common.utils.NameUtils;\r
+import org.simantics.db.event.ChangeEvent;\r
+import org.simantics.db.event.ChangeListener;\r
+import org.simantics.db.exception.DatabaseException;\r
+import org.simantics.db.procedure.AsyncMultiProcedure;\r
+import org.simantics.db.procedure.AsyncProcedure;\r
+import org.simantics.db.service.GraphChangeListenerSupport;\r
+import org.simantics.layer0.Layer0;\r
+import org.simantics.modeling.ComponentUtils;\r
+import org.simantics.project.IProject;\r
+import org.simantics.structural.stubs.StructuralResource2;\r
+import org.simantics.utils.datastructures.Pair;\r
+import org.simantics.utils.ui.ErrorLogger;\r
+\r
+/**\r
+ * A first-hand component naming strategy implementation for structural models.\r
+ * \r
+ * This version is somewhat optimized for case-insensitivity by using custom\r
+ * comparators in maps and sets. It uses a soft-referenced cache of\r
+ * used/requested names per a single root of a configuration's component\r
+ * hierarchy.\r
+ * \r
+ * @author Tuukka Lehtonen\r
+ * \r
+ * @see ComponentNamingStrategy\r
+ */\r
+public class CaseInsensitiveComponentNamingStrategy2 extends ComponentNamingStrategyBase implements ChangeListener {\r
+\r
+    private static final boolean            DEBUG_ALL                                     = false;\r
+    private static final boolean            DEBUG_GRAPH_UPDATES                           = false | DEBUG_ALL;\r
+    private static final boolean            DEBUG_CACHE_INITIALIZATION                    = false | DEBUG_ALL;\r
+    private static final boolean            DEBUG_CACHE_INITIALIZATION_BROWSE             = false | DEBUG_ALL;\r
+    private static final boolean            DEBUG_CACHE_UPDATES                           = false | DEBUG_ALL;\r
+\r
+    static class Cache {\r
+        private final Resource                        root;\r
+        private final Set<String>                     reserved;\r
+\r
+        // Having cache as soft references should somewhat address the problem\r
+        // of the amount of requested names growing too large.\r
+        private final Set<String>                     requested;\r
+\r
+        // Only internally used data\r
+        private final ConcurrentMap<Resource, String> r2s;\r
+        private final ConcurrentMap<String, Set<Resource>> s2r;\r
+\r
+        public Cache(Resource root, Set<String> reserved, ConcurrentMap<Resource, String> r2s, ConcurrentMap<String, Set<Resource>> s2r, boolean caseInsensitive) {\r
+            assert root != null;\r
+            assert reserved != null;\r
+            assert r2s != null;\r
+            assert s2r != null;\r
+            this.root = root;\r
+            this.reserved = reserved;\r
+            this.r2s = r2s;\r
+            this.s2r = s2r;\r
+            this.requested = new ConcurrentSkipListSet<String>(getComparator(caseInsensitive));\r
+        }\r
+\r
+        @Override\r
+        protected void finalize() throws Throwable {\r
+            if (DEBUG_CACHE_UPDATES)\r
+                debug("FINALIZE");\r
+            super.finalize();\r
+        }\r
+\r
+        public Resource getRoot() {\r
+            return root;\r
+        }\r
+\r
+        public Set<String> getReserved() {\r
+            // This prevents the map from being retrieved during a cache update.\r
+            synchronized (this) {\r
+                return reserved;\r
+            }\r
+        }\r
+\r
+        public Set<String> getRequested() {\r
+            // This prevents the map from being retrieved during a cache update.\r
+            synchronized (this) {\r
+                return requested;\r
+            }\r
+        }\r
+\r
+        public void addRequested(String name) {\r
+            requested.add(name);\r
+        }\r
+\r
+        public void replaceEntries(Collection<Pair<Resource, String>> entries) {\r
+            if (entries.isEmpty())\r
+                return;\r
+\r
+            if (DEBUG_CACHE_UPDATES)\r
+                debug(" updating " + entries.size() +" cache entries");\r
+\r
+            synchronized (this) {\r
+                for (Pair<Resource, String> entry : entries) {\r
+                    Resource component = entry.first;\r
+                    String newName = entry.second;\r
+\r
+                    assert component != null;\r
+                    assert newName != null;\r
+\r
+                    String oldName = r2s.get(component);\r
+                    if (oldName == null) {\r
+                        // This must be an uncached new component.\r
+\r
+                        // Validate cache.\r
+                        Set<Resource> existingEntries = getMapSet(s2r, newName);\r
+                        if (!existingEntries.isEmpty()) {\r
+                            System.out.println("WARNING: Somebody is screwing the model up with duplicate name: " + newName);\r
+                            // TODO: generate issue or message\r
+                        }\r
+\r
+                        Object prev = r2s.putIfAbsent(component, newName);\r
+                        assert prev == null;\r
+                        addToMapSet(s2r, newName, component);\r
+\r
+                        reserved.add(newName);\r
+                        requested.remove(newName);\r
+\r
+                        if (DEBUG_CACHE_UPDATES)\r
+                            debug("\tnew component name: " + newName);\r
+                    } else {\r
+                        // This must be a change to an existing cached component.\r
+\r
+                        // Validate cache\r
+                        Set<Resource> existingEntries = getMapSet(s2r, newName);\r
+                        if (!existingEntries.isEmpty()) {\r
+                            // Currently changesets can contain multiple entries for a same change.\r
+                            // This picks out one of such cases where the value of a resource has been\r
+                            // set multiple times to the same value.\r
+                            if (existingEntries.contains(component))\r
+                                continue;\r
+\r
+                            System.out.println("WARNING: Somebody is screwing the model up with duplicate name: " + newName);\r
+                            // TODO: generate issue or message\r
+                        }\r
+\r
+                        Set<Resource> resourcesWithOldName = removeFromMapSet(s2r, oldName, component);\r
+                        addToMapSet(s2r, newName, component);\r
+                        boolean updated = r2s.replace(component, oldName, newName);\r
+                        assert updated;\r
+                        if (resourcesWithOldName.isEmpty()) {\r
+                            reserved.remove(oldName);\r
+                        }\r
+                        reserved.add(newName);\r
+                        requested.remove(newName);\r
+\r
+                        if (DEBUG_CACHE_UPDATES)\r
+                            debug("\tcomponent name changed: " + oldName + " -> " + newName);\r
+                    }\r
+                }\r
+\r
+                if (DEBUG_CACHE_UPDATES) {\r
+                    debug("reserved names after update: " + reserved);\r
+                    debug("requested names after update: " + requested);\r
+                }\r
+            }\r
+        }\r
+\r
+        private void debug(String string) {\r
+            CaseInsensitiveComponentNamingStrategy2.debug(this, string);\r
+        }\r
+    }\r
+\r
+    static class CacheFactory {\r
+        final ReadGraph           graph;\r
+        final Resource            root;\r
+        final Layer0            b;\r
+        final StructuralResource2 sr;\r
+        final boolean caseInsensitive;\r
+\r
+        final Set<String>         reserved;\r
+        final ConcurrentMap<Resource, String> r2s = new ConcurrentSkipListMap<Resource, String>();\r
+        final ConcurrentMap<String, Set<Resource>> s2r;\r
+\r
+        CacheFactory(ReadGraph graph, Resource root, boolean caseInsensitive) {\r
+            this.graph = graph;\r
+            this.root = root;\r
+            this.b = Layer0.getInstance(graph);\r
+            this.sr = StructuralResource2.getInstance(graph);\r
+            this.caseInsensitive = caseInsensitive;\r
+\r
+            this.reserved = new ConcurrentSkipListSet<String>(getComparator(caseInsensitive));\r
+            this.s2r = new ConcurrentSkipListMap<String, Set<Resource>>(getComparator(caseInsensitive));\r
+        }\r
+\r
+        private void debug(String string) {\r
+            CaseInsensitiveComponentNamingStrategy2.debug(this, string);\r
+        }\r
+\r
+        public Cache create() throws DatabaseException {\r
+            if (DEBUG_CACHE_INITIALIZATION_BROWSE)\r
+                debug("browsing all components from root " + root);\r
+\r
+            graph.syncRequest(new AsyncReadRequest() {\r
+                @Override\r
+                public void run(AsyncReadGraph graph) {\r
+                    browseComposite(graph, root);\r
+                }\r
+            });\r
+\r
+            if (DEBUG_CACHE_INITIALIZATION_BROWSE)\r
+                debug("browsing completed, results:\n\treserved: " + reserved + "\n\tr2s: " + r2s + "\n\ts2r: " + s2r);\r
+\r
+            return new Cache(root, reserved, r2s, s2r, caseInsensitive);\r
+        }\r
+\r
+        static abstract class MultiProc<T> implements AsyncMultiProcedure<T> {\r
+            @Override\r
+            public void finished(AsyncReadGraph graph) {\r
+            }\r
+            @Override\r
+            public void exception(AsyncReadGraph graph, Throwable t) {\r
+                ErrorLogger.defaultLogError(t);\r
+            }\r
+        }\r
+\r
+        static abstract class AsyncProc<T> implements AsyncProcedure<T> {\r
+            @Override\r
+            public void exception(AsyncReadGraph graph, Throwable t) {\r
+                ErrorLogger.defaultLogError(t);\r
+            }\r
+        }\r
+\r
+        private void browseComposite(AsyncReadGraph graph, Resource composite) {\r
+            if (DEBUG_CACHE_INITIALIZATION_BROWSE)\r
+                debug("browsing composite " + composite);\r
+            graph.forEachObject(composite, b.ConsistsOf, new MultiProc<Resource>() {\r
+                @Override\r
+                public void execute(AsyncReadGraph graph, Resource component) {\r
+                    browseComponent(graph, component);\r
+                }\r
+\r
+                private void browseComponent(AsyncReadGraph graph, final Resource component) {\r
+                    if (DEBUG_CACHE_INITIALIZATION_BROWSE)\r
+                        debug("browsing component " + component);\r
+                    reserveName(graph, component);\r
+                    graph.forIsInstanceOf(component, sr.Composite, new AsyncProc<Boolean>() {\r
+                        @Override\r
+                        public void execute(AsyncReadGraph graph, Boolean result) {\r
+                            if (result)\r
+                                browseComposite(graph, component);\r
+                        }\r
+                    });\r
+                }\r
+\r
+                private void reserveName(AsyncReadGraph graph, final Resource component) {\r
+                    graph.forPossibleRelatedValue(component, b.HasName, new AsyncProc<String>() {\r
+                        @Override\r
+                        public void execute(AsyncReadGraph graph, String componentName) {\r
+                            if (componentName != null) {\r
+                                if (DEBUG_CACHE_INITIALIZATION_BROWSE)\r
+                                    debug("reserving name of component " + component + " '" + componentName + "'");\r
+                                Set<Resource> components = addToMapSet(s2r, componentName, component);\r
+                                if (components.size() > 1) {\r
+                                    // Found duplicate names in the model !!\r
+                                    // TODO: generate issue!\r
+                                    System.err.println("WARNING: found multiple components with same name '" + componentName + "': " + components);\r
+                                    System.err.println("TODO: generate issue");\r
+                                } else {\r
+                                    String prevName = r2s.putIfAbsent(component, componentName);\r
+                                    if (prevName == null)\r
+                                        reserved.add(componentName);\r
+                                }\r
+                            }\r
+                        }\r
+                    });\r
+                }\r
+            });\r
+        }\r
+    }\r
+\r
+    private SoftReference<THashMap<Resource, SoftReference<Cache>>> mapRef =\r
+        new SoftReference<THashMap<Resource, SoftReference<Cache>>>(new THashMap<Resource, SoftReference<Cache>>());\r
+\r
+    private final GraphChangeListenerSupport                        changeSupport;\r
+    private Resource                                                inverseOfHasName;\r
+\r
+    public CaseInsensitiveComponentNamingStrategy2() {\r
+        this(Simantics.getSession().getService(GraphChangeListenerSupport.class), "%s_%d");\r
+    }\r
+    \r
+    public CaseInsensitiveComponentNamingStrategy2(GraphChangeListenerSupport changeSupport) {\r
+        this(changeSupport, "%s %d");\r
+    }\r
+\r
+    /**\r
+     * @param changeSupport\r
+     * @param generatedNameFormat the format to use for generated names, see\r
+     *        {@link Formatter}\r
+     */\r
+    public CaseInsensitiveComponentNamingStrategy2(GraphChangeListenerSupport changeSupport, String generatedNameFormat) {\r
+        super(generatedNameFormat);\r
+        this.changeSupport = changeSupport;\r
+        changeSupport.addListener(this);\r
+    }\r
+\r
+    public void dispose() {\r
+        changeSupport.removeListener(this);\r
+    }\r
+\r
+    static class CacheUpdateBundle {\r
+        IdentityHashMap<Cache, Collection<Pair<Resource, String>>> updates = new IdentityHashMap<Cache, Collection<Pair<Resource, String>>>();\r
+\r
+        public boolean isEmpty() {\r
+            return updates.isEmpty();\r
+        }\r
+\r
+        public void add(Cache cache, Resource component, String newName) {\r
+            assert cache != null;\r
+            assert component != null;\r
+            assert newName != null;\r
+\r
+            Collection<Pair<Resource, String>> collection = updates.get(cache);\r
+            if (collection == null) {\r
+                collection = new ArrayList<Pair<Resource, String>>();\r
+                updates.put(cache, collection);\r
+            }\r
+\r
+            collection.add(Pair.make(component, newName));\r
+        }\r
+\r
+        public void commitAll() {\r
+            for (Map.Entry<Cache, Collection<Pair<Resource, String>>> entry : updates.entrySet()) {\r
+                Cache cache = entry.getKey();\r
+                cache.replaceEntries(entry.getValue());\r
+            }\r
+        }\r
+\r
+        @Override\r
+        public String toString() {\r
+            return getClass().getSimpleName() + " [" + updates.size() + " changed caches]";\r
+        }\r
+    }\r
+\r
+    @Override\r
+    public void graphChanged(ChangeEvent e) throws DatabaseException {\r
+        Collection<Resource> changedValues = e.getChanges().changedValues();\r
+        if (DEBUG_GRAPH_UPDATES)\r
+            debug("graph updated with " + changedValues.size() + " value changes");\r
+\r
+        ReadGraph graph = e.getGraph();\r
+       Layer0 b = Layer0.getInstance(graph);\r
+\r
+        // Cache inverse of Has Name relation.\r
+        if (inverseOfHasName == null) {\r
+            inverseOfHasName = graph.getInverse(b.HasName);\r
+        }\r
+\r
+        CacheUpdateBundle bundle = new CacheUpdateBundle();\r
+\r
+        for (Resource value : changedValues) {\r
+            //System.out.println("VALUE CHANGE: " + GraphUtils.getReadableName(graph, value));\r
+            for (Resource nameOfComponent : graph.getObjects(value, inverseOfHasName)) {\r
+                if (DEBUG_GRAPH_UPDATES)\r
+                    debug("\tNAME CHANGE: " + NameUtils.getSafeName(graph, value));\r
+                Resource root = ComponentUtils.tryGetComponentConfigurationRoot(graph, nameOfComponent);\r
+                Cache cache = peekCache(graph, root);\r
+                if (cache != null) {\r
+                    String newName = graph.getPossibleValue(value, Bindings.STRING);\r
+                    if (newName != null) {\r
+                        if (DEBUG_GRAPH_UPDATES)\r
+                            debug("\t\tqueued cache update");\r
+                        bundle.add(cache, nameOfComponent, newName);\r
+                    }\r
+                }\r
+            }\r
+        }\r
+\r
+        if (!bundle.isEmpty()) {\r
+            if (DEBUG_GRAPH_UPDATES)\r
+                debug("committing " + bundle);\r
+            bundle.commitAll();\r
+        }\r
+    }\r
+\r
+    private Cache getCache(ReadGraph graph, Resource configurationRoot) throws DatabaseException {\r
+        Cache cache = null;\r
+        THashMap<Resource, SoftReference<Cache>> map = mapRef.get();\r
+\r
+        if (map != null) {\r
+            SoftReference<Cache> cacheRef = map.get(configurationRoot);\r
+            if (cacheRef != null) {\r
+                cache = cacheRef.get();\r
+                if (cache != null)\r
+                    // Cache hit!\r
+                    return cache;\r
+            }\r
+        } else {\r
+            // Cache miss, rebuild cache index\r
+            map = new THashMap<Resource, SoftReference<Cache>>();\r
+            mapRef = new SoftReference<THashMap<Resource,SoftReference<Cache>>>(map);\r
+        }\r
+\r
+        // Cache miss, rebuild local cache\r
+        if (DEBUG_CACHE_INITIALIZATION)\r
+            debug("Constructing new cache for root " + NameUtils.getSafeName(graph, configurationRoot) + " (" + configurationRoot + ")");\r
+        cache = new CacheFactory(graph, configurationRoot, caseInsensitive).create();\r
+        if (DEBUG_CACHE_INITIALIZATION)\r
+            debug("\tInitialized with reservations: " + cache.getReserved());\r
+        map.put(configurationRoot, new SoftReference<Cache>(cache));\r
+        return cache;\r
+    }\r
+\r
+    private Cache peekCache(ReadGraph graph, Resource configurationRoot) {\r
+        THashMap<Resource, SoftReference<Cache>> map = mapRef.get();\r
+        if (map == null)\r
+            return null;\r
+        SoftReference<Cache> cacheRef = map.get(configurationRoot);\r
+        if (cacheRef == null)\r
+            return null;\r
+        Cache cache = cacheRef.get();\r
+        if (cache == null)\r
+            return null;\r
+        // Cache hit!\r
+        return cache;\r
+    }\r
+\r
+    @Override\r
+    public String validateInstanceName(ReadGraph graph,\r
+               Resource configurationRoot, Resource component, String proposition, boolean acceptProposition)\r
+               throws NamingException, DatabaseException {\r
+\r
+       Layer0 L0 = Layer0.getInstance(graph);\r
+        StructuralResource2 STR = StructuralResource2.getInstance(graph);\r
+        Resource container = graph.getSingleObject(component, L0.PartOf);\r
+        Resource componentType = graph.getSingleType(component, STR.Component);\r
+        return validateInstanceName(graph, configurationRoot, container, componentType, proposition, acceptProposition);\r
+       \r
+    }\r
+    \r
+    @Override\r
+    public String validateInstanceName(ReadGraph graph, Resource configurationRoot, Resource container,\r
+            Resource componentType, String proposition, boolean acceptProposition) throws NamingException, DatabaseException {\r
+        Cache cache = getCache(graph, configurationRoot);\r
+        synchronized (cache) {\r
+            String result = findFreshName(cache.getReserved(), cache.getRequested(), proposition, acceptProposition);\r
+            cache.addRequested(result);\r
+            return result;\r
+        }\r
+    }\r
+\r
+    private void debug(String string) {\r
+        debug(this, string);\r
+    }\r
+\r
+    private static void debug(Object obj, String string) {\r
+        System.out.println("[" + obj.getClass().getSimpleName() + "(" + System.identityHashCode(obj) + ")] " + string);\r
+    }\r
+\r
+    private static <K,V> Set<V> addToMapSet(ConcurrentMap<K, Set<V>> map, K key, V value) {\r
+        Set<V> set = map.get(key);\r
+        if (set == null) {\r
+            set = new HashSet<V>(1);\r
+            map.putIfAbsent(key, set);\r
+        }\r
+        set.add(value);\r
+        return set;\r
+    }\r
+\r
+    private static <K,V> Set<V> getMapSet(ConcurrentMap<K, Set<V>> map, K key) {\r
+        Set<V> set = map.get(key);\r
+        if (set == null)\r
+            return Collections.emptySet();\r
+        return set;\r
+    }\r
+\r
+    private static <K,V> Set<V> removeFromMapSet(ConcurrentMap<K, Set<V>> map, K key, V value) {\r
+        Set<V> set = map.get(key);\r
+        if (set == null)\r
+            return Collections.emptySet();\r
+        if (set.remove(value)) {\r
+            if (set.isEmpty()) {\r
+                map.remove(key);\r
+                return Collections.emptySet();\r
+            }\r
+        }\r
+        return set;\r
+    }\r
+    \r
+    public static CaseInsensitiveComponentNamingStrategy2 install(IProject project) {\r
+       \r
+       Session session = project.getSession();\r
+       \r
+        GraphChangeListenerSupport changeSupport = session.peekService(GraphChangeListenerSupport.class);\r
+        if (changeSupport != null) {\r
+               CaseInsensitiveComponentNamingStrategy2 namingStrategy = new CaseInsensitiveComponentNamingStrategy2(changeSupport, "%s%02d");\r
+               project.setHint(ComponentNamingStrategy.PROJECT_KEY, namingStrategy);\r
+               return namingStrategy;\r
+        } else {\r
+            System.out.println("WARNING: No GraphChangeListenerSupport in session " + session +", can't initialize all services.");\r
+        }\r
+        \r
+        return null;\r
+       \r
+    }\r
+\r
+}\r