1 /*******************************************************************************
2 * Copyright (c) 2007, 2010 Association for Decentralized Information Management
4 * All rights reserved. This program and the accompanying materials
5 * are made available under the terms of the Eclipse Public License v1.0
6 * which accompanies this distribution, and is available at
7 * http://www.eclipse.org/legal/epl-v10.html
10 * VTT Technical Research Centre of Finland - initial API and implementation
11 *******************************************************************************/
12 package org.simantics.modeling.services;
14 import gnu.trove.map.hash.THashMap;
16 import java.lang.ref.SoftReference;
17 import java.util.ArrayList;
18 import java.util.Collection;
19 import java.util.Collections;
20 import java.util.Formatter;
21 import java.util.HashSet;
22 import java.util.IdentityHashMap;
25 import java.util.concurrent.ConcurrentMap;
26 import java.util.concurrent.ConcurrentSkipListMap;
27 import java.util.concurrent.ConcurrentSkipListSet;
29 import org.simantics.Simantics;
30 import org.simantics.databoard.Bindings;
31 import org.simantics.db.AsyncReadGraph;
32 import org.simantics.db.ReadGraph;
33 import org.simantics.db.Resource;
34 import org.simantics.db.Session;
35 import org.simantics.db.common.request.AsyncReadRequest;
36 import org.simantics.db.common.utils.NameUtils;
37 import org.simantics.db.event.ChangeEvent;
38 import org.simantics.db.event.ChangeListener;
39 import org.simantics.db.exception.DatabaseException;
40 import org.simantics.db.procedure.AsyncMultiProcedure;
41 import org.simantics.db.procedure.AsyncProcedure;
42 import org.simantics.db.service.GraphChangeListenerSupport;
43 import org.simantics.layer0.Layer0;
44 import org.simantics.modeling.ComponentUtils;
45 import org.simantics.project.IProject;
46 import org.simantics.structural.stubs.StructuralResource2;
47 import org.simantics.utils.datastructures.Pair;
48 import org.simantics.utils.ui.ErrorLogger;
49 import org.slf4j.Logger;
50 import org.slf4j.LoggerFactory;
53 * A first-hand component naming strategy implementation for structural models.
55 * This version is somewhat optimized for case-insensitivity by using custom
56 * comparators in maps and sets. It uses a soft-referenced cache of
57 * used/requested names per a single root of a configuration's component
60 * @author Tuukka Lehtonen
62 * @see ComponentNamingStrategy
64 public class CaseInsensitiveComponentNamingStrategy2 extends ComponentNamingStrategyBase implements ChangeListener {
66 private static final Logger LOGGER = LoggerFactory.getLogger(CaseInsensitiveComponentNamingStrategy2.class);
67 private static final boolean DEBUG_ALL = false;
68 private static final boolean DEBUG_GRAPH_UPDATES = false | DEBUG_ALL;
69 private static final boolean DEBUG_CACHE_INITIALIZATION = false | DEBUG_ALL;
70 private static final boolean DEBUG_CACHE_INITIALIZATION_BROWSE = false | DEBUG_ALL;
71 private static final boolean DEBUG_CACHE_UPDATES = false | DEBUG_ALL;
74 private final Resource root;
75 private final Set<String> reserved;
77 // Having cache as soft references should somewhat address the problem
78 // of the amount of requested names growing too large.
79 private final Set<String> requested;
81 // Only internally used data
82 private final ConcurrentMap<Resource, String> r2s;
83 private final ConcurrentMap<String, Set<Resource>> s2r;
85 public Cache(Resource root, Set<String> reserved, ConcurrentMap<Resource, String> r2s, ConcurrentMap<String, Set<Resource>> s2r, boolean caseInsensitive) {
87 assert reserved != null;
91 this.reserved = reserved;
94 this.requested = new ConcurrentSkipListSet<String>(getComparator(caseInsensitive));
98 protected void finalize() throws Throwable {
99 if (DEBUG_CACHE_UPDATES)
104 public Resource getRoot() {
108 public Set<String> getReserved() {
109 // This prevents the map from being retrieved during a cache update.
110 synchronized (this) {
115 public Set<String> getRequested() {
116 // This prevents the map from being retrieved during a cache update.
117 synchronized (this) {
122 public void addRequested(String name) {
126 public void replaceEntries(Collection<Pair<Resource, String>> entries) {
127 if (entries.isEmpty())
130 if (DEBUG_CACHE_UPDATES)
131 debug(" updating " + entries.size() +" cache entries");
133 synchronized (this) {
134 for (Pair<Resource, String> entry : entries) {
135 Resource component = entry.first;
136 String newName = entry.second;
138 assert component != null;
139 assert newName != null;
141 String oldName = r2s.get(component);
142 if (oldName == null) {
143 // This must be an uncached new component.
146 Set<Resource> existingEntries = getMapSet(s2r, newName);
147 if (!existingEntries.isEmpty()) {
148 System.out.println("WARNING: Somebody is screwing the model up with duplicate name: " + newName);
149 // TODO: generate issue or message
152 Object prev = r2s.putIfAbsent(component, newName);
154 addToMapSet(s2r, newName, component);
156 reserved.add(newName);
157 requested.remove(newName);
159 if (DEBUG_CACHE_UPDATES)
160 debug("\tnew component name: " + newName);
162 // This must be a change to an existing cached component.
165 Set<Resource> existingEntries = getMapSet(s2r, newName);
166 if (!existingEntries.isEmpty()) {
167 // Currently changesets can contain multiple entries for a same change.
168 // This picks out one of such cases where the value of a resource has been
169 // set multiple times to the same value.
170 if (existingEntries.contains(component))
173 System.out.println("WARNING: Somebody is screwing the model up with duplicate name: " + newName);
174 // TODO: generate issue or message
177 Set<Resource> resourcesWithOldName = removeFromMapSet(s2r, oldName, component);
178 addToMapSet(s2r, newName, component);
179 boolean updated = r2s.replace(component, oldName, newName);
181 if (resourcesWithOldName.isEmpty()) {
182 reserved.remove(oldName);
184 reserved.add(newName);
185 requested.remove(newName);
187 if (DEBUG_CACHE_UPDATES)
188 debug("\tcomponent name changed: " + oldName + " -> " + newName);
192 if (DEBUG_CACHE_UPDATES) {
193 debug("reserved names after update: " + reserved);
194 debug("requested names after update: " + requested);
199 private void debug(String string) {
200 CaseInsensitiveComponentNamingStrategy2.debug(this, string);
204 static class CacheFactory {
205 final ReadGraph graph;
208 final StructuralResource2 sr;
209 final boolean caseInsensitive;
211 final Set<String> reserved;
212 final ConcurrentMap<Resource, String> r2s = new ConcurrentSkipListMap<Resource, String>();
213 final ConcurrentMap<String, Set<Resource>> s2r;
215 CacheFactory(ReadGraph graph, Resource root, boolean caseInsensitive) {
218 this.b = Layer0.getInstance(graph);
219 this.sr = StructuralResource2.getInstance(graph);
220 this.caseInsensitive = caseInsensitive;
222 this.reserved = new ConcurrentSkipListSet<String>(getComparator(caseInsensitive));
223 this.s2r = new ConcurrentSkipListMap<String, Set<Resource>>(getComparator(caseInsensitive));
226 private void debug(String string) {
227 CaseInsensitiveComponentNamingStrategy2.debug(this, string);
230 public Cache create() throws DatabaseException {
231 if (DEBUG_CACHE_INITIALIZATION_BROWSE)
232 debug("browsing all components from root " + root);
234 graph.syncRequest(new AsyncReadRequest() {
236 public void run(AsyncReadGraph graph) {
237 browseComposite(graph, root);
241 if (DEBUG_CACHE_INITIALIZATION_BROWSE)
242 debug("browsing completed, results:\n\treserved: " + reserved + "\n\tr2s: " + r2s + "\n\ts2r: " + s2r);
244 return new Cache(root, reserved, r2s, s2r, caseInsensitive);
247 static abstract class MultiProc<T> implements AsyncMultiProcedure<T> {
249 public void finished(AsyncReadGraph graph) {
252 public void exception(AsyncReadGraph graph, Throwable t) {
253 ErrorLogger.defaultLogError(t);
257 static abstract class AsyncProc<T> implements AsyncProcedure<T> {
259 public void exception(AsyncReadGraph graph, Throwable t) {
260 ErrorLogger.defaultLogError(t);
264 private void browseComposite(AsyncReadGraph graph, Resource composite) {
265 if (DEBUG_CACHE_INITIALIZATION_BROWSE)
266 debug("browsing composite " + composite);
267 graph.forEachObject(composite, b.ConsistsOf, new MultiProc<Resource>() {
269 public void execute(AsyncReadGraph graph, Resource component) {
270 browseComponent(graph, component);
273 private void browseComponent(AsyncReadGraph graph, final Resource component) {
274 if (DEBUG_CACHE_INITIALIZATION_BROWSE)
275 debug("browsing component " + component);
276 reserveName(graph, component);
277 graph.forIsInstanceOf(component, sr.Composite, new AsyncProc<Boolean>() {
279 public void execute(AsyncReadGraph graph, Boolean result) {
281 browseComposite(graph, component);
286 private void reserveName(AsyncReadGraph graph, final Resource component) {
287 graph.forPossibleRelatedValue(component, b.HasName, new AsyncProc<String>() {
289 public void execute(AsyncReadGraph graph, String componentName) {
290 if (componentName != null) {
291 if (DEBUG_CACHE_INITIALIZATION_BROWSE)
292 debug("reserving name of component " + component + " '" + componentName + "'");
293 Set<Resource> components = addToMapSet(s2r, componentName, component);
294 if (components.size() > 1) {
295 // Found duplicate names in the model !!
296 // TODO: generate issue!
297 LOGGER.warn("WARNING: found multiple components with same name '" + componentName + "': " + components);
298 LOGGER.warn("TODO: generate issue");
300 String prevName = r2s.putIfAbsent(component, componentName);
301 if (prevName == null)
302 reserved.add(componentName);
312 private SoftReference<THashMap<Resource, SoftReference<Cache>>> mapRef =
313 new SoftReference<THashMap<Resource, SoftReference<Cache>>>(new THashMap<Resource, SoftReference<Cache>>());
315 private final GraphChangeListenerSupport changeSupport;
316 private Resource inverseOfHasName;
318 public CaseInsensitiveComponentNamingStrategy2() {
319 this(Simantics.getSession().getService(GraphChangeListenerSupport.class), "%s_%d");
322 public CaseInsensitiveComponentNamingStrategy2(GraphChangeListenerSupport changeSupport) {
323 this(changeSupport, "%s %d");
327 * @param changeSupport
328 * @param generatedNameFormat the format to use for generated names, see
331 public CaseInsensitiveComponentNamingStrategy2(GraphChangeListenerSupport changeSupport, String generatedNameFormat) {
332 super(generatedNameFormat);
333 this.changeSupport = changeSupport;
334 changeSupport.addListener(this);
337 public void dispose() {
338 changeSupport.removeListener(this);
341 static class CacheUpdateBundle {
342 IdentityHashMap<Cache, Collection<Pair<Resource, String>>> updates = new IdentityHashMap<Cache, Collection<Pair<Resource, String>>>();
344 public boolean isEmpty() {
345 return updates.isEmpty();
348 public void add(Cache cache, Resource component, String newName) {
349 assert cache != null;
350 assert component != null;
351 assert newName != null;
353 Collection<Pair<Resource, String>> collection = updates.get(cache);
354 if (collection == null) {
355 collection = new ArrayList<Pair<Resource, String>>();
356 updates.put(cache, collection);
359 collection.add(Pair.make(component, newName));
362 public void commitAll() {
363 for (Map.Entry<Cache, Collection<Pair<Resource, String>>> entry : updates.entrySet()) {
364 Cache cache = entry.getKey();
365 cache.replaceEntries(entry.getValue());
370 public String toString() {
371 return getClass().getSimpleName() + " [" + updates.size() + " changed caches]";
376 public void graphChanged(ChangeEvent e) throws DatabaseException {
377 Collection<Resource> changedValues = e.getChanges().changedValues();
378 if (DEBUG_GRAPH_UPDATES)
379 debug("graph updated with " + changedValues.size() + " value changes");
381 ReadGraph graph = e.getGraph();
382 Layer0 b = Layer0.getInstance(graph);
384 // Cache inverse of Has Name relation.
385 if (inverseOfHasName == null) {
386 inverseOfHasName = graph.getInverse(b.HasName);
389 CacheUpdateBundle bundle = new CacheUpdateBundle();
391 for (Resource value : changedValues) {
392 //System.out.println("VALUE CHANGE: " + GraphUtils.getReadableName(graph, value));
393 for (Resource nameOfComponent : graph.getObjects(value, inverseOfHasName)) {
394 if (DEBUG_GRAPH_UPDATES)
395 debug("\tNAME CHANGE: " + NameUtils.getSafeName(graph, value));
396 Resource root = ComponentUtils.tryGetComponentConfigurationRoot(graph, nameOfComponent);
397 Cache cache = peekCache(graph, root);
399 String newName = graph.getPossibleValue(value, Bindings.STRING);
400 if (newName != null) {
401 if (DEBUG_GRAPH_UPDATES)
402 debug("\t\tqueued cache update");
403 bundle.add(cache, nameOfComponent, newName);
409 if (!bundle.isEmpty()) {
410 if (DEBUG_GRAPH_UPDATES)
411 debug("committing " + bundle);
416 private Cache getCache(ReadGraph graph, Resource configurationRoot) throws DatabaseException {
418 THashMap<Resource, SoftReference<Cache>> map = mapRef.get();
421 SoftReference<Cache> cacheRef = map.get(configurationRoot);
422 if (cacheRef != null) {
423 cache = cacheRef.get();
429 // Cache miss, rebuild cache index
430 map = new THashMap<Resource, SoftReference<Cache>>();
431 mapRef = new SoftReference<THashMap<Resource,SoftReference<Cache>>>(map);
434 // Cache miss, rebuild local cache
435 if (DEBUG_CACHE_INITIALIZATION)
436 debug("Constructing new cache for root " + NameUtils.getSafeName(graph, configurationRoot) + " (" + configurationRoot + ")");
437 cache = new CacheFactory(graph, configurationRoot, caseInsensitive).create();
438 if (DEBUG_CACHE_INITIALIZATION)
439 debug("\tInitialized with reservations: " + cache.getReserved());
440 map.put(configurationRoot, new SoftReference<Cache>(cache));
444 private Cache peekCache(ReadGraph graph, Resource configurationRoot) {
445 THashMap<Resource, SoftReference<Cache>> map = mapRef.get();
448 SoftReference<Cache> cacheRef = map.get(configurationRoot);
449 if (cacheRef == null)
451 Cache cache = cacheRef.get();
459 public String validateInstanceName(ReadGraph graph,
460 Resource configurationRoot, Resource component, String proposition, boolean acceptProposition)
461 throws NamingException, DatabaseException {
463 Layer0 L0 = Layer0.getInstance(graph);
464 StructuralResource2 STR = StructuralResource2.getInstance(graph);
465 Resource container = graph.getSingleObject(component, L0.PartOf);
466 Resource componentType = graph.getSingleType(component, STR.Component);
467 return validateInstanceName(graph, configurationRoot, container, componentType, proposition, acceptProposition);
472 public String validateInstanceName(ReadGraph graph, Resource configurationRoot, Resource container,
473 Resource componentType, String proposition, boolean acceptProposition) throws NamingException, DatabaseException {
474 Cache cache = getCache(graph, configurationRoot);
475 synchronized (cache) {
476 String result = findFreshName(cache.getReserved(), cache.getRequested(), proposition, acceptProposition);
477 cache.addRequested(result);
482 private void debug(String string) {
486 private static void debug(Object obj, String string) {
487 System.out.println("[" + obj.getClass().getSimpleName() + "(" + System.identityHashCode(obj) + ")] " + string);
490 private static <K,V> Set<V> addToMapSet(ConcurrentMap<K, Set<V>> map, K key, V value) {
491 Set<V> set = map.get(key);
493 set = new HashSet<V>(1);
494 map.putIfAbsent(key, set);
500 private static <K,V> Set<V> getMapSet(ConcurrentMap<K, Set<V>> map, K key) {
501 Set<V> set = map.get(key);
503 return Collections.emptySet();
507 private static <K,V> Set<V> removeFromMapSet(ConcurrentMap<K, Set<V>> map, K key, V value) {
508 Set<V> set = map.get(key);
510 return Collections.emptySet();
511 if (set.remove(value)) {
514 return Collections.emptySet();
520 public static CaseInsensitiveComponentNamingStrategy2 install(IProject project) {
522 Session session = project.getSession();
524 GraphChangeListenerSupport changeSupport = session.peekService(GraphChangeListenerSupport.class);
525 if (changeSupport != null) {
526 CaseInsensitiveComponentNamingStrategy2 namingStrategy = new CaseInsensitiveComponentNamingStrategy2(changeSupport, "%s%02d");
527 project.setHint(ComponentNamingStrategy.PROJECT_KEY, namingStrategy);
528 return namingStrategy;
530 System.out.println("WARNING: No GraphChangeListenerSupport in session " + session +", can't initialize all services.");