]> gerrit.simantics Code Review - simantics/platform.git/blob - bundles/org.simantics.modeling/src/org/simantics/modeling/services/CaseInsensitiveComponentNamingStrategy2.java
Migrated source code from Simantics SVN
[simantics/platform.git] / bundles / org.simantics.modeling / src / org / simantics / modeling / services / CaseInsensitiveComponentNamingStrategy2.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.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
49 \r
50 /**\r
51  * A first-hand component naming strategy implementation for structural models.\r
52  * \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
56  * hierarchy.\r
57  * \r
58  * @author Tuukka Lehtonen\r
59  * \r
60  * @see ComponentNamingStrategy\r
61  */\r
62 public class CaseInsensitiveComponentNamingStrategy2 extends ComponentNamingStrategyBase implements ChangeListener {\r
63 \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
69 \r
70     static class Cache {\r
71         private final Resource                        root;\r
72         private final Set<String>                     reserved;\r
73 \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
77 \r
78         // Only internally used data\r
79         private final ConcurrentMap<Resource, String> r2s;\r
80         private final ConcurrentMap<String, Set<Resource>> s2r;\r
81 \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
85             assert r2s != null;\r
86             assert s2r != null;\r
87             this.root = root;\r
88             this.reserved = reserved;\r
89             this.r2s = r2s;\r
90             this.s2r = s2r;\r
91             this.requested = new ConcurrentSkipListSet<String>(getComparator(caseInsensitive));\r
92         }\r
93 \r
94         @Override\r
95         protected void finalize() throws Throwable {\r
96             if (DEBUG_CACHE_UPDATES)\r
97                 debug("FINALIZE");\r
98             super.finalize();\r
99         }\r
100 \r
101         public Resource getRoot() {\r
102             return root;\r
103         }\r
104 \r
105         public Set<String> getReserved() {\r
106             // This prevents the map from being retrieved during a cache update.\r
107             synchronized (this) {\r
108                 return reserved;\r
109             }\r
110         }\r
111 \r
112         public Set<String> getRequested() {\r
113             // This prevents the map from being retrieved during a cache update.\r
114             synchronized (this) {\r
115                 return requested;\r
116             }\r
117         }\r
118 \r
119         public void addRequested(String name) {\r
120             requested.add(name);\r
121         }\r
122 \r
123         public void replaceEntries(Collection<Pair<Resource, String>> entries) {\r
124             if (entries.isEmpty())\r
125                 return;\r
126 \r
127             if (DEBUG_CACHE_UPDATES)\r
128                 debug(" updating " + entries.size() +" cache entries");\r
129 \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
134 \r
135                     assert component != null;\r
136                     assert newName != null;\r
137 \r
138                     String oldName = r2s.get(component);\r
139                     if (oldName == null) {\r
140                         // This must be an uncached new component.\r
141 \r
142                         // Validate cache.\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
147                         }\r
148 \r
149                         Object prev = r2s.putIfAbsent(component, newName);\r
150                         assert prev == null;\r
151                         addToMapSet(s2r, newName, component);\r
152 \r
153                         reserved.add(newName);\r
154                         requested.remove(newName);\r
155 \r
156                         if (DEBUG_CACHE_UPDATES)\r
157                             debug("\tnew component name: " + newName);\r
158                     } else {\r
159                         // This must be a change to an existing cached component.\r
160 \r
161                         // Validate cache\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
168                                 continue;\r
169 \r
170                             System.out.println("WARNING: Somebody is screwing the model up with duplicate name: " + newName);\r
171                             // TODO: generate issue or message\r
172                         }\r
173 \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
177                         assert updated;\r
178                         if (resourcesWithOldName.isEmpty()) {\r
179                             reserved.remove(oldName);\r
180                         }\r
181                         reserved.add(newName);\r
182                         requested.remove(newName);\r
183 \r
184                         if (DEBUG_CACHE_UPDATES)\r
185                             debug("\tcomponent name changed: " + oldName + " -> " + newName);\r
186                     }\r
187                 }\r
188 \r
189                 if (DEBUG_CACHE_UPDATES) {\r
190                     debug("reserved names after update: " + reserved);\r
191                     debug("requested names after update: " + requested);\r
192                 }\r
193             }\r
194         }\r
195 \r
196         private void debug(String string) {\r
197             CaseInsensitiveComponentNamingStrategy2.debug(this, string);\r
198         }\r
199     }\r
200 \r
201     static class CacheFactory {\r
202         final ReadGraph           graph;\r
203         final Resource            root;\r
204         final Layer0            b;\r
205         final StructuralResource2 sr;\r
206         final boolean caseInsensitive;\r
207 \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
211 \r
212         CacheFactory(ReadGraph graph, Resource root, boolean caseInsensitive) {\r
213             this.graph = graph;\r
214             this.root = root;\r
215             this.b = Layer0.getInstance(graph);\r
216             this.sr = StructuralResource2.getInstance(graph);\r
217             this.caseInsensitive = caseInsensitive;\r
218 \r
219             this.reserved = new ConcurrentSkipListSet<String>(getComparator(caseInsensitive));\r
220             this.s2r = new ConcurrentSkipListMap<String, Set<Resource>>(getComparator(caseInsensitive));\r
221         }\r
222 \r
223         private void debug(String string) {\r
224             CaseInsensitiveComponentNamingStrategy2.debug(this, string);\r
225         }\r
226 \r
227         public Cache create() throws DatabaseException {\r
228             if (DEBUG_CACHE_INITIALIZATION_BROWSE)\r
229                 debug("browsing all components from root " + root);\r
230 \r
231             graph.syncRequest(new AsyncReadRequest() {\r
232                 @Override\r
233                 public void run(AsyncReadGraph graph) {\r
234                     browseComposite(graph, root);\r
235                 }\r
236             });\r
237 \r
238             if (DEBUG_CACHE_INITIALIZATION_BROWSE)\r
239                 debug("browsing completed, results:\n\treserved: " + reserved + "\n\tr2s: " + r2s + "\n\ts2r: " + s2r);\r
240 \r
241             return new Cache(root, reserved, r2s, s2r, caseInsensitive);\r
242         }\r
243 \r
244         static abstract class MultiProc<T> implements AsyncMultiProcedure<T> {\r
245             @Override\r
246             public void finished(AsyncReadGraph graph) {\r
247             }\r
248             @Override\r
249             public void exception(AsyncReadGraph graph, Throwable t) {\r
250                 ErrorLogger.defaultLogError(t);\r
251             }\r
252         }\r
253 \r
254         static abstract class AsyncProc<T> implements AsyncProcedure<T> {\r
255             @Override\r
256             public void exception(AsyncReadGraph graph, Throwable t) {\r
257                 ErrorLogger.defaultLogError(t);\r
258             }\r
259         }\r
260 \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
265                 @Override\r
266                 public void execute(AsyncReadGraph graph, Resource component) {\r
267                     browseComponent(graph, component);\r
268                 }\r
269 \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
275                         @Override\r
276                         public void execute(AsyncReadGraph graph, Boolean result) {\r
277                             if (result)\r
278                                 browseComposite(graph, component);\r
279                         }\r
280                     });\r
281                 }\r
282 \r
283                 private void reserveName(AsyncReadGraph graph, final Resource component) {\r
284                     graph.forPossibleRelatedValue(component, b.HasName, new AsyncProc<String>() {\r
285                         @Override\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
296                                 } else {\r
297                                     String prevName = r2s.putIfAbsent(component, componentName);\r
298                                     if (prevName == null)\r
299                                         reserved.add(componentName);\r
300                                 }\r
301                             }\r
302                         }\r
303                     });\r
304                 }\r
305             });\r
306         }\r
307     }\r
308 \r
309     private SoftReference<THashMap<Resource, SoftReference<Cache>>> mapRef =\r
310         new SoftReference<THashMap<Resource, SoftReference<Cache>>>(new THashMap<Resource, SoftReference<Cache>>());\r
311 \r
312     private final GraphChangeListenerSupport                        changeSupport;\r
313     private Resource                                                inverseOfHasName;\r
314 \r
315     public CaseInsensitiveComponentNamingStrategy2() {\r
316         this(Simantics.getSession().getService(GraphChangeListenerSupport.class), "%s_%d");\r
317     }\r
318     \r
319     public CaseInsensitiveComponentNamingStrategy2(GraphChangeListenerSupport changeSupport) {\r
320         this(changeSupport, "%s %d");\r
321     }\r
322 \r
323     /**\r
324      * @param changeSupport\r
325      * @param generatedNameFormat the format to use for generated names, see\r
326      *        {@link Formatter}\r
327      */\r
328     public CaseInsensitiveComponentNamingStrategy2(GraphChangeListenerSupport changeSupport, String generatedNameFormat) {\r
329         super(generatedNameFormat);\r
330         this.changeSupport = changeSupport;\r
331         changeSupport.addListener(this);\r
332     }\r
333 \r
334     public void dispose() {\r
335         changeSupport.removeListener(this);\r
336     }\r
337 \r
338     static class CacheUpdateBundle {\r
339         IdentityHashMap<Cache, Collection<Pair<Resource, String>>> updates = new IdentityHashMap<Cache, Collection<Pair<Resource, String>>>();\r
340 \r
341         public boolean isEmpty() {\r
342             return updates.isEmpty();\r
343         }\r
344 \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
349 \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
354             }\r
355 \r
356             collection.add(Pair.make(component, newName));\r
357         }\r
358 \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
363             }\r
364         }\r
365 \r
366         @Override\r
367         public String toString() {\r
368             return getClass().getSimpleName() + " [" + updates.size() + " changed caches]";\r
369         }\r
370     }\r
371 \r
372     @Override\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
377 \r
378         ReadGraph graph = e.getGraph();\r
379         Layer0 b = Layer0.getInstance(graph);\r
380 \r
381         // Cache inverse of Has Name relation.\r
382         if (inverseOfHasName == null) {\r
383             inverseOfHasName = graph.getInverse(b.HasName);\r
384         }\r
385 \r
386         CacheUpdateBundle bundle = new CacheUpdateBundle();\r
387 \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
401                     }\r
402                 }\r
403             }\r
404         }\r
405 \r
406         if (!bundle.isEmpty()) {\r
407             if (DEBUG_GRAPH_UPDATES)\r
408                 debug("committing " + bundle);\r
409             bundle.commitAll();\r
410         }\r
411     }\r
412 \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
416 \r
417         if (map != null) {\r
418             SoftReference<Cache> cacheRef = map.get(configurationRoot);\r
419             if (cacheRef != null) {\r
420                 cache = cacheRef.get();\r
421                 if (cache != null)\r
422                     // Cache hit!\r
423                     return cache;\r
424             }\r
425         } else {\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
429         }\r
430 \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
438         return cache;\r
439     }\r
440 \r
441     private Cache peekCache(ReadGraph graph, Resource configurationRoot) {\r
442         THashMap<Resource, SoftReference<Cache>> map = mapRef.get();\r
443         if (map == null)\r
444             return null;\r
445         SoftReference<Cache> cacheRef = map.get(configurationRoot);\r
446         if (cacheRef == null)\r
447             return null;\r
448         Cache cache = cacheRef.get();\r
449         if (cache == null)\r
450             return null;\r
451         // Cache hit!\r
452         return cache;\r
453     }\r
454 \r
455     @Override\r
456     public String validateInstanceName(ReadGraph graph,\r
457                 Resource configurationRoot, Resource component, String proposition, boolean acceptProposition)\r
458                 throws NamingException, DatabaseException {\r
459 \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
465         \r
466     }\r
467     \r
468     @Override\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
475             return result;\r
476         }\r
477     }\r
478 \r
479     private void debug(String string) {\r
480         debug(this, string);\r
481     }\r
482 \r
483     private static void debug(Object obj, String string) {\r
484         System.out.println("[" + obj.getClass().getSimpleName() + "(" + System.identityHashCode(obj) + ")] " + string);\r
485     }\r
486 \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
489         if (set == null) {\r
490             set = new HashSet<V>(1);\r
491             map.putIfAbsent(key, set);\r
492         }\r
493         set.add(value);\r
494         return set;\r
495     }\r
496 \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
499         if (set == null)\r
500             return Collections.emptySet();\r
501         return set;\r
502     }\r
503 \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
506         if (set == null)\r
507             return Collections.emptySet();\r
508         if (set.remove(value)) {\r
509             if (set.isEmpty()) {\r
510                 map.remove(key);\r
511                 return Collections.emptySet();\r
512             }\r
513         }\r
514         return set;\r
515     }\r
516     \r
517     public static CaseInsensitiveComponentNamingStrategy2 install(IProject project) {\r
518         \r
519         Session session = project.getSession();\r
520         \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
526         } else {\r
527             System.out.println("WARNING: No GraphChangeListenerSupport in session " + session +", can't initialize all services.");\r
528         }\r
529         \r
530         return null;\r
531         \r
532     }\r
533 \r
534 }\r