APIs for skipping state restoration for non-undo synchronization events
[simantics/platform.git] / bundles / org.simantics.structural.synchronization / src / org / simantics / structural / synchronization / base / SynchronizationEventHandlerBase.java
1 package org.simantics.structural.synchronization.base;
2
3 import java.util.ArrayDeque;
4 import java.util.Collection;
5 import java.util.Collections;
6 import java.util.Map;
7 import java.util.Queue;
8
9 import org.simantics.databoard.Bindings;
10 import org.simantics.databoard.adapter.AdaptException;
11 import org.simantics.structural.synchronization.internal.Policy;
12 import org.simantics.structural.synchronization.protocol.ChildInfo;
13 import org.simantics.structural.synchronization.protocol.Connection;
14 import org.simantics.structural.synchronization.protocol.SerializedVariable;
15 import org.simantics.structural.synchronization.protocol.SynchronizationEventHandler;
16 import org.simantics.structural.synchronization.protocol.SynchronizationException;
17 import org.simantics.structural.synchronization.utils.ComponentBase;
18 import org.simantics.structural.synchronization.utils.ComponentFactory;
19 import org.simantics.structural.synchronization.utils.MappingBase;
20 import org.simantics.structural.synchronization.utils.Solver;
21 import org.slf4j.Logger;
22
23 import gnu.trove.map.hash.THashMap;
24 import gnu.trove.set.hash.THashSet;
25
26 /**
27  * Handles synchronization events by updating the simulator designated by the
28  * provided {@link Solver} instance.
29  * 
30  * @author Hannu Niemistö
31  */
32 public abstract class SynchronizationEventHandlerBase<T extends ComponentBase<T>> implements SynchronizationEventHandler {
33
34     public static final boolean TRACE_EVENTS = false;
35     
36     public final Solver solver;
37     protected final SolverNameUtil nameUtil;
38     protected final MappingBase<T> mapping;
39     final ModuleUpdaterFactoryBase<T> moduleUpdaterFactory;
40     final ComponentFactory<T> componentFactory;
41     public final ReferenceResolverBase<T> resolver;
42     private boolean didChanges = false;
43
44     protected T component; // Current active component
45     THashMap<String, ModuleUpdaterBase<T>> moduleUpdaters = new THashMap<>();
46     Queue<Runnable> postSynchronizationActions = new ArrayDeque<>();
47     protected THashMap<String, ComponentBase<T>> solverComponentNameToComponent = new THashMap<>();
48     
49     /**
50      * This is a set of components satisfying the following conditions
51      * <ul>
52      *     <li>beginComponent is called for their parents
53      *     <li>endComponent is not yet called for their parents
54      *     <li>beginComponent is not yet called for them
55      * </ul>
56      */
57     THashSet<T> potentiallyUpdatedComponents = new THashSet<>();
58
59     /**
60      * Is this potentially an undo/redo-related synchronization?
61      * 
62      * Default is true for backwards compatibility.
63      */
64     protected boolean isUndo = true;
65
66     public SynchronizationEventHandlerBase(Solver solver, ReferenceResolverBase<T> resolver, SolverNameUtil nameUtil,
67             ComponentFactory<T> componentFactory, ModuleUpdaterFactoryBase<T> moduleUpdaterFactory, MappingBase<T> mapping) {
68         this.solver = solver;
69         this.nameUtil = nameUtil;
70         this.mapping = mapping;
71         this.componentFactory = componentFactory;
72         this.moduleUpdaterFactory = moduleUpdaterFactory;
73         this.resolver = resolver;
74     }
75     
76     /**
77      * Mark the undo/redo status of this handler.
78      * 
79      * Set 'isUndo' to false when processing a normal synchronization and true when
80      * processing an undo/redo.
81      * 
82      * When 'isUndo' is false, loading of component solver state from the state undo context
83      * is skipped for added components.
84      */
85     public void setAsUndo(boolean isUndo) {
86         this.isUndo = isUndo;
87     }
88
89     @Override
90     public void beginSynchronization() {
91         if(TRACE_EVENTS) {
92             System.out.println("beginSynchronization()");
93             //mapping.printUidMap();
94         }
95         component = null;
96     }
97     
98     @Override
99     public void endSynchronization() {
100         try {
101             if(TRACE_EVENTS)
102                 System.out.println("endSynchronization()");
103             if(component != null)
104                 throw new SynchronizationException("beginComponent/endComponent calls do not match.");
105             
106             resolver.resolvePendingSelfReferences();
107             resolver.printPending();
108             
109             // Do removals
110             mapping.removePending(solver);
111             
112             // Post synchronization actions
113             Runnable action;
114             while((action = postSynchronizationActions.poll()) != null)
115                 action.run();
116
117             // Rename modules to suggested names where possible.
118             nameUtil.applySuggestedNames((creationName, newName) -> {
119                 ComponentBase<T> component = solverComponentNameToComponent.get(creationName);
120                 if (component != null) {
121                     component.solverComponentName = newName;
122                 }
123             });
124             solverComponentNameToComponent.clear();
125         } catch(Throwable e) {
126             Policy.logError(e);
127             throw new SynchronizationException(e);
128         }
129     }
130
131     private boolean isAttached(Collection<SerializedVariable> properties) {
132         for(SerializedVariable property : properties)
133             if(property.name.equals("IsAttached"))
134                 try {
135                     return (Boolean)property.value.getValue(Bindings.BOOLEAN);
136                 } catch (AdaptException e) {
137                     throw new SynchronizationException(e);
138                 }
139         return false;
140     }
141     
142     private boolean isDesynchronized(Collection<SerializedVariable> properties) {
143         for(SerializedVariable property : properties)
144             if(property.name.equals("IsDesynchronized"))
145                 try {
146                     return (Boolean)property.value.getValue(Bindings.BOOLEAN);
147                 } catch (AdaptException e) {
148                     throw new SynchronizationException(e);
149                 }
150         return false;
151     }
152     
153     @Override
154     public void beginComponent(String name, String typeId,
155             Collection<SerializedVariable> properties,
156             Collection<Connection> connections,
157             Collection<ChildInfo> children)
158                     throws SynchronizationException {
159         try {
160             if(TRACE_EVENTS) {
161                 System.out.println("beginComponent("+name+", " + (component != null ? component.uid : "null") + "," + typeId + ")");
162                 if(!children.isEmpty()) {
163                     System.out.println("    Children:");
164                     for(ChildInfo child : children)
165                         System.out.println("        " + child.name + " " + child.uid);
166                 }
167                 if(!connections.isEmpty()) {
168                     System.out.println("    Connections:");
169                     for(Connection connection : connections)
170                         System.out.println("        " + connection.relation + " " + connection.connectionPoints);
171                 }
172             }
173             
174             String parentSolverComponentName;
175             
176             // Finds the composite
177             if(component == null) {
178                 name = "COMP_ROOT";
179                 parentSolverComponentName = "";
180                 component = mapping.getConfiguration();
181                 component.setModuleId(solver.getId(name));
182                 component.solverComponentName = name;
183             }
184             else {
185                 parentSolverComponentName = component.solverComponentName;
186                 component = component.getChild(name);
187                 if(component == null)
188                     throw new SynchronizationException("Didn't find '"+name+"'. "
189                             + "It should have been mentioned as a child in the parent beginComponent method.");
190             }
191             
192             potentiallyUpdatedComponents.remove(component);
193     
194             ModuleUpdaterBase<T> updater = null;
195             if(typeId != null) {
196                 updater = moduleUpdaters.get(typeId);
197                 if(updater == null)
198                     throw new SynchronizationException("Undefined typeId " + typeId + ".");
199             }
200             
201             // Handle composite
202             if(typeId == null || updater.isUserComponent || updater.isComposite) {
203                 // Create or update a subprocess
204                 int moduleId = component.getModuleId();
205                 boolean justCreated = false;
206                 if(isAttached(properties))
207                     ; // Subprocesses are not created for attached composites
208                 else if(moduleId <= 0) {
209                     String subprocessName = nameUtil.getFreshName(
210                             parentSolverComponentName,
211                             getSubprocessName(name, properties));
212                     try {
213                         solver.addSubprocess(subprocessName, updater.subprocessType);
214                     } catch(Exception e) {
215                         reportProblem("Exception while adding subprocess.", e);
216                     }
217                     moduleId = solver.getId(subprocessName);
218                     if(moduleId <= 0)
219                         throw new SynchronizationException("Failed to create a subprocess " + subprocessName);
220                     component.setModuleId(moduleId);
221
222                     // TODO these two lines can be removed when IncludedInSimulation -property is given to all composites
223                     if(component.getParent() != null) {
224                         int nearestParentId = getNearestParentComponentId(component);
225                         if (nearestParentId <= 0) {
226                             throw new SynchronizationException("Could not find parent with non-zero component id from Component("+name+", " + (component != null ? component.uid : "null") + "," + typeId + ")");
227                         }
228                         String parentName = solver.getName(nearestParentId);
229                         solver.includeSubprocess(parentName, subprocessName);
230                         component.solverComponentName = subprocessName;
231                         solverComponentNameToComponent.put(subprocessName, component);
232                     }
233                     
234                     if(updater.isComposite) {
235                         final ModuleUpdateContext<T> context = new ModuleUpdateContext<T>(this, updater, component);
236                         updater.create(context, properties, connections);
237                     }
238                     
239                     justCreated = true;
240                     
241                 } else {
242                         
243                     component.solverComponentName = nameUtil.ensureNameIsVariationOf(
244                             parentSolverComponentName, moduleId,
245                             getSubprocessName(name, properties));
246                     
247                     if(updater.isComposite) {
248                         final ModuleUpdateContext<T> context = new ModuleUpdateContext<T>(this, updater, component);
249                         updater.update(context, properties, connections);
250                     }
251                     
252                 }
253                 if(mapping.getTrustUids()) {
254                         // Create a new child map
255                         THashMap<String, T> newChildMap =
256                                         new THashMap<String, T>();
257                     for(ChildInfo info : children) {
258                         // Detach from the existing configuration the children with
259                         // the right uids or create new components if uid is unknown.
260                         T conf = mapping.detachOrCreateComponent(info.uid);
261                         newChildMap.put(info.name, conf);
262                         resolver.markPending(conf);
263                         potentiallyUpdatedComponents.add(conf);
264                     }
265         
266                     // Put old children not detached in the previous phase
267                     // to the pending removal set. They might have been
268                     // moved somewhere else.
269                     THashMap<String, T> oldChildMap =
270                             component.setChildMapAndReturnOld(newChildMap);
271                     resolver.unmarkPending(component);
272                     if(oldChildMap != null)
273                         for(T component : oldChildMap.values()) {
274                             component.clearParent();
275                             addPendingRemoval(component);
276                         }
277                 }
278                 // Alternative implementation when uids are not available.
279                 else {
280                     // Create a new child map
281                     THashMap<String, T> newChildMap =
282                             new THashMap<String, T>();
283                     Map<String, T> oldChildMap =
284                             component.getChildMap();
285                     if(oldChildMap == null)
286                         oldChildMap = Collections.<String,T>emptyMap();
287                     for(ChildInfo info : children) {
288                         T conf = oldChildMap.remove(info.name);
289                         if(conf == null)
290                             conf = componentFactory.create(info.uid);
291                         else
292                             conf.uid = info.uid;
293                         newChildMap.put(info.name, conf);
294                         resolver.markPending(conf);
295                     }
296                     component.setChildMap(newChildMap);
297
298                     resolver.unmarkPending(component);
299                     if(oldChildMap != null)
300                         for(T component : oldChildMap.values()) {
301                             component.clearParent();
302                             addPendingRemoval(component);
303                         }
304                 }
305
306                 postCompositeAction(justCreated, properties, connections, updater);
307                 
308             }
309             // Handle component
310             else {
311                 if(!children.isEmpty())
312                     throw new SynchronizationException("Component with type " + typeId + " cannot have children.");
313                 
314                 boolean attached = isAttached(properties);
315                 component.attached = attached;
316
317                 // Create or update the component
318                 final ModuleUpdateContext<T> context = new ModuleUpdateContext<T>(this, updater, component);
319                 int moduleId = component.getModuleId();
320                 if(moduleId <= 0) {
321                     if(attached) {
322                         component.attached = true;
323                         context.setModuleName(name);
324                         context.setModuleId(solver.getId(name));
325                         if(context.getModuleId() <= 0)
326                             reportProblem("Didn't find attached module " + name + ".");
327                         else if(!isDesynchronized(properties))
328                             updater.update(context, properties, connections);
329                         setDidChanges();
330                     }
331                     else {
332                         component.attached = false;
333                         context.setModuleName(nameUtil.getFreshName(parentSolverComponentName, name));
334                         if (isUndo) {
335                             context.addPostUpdateAction(new Runnable() {
336                                 @Override
337                                 public void run() {
338                                     context.stateLoadedFromUndo = mapping.undoContext.loadState(solver,
339                                             context.component.componentId, 
340                                             context.component.uid);
341                                 }
342                             });
343                         }
344                         updater.create(context, properties, connections);
345                         solverComponentNameToComponent.put(context.getModuleName(), component);
346                     }
347                 }
348                 else if(!isDesynchronized(properties)) {
349                     context.setModuleName(nameUtil.ensureNameIsVariationOf(parentSolverComponentName,
350                             moduleId, name));
351                     updater.update(context, properties, connections);
352                 }
353                 else {
354                     resolver.unmarkPending(component);
355                 }
356             }
357         } catch(Throwable e) {
358             Policy.logError(e);
359             throw new SynchronizationException(e);
360         }
361     }
362
363     private static int getNearestParentComponentId(ComponentBase<?> component) {
364         ComponentBase<?> parent = component.getParent();
365         while (parent != null) {
366             int pid = parent.getModuleId();
367             if (pid > 0)
368                 return pid;
369             parent = parent.getParent();
370         }
371         return -1;
372     }
373
374     protected void addPendingRemoval(T component) {
375         if (TRACE_EVENTS)
376             System.out.println("addPendingRemoval(" + component.componentId + " : " + component.solverComponentName + ")");
377         mapping.addPendingRemoval(component);
378     }
379
380     private String getSubprocessName(String name,
381             Collection<SerializedVariable> properties) {
382         for(SerializedVariable property : properties)
383             if(property.name.equals("HasSubprocessName"))
384                 try {
385                     String value = (String)property.value.getValue(Bindings.STRING);
386                     if (!value.isEmpty())
387                         return value;
388                 } catch (AdaptException e) {
389                     // This is very improbable exception.
390                     // Just ignore it and return the name.
391                     e.printStackTrace();
392                     break;
393                 }
394         return name;
395     }
396
397     @Override
398     public void endComponent() {
399         try {
400             if(TRACE_EVENTS)
401                 System.out.println("endComponent(" + (component != null ? component.solverComponentName : "null") + ")");
402             if(component == null) return;
403             for(T child : component.getChildren())
404                 if(potentiallyUpdatedComponents.remove(child))
405                     resolver.unmarkPending(child);
406             T parent = component.getParent();
407             if (parent == null && mapping.getConfiguration() != component)
408                 throw new SynchronizationException("BUG: beginComponent/endComponent calls do not match.");
409             component = parent;
410         } catch(Throwable e) {
411             Policy.logError(e);
412             throw new SynchronizationException(e);
413         }
414     }
415
416     @Override
417     public void beginType(String id, Collection<SerializedVariable> properties) {
418         try {
419             /*if(TRACE_EVENTS)
420                 System.out.println("beginType("+id+")");*/
421             ModuleUpdaterBase<T> updater;
422             try {
423                 updater = moduleUpdaterFactory.createUpdater(id);
424             } catch (Exception e) {
425                 throw new RuntimeException(e);
426             }
427             if(updater == null)
428                 throw new SynchronizationException("Failed to create module updater for id " + id + ".");
429             moduleUpdaters.put(id, updater);
430         } catch(Throwable e) {
431             Policy.logError(e);
432             throw new SynchronizationException(e);
433         }
434     }
435
436     @Override
437     public void endType() {
438         /*if(TRACE_EVENTS)
439             System.out.println("endType()");*/
440     }
441
442     public boolean getDidChanges() {
443         return didChanges;
444     }
445
446     public void setDidChanges() {
447         didChanges = true;
448     }
449
450     public void reportProblem(String description) {
451         getLogger().error(description);
452     }
453     
454     public void reportProblem(String description, Exception e) {
455         getLogger().error(description, e);
456     }
457     
458     public void addPostSynchronizationAction(Runnable action) {
459         postSynchronizationActions.add(action);
460     }
461     
462     protected void postCompositeAction(boolean justCreated, Collection<SerializedVariable> properties, 
463             Collection<Connection> connections, ModuleUpdaterBase<T> updater) throws Exception { 
464     }
465
466     
467     public long getFromRevision() {
468         return mapping.currentRevision;
469     }
470     
471     public abstract Logger getLogger();
472 }