/******************************************************************************* * 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.databoard.Bindings; import org.simantics.db.AsyncReadGraph; import org.simantics.db.ReadGraph; import org.simantics.db.Resource; 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.structural.stubs.StructuralResource2; import org.simantics.utils.datastructures.Pair; import org.simantics.utils.ui.ErrorLogger; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * 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 CaseInsensitiveComponentNamingStrategy extends ComponentNamingStrategyBase implements ChangeListener { private static final Logger LOGGER = LoggerFactory.getLogger(CaseInsensitiveComponentNamingStrategy.class); 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()) { LOGGER.warn("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; LOGGER.warn("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) { CaseInsensitiveComponentNamingStrategy.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) { CaseInsensitiveComponentNamingStrategy.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, Resource component) { if (DEBUG_CACHE_INITIALIZATION_BROWSE) debug("browsing component " + component); reserveName(graph, component); graph.forPossibleType(component, sr.Component, new AsyncProc() { @Override public void execute(AsyncReadGraph graph, Resource componentType) { if (componentType != null) browseComponentType(graph, componentType); } }); } private void browseComponentType(AsyncReadGraph graph, Resource componentType) { if (DEBUG_CACHE_INITIALIZATION_BROWSE) debug("browsing user component " + componentType); graph.forPossibleObject(componentType, sr.IsDefinedBy, new AsyncProc() { @Override public void execute(AsyncReadGraph graph, Resource composite) { if (composite != null) browseComposite(graph, composite); } }); } 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! LOGGER.warn("WARNING: found multiple components with same name '" + componentName + "': " + components); LOGGER.warn("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 CaseInsensitiveComponentNamingStrategy(GraphChangeListenerSupport changeSupport) { this(changeSupport, "%s %d"); } /** * @param changeSupport * @param generatedNameFormat the format to use for generated names, see * {@link Formatter} */ public CaseInsensitiveComponentNamingStrategy(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) { LOGGER.info("[" + 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; } }