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