package org.simantics.structural.synchronization.protocol; import gnu.trove.map.hash.THashMap; import gnu.trove.set.hash.THashSet; import java.util.ArrayList; import java.util.Collection; import java.util.Map; import org.simantics.databoard.util.URIStringUtils; /** *

A partial implementation of SynchronizationEventHandler that contains all generic * logic for synchronization that is not domain specific. * *

It is assumed that the current state of the simulator is stored in some tree-like * data structure and Component is the class that refers to currently instantiated components. * * @author Hannu Niemistö */ public abstract class AbstractSynchronizationEventHandler implements SynchronizationEventHandler { /** * Returns the root of the current configuration. It must not be null. */ protected abstract Component getConfigurationRoot(); /** * Returns the parent of the component. It may be null, if the component * is the configuration root or if it is detached or just created. */ protected abstract Component getParent(Component component); /** * Returns the map of current children of the component. The method may assume * that the caller will not modify the map directly. */ protected abstract Map getChildMap(Component component); /** * Sets the children of the component. The method may assume that all components * in the newChildMap are detached (i.e. do not have a parent). */ protected abstract void setChildMap(Component component, THashMap newChildMap); /** *

Detaches the component from its parent. After calling this method * getParent(component) and getChildMap(parent).get(name) should return null where * parent is the old parent of the component and name the name of the component. * *

It is important that after detaching, the method {@link #getComponentByUid} * still returns the component. */ protected abstract void detachFromParent(Component component); /** * Try to find the component by given uid. If it is not found, returns null. */ protected abstract Component getComponentByUid(String uid); /** * Creates a new component with the given uid. The method may * assume that there is no existing component with the same uid. * After the method returns, getComponentByUid must return the * same component with the given uid. */ protected abstract Component createComponent(String uid); /** * Removes the component. The method may assume that the component * has already been detached from its parent. */ protected abstract void removeComponent(Component component); /** * Calls connectionResolvedCallback with the result of resolving the given connection point * of the given component. The method is called only for components whose updating is completed. */ protected abstract void resolveConnectionPointLocally(Component component, String connectionPointName, ConnectionResolvedCallback connectionResolvedCallback); /** *

Updates the component based on the given name, type, properties and connections. * *

When updating has been finished (this may happen after this method has been finished) * the method {@link #componentUpdated} must be called. * *

The method may use {@link resolveReference} to resolve connection point references. */ protected abstract void updateComponent(Component component, String name, String typeId, Collection properties, Collection connections); /** * The component that synchronizer is currently updating. * If currentComponent=null, beginComponent method is not yet called * or endComponent is called matching the first beginComponent call. */ private Component currentComponent; /** * The set of components which or whose children may still be * updated by calling beginComponent during this synchronization. */ private THashSet mayBeUpdated = new THashSet(); /** * The key set of this map contains all components in {@link #mayBeUpdated} and additionally * components whose updating has began by beginComponent call, but that are not yet completely * updated (for example because they are waiting other unresolved components). */ private THashMap>> pendingResolves = new THashMap>>(); /** * This set contains all components that currently have no parents and will be removed * at the end of synchronization. */ private THashSet pendingRemoval = new THashSet(); private static class PendingResolve { public final Component component; public final String connectionPoint; public final ConnectionResolvedCallback connectionResolvedCallback; public PendingResolve(Component component, String connectionPoint, ConnectionResolvedCallback connectionResolvedCallback) { this.component = component; this.connectionPoint = connectionPoint; this.connectionResolvedCallback = connectionResolvedCallback; } @Override public String toString() { return connectionPoint + "->" + connectionResolvedCallback; } } @Override public void beginSynchronization() { currentComponent = null; } @Override public void endSynchronization() { if(currentComponent != null) throw new SynchronizationException("beginComponent is called more often than endComponent at the end of synchronization."); for(Component component : pendingRemoval) removeComponent(component); pendingRemoval.clear(); } @Override public void beginComponent(String name, String typeId, Collection properties, Collection connections, Collection children) throws SynchronizationException { if(currentComponent == null) { currentComponent = getConfigurationRoot(); if(currentComponent == null) throw new SynchronizationException("getConfiguration root returned null."); } else { currentComponent = getChildMap(currentComponent).get(name); if(currentComponent == null) throw new SynchronizationException("Didn't find '"+name+"'. " + "It should have been mentioned as a child in the parent beginComponent method."); } // Handle children if(!getChildMap(currentComponent).isEmpty() || !children.isEmpty()){ THashMap newChildMap = new THashMap(); for(ChildInfo info : children) { // Detach from the existing configuration the children with // the right uids or create new components if uid is unknown. Component component = getComponentByUid(info.uid); if(component == null) component = createComponent(info.uid); else { pendingRemoval.remove(component); detachFromParent(component); } newChildMap.put(info.name, component); componentMayBeUpdated(component); } // Put old children not detached in the previous phase // to the pending removal set. They might have been // moved somewhere else. Map oldChildMap = getChildMap(currentComponent); for(Component component : oldChildMap.values()) { detachFromParent(component); pendingRemoval.add(component); } setChildMap(currentComponent, newChildMap); } // Update/create component itself mayBeUpdated.remove(currentComponent); updateComponent(currentComponent, name, typeId, properties, connections); } @Override public void endComponent() { if(currentComponent == null) throw new SynchronizationException("endComponent is called more often than beginComponent."); for(Component child : getChildMap(currentComponent).values()) if(mayBeUpdated.remove(child)) // clear pending status of components which will not change // during synchronization componentUpdated(child); currentComponent = getParent(currentComponent); } private void componentMayBeUpdated(Component component) { mayBeUpdated.add(component); pendingResolves.put(component, new ArrayList>(2)); } /** * Signals that the component is updated so far that its connection may be resolved * (with resolveConnectionPointLocally). */ public void componentUpdated(Component component) { ArrayList> resolves = pendingResolves.remove(component); if(resolves != null) for(PendingResolve resolve : resolves) { resolveConnectionPoint(resolve.component, resolve.connectionPoint, resolve.connectionResolvedCallback); } } /** * Resolves relative connection point reference starting from the given component and calls * ConnectionResolvedCallback with the resolved reference. */ public void resolveConnectionPoint(Component component, String connectionPoint, ConnectionResolvedCallback connectionResolvedCallback) { int pos = 0; while(true) { char c = connectionPoint.charAt(pos++); switch(c) { case '.': component = getParent(component); break; case '/': { int endPos = pos; while(true) { c = connectionPoint.charAt(endPos); if(c == '/' || c == '#') break; ++endPos; } String segment = URIStringUtils.unescape(connectionPoint.substring(pos, endPos)); pos = endPos; component = getChildMap(component).get(segment); if(component == null) { String message = "Couldn't resolve " + connectionPoint + ", because child " + segment + " does not exist."; reportProblem(message); return; } ArrayList> pendingList = pendingResolves.get(component); if(pendingList != null) { pendingList.add( new PendingResolve( component, connectionPoint.substring(pos), connectionResolvedCallback)); return; } } break; case '#': { String segment = connectionPoint.substring(pos); resolveConnectionPointLocally(component, segment, connectionResolvedCallback); } return; } } } }