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