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