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