]> gerrit.simantics Code Review - simantics/platform.git/blob - bundles/org.simantics.modeling/src/org/simantics/modeling/services/CaseInsensitiveComponentNamingStrategy.java
ff1c17bd36c72da78f14561218d0d858a772a00d
[simantics/platform.git] / bundles / org.simantics.modeling / src / org / simantics / modeling / services / CaseInsensitiveComponentNamingStrategy.java
1 /*******************************************************************************
2  * Copyright (c) 2007, 2010 Association for Decentralized Information Management
3  * in Industry THTH ry.
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
8  *
9  * Contributors:
10  *     VTT Technical Research Centre of Finland - initial API and implementation
11  *******************************************************************************/
12 package org.simantics.modeling.services;
13
14 import gnu.trove.map.hash.THashMap;
15
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;
23 import java.util.Map;
24 import java.util.Set;
25 import java.util.concurrent.ConcurrentMap;
26 import java.util.concurrent.ConcurrentSkipListMap;
27 import java.util.concurrent.ConcurrentSkipListSet;
28
29 import org.simantics.databoard.Bindings;
30 import org.simantics.db.AsyncReadGraph;
31 import org.simantics.db.ReadGraph;
32 import org.simantics.db.Resource;
33 import org.simantics.db.common.request.AsyncReadRequest;
34 import org.simantics.db.common.utils.NameUtils;
35 import org.simantics.db.event.ChangeEvent;
36 import org.simantics.db.event.ChangeListener;
37 import org.simantics.db.exception.DatabaseException;
38 import org.simantics.db.procedure.AsyncMultiProcedure;
39 import org.simantics.db.procedure.AsyncProcedure;
40 import org.simantics.db.service.GraphChangeListenerSupport;
41 import org.simantics.layer0.Layer0;
42 import org.simantics.modeling.ComponentUtils;
43 import org.simantics.structural.stubs.StructuralResource2;
44 import org.simantics.utils.datastructures.Pair;
45 import org.simantics.utils.ui.ErrorLogger;
46
47 /**
48  * A first-hand component naming strategy implementation for structural models.
49  * 
50  * This version is somewhat optimized for case-insensitivity by using custom
51  * comparators in maps and sets. It uses a soft-referenced cache of
52  * used/requested names per a single root of a configuration's component
53  * hierarchy.
54  * 
55  * @author Tuukka Lehtonen
56  * 
57  * @see ComponentNamingStrategy
58  */
59 public class CaseInsensitiveComponentNamingStrategy extends ComponentNamingStrategyBase implements ChangeListener {
60
61     private static final boolean            DEBUG_ALL                                     = false;
62     private static final boolean            DEBUG_GRAPH_UPDATES                           = false | DEBUG_ALL;
63     private static final boolean            DEBUG_CACHE_INITIALIZATION                    = false | DEBUG_ALL;
64     private static final boolean            DEBUG_CACHE_INITIALIZATION_BROWSE             = false | DEBUG_ALL;
65     private static final boolean            DEBUG_CACHE_UPDATES                           = false | DEBUG_ALL;
66
67     static class Cache {
68         private final Resource                        root;
69         private final Set<String>                     reserved;
70
71         // Having cache as soft references should somewhat address the problem
72         // of the amount of requested names growing too large.
73         private final Set<String>                     requested;
74
75         // Only internally used data
76         private final ConcurrentMap<Resource, String> r2s;
77         private final ConcurrentMap<String, Set<Resource>> s2r;
78
79         public Cache(Resource root, Set<String> reserved, ConcurrentMap<Resource, String> r2s, ConcurrentMap<String, Set<Resource>> s2r, boolean caseInsensitive) {
80             assert root != null;
81             assert reserved != null;
82             assert r2s != null;
83             assert s2r != null;
84             this.root = root;
85             this.reserved = reserved;
86             this.r2s = r2s;
87             this.s2r = s2r;
88             this.requested = new ConcurrentSkipListSet<String>(getComparator(caseInsensitive));
89         }
90
91         @Override
92         protected void finalize() throws Throwable {
93             if (DEBUG_CACHE_UPDATES)
94                 debug("FINALIZE");
95             super.finalize();
96         }
97
98         public Resource getRoot() {
99             return root;
100         }
101
102         public Set<String> getReserved() {
103             // This prevents the map from being retrieved during a cache update.
104             synchronized (this) {
105                 return reserved;
106             }
107         }
108
109         public Set<String> getRequested() {
110             // This prevents the map from being retrieved during a cache update.
111             synchronized (this) {
112                 return requested;
113             }
114         }
115
116         public void addRequested(String name) {
117             requested.add(name);
118         }
119
120         public void replaceEntries(Collection<Pair<Resource, String>> entries) {
121             if (entries.isEmpty())
122                 return;
123
124             if (DEBUG_CACHE_UPDATES)
125                 debug(" updating " + entries.size() +" cache entries");
126
127             synchronized (this) {
128                 for (Pair<Resource, String> entry : entries) {
129                     Resource component = entry.first;
130                     String newName = entry.second;
131
132                     assert component != null;
133                     assert newName != null;
134
135                     String oldName = r2s.get(component);
136                     if (oldName == null) {
137                         // This must be an uncached new component.
138
139                         // Validate cache.
140                         Set<Resource> existingEntries = getMapSet(s2r, newName);
141                         if (!existingEntries.isEmpty()) {
142                             System.out.println("WARNING: Somebody is screwing the model up with duplicate name: " + newName);
143                             // TODO: generate issue or message
144                         }
145
146                         Object prev = r2s.putIfAbsent(component, newName);
147                         assert prev == null;
148                         addToMapSet(s2r, newName, component);
149
150                         reserved.add(newName);
151                         requested.remove(newName);
152
153                         if (DEBUG_CACHE_UPDATES)
154                             debug("\tnew component name: " + newName);
155                     } else {
156                         // This must be a change to an existing cached component.
157
158                         // Validate cache
159                         Set<Resource> existingEntries = getMapSet(s2r, newName);
160                         if (!existingEntries.isEmpty()) {
161                             // Currently changesets can contain multiple entries for a same change.
162                             // This picks out one of such cases where the value of a resource has been
163                             // set multiple times to the same value.
164                             if (existingEntries.contains(component))
165                                 continue;
166
167                             System.out.println("WARNING: Somebody is screwing the model up with duplicate name: " + newName);
168                             // TODO: generate issue or message
169                         }
170
171                         Set<Resource> resourcesWithOldName = removeFromMapSet(s2r, oldName, component);
172                         addToMapSet(s2r, newName, component);
173                         boolean updated = r2s.replace(component, oldName, newName);
174                         assert updated;
175                         if (resourcesWithOldName.isEmpty()) {
176                             reserved.remove(oldName);
177                         }
178                         reserved.add(newName);
179                         requested.remove(newName);
180
181                         if (DEBUG_CACHE_UPDATES)
182                             debug("\tcomponent name changed: " + oldName + " -> " + newName);
183                     }
184                 }
185
186                 if (DEBUG_CACHE_UPDATES) {
187                     debug("reserved names after update: " + reserved);
188                     debug("requested names after update: " + requested);
189                 }
190             }
191         }
192
193         private void debug(String string) {
194             CaseInsensitiveComponentNamingStrategy.debug(this, string);
195         }
196     }
197
198     static class CacheFactory {
199         final ReadGraph           graph;
200         final Resource            root;
201         final Layer0            b;
202         final StructuralResource2 sr;
203         final boolean caseInsensitive;
204
205         final Set<String>         reserved;
206         final ConcurrentMap<Resource, String> r2s = new ConcurrentSkipListMap<Resource, String>();
207         final ConcurrentMap<String, Set<Resource>> s2r;
208
209         CacheFactory(ReadGraph graph, Resource root, boolean caseInsensitive) {
210             this.graph = graph;
211             this.root = root;
212                 this.b = Layer0.getInstance(graph);
213             this.sr = StructuralResource2.getInstance(graph);
214             this.caseInsensitive = caseInsensitive;
215
216             this.reserved = new ConcurrentSkipListSet<String>(getComparator(caseInsensitive));
217             this.s2r = new ConcurrentSkipListMap<String, Set<Resource>>(getComparator(caseInsensitive));
218         }
219
220         private void debug(String string) {
221             CaseInsensitiveComponentNamingStrategy.debug(this, string);
222         }
223
224         public Cache create() throws DatabaseException {
225             if (DEBUG_CACHE_INITIALIZATION_BROWSE)
226                 debug("browsing all components from root " + root);
227
228             graph.syncRequest(new AsyncReadRequest() {
229                 @Override
230                 public void run(AsyncReadGraph graph) {
231                     browseComposite(graph, root);
232                 }
233             });
234
235             if (DEBUG_CACHE_INITIALIZATION_BROWSE)
236                 debug("browsing completed, results:\n\treserved: " + reserved + "\n\tr2s: " + r2s + "\n\ts2r: " + s2r);
237
238             return new Cache(root, reserved, r2s, s2r, caseInsensitive);
239         }
240
241         static abstract class MultiProc<T> implements AsyncMultiProcedure<T> {
242             @Override
243             public void finished(AsyncReadGraph graph) {
244             }
245             @Override
246             public void exception(AsyncReadGraph graph, Throwable t) {
247                 ErrorLogger.defaultLogError(t);
248             }
249         }
250
251         static abstract class AsyncProc<T> implements AsyncProcedure<T> {
252             @Override
253             public void exception(AsyncReadGraph graph, Throwable t) {
254                 ErrorLogger.defaultLogError(t);
255             }
256         }
257
258         private void browseComposite(AsyncReadGraph graph, Resource composite) {
259             if (DEBUG_CACHE_INITIALIZATION_BROWSE)
260                 debug("browsing composite " + composite);
261             graph.forEachObject(composite, b.ConsistsOf, new MultiProc<Resource>() {
262                 @Override
263                 public void execute(AsyncReadGraph graph, Resource component) {
264                     browseComponent(graph, component);
265                 }
266
267                 private void browseComponent(AsyncReadGraph graph, Resource component) {
268                     if (DEBUG_CACHE_INITIALIZATION_BROWSE)
269                         debug("browsing component " + component);
270                     reserveName(graph, component);
271                     graph.forPossibleType(component, sr.Component, new AsyncProc<Resource>() {
272                         @Override
273                         public void execute(AsyncReadGraph graph, Resource componentType) {
274                             if (componentType != null)
275                                 browseComponentType(graph, componentType);
276                         }
277                     });
278                 }
279
280                 private void browseComponentType(AsyncReadGraph graph, Resource componentType) {
281                     if (DEBUG_CACHE_INITIALIZATION_BROWSE)
282                         debug("browsing user component " + componentType);
283                     graph.forPossibleObject(componentType, sr.IsDefinedBy, new AsyncProc<Resource>() {
284                         @Override
285                         public void execute(AsyncReadGraph graph, Resource composite) {
286                             if (composite != null)
287                                 browseComposite(graph, composite);
288                         }
289                     });
290                 }
291
292                 private void reserveName(AsyncReadGraph graph, final Resource component) {
293                     graph.forPossibleRelatedValue(component, b.HasName, new AsyncProc<String>() {
294                         @Override
295                         public void execute(AsyncReadGraph graph, String componentName) {
296                             if (componentName != null) {
297                                 if (DEBUG_CACHE_INITIALIZATION_BROWSE)
298                                     debug("reserving name of component " + component + " '" + componentName + "'");
299                                 Set<Resource> components = addToMapSet(s2r, componentName, component);
300                                 if (components.size() > 1) {
301                                     // Found duplicate names in the model !!
302                                     // TODO: generate issue!
303                                     System.err.println("WARNING: found multiple components with same name '" + componentName + "': " + components);
304                                     System.err.println("TODO: generate issue");
305                                 } else {
306                                     String prevName = r2s.putIfAbsent(component, componentName);
307                                     if (prevName == null)
308                                         reserved.add(componentName);
309                                 }
310                             }
311                         }
312                     });
313                 }
314             });
315         }
316     }
317
318     private SoftReference<THashMap<Resource, SoftReference<Cache>>> mapRef =
319         new SoftReference<THashMap<Resource, SoftReference<Cache>>>(new THashMap<Resource, SoftReference<Cache>>());
320
321     private final GraphChangeListenerSupport                        changeSupport;
322     private Resource                                                inverseOfHasName;
323
324     public CaseInsensitiveComponentNamingStrategy(GraphChangeListenerSupport changeSupport) {
325         this(changeSupport, "%s %d");
326     }
327
328     /**
329      * @param changeSupport
330      * @param generatedNameFormat the format to use for generated names, see
331      *        {@link Formatter}
332      */
333     public CaseInsensitiveComponentNamingStrategy(GraphChangeListenerSupport changeSupport, String generatedNameFormat) {
334         super(generatedNameFormat);
335         this.changeSupport = changeSupport;
336         changeSupport.addListener(this);
337     }
338
339     public void dispose() {
340         changeSupport.removeListener(this);
341     }
342
343     static class CacheUpdateBundle {
344         IdentityHashMap<Cache, Collection<Pair<Resource, String>>> updates = new IdentityHashMap<Cache, Collection<Pair<Resource, String>>>();
345
346         public boolean isEmpty() {
347             return updates.isEmpty();
348         }
349
350         public void add(Cache cache, Resource component, String newName) {
351             assert cache != null;
352             assert component != null;
353             assert newName != null;
354
355             Collection<Pair<Resource, String>> collection = updates.get(cache);
356             if (collection == null) {
357                 collection = new ArrayList<Pair<Resource, String>>();
358                 updates.put(cache, collection);
359             }
360
361             collection.add(Pair.make(component, newName));
362         }
363
364         public void commitAll() {
365             for (Map.Entry<Cache, Collection<Pair<Resource, String>>> entry : updates.entrySet()) {
366                 Cache cache = entry.getKey();
367                 cache.replaceEntries(entry.getValue());
368             }
369         }
370
371         @Override
372         public String toString() {
373             return getClass().getSimpleName() + " [" + updates.size() + " changed caches]";
374         }
375     }
376
377     @Override
378     public void graphChanged(ChangeEvent e) throws DatabaseException {
379         Collection<Resource> changedValues = e.getChanges().changedValues();
380         if (DEBUG_GRAPH_UPDATES)
381             debug("graph updated with " + changedValues.size() + " value changes");
382
383         ReadGraph graph = e.getGraph();
384         Layer0 b = Layer0.getInstance(graph);
385
386         // Cache inverse of Has Name relation.
387         if (inverseOfHasName == null) {
388             inverseOfHasName = graph.getInverse(b.HasName);
389         }
390
391         CacheUpdateBundle bundle = new CacheUpdateBundle();
392
393         for (Resource value : changedValues) {
394             //System.out.println("VALUE CHANGE: " + GraphUtils.getReadableName(graph, value));
395             for (Resource nameOfComponent : graph.getObjects(value, inverseOfHasName)) {
396                 if (DEBUG_GRAPH_UPDATES)
397                     debug("\tNAME CHANGE: " + NameUtils.getSafeName(graph, value));
398                 Resource root = ComponentUtils.tryGetComponentConfigurationRoot(graph, nameOfComponent);
399                 Cache cache = peekCache(graph, root);
400                 if (cache != null) {
401                     String newName = graph.getPossibleValue(value, Bindings.STRING);
402                     if (newName != null) {
403                         if (DEBUG_GRAPH_UPDATES)
404                             debug("\t\tqueued cache update");
405                         bundle.add(cache, nameOfComponent, newName);
406                     }
407                 }
408             }
409         }
410
411         if (!bundle.isEmpty()) {
412             if (DEBUG_GRAPH_UPDATES)
413                 debug("committing " + bundle);
414             bundle.commitAll();
415         }
416     }
417
418     private Cache getCache(ReadGraph graph, Resource configurationRoot) throws DatabaseException {
419         Cache cache = null;
420         THashMap<Resource, SoftReference<Cache>> map = mapRef.get();
421
422         if (map != null) {
423             SoftReference<Cache> cacheRef = map.get(configurationRoot);
424             if (cacheRef != null) {
425                 cache = cacheRef.get();
426                 if (cache != null)
427                     // Cache hit!
428                     return cache;
429             }
430         } else {
431             // Cache miss, rebuild cache index
432             map = new THashMap<Resource, SoftReference<Cache>>();
433             mapRef = new SoftReference<THashMap<Resource,SoftReference<Cache>>>(map);
434         }
435
436         // Cache miss, rebuild local cache
437         if (DEBUG_CACHE_INITIALIZATION)
438             debug("Constructing new cache for root " + NameUtils.getSafeName(graph, configurationRoot) + " (" + configurationRoot + ")");
439         cache = new CacheFactory(graph, configurationRoot, caseInsensitive).create();
440         if (DEBUG_CACHE_INITIALIZATION)
441             debug("\tInitialized with reservations: " + cache.getReserved());
442         map.put(configurationRoot, new SoftReference<Cache>(cache));
443         return cache;
444     }
445
446     private Cache peekCache(ReadGraph graph, Resource configurationRoot) {
447         THashMap<Resource, SoftReference<Cache>> map = mapRef.get();
448         if (map == null)
449             return null;
450         SoftReference<Cache> cacheRef = map.get(configurationRoot);
451         if (cacheRef == null)
452             return null;
453         Cache cache = cacheRef.get();
454         if (cache == null)
455             return null;
456         // Cache hit!
457         return cache;
458     }
459
460     @Override
461     public String validateInstanceName(ReadGraph graph,
462                 Resource configurationRoot, Resource component, String proposition, boolean acceptProposition)
463                 throws NamingException, DatabaseException {
464
465         Layer0 L0 = Layer0.getInstance(graph);
466         StructuralResource2 STR = StructuralResource2.getInstance(graph);
467         Resource container = graph.getSingleObject(component, L0.PartOf);
468         Resource componentType = graph.getSingleType(component, STR.Component);
469         return validateInstanceName(graph, configurationRoot, container, componentType, proposition, acceptProposition);
470         
471     }
472     
473     @Override
474     public String validateInstanceName(ReadGraph graph, Resource configurationRoot, Resource container,
475             Resource componentType, String proposition, boolean acceptProposition) throws NamingException, DatabaseException {
476         Cache cache = getCache(graph, configurationRoot);
477         synchronized (cache) {
478             String result = findFreshName(cache.getReserved(), cache.getRequested(), proposition, acceptProposition);
479             cache.addRequested(result);
480             return result;
481         }
482     }
483
484     private void debug(String string) {
485         debug(this, string);
486     }
487
488     private static void debug(Object obj, String string) {
489         System.out.println("[" + obj.getClass().getSimpleName() + "(" + System.identityHashCode(obj) + ")] " + string);
490     }
491
492     private static <K,V> Set<V> addToMapSet(ConcurrentMap<K, Set<V>> map, K key, V value) {
493         Set<V> set = map.get(key);
494         if (set == null) {
495             set = new HashSet<V>(1);
496             map.putIfAbsent(key, set);
497         }
498         set.add(value);
499         return set;
500     }
501
502     private static <K,V> Set<V> getMapSet(ConcurrentMap<K, Set<V>> map, K key) {
503         Set<V> set = map.get(key);
504         if (set == null)
505             return Collections.emptySet();
506         return set;
507     }
508
509     private static <K,V> Set<V> removeFromMapSet(ConcurrentMap<K, Set<V>> map, K key, V value) {
510         Set<V> set = map.get(key);
511         if (set == null)
512             return Collections.emptySet();
513         if (set.remove(value)) {
514             if (set.isEmpty()) {
515                 map.remove(key);
516                 return Collections.emptySet();
517             }
518         }
519         return set;
520     }
521
522 }