1 /*******************************************************************************
\r
2 * Copyright (c) 2007, 2010 Association for Decentralized Information Management
\r
3 * in Industry THTH ry.
\r
4 * All rights reserved. This program and the accompanying materials
\r
5 * are made available under the terms of the Eclipse Public License v1.0
\r
6 * which accompanies this distribution, and is available at
\r
7 * http://www.eclipse.org/legal/epl-v10.html
\r
10 * VTT Technical Research Centre of Finland - initial API and implementation
\r
11 *******************************************************************************/
\r
12 package org.simantics.modeling.services;
\r
14 import gnu.trove.map.hash.THashMap;
\r
16 import java.lang.ref.SoftReference;
\r
17 import java.util.ArrayList;
\r
18 import java.util.Collection;
\r
19 import java.util.Collections;
\r
20 import java.util.Formatter;
\r
21 import java.util.HashSet;
\r
22 import java.util.IdentityHashMap;
\r
23 import java.util.Map;
\r
24 import java.util.Set;
\r
25 import java.util.concurrent.ConcurrentMap;
\r
26 import java.util.concurrent.ConcurrentSkipListMap;
\r
27 import java.util.concurrent.ConcurrentSkipListSet;
\r
29 import org.simantics.Simantics;
\r
30 import org.simantics.databoard.Bindings;
\r
31 import org.simantics.db.AsyncReadGraph;
\r
32 import org.simantics.db.ReadGraph;
\r
33 import org.simantics.db.Resource;
\r
34 import org.simantics.db.Session;
\r
35 import org.simantics.db.common.request.AsyncReadRequest;
\r
36 import org.simantics.db.common.utils.NameUtils;
\r
37 import org.simantics.db.event.ChangeEvent;
\r
38 import org.simantics.db.event.ChangeListener;
\r
39 import org.simantics.db.exception.DatabaseException;
\r
40 import org.simantics.db.procedure.AsyncMultiProcedure;
\r
41 import org.simantics.db.procedure.AsyncProcedure;
\r
42 import org.simantics.db.service.GraphChangeListenerSupport;
\r
43 import org.simantics.layer0.Layer0;
\r
44 import org.simantics.modeling.ComponentUtils;
\r
45 import org.simantics.project.IProject;
\r
46 import org.simantics.structural.stubs.StructuralResource2;
\r
47 import org.simantics.utils.datastructures.Pair;
\r
48 import org.simantics.utils.ui.ErrorLogger;
\r
51 * A first-hand component naming strategy implementation for structural models.
\r
53 * This version is somewhat optimized for case-insensitivity by using custom
\r
54 * comparators in maps and sets. It uses a soft-referenced cache of
\r
55 * used/requested names per a single root of a configuration's component
\r
58 * @author Tuukka Lehtonen
\r
60 * @see ComponentNamingStrategy
\r
62 public class CaseInsensitiveComponentNamingStrategy2 extends ComponentNamingStrategyBase implements ChangeListener {
\r
64 private static final boolean DEBUG_ALL = false;
\r
65 private static final boolean DEBUG_GRAPH_UPDATES = false | DEBUG_ALL;
\r
66 private static final boolean DEBUG_CACHE_INITIALIZATION = false | DEBUG_ALL;
\r
67 private static final boolean DEBUG_CACHE_INITIALIZATION_BROWSE = false | DEBUG_ALL;
\r
68 private static final boolean DEBUG_CACHE_UPDATES = false | DEBUG_ALL;
\r
70 static class Cache {
\r
71 private final Resource root;
\r
72 private final Set<String> reserved;
\r
74 // Having cache as soft references should somewhat address the problem
\r
75 // of the amount of requested names growing too large.
\r
76 private final Set<String> requested;
\r
78 // Only internally used data
\r
79 private final ConcurrentMap<Resource, String> r2s;
\r
80 private final ConcurrentMap<String, Set<Resource>> s2r;
\r
82 public Cache(Resource root, Set<String> reserved, ConcurrentMap<Resource, String> r2s, ConcurrentMap<String, Set<Resource>> s2r, boolean caseInsensitive) {
\r
83 assert root != null;
\r
84 assert reserved != null;
\r
88 this.reserved = reserved;
\r
91 this.requested = new ConcurrentSkipListSet<String>(getComparator(caseInsensitive));
\r
95 protected void finalize() throws Throwable {
\r
96 if (DEBUG_CACHE_UPDATES)
\r
101 public Resource getRoot() {
\r
105 public Set<String> getReserved() {
\r
106 // This prevents the map from being retrieved during a cache update.
\r
107 synchronized (this) {
\r
112 public Set<String> getRequested() {
\r
113 // This prevents the map from being retrieved during a cache update.
\r
114 synchronized (this) {
\r
119 public void addRequested(String name) {
\r
120 requested.add(name);
\r
123 public void replaceEntries(Collection<Pair<Resource, String>> entries) {
\r
124 if (entries.isEmpty())
\r
127 if (DEBUG_CACHE_UPDATES)
\r
128 debug(" updating " + entries.size() +" cache entries");
\r
130 synchronized (this) {
\r
131 for (Pair<Resource, String> entry : entries) {
\r
132 Resource component = entry.first;
\r
133 String newName = entry.second;
\r
135 assert component != null;
\r
136 assert newName != null;
\r
138 String oldName = r2s.get(component);
\r
139 if (oldName == null) {
\r
140 // This must be an uncached new component.
\r
143 Set<Resource> existingEntries = getMapSet(s2r, newName);
\r
144 if (!existingEntries.isEmpty()) {
\r
145 System.out.println("WARNING: Somebody is screwing the model up with duplicate name: " + newName);
\r
146 // TODO: generate issue or message
\r
149 Object prev = r2s.putIfAbsent(component, newName);
\r
150 assert prev == null;
\r
151 addToMapSet(s2r, newName, component);
\r
153 reserved.add(newName);
\r
154 requested.remove(newName);
\r
156 if (DEBUG_CACHE_UPDATES)
\r
157 debug("\tnew component name: " + newName);
\r
159 // This must be a change to an existing cached component.
\r
162 Set<Resource> existingEntries = getMapSet(s2r, newName);
\r
163 if (!existingEntries.isEmpty()) {
\r
164 // Currently changesets can contain multiple entries for a same change.
\r
165 // This picks out one of such cases where the value of a resource has been
\r
166 // set multiple times to the same value.
\r
167 if (existingEntries.contains(component))
\r
170 System.out.println("WARNING: Somebody is screwing the model up with duplicate name: " + newName);
\r
171 // TODO: generate issue or message
\r
174 Set<Resource> resourcesWithOldName = removeFromMapSet(s2r, oldName, component);
\r
175 addToMapSet(s2r, newName, component);
\r
176 boolean updated = r2s.replace(component, oldName, newName);
\r
178 if (resourcesWithOldName.isEmpty()) {
\r
179 reserved.remove(oldName);
\r
181 reserved.add(newName);
\r
182 requested.remove(newName);
\r
184 if (DEBUG_CACHE_UPDATES)
\r
185 debug("\tcomponent name changed: " + oldName + " -> " + newName);
\r
189 if (DEBUG_CACHE_UPDATES) {
\r
190 debug("reserved names after update: " + reserved);
\r
191 debug("requested names after update: " + requested);
\r
196 private void debug(String string) {
\r
197 CaseInsensitiveComponentNamingStrategy2.debug(this, string);
\r
201 static class CacheFactory {
\r
202 final ReadGraph graph;
\r
203 final Resource root;
\r
205 final StructuralResource2 sr;
\r
206 final boolean caseInsensitive;
\r
208 final Set<String> reserved;
\r
209 final ConcurrentMap<Resource, String> r2s = new ConcurrentSkipListMap<Resource, String>();
\r
210 final ConcurrentMap<String, Set<Resource>> s2r;
\r
212 CacheFactory(ReadGraph graph, Resource root, boolean caseInsensitive) {
\r
213 this.graph = graph;
\r
215 this.b = Layer0.getInstance(graph);
\r
216 this.sr = StructuralResource2.getInstance(graph);
\r
217 this.caseInsensitive = caseInsensitive;
\r
219 this.reserved = new ConcurrentSkipListSet<String>(getComparator(caseInsensitive));
\r
220 this.s2r = new ConcurrentSkipListMap<String, Set<Resource>>(getComparator(caseInsensitive));
\r
223 private void debug(String string) {
\r
224 CaseInsensitiveComponentNamingStrategy2.debug(this, string);
\r
227 public Cache create() throws DatabaseException {
\r
228 if (DEBUG_CACHE_INITIALIZATION_BROWSE)
\r
229 debug("browsing all components from root " + root);
\r
231 graph.syncRequest(new AsyncReadRequest() {
\r
233 public void run(AsyncReadGraph graph) {
\r
234 browseComposite(graph, root);
\r
238 if (DEBUG_CACHE_INITIALIZATION_BROWSE)
\r
239 debug("browsing completed, results:\n\treserved: " + reserved + "\n\tr2s: " + r2s + "\n\ts2r: " + s2r);
\r
241 return new Cache(root, reserved, r2s, s2r, caseInsensitive);
\r
244 static abstract class MultiProc<T> implements AsyncMultiProcedure<T> {
\r
246 public void finished(AsyncReadGraph graph) {
\r
249 public void exception(AsyncReadGraph graph, Throwable t) {
\r
250 ErrorLogger.defaultLogError(t);
\r
254 static abstract class AsyncProc<T> implements AsyncProcedure<T> {
\r
256 public void exception(AsyncReadGraph graph, Throwable t) {
\r
257 ErrorLogger.defaultLogError(t);
\r
261 private void browseComposite(AsyncReadGraph graph, Resource composite) {
\r
262 if (DEBUG_CACHE_INITIALIZATION_BROWSE)
\r
263 debug("browsing composite " + composite);
\r
264 graph.forEachObject(composite, b.ConsistsOf, new MultiProc<Resource>() {
\r
266 public void execute(AsyncReadGraph graph, Resource component) {
\r
267 browseComponent(graph, component);
\r
270 private void browseComponent(AsyncReadGraph graph, final Resource component) {
\r
271 if (DEBUG_CACHE_INITIALIZATION_BROWSE)
\r
272 debug("browsing component " + component);
\r
273 reserveName(graph, component);
\r
274 graph.forIsInstanceOf(component, sr.Composite, new AsyncProc<Boolean>() {
\r
276 public void execute(AsyncReadGraph graph, Boolean result) {
\r
278 browseComposite(graph, component);
\r
283 private void reserveName(AsyncReadGraph graph, final Resource component) {
\r
284 graph.forPossibleRelatedValue(component, b.HasName, new AsyncProc<String>() {
\r
286 public void execute(AsyncReadGraph graph, String componentName) {
\r
287 if (componentName != null) {
\r
288 if (DEBUG_CACHE_INITIALIZATION_BROWSE)
\r
289 debug("reserving name of component " + component + " '" + componentName + "'");
\r
290 Set<Resource> components = addToMapSet(s2r, componentName, component);
\r
291 if (components.size() > 1) {
\r
292 // Found duplicate names in the model !!
\r
293 // TODO: generate issue!
\r
294 System.err.println("WARNING: found multiple components with same name '" + componentName + "': " + components);
\r
295 System.err.println("TODO: generate issue");
\r
297 String prevName = r2s.putIfAbsent(component, componentName);
\r
298 if (prevName == null)
\r
299 reserved.add(componentName);
\r
309 private SoftReference<THashMap<Resource, SoftReference<Cache>>> mapRef =
\r
310 new SoftReference<THashMap<Resource, SoftReference<Cache>>>(new THashMap<Resource, SoftReference<Cache>>());
\r
312 private final GraphChangeListenerSupport changeSupport;
\r
313 private Resource inverseOfHasName;
\r
315 public CaseInsensitiveComponentNamingStrategy2() {
\r
316 this(Simantics.getSession().getService(GraphChangeListenerSupport.class), "%s_%d");
\r
319 public CaseInsensitiveComponentNamingStrategy2(GraphChangeListenerSupport changeSupport) {
\r
320 this(changeSupport, "%s %d");
\r
324 * @param changeSupport
\r
325 * @param generatedNameFormat the format to use for generated names, see
\r
326 * {@link Formatter}
\r
328 public CaseInsensitiveComponentNamingStrategy2(GraphChangeListenerSupport changeSupport, String generatedNameFormat) {
\r
329 super(generatedNameFormat);
\r
330 this.changeSupport = changeSupport;
\r
331 changeSupport.addListener(this);
\r
334 public void dispose() {
\r
335 changeSupport.removeListener(this);
\r
338 static class CacheUpdateBundle {
\r
339 IdentityHashMap<Cache, Collection<Pair<Resource, String>>> updates = new IdentityHashMap<Cache, Collection<Pair<Resource, String>>>();
\r
341 public boolean isEmpty() {
\r
342 return updates.isEmpty();
\r
345 public void add(Cache cache, Resource component, String newName) {
\r
346 assert cache != null;
\r
347 assert component != null;
\r
348 assert newName != null;
\r
350 Collection<Pair<Resource, String>> collection = updates.get(cache);
\r
351 if (collection == null) {
\r
352 collection = new ArrayList<Pair<Resource, String>>();
\r
353 updates.put(cache, collection);
\r
356 collection.add(Pair.make(component, newName));
\r
359 public void commitAll() {
\r
360 for (Map.Entry<Cache, Collection<Pair<Resource, String>>> entry : updates.entrySet()) {
\r
361 Cache cache = entry.getKey();
\r
362 cache.replaceEntries(entry.getValue());
\r
367 public String toString() {
\r
368 return getClass().getSimpleName() + " [" + updates.size() + " changed caches]";
\r
373 public void graphChanged(ChangeEvent e) throws DatabaseException {
\r
374 Collection<Resource> changedValues = e.getChanges().changedValues();
\r
375 if (DEBUG_GRAPH_UPDATES)
\r
376 debug("graph updated with " + changedValues.size() + " value changes");
\r
378 ReadGraph graph = e.getGraph();
\r
379 Layer0 b = Layer0.getInstance(graph);
\r
381 // Cache inverse of Has Name relation.
\r
382 if (inverseOfHasName == null) {
\r
383 inverseOfHasName = graph.getInverse(b.HasName);
\r
386 CacheUpdateBundle bundle = new CacheUpdateBundle();
\r
388 for (Resource value : changedValues) {
\r
389 //System.out.println("VALUE CHANGE: " + GraphUtils.getReadableName(graph, value));
\r
390 for (Resource nameOfComponent : graph.getObjects(value, inverseOfHasName)) {
\r
391 if (DEBUG_GRAPH_UPDATES)
\r
392 debug("\tNAME CHANGE: " + NameUtils.getSafeName(graph, value));
\r
393 Resource root = ComponentUtils.tryGetComponentConfigurationRoot(graph, nameOfComponent);
\r
394 Cache cache = peekCache(graph, root);
\r
395 if (cache != null) {
\r
396 String newName = graph.getPossibleValue(value, Bindings.STRING);
\r
397 if (newName != null) {
\r
398 if (DEBUG_GRAPH_UPDATES)
\r
399 debug("\t\tqueued cache update");
\r
400 bundle.add(cache, nameOfComponent, newName);
\r
406 if (!bundle.isEmpty()) {
\r
407 if (DEBUG_GRAPH_UPDATES)
\r
408 debug("committing " + bundle);
\r
409 bundle.commitAll();
\r
413 private Cache getCache(ReadGraph graph, Resource configurationRoot) throws DatabaseException {
\r
414 Cache cache = null;
\r
415 THashMap<Resource, SoftReference<Cache>> map = mapRef.get();
\r
418 SoftReference<Cache> cacheRef = map.get(configurationRoot);
\r
419 if (cacheRef != null) {
\r
420 cache = cacheRef.get();
\r
426 // Cache miss, rebuild cache index
\r
427 map = new THashMap<Resource, SoftReference<Cache>>();
\r
428 mapRef = new SoftReference<THashMap<Resource,SoftReference<Cache>>>(map);
\r
431 // Cache miss, rebuild local cache
\r
432 if (DEBUG_CACHE_INITIALIZATION)
\r
433 debug("Constructing new cache for root " + NameUtils.getSafeName(graph, configurationRoot) + " (" + configurationRoot + ")");
\r
434 cache = new CacheFactory(graph, configurationRoot, caseInsensitive).create();
\r
435 if (DEBUG_CACHE_INITIALIZATION)
\r
436 debug("\tInitialized with reservations: " + cache.getReserved());
\r
437 map.put(configurationRoot, new SoftReference<Cache>(cache));
\r
441 private Cache peekCache(ReadGraph graph, Resource configurationRoot) {
\r
442 THashMap<Resource, SoftReference<Cache>> map = mapRef.get();
\r
445 SoftReference<Cache> cacheRef = map.get(configurationRoot);
\r
446 if (cacheRef == null)
\r
448 Cache cache = cacheRef.get();
\r
456 public String validateInstanceName(ReadGraph graph,
\r
457 Resource configurationRoot, Resource component, String proposition, boolean acceptProposition)
\r
458 throws NamingException, DatabaseException {
\r
460 Layer0 L0 = Layer0.getInstance(graph);
\r
461 StructuralResource2 STR = StructuralResource2.getInstance(graph);
\r
462 Resource container = graph.getSingleObject(component, L0.PartOf);
\r
463 Resource componentType = graph.getSingleType(component, STR.Component);
\r
464 return validateInstanceName(graph, configurationRoot, container, componentType, proposition, acceptProposition);
\r
469 public String validateInstanceName(ReadGraph graph, Resource configurationRoot, Resource container,
\r
470 Resource componentType, String proposition, boolean acceptProposition) throws NamingException, DatabaseException {
\r
471 Cache cache = getCache(graph, configurationRoot);
\r
472 synchronized (cache) {
\r
473 String result = findFreshName(cache.getReserved(), cache.getRequested(), proposition, acceptProposition);
\r
474 cache.addRequested(result);
\r
479 private void debug(String string) {
\r
480 debug(this, string);
\r
483 private static void debug(Object obj, String string) {
\r
484 System.out.println("[" + obj.getClass().getSimpleName() + "(" + System.identityHashCode(obj) + ")] " + string);
\r
487 private static <K,V> Set<V> addToMapSet(ConcurrentMap<K, Set<V>> map, K key, V value) {
\r
488 Set<V> set = map.get(key);
\r
490 set = new HashSet<V>(1);
\r
491 map.putIfAbsent(key, set);
\r
497 private static <K,V> Set<V> getMapSet(ConcurrentMap<K, Set<V>> map, K key) {
\r
498 Set<V> set = map.get(key);
\r
500 return Collections.emptySet();
\r
504 private static <K,V> Set<V> removeFromMapSet(ConcurrentMap<K, Set<V>> map, K key, V value) {
\r
505 Set<V> set = map.get(key);
\r
507 return Collections.emptySet();
\r
508 if (set.remove(value)) {
\r
509 if (set.isEmpty()) {
\r
511 return Collections.emptySet();
\r
517 public static CaseInsensitiveComponentNamingStrategy2 install(IProject project) {
\r
519 Session session = project.getSession();
\r
521 GraphChangeListenerSupport changeSupport = session.peekService(GraphChangeListenerSupport.class);
\r
522 if (changeSupport != null) {
\r
523 CaseInsensitiveComponentNamingStrategy2 namingStrategy = new CaseInsensitiveComponentNamingStrategy2(changeSupport, "%s%02d");
\r
524 project.setHint(ComponentNamingStrategy.PROJECT_KEY, namingStrategy);
\r
525 return namingStrategy;
\r
527 System.out.println("WARNING: No GraphChangeListenerSupport in session " + session +", can't initialize all services.");
\r