/******************************************************************************* * Copyright (c) 2007, 2010 Association for Decentralized Information Management * in Industry THTH ry. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * VTT Technical Research Centre of Finland - initial API and implementation *******************************************************************************/ package org.simantics.diagram.adapter; import gnu.trove.map.hash.TObjectIntHashMap; import gnu.trove.set.hash.THashSet; import java.awt.geom.AffineTransform; import java.lang.reflect.InvocationTargetException; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.Deque; import java.util.EnumSet; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Queue; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.ReentrantLock; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.SubMonitor; import org.simantics.db.AsyncReadGraph; import org.simantics.db.ReadGraph; import org.simantics.db.RequestProcessor; import org.simantics.db.Resource; import org.simantics.db.Session; import org.simantics.db.common.ResourceArray; import org.simantics.db.common.exception.DebugException; import org.simantics.db.common.procedure.adapter.AsyncProcedureAdapter; import org.simantics.db.common.procedure.adapter.CacheListener; import org.simantics.db.common.procedure.adapter.ListenerSupport; import org.simantics.db.common.procedure.adapter.ProcedureAdapter; import org.simantics.db.common.request.AsyncReadRequest; import org.simantics.db.common.request.ReadRequest; import org.simantics.db.common.session.SessionEventListenerAdapter; import org.simantics.db.common.utils.NameUtils; import org.simantics.db.exception.CancelTransactionException; import org.simantics.db.exception.DatabaseException; import org.simantics.db.exception.NoSingleResultException; import org.simantics.db.exception.ServiceException; import org.simantics.db.procedure.AsyncListener; import org.simantics.db.procedure.AsyncProcedure; import org.simantics.db.procedure.Listener; import org.simantics.db.procedure.Procedure; import org.simantics.db.request.Read; import org.simantics.db.service.SessionEventSupport; import org.simantics.diagram.connection.ConnectionSegmentEnd; import org.simantics.diagram.content.Change; import org.simantics.diagram.content.ConnectionUtil; import org.simantics.diagram.content.DesignatedTerminal; import org.simantics.diagram.content.DiagramContentChanges; import org.simantics.diagram.content.DiagramContents; import org.simantics.diagram.content.EdgeResource; import org.simantics.diagram.content.ResourceTerminal; import org.simantics.diagram.internal.DebugPolicy; import org.simantics.diagram.internal.timing.GTask; import org.simantics.diagram.internal.timing.Timing; import org.simantics.diagram.profile.ProfileKeys; import org.simantics.diagram.synchronization.CollectingModificationQueue; import org.simantics.diagram.synchronization.CompositeModification; import org.simantics.diagram.synchronization.CopyAdvisor; import org.simantics.diagram.synchronization.ErrorHandler; import org.simantics.diagram.synchronization.IHintSynchronizer; import org.simantics.diagram.synchronization.IModifiableSynchronizationContext; import org.simantics.diagram.synchronization.IModification; import org.simantics.diagram.synchronization.LogErrorHandler; import org.simantics.diagram.synchronization.ModificationAdapter; import org.simantics.diagram.synchronization.SynchronizationHints; import org.simantics.diagram.synchronization.graph.AddElement; import org.simantics.diagram.synchronization.graph.BasicResources; import org.simantics.diagram.synchronization.graph.DiagramGraphUtil; import org.simantics.diagram.synchronization.graph.ElementLoader; import org.simantics.diagram.synchronization.graph.ElementReorder; import org.simantics.diagram.synchronization.graph.ElementWriter; import org.simantics.diagram.synchronization.graph.GraphSynchronizationContext; import org.simantics.diagram.synchronization.graph.GraphSynchronizationHints; import org.simantics.diagram.synchronization.graph.ModificationQueue; import org.simantics.diagram.synchronization.graph.TagChange; import org.simantics.diagram.synchronization.graph.TransformElement; import org.simantics.diagram.synchronization.graph.layer.GraphLayer; import org.simantics.diagram.synchronization.graph.layer.GraphLayerManager; import org.simantics.diagram.ui.DiagramModelHints; import org.simantics.g2d.canvas.Hints; import org.simantics.g2d.canvas.ICanvasContext; import org.simantics.g2d.connection.ConnectionEntity; import org.simantics.g2d.connection.EndKeyOf; import org.simantics.g2d.connection.TerminalKeyOf; import org.simantics.g2d.diagram.DiagramClass; import org.simantics.g2d.diagram.DiagramHints; import org.simantics.g2d.diagram.DiagramMutator; import org.simantics.g2d.diagram.DiagramUtils; import org.simantics.g2d.diagram.IDiagram; import org.simantics.g2d.diagram.IDiagram.CompositionListener; import org.simantics.g2d.diagram.IDiagram.CompositionVetoListener; import org.simantics.g2d.diagram.handler.DataElementMap; import org.simantics.g2d.diagram.handler.ElementFactory; import org.simantics.g2d.diagram.handler.Relationship; import org.simantics.g2d.diagram.handler.RelationshipHandler; import org.simantics.g2d.diagram.handler.SubstituteElementClass; import org.simantics.g2d.diagram.handler.Topology; import org.simantics.g2d.diagram.handler.Topology.Connection; import org.simantics.g2d.diagram.handler.Topology.Terminal; import org.simantics.g2d.diagram.handler.TransactionContext.TransactionType; import org.simantics.g2d.diagram.impl.Diagram; import org.simantics.g2d.diagram.participant.ElementPainter; import org.simantics.g2d.element.ElementClass; import org.simantics.g2d.element.ElementHints; import org.simantics.g2d.element.ElementHints.DiscardableKey; import org.simantics.g2d.element.ElementUtils; import org.simantics.g2d.element.IElement; import org.simantics.g2d.element.IElementClassProvider; import org.simantics.g2d.element.handler.EdgeVisuals.EdgeEnd; import org.simantics.g2d.element.handler.ElementHandler; import org.simantics.g2d.element.handler.ElementLayerListener; import org.simantics.g2d.element.handler.TerminalTopology; import org.simantics.g2d.element.impl.Element; import org.simantics.g2d.layers.ILayer; import org.simantics.g2d.layers.ILayersEditor; import org.simantics.g2d.routing.RouterFactory; import org.simantics.scenegraph.INode; import org.simantics.scenegraph.profile.DataNodeConstants; import org.simantics.scenegraph.profile.DataNodeMap; import org.simantics.scenegraph.profile.common.ProfileObserver; import org.simantics.structural2.modelingRules.IModelingRules; import org.simantics.utils.datastructures.ArrayMap; import org.simantics.utils.datastructures.MapSet; import org.simantics.utils.datastructures.Pair; import org.simantics.utils.datastructures.disposable.AbstractDisposable; import org.simantics.utils.datastructures.hints.HintListenerAdapter; import org.simantics.utils.datastructures.hints.IHintContext.Key; import org.simantics.utils.datastructures.hints.IHintContext.KeyOf; import org.simantics.utils.datastructures.hints.IHintListener; import org.simantics.utils.datastructures.hints.IHintObservable; import org.simantics.utils.datastructures.map.AssociativeMap; import org.simantics.utils.datastructures.map.Associativity; import org.simantics.utils.datastructures.map.Tuple; import org.simantics.utils.strings.EString; import org.simantics.utils.threads.ThreadUtils; import org.simantics.utils.threads.logger.ITask; import org.simantics.utils.threads.logger.ThreadLogger; /** * This class loads a diagram contained in the graph database into the runtime * diagram model and synchronizes changes in the graph into the run-time * diagram. Any modifications to the graph model will be reflected to the * run-time diagram model. Hence the name GraphToDiagramSynchronizer. * *

* This class does not in itself support modification of the graph diagram * model. This manipulation is meant to be performed through a * {@link DiagramMutator} implementation that is installed into the diagram * using the {@link DiagramHints#KEY_MUTATOR} hint key. * * This implementations is built to only support diagrams defined in the graph * model as indicated in this * document. * *

* The synchronizer in itself is an {@link IDiagramLoader} which means that it * can be used for loading a diagram from the graph. In order for the * synchronizer to keep tracking the graph diagram model for changes the diagram * must be loaded with it. The tracking is implemented using graph database * queries. If you just want to load the diagram but detach it from the * synchronizer's tracking mechanisms, all you need to do is to dispose the * synchronizer after loading the diagram. * *

* This class guarantees that a single diagram element (IElement) representing a * single back-end object ({@link ElementHints#KEY_OBJECT}) will stay the same * object for the same back-end object. * *

* TODO: Currently it just happens that {@link GraphToDiagramSynchronizer} * contains {@link DefaultDiagramMutator} which depends on some internal details * of {@link GraphToDiagramSynchronizer} but it should be moved out of here by * introducing new interfaces. * *

Basic usage example

*

* This example shows how to initialize {@link GraphToDiagramSynchronizer} for a * specified {@link ICanvasContext} and load a diagram from a specified diagram * resource in the graph. * *

 * IDiagram loadDiagram(final ICanvasContext canvasContext, RequestProcessor processor, Resource diagramResource,
 *         ResourceArray structuralPath) throws DatabaseException {
 *     GraphToDiagramSynchronizer synchronizer = processor.syncRequest(new Read<GraphToDiagramSynchronizer>() {
 *         public GraphToDiagramSynchronizer perform(ReadGraph graph) throws DatabaseException {
 *             return new GraphToDiagramSynchronizer(graph, canvasContext, createElementClassProvider(graph));
 *         }
 *     });
 *     IDiagram d = requestProcessor
 *             .syncRequest(new DiagramLoadQuery(diagramResource, structuralPath, synchronizer, null));
 *     return d;
 * }
 * 
 * protected IElementClassProvider createElementClassProvider(ReadGraph graph) {
 *     DiagramResource dr = DiagramResource.getInstance(graph);
 *     return ElementClassProviders.mappedProvider(ElementClasses.CONNECTION, DefaultConnectionClassFactory.CLASS
 *             .newClassWith(new ResourceAdapterImpl(dr.Connection)), ElementClasses.FLAG, FlagClassFactory
 *             .createFlagClass(dr.Flag));
 * }
 * 
* *

* TODO: make GraphToDiagramSynchronizer a canvas participant to make it more * uniform with the rest of the canvas system. This does not mean that G2DS must * be attached to an ICanvasContext in order to be used, rather that it can be * attached to one. * *

* TODO: test that detaching the synchronizer via {@link #dispose()} actually * works. *

* TODO: remove {@link DefaultDiagramMutator} and all {@link DiagramMutator} * stuff altogether * *

* TODO: diagram connection loading has no listener * * @author Tuukka Lehtonen * * @see GraphElementClassFactory * @see GraphElementFactory * @see ElementLoader * @see ElementWriter * @see IHintSynchronizer * @see CopyAdvisor */ public class GraphToDiagramSynchronizer extends AbstractDisposable implements IDiagramLoader, IModifiableSynchronizationContext { /** * Controls whether the class adds hint listeners to each diagram element * that try to perform basic sanity checks on changes happening in element * hints. Having this will immediately inform you of bugs that corrupt the * diagram model within the element hints in some way. */ private static final boolean USE_ELEMENT_VALIDATING_LISTENERS = false; /** * These keys are used to hang on to Connection instances of edges that will * be later installed as EndKeyOf/TerminalKeyOf hints into the loaded * element during the "graph to diagram update transaction". */ private static final Key KEY_CONNECTION_BEGIN_PLACEHOLDER = new KeyOf(PlaceholderConnection.class, "CONNECTION_BEGIN_PLACEHOLDER"); private static final Key KEY_CONNECTION_END_PLACEHOLDER = new KeyOf(PlaceholderConnection.class, "CONNECTION_END_PLACEHOLDER"); /** * Stored into an edge node during connection edge requests using the * KEY_CONNECTION_BEGIN_PLACEHOLDER and KEY_CONNECTION_END_PLACEHOLDER keys. */ static class PlaceholderConnection { public final EdgeEnd end; public final Object node; public final Terminal terminal; public PlaceholderConnection(EdgeEnd end, Object node, Terminal terminal) { this.end = end; this.node = node; this.terminal = terminal; } } /** * Indicates to the diagram CompositionListener of this synchronizer that is * should deny all relationships for the element this hint is attached to. */ private static final Key KEY_REMOVE_RELATIONSHIPS = new KeyOf(Boolean.class, "REMOVE_RELATIONSHIPS"); static ErrorHandler errorHandler = LogErrorHandler.INSTANCE; /** * The canvas context which is being synchronized with the graph. Received * during construction. * *

* Is not nulled during disposal of this class since internal listener's * life-cycles depend on canvas.isDisposed. */ ICanvasContext canvas; /** * The session used by this synchronizer. Received during construction. */ Session session; /** * Locked while updating diagram contents from the graph. */ ReentrantLock diagramUpdateLock = new ReentrantLock(); // <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< // BI-DIRECTIONAL DIAGRAM ELEMENT <-> BACKEND OBJECT MAPPING BEGIN // ------------------------------------------------------------------------ /** * Holds a GraphToDiagramUpdater instance while a diagram content update is * currently in progress. * *

* Basically this is a hack solution to the problem of properly finding * newly added Resource<->IElement mappings while loading the diagram * contents. See {@link DataElementMapImpl} for why this is necessary. */ GraphToDiagramUpdater currentUpdater = null; /** * A map from data objects to elements. Elements should already contain the * data objects as {@link ElementHints#KEY_OBJECT} hints. */ ConcurrentMap dataElement = new ConcurrentHashMap(); /** * Temporary structure for single-threaded use in #{@link DiagramUpdater}. */ Collection tempConnections = new ArrayList(); /** * A dummy class of which an instance will be given to each new edge element * to make {@link TerminalKeyOf} keys unique for each edge. */ static class TransientElementObject { @Override public String toString() { return "MUTATOR GENERATED (hash=" + System.identityHashCode(this) + ")"; } } private static class ConnectionChildren { public Set branchPoints; public Set segments; public ConnectionChildren(Set branchPoints, Set segments) { this.branchPoints = branchPoints; this.segments = segments; } } ListenerSupport canvasListenerSupport = new ListenerSupport() { @Override public void exception(Throwable t) { error(t); } @Override public boolean isDisposed() { return !isAlive() || canvas.isDisposed(); } }; /** * @see ElementHints#KEY_CONNECTION_ENTITY */ class ConnectionEntityImpl implements ConnectionEntity { /** * The connection instance resource in the graph backend. * * May be null if the connection has not been synchronized * yet. */ Resource connection; /** * The connection type resource in the graph backend. * * May be null if the connection has not been synchronized * yet. */ Resource connectionType; /** * The connection entity element which is a part of the diagram. */ IElement connectionElement; /** * List of backend-synchronized branch points that are part of this * connection. */ Collection branchPoints = Collections.emptyList(); /** * List of backend-synchronized edges that are part of this connection. */ Collection segments = Collections.emptyList(); Set removedBranchPoints = new HashSet(4); Set removedSegments = new HashSet(4); /** * List of non-backend-synchronized branch point element that are part * of this connection. */ List branchPointElements = new ArrayList(1); /** * List of non-backend-synchronized edge element that are part of this * connection. */ List segmentElements = new ArrayList(2); ConnectionListener listener; ConnectionEntityImpl(Resource connection, Resource connectionType, IElement connectionElement) { this.connection = connection; this.connectionType = connectionType; this.connectionElement = connectionElement; } ConnectionEntityImpl(Resource connectionType, IElement connectionElement) { this.connectionType = connectionType; this.connectionElement = connectionElement; } ConnectionEntityImpl(ReadGraph graph, Resource connection, IElement connectionElement) throws NoSingleResultException, ServiceException { this.connection = connection; this.connectionType = graph.getSingleType(connection, br.DIA.Connection); this.connectionElement = connectionElement; } @Override public IElement getConnection() { return connectionElement; } public Object getConnectionObject() { return connection; } public IElement getConnectionElement() { if (connectionElement == null) return getMappedConnectionElement(); return connectionElement; } private IElement getMappedConnectionElement() { IElement ce = null; if (connection != null) ce = getMappedElement(connection); return ce == null ? connectionElement : ce; } void fix() { Collection segments = getSegments(null); // Remove all TerminalKeyOf hints from branch points that do not // match ArrayList pruned = null; for (IElement bp : getBranchPoints(null)) { if (pruned == null) pruned = new ArrayList(4); pruned.clear(); for (Map.Entry entry : bp.getHintsOfClass(TerminalKeyOf.class).entrySet()) { // First check that the terminal matches. Connection c = (Connection) entry.getValue(); if (!segments.contains(c.edge)) pruned.add(entry.getKey()); } removeNodeTopologyHints((Element) bp, pruned); } } public ConnectionChildren getConnectionChildren() { Set bps = Collections.emptySet(); Set segs = Collections.emptySet(); if (!branchPoints.isEmpty()) { bps = new HashSet(branchPoints.size()); for (Resource bp : branchPoints) { IElement e = getMappedElement(bp); if (e != null) bps.add(e); } } if (!segments.isEmpty()) { segs = new HashSet(segments.size()); for (EdgeResource seg : segments) { IElement e = getMappedElement(seg); if (e != null) segs.add(e); } } return new ConnectionChildren(bps, segs); } public void setData(Collection segments, Collection branchPoints) { // System.out.println("setData " + segments.size()); this.branchPoints = branchPoints; this.segments = segments; // Reset the added/removed state of segments and branchpoints. this.removedBranchPoints = new HashSet(4); this.removedSegments = new HashSet(4); this.branchPointElements = new ArrayList(4); this.segmentElements = new ArrayList(4); } public void fireListener(ConnectionChildren old, ConnectionChildren current) { if (listener != null) { List removed = new ArrayList(); List added = new ArrayList(); for (IElement oldBp : old.branchPoints) if (!current.branchPoints.contains(oldBp)) removed.add(oldBp); for (IElement oldSeg : old.segments) if (!current.segments.contains(oldSeg)) removed.add(oldSeg); for (IElement bp : current.branchPoints) if (!old.branchPoints.contains(bp)) added.add(bp); for (IElement seg : current.segments) if (!old.segments.contains(seg)) added.add(seg); if (!removed.isEmpty() || !added.isEmpty()) { listener.connectionChanged(new ConnectionEvent(this.connectionElement, removed, added)); } } } @Override public Collection getBranchPoints(Collection result) { if (result == null) result = new ArrayList(branchPoints.size()); for (Resource bp : branchPoints) { if (!removedBranchPoints.contains(bp)) { IElement e = getMappedElement(bp); if (e != null) result.add(e); } } result.addAll(branchPointElements); return result; } @Override public Collection getSegments(Collection result) { if (result == null) result = new ArrayList(segments.size()); for (EdgeResource seg : segments) { if (!removedSegments.contains(seg)) { IElement e = getMappedElement(seg); if (e != null) result.add(e); } } result.addAll(segmentElements); return result; } @Override public Collection getTerminalConnections(Collection result) { if (result == null) result = new ArrayList(segments.size() * 2); Set> processed = new HashSet>(); for (EdgeResource seg : segments) { IElement edge = getMappedElement(seg); if (edge != null) { for (EndKeyOf key : EndKeyOf.KEYS) { Connection c = edge.getHint(key); if (c != null && (c.terminal instanceof ResourceTerminal) && processed.add(Pair.make(c.node, c.terminal))) result.add(c); } } } return result; } @Override public void setListener(ConnectionListener listener) { this.listener = listener; } @Override public String toString() { return getClass().getSimpleName() + "[resource=" + connection + ", branch points=" + branchPoints + ", segments=" + segments + ", connectionElement=" + connectionElement + ", branch point elements=" + branchPointElements + ", segment elements=" + segmentElements + ", removed branch points=" + removedBranchPoints + ", removed segments=" + removedSegments + "]"; } } /** * A map from connection data objects to connection entities. The connection * part elements should already contain the data objects as * {@link ElementHints#KEY_OBJECT} hints. */ ConcurrentMap dataConnection = new ConcurrentHashMap(); /** * @param data * @param element */ void mapElement(final Object data, final IElement element) { if (!(element instanceof Element)) { throw new IllegalArgumentException("mapElement: expected instance of Element, got " + element + " with data " + data); } assert data != null; assert element != null; if (DebugPolicy.DEBUG_MAPPING) new Exception(Thread.currentThread() + " MAPPING: " + data + " -> " + element).printStackTrace(); dataElement.put(data, element); } /** * @param data * @return */ IElement getMappedElement(final Object data) { assert (data != null); IElement element = dataElement.get(data); return element; } IElement getMappedElementByElementObject(IElement e) { if (e == null) return null; Object o = e.getHint(ElementHints.KEY_OBJECT); if (o == null) return null; return getMappedElement(o); } /** * @param data * @return */ IElement assertMappedElement(final Object data) { IElement element = dataElement.get(data); assert element != null; return element; } /** * @param data * @return */ IElement unmapElement(final Object data) { IElement element = dataElement.remove(data); if (DebugPolicy.DEBUG_MAPPING) new Exception(Thread.currentThread() + " UN-MAPPED: " + data + " -> " + element).printStackTrace(); return element; } /** * @param data * @param element */ void mapConnection(final Object data, final ConnectionEntityImpl connection) { assert data != null; assert connection != null; if (DebugPolicy.DEBUG_MAPPING) System.out.println(Thread.currentThread() + " MAPPING CONNECTION: " + data + " -> " + connection); dataConnection.put(data, connection); } /** * @param data * @return */ ConnectionEntityImpl getMappedConnection(final Object data) { ConnectionEntityImpl connection = dataConnection.get(data); return connection; } /** * @param data * @return */ ConnectionEntityImpl assertMappedConnection(final Object data) { ConnectionEntityImpl connection = getMappedConnection(data); assert connection != null; return connection; } /** * @param data * @return */ ConnectionEntityImpl unmapConnection(final Object data) { ConnectionEntityImpl connection = dataConnection.remove(data); if (DebugPolicy.DEBUG_MAPPING) System.out.println(Thread.currentThread() + " UN-MAPPED CONNECTION: " + data + " -> " + connection); return connection; } class DataElementMapImpl implements DataElementMap { @Override public Object getData(IDiagram d, IElement element) { if (d == null) throw new NullPointerException("null diagram"); if (element == null) throw new NullPointerException("null element"); assert ElementUtils.getDiagram(element) == d; return element.getHint(ElementHints.KEY_OBJECT); } @Override public IElement getElement(IDiagram d, Object data) { if (d == null) throw new NullPointerException("null diagram"); if (data == null) throw new NullPointerException("null data"); GraphToDiagramUpdater updater = currentUpdater; if (updater != null) { // This HACK is for allowing GraphElementFactory implementations // to find the IElements they are related to. IElement e = updater.addedElementMap.get(data); if (e != null) return e; } IElement e = getMappedElement(data); if (e != null) return e; return null; } } class SubstituteElementClassImpl implements SubstituteElementClass { @Override public ElementClass substitute(IDiagram d, ElementClass ec) { if (d != diagram) throw new IllegalArgumentException("specified diagram does not have this SubstituteElementClass handler"); // If the element class is our own, there's no point in creating // a copy of it. if (ec.contains(elementLayerListener)) return ec; List all = ec.getAll(); List result = new ArrayList(all.size()); for (ElementHandler eh : all) { if (eh instanceof ElementLayerListenerImpl) result.add(elementLayerListener); else result.add(eh); } return ElementClass.compile(result).setId(ec.getId()); } } final DataElementMapImpl dataElementMap = new DataElementMapImpl(); final SubstituteElementClassImpl substituteElementClass = new SubstituteElementClassImpl(); // ------------------------------------------------------------------------ // BI-DIRECTIONAL DIAGRAM ELEMENT <-> BACKEND OBJECT MAPPING END // >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> void warning(String message, Exception e) { errorHandler.warning(message, e); } void warning(Exception e) { errorHandler.warning(e.getMessage(), e); } void error(String message, Throwable e) { errorHandler.error(message, e); } void error(Throwable e) { errorHandler.error(e.getMessage(), e); } // <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< // GRAPH MODIFICATION QUEUE BEGIN // ------------------------------------------------------------------------ ModificationQueue modificationQueue; IModifiableSynchronizationContext synchronizationContext; @Override public T set(Key key, Object value) { if (synchronizationContext == null) return null; return synchronizationContext.set(key, value); } @Override public T get(Key key) { if (synchronizationContext == null) return null; return synchronizationContext.get(key); } // ------------------------------------------------------------------------ // GRAPH MODIFICATION QUEUE END // >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> /** * The previously loaded version of the diagram content. This is needed to * calculate the difference between new and old content on each * {@link #diagramGraphUpdater(DiagramContents)} invocation. */ DiagramContents previousContent; /** * The diagram instance that this synchronizer is synchronizing with the * graph. */ IDiagram diagram; /** * An observer for diagram profile entries. Has a life-cycle that must be * bound to the life-cycle of this GraphToDiagramSynchronizer instance. * Disposed if synchronizer is detached in {@link #doDispose()} or finally * when the canvas is disposed. */ ProfileObserver profileObserver; IElementClassProvider elementClassProvider; BasicResources br; // <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< // Internal state machine handling BEGIN // ------------------------------------------------------------------------ /** * An indicator for the current state of this synchronizer. This is a simple * state machine with the following possible state transitions: * *
    *
  • INITIAL -> LOADING, DISPOSED
  • *
  • LOADING -> IDLE
  • *
  • IDLE -> UPDATING_DIAGRAM, DISPOSED
  • *
  • UPDATING_DIAGRAM -> IDLE
  • *
* * Start states: INITIAL * End states: DISPOSED */ static enum State { /** * The initial state of the synchronizer. */ INITIAL, /** * The synchronizer is performing load-time initialization. During this * time no canvas refreshes should be forced. */ LOADING, /** * The synchronizer is performing updates to the diagram model. This * process goes on in the canvas context thread. */ UPDATING_DIAGRAM, /** * The synchronizer is doing nothing. */ IDLE, /** * The synchronized diagram is being disposed, which means that this * synchronizer should not accept any further actions. */ DISPOSED, } public static final EnumSet FROM_INITIAL = EnumSet.of(State.LOADING, State.DISPOSED); public static final EnumSet FROM_LOADING = EnumSet.of(State.IDLE); public static final EnumSet FROM_UPDATING_DIAGRAM = EnumSet.of(State.IDLE); public static final EnumSet FROM_IDLE = EnumSet.of(State.UPDATING_DIAGRAM, State.DISPOSED); public static final EnumSet NO_STATES = EnumSet.noneOf(State.class); private EnumSet validTargetStates(State start) { switch (start) { case INITIAL: return FROM_INITIAL; case LOADING: return FROM_LOADING; case UPDATING_DIAGRAM: return FROM_UPDATING_DIAGRAM; case IDLE: return FROM_IDLE; case DISPOSED: return NO_STATES; } throw new IllegalArgumentException("unrecognized state " + start); } private String validateStateChange(State start, State end) { EnumSet validTargets = validTargetStates(start); if (!validTargets.contains(end)) return "Cannot transition from " + start + " state to " + end + "."; return null; } /** * The current state of the synchronizer. At start it is * {@link State#INITIAL} and after loading it is {@link State#IDLE}. */ State synchronizerState = State.INITIAL; /** * A condition variable used to synchronize synchronizer state changes. */ ReentrantLock stateLock = new ReentrantLock(); /** * A condition that is signaled when the synchronizer state changes to IDLE. */ Condition idleCondition = stateLock.newCondition(); State getState() { return synchronizerState; } /** * Activates the desired state after making sure that the synchronizer has * been IDLE in between its current state and this invocation. * * @param newState the new state to activate * @throws InterruptedException if waiting for IDLE state gets interrupted * @throws IllegalStateException if the requested transition from the * current state to the desired state would be illegal. */ void activateState(State newState, boolean waitForIdle) throws InterruptedException { stateLock.lock(); try { // Wait until the state of the synchronizer IDLEs if necessary. if (waitForIdle && synchronizerState != State.IDLE) { String error = validateStateChange(synchronizerState, State.IDLE); if (error != null) throw new IllegalStateException(error); while (synchronizerState != State.IDLE) { if (DebugPolicy.DEBUG_STATE) System.out.println(Thread.currentThread() + " waiting for IDLE state, current=" + synchronizerState); idleCondition.await(); } } String error = validateStateChange(synchronizerState, newState); if (error != null) throw new IllegalStateException(error); if (DebugPolicy.DEBUG_STATE) System.out.println(Thread.currentThread() + " activated state " + newState); this.synchronizerState = newState; if (newState == State.IDLE) idleCondition.signalAll(); } finally { stateLock.unlock(); } } void idle() throws IllegalStateException, InterruptedException { activateState(State.IDLE, false); } static interface StateRunnable extends Runnable { void execute() throws InvocationTargetException; public abstract class Stub implements StateRunnable { @Override public void run() { } @Override public final void execute() throws InvocationTargetException { try { perform(); } catch (Exception e) { throw new InvocationTargetException(e); } catch (LinkageError e) { throw new InvocationTargetException(e); } } protected abstract void perform() throws Exception; } } protected void runInState(State state, StateRunnable runnable) throws InvocationTargetException { try { activateState(state, true); try { runnable.execute(); } finally { idle(); } } catch (IllegalStateException e) { throw new InvocationTargetException(e); } catch (InterruptedException e) { throw new InvocationTargetException(e); } } protected void safeRunInState(State state, StateRunnable runnable) { try { runInState(state, runnable); } catch (InvocationTargetException e) { error("Failed to run runnable " + runnable + " in state " + state + ". See exception for details.", e .getCause()); } } // ------------------------------------------------------------------------ // Internal state machine handling END // >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> /** * @param processor * @param canvas * @param elementClassProvider * @throws DatabaseException */ public GraphToDiagramSynchronizer(RequestProcessor processor, ICanvasContext canvas, IElementClassProvider elementClassProvider) throws DatabaseException { if (processor == null) throw new IllegalArgumentException("null processor"); if (canvas == null) throw new IllegalArgumentException("null canvas"); if (elementClassProvider == null) throw new IllegalArgumentException("null element class provider"); this.session = processor.getSession(); this.canvas = canvas; this.modificationQueue = new ModificationQueue(session, errorHandler); processor.syncRequest(new ReadRequest() { @Override public void run(ReadGraph graph) throws DatabaseException { initializeResources(graph); } }); this.elementClassProvider = elementClassProvider; synchronizationContext.set(SynchronizationHints.ELEMENT_CLASS_PROVIDER, elementClassProvider); attachSessionListener(processor.getSession()); } /** * @return */ public IElementClassProvider getElementClassProvider() { return elementClassProvider; } public Session getSession() { return session; } public ICanvasContext getCanvasContext() { return canvas; } public IDiagram getDiagram() { return diagram; } void setCanvasDirty() { ICanvasContext c = canvas; if (synchronizerState != State.LOADING && c != null && !c.isDisposed()) { // TODO: Consider adding an invocation limiter here, to prevent // calling setDirty too often if enough time hasn't passed yet since // the last invocation. c.getContentContext().setDirty(); } } /** * @param elementType * @return * @throws DatabaseException if ElementClass cannot be retrieved */ public ElementClass getNodeClass(Resource elementType) throws DatabaseException { return getNodeClass(session, elementType); } public ElementClass getNodeClass(RequestProcessor processor, Resource elementType) throws DatabaseException { ElementClass ec = processor.syncRequest(new NodeClassRequest(canvas, diagram, elementType, true)); return ec; } @Override protected void doDispose() { try { try { stateLock.lock(); boolean isInitial = getState() == State.INITIAL; activateState(State.DISPOSED, !isInitial); } finally { stateLock.unlock(); } } catch (InterruptedException e) { // Shouldn't happen. e.printStackTrace(); } finally { detachSessionListener(); if (profileObserver != null) { profileObserver.dispose(); profileObserver = null; } if (diagram != null) { diagram.removeCompositionListener(diagramListener); diagram.removeCompositionVetoListener(diagramListener); } // TODO: we should probably leave the dataElement map as is since DataElementMap needs it even after the synchronizer has been disposed. // Currently the diagram's DataElementMap will be broken after disposal. // dataElement.clear(); // dataConnection.clear(); if (layerManager != null) { layerManager.dispose(); } // Let GC work. modificationQueue.dispose(); } } void initializeResources(ReadGraph graph) { this.br = new BasicResources(graph); // Initialize synchronization context synchronizationContext = new GraphSynchronizationContext(graph, modificationQueue); synchronizationContext.set(SynchronizationHints.ERROR_HANDLER, errorHandler); } // <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< // LAYERS BEGIN // ------------------------------------------------------------------------ GraphLayerManager layerManager; /** * A common handler for all elements that is used to listen to changes in * element visibility and focusability on diagram layers. */ class ElementLayerListenerImpl implements ElementLayerListener { private static final long serialVersionUID = -3410052116598828129L; @Override public void visibilityChanged(IElement e, ILayer layer, boolean visible) { if (!isAlive()) return; if (DebugPolicy.DEBUG_LAYERS) System.out.println("visibility changed: " + e + ", " + layer + ", " + visible); GraphLayer gl = layerManager.getGraphLayer(layer.getName()); if (gl != null) { changeTag(e, gl.getVisible(), visible); } } @Override public void focusabilityChanged(IElement e, ILayer layer, boolean focusable) { if (!isAlive()) return; if (DebugPolicy.DEBUG_LAYERS) System.out.println("focusability changed: " + e + ", " + layer + ", " + focusable); GraphLayer gl = layerManager.getGraphLayer(layer.getName()); if (gl != null) { changeTag(e, gl.getFocusable(), focusable); } } void changeTag(IElement e, Resource tag, boolean set) { Object object = e.getHint(ElementHints.KEY_OBJECT); Resource tagged = null; if (object instanceof Resource) { tagged = (Resource) object; } else if (object instanceof EdgeResource) { ConnectionEntity ce = e.getHint(ElementHints.KEY_CONNECTION_ENTITY); if (ce instanceof ConnectionEntityImpl) { tagged = ((ConnectionEntityImpl) ce).connection; } } if (tagged == null) return; modificationQueue.async(new TagChange(tagged, tag, set), null); } }; ElementLayerListenerImpl elementLayerListener = new ElementLayerListenerImpl(); // ------------------------------------------------------------------------ // LAYERS END // >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> @Override public IDiagram loadDiagram(IProgressMonitor progressMonitor, ReadGraph g, final String modelURI, final Resource diagram, final Resource runtime, final ResourceArray structuralPath, IHintObservable initialHints) throws DatabaseException { if (DebugPolicy.DEBUG_LOAD) System.out.println(Thread.currentThread() + " loadDiagram: " + NameUtils.getSafeName(g, diagram)); SubMonitor monitor = SubMonitor.convert(progressMonitor, "Load Diagram", 100); Object loadTask = Timing.BEGIN("GDS.loadDiagram"); try { try { activateState(State.LOADING, false); } catch (IllegalStateException e) { // Disposed already before loading even began. this.diagram = Diagram.spawnNew(DiagramClass.DEFAULT); return this.diagram; } try { // Query for diagram class Resource diagramClassResource = g.getPossibleType(diagram, br.DIA.Composite); if (diagramClassResource != null) { // Spawn new diagram Object task = Timing.BEGIN("GDS.DiagramClassRequest"); final DiagramClass diagramClass = g.syncRequest(new DiagramClassRequest(diagram)); Timing.END(task); final IDiagram d = Diagram.spawnNew(diagramClass); { d.setHint(DiagramModelHints.KEY_DIAGRAM_RESOURCE, diagram); if (runtime != null) d.setHint(DiagramModelHints.KEY_DIAGRAM_RUNTIME_RESOURCE, runtime); if (modelURI != null) d.setHint(DiagramModelHints.KEY_DIAGRAM_MODEL_URI, modelURI); d.setHint(DiagramModelHints.KEY_DIAGRAM_RESOURCE_ARRAY, structuralPath); // Set dumb default routing when DiagramClass does not // predefine the default connection routing for the diagram. if (!d.containsHint(DiagramHints.ROUTE_ALGORITHM)) d.setHint(DiagramHints.ROUTE_ALGORITHM, RouterFactory.create(true, false)); d.setHint(SynchronizationHints.CONTEXT, this); // Initialize hints with hints from initialHints if given if (initialHints != null) { d.setHints(initialHints.getHints()); } } // ITask task2 = ThreadLogger.getInstance().begin("loadLayers"); monitor.subTask("Layers"); { this.layerManager = new GraphLayerManager(g, modificationQueue, diagram); synchronizationContext.set(GraphSynchronizationHints.GRAPH_LAYER_MANAGER, this.layerManager); ILayersEditor layers = layerManager.loadLayers(d, g, diagram); // task2.finish(); d.setHint(DiagramHints.KEY_LAYERS, layers); d.setHint(DiagramHints.KEY_LAYERS_EDITOR, layers); d.addCompositionVetoListener(diagramListener); d.addCompositionListener(diagramListener); this.diagram = d; d.setHint(DiagramHints.KEY_MUTATOR, new DefaultDiagramMutator(d, diagram, synchronizationContext)); // Add default layer if no layers exist. // NOTE: this must be done after this.diagram has been set // as it will trigger a graph modification which needs the // diagram resource. // ITask task3 = ThreadLogger.getInstance().begin("addDefaultLayer"); // if (layers.getLayers().isEmpty()) { // if (DebugPolicy.DEBUG_LAYERS) // System.out.println("No layers, creating default layer '" // + DiagramConstants.DEFAULT_LAYER_NAME + "'"); // SimpleLayer defaultLayer = new SimpleLayer(DiagramConstants.DEFAULT_LAYER_NAME); // layers.addLayer(defaultLayer); // layers.activate(defaultLayer); // } // // task3.finish(); } monitor.worked(10); monitor.subTask("Contents"); // Discover the plain resources that form the content of the // diagram through a separate query. This allows us to // separately // track changes to the diagram structure itself, not the // substructures contained by the structure elements. ITask task4 = ThreadLogger.getInstance().begin("DiagramContentRequest1"); DiagramContentRequest query = new DiagramContentRequest(canvas, diagram, errorHandler); g.syncRequest(query, new DiagramContentListener(diagram)); task4.finish(); // ITask task5 = ThreadLogger.getInstance().begin("DiagramContentRequest2"); ITask task42 = ThreadLogger.getInstance().begin("DiagramContentRequest2"); DiagramContents contents = g.syncRequest(query); task42.finish(); // task5.finish(); monitor.worked(10); monitor.subTask("Graphical elements"); { Object applyDiagramContents = Timing.BEGIN("GDS.applyDiagramContents"); ITask task6 = ThreadLogger.getInstance().begin("applyDiagramContents"); processGraphUpdates(g, Collections.singleton(diagramGraphUpdater(contents))); task6.finish(); Timing.END(applyDiagramContents); } monitor.worked(80); DataNodeMap dn = new DataNodeMap() { @Override public INode getNode(Object data) { if (DataNodeConstants.CANVAS_ROOT == data) return canvas.getCanvasNode(); if (DataNodeConstants.DIAGRAM_ELEMENT_PARENT == data) { ElementPainter ep = canvas.getAtMostOneItemOfClass(ElementPainter.class); return ep != null ? ep.getDiagramElementParentNode() : null; } DataElementMap emap = GraphToDiagramSynchronizer.this.diagram.getDiagramClass().getSingleItem(DataElementMap.class); IElement element = emap.getElement(GraphToDiagramSynchronizer.this.diagram, data); if(element == null) return null; return element.getHint(ElementHints.KEY_SG_NODE); } }; profileObserver = new ProfileObserver(g.getSession(), runtime, canvas.getThreadAccess(), canvas, canvas.getSceneGraph(), diagram, ArrayMap.keys(ProfileKeys.DIAGRAM, ProfileKeys.CANVAS, ProfileKeys.NODE_MAP).values(GraphToDiagramSynchronizer.this.diagram, canvas, dn), new CanvasNotification(canvas)); profileObserver.listen(g, GraphToDiagramSynchronizer.this); return d; } this.diagram = Diagram.spawnNew(DiagramClass.DEFAULT); return this.diagram; } finally { idle(); } } catch (InterruptedException e) { throw new RuntimeException(e); } catch (IllegalStateException e) { // If the synchronizer was disposed ahead of time, it was done // for a reason, such as the user having closed the owner editor. if (!isAlive()) throw new CancelTransactionException(e); throw new RuntimeException(e); } finally { Timing.END(loadTask); } } static class CanvasNotification implements Runnable { final private ICanvasContext canvas; public CanvasNotification(ICanvasContext canvas) { this.canvas = canvas; } public void run() { canvas.getContentContext().setDirty(); } } ArrayList pendingModifications = new ArrayList(); MapSet modificationIndex = new MapSet.Hash(); void addModification(IElement element, IModification modification) { pendingModifications.add(modification); if (element != null) modificationIndex.add(element, modification); } class DefaultDiagramMutator implements DiagramMutator { Map creation = new HashMap(); IDiagram d; Resource diagram; IModifiableSynchronizationContext synchronizationContext; public DefaultDiagramMutator(IDiagram d, Resource diagram, IModifiableSynchronizationContext synchronizationContext) { this.d = d; this.diagram = diagram; this.synchronizationContext = synchronizationContext; if (synchronizationContext.get(SynchronizationHints.ELEMENT_CLASS_PROVIDER) == null) throw new IllegalArgumentException("SynchronizationHints.ELEMENT_CLASS_PROVIDER not available"); } void assertNotDisposed() { if (!isAlive()) throw new IllegalStateException(getClass().getSimpleName() + " is disposed"); } @Override public IElement newElement(ElementClass clazz) { assertNotDisposed(); ElementFactory ef = d.getDiagramClass().getAtMostOneItemOfClass(ElementFactory.class); IElement element = null; if (ef != null) element = ef.spawnNew(clazz); else element = Element.spawnNew(clazz); element.setHint(ElementHints.KEY_OBJECT, new TransientElementObject()); addModification(element, new AddElement(synchronizationContext, d, element)); return element; } @Override public void commit() { assertNotDisposed(); if (DebugPolicy.DEBUG_MUTATOR_COMMIT) { System.out.println("DiagramMutator is about to commit changes:"); for (IModification mod : pendingModifications) System.out.println("\t- " + mod); } Collections.sort(pendingModifications); if (DebugPolicy.DEBUG_MUTATOR_COMMIT) { if (pendingModifications.size() > 1) { System.out.println("* changes were re-ordered to:"); for (IModification mod : pendingModifications) System.out.println("\t" + mod); } } Timing.safeTimed(errorHandler, "QUEUE AND WAIT FOR MODIFICATIONS TO FINISH", new GTask() { @Override public void run() throws DatabaseException { // Performs a separate write request and query result update // for each modification // for (IModification mod : pendingModifications) { // try { // modificationQueue.sync(mod); // } catch (InterruptedException e) { // error("Pending diagram modification " + mod // + " was interrupted. See exception for details.", e); // } // } // NOTE: this is still under testing, the author is not // truly certain that it should work in all cases ATM. // Performs all modifications with in a single write request for (IModification mod : pendingModifications) { modificationQueue.offer(mod, null); } try { // Perform the modifications in a single request. modificationQueue.finish(); } catch (InterruptedException e) { errorHandler.error("Diagram modification finishing was interrupted. See exception for details.", e); } } }); pendingModifications.clear(); modificationIndex.clear(); creation.clear(); if (DebugPolicy.DEBUG_MUTATOR_COMMIT) System.out.println("DiagramMutator has committed"); } @Override public void clear() { assertNotDisposed(); pendingModifications.clear(); modificationIndex.clear(); creation.clear(); if (DebugPolicy.DEBUG_MUTATOR) System.out.println("DiagramMutator has been cleared"); } @Override public void modifyTransform(IElement element) { assertNotDisposed(); Resource resource = backendObject(element); AffineTransform tr = element.getHint(ElementHints.KEY_TRANSFORM); if (resource != null && tr != null) { addModification(element, new TransformElement(resource, tr)); } } @Override public void synchronizeHintsToBackend(IElement element) { assertNotDisposed(); IHintSynchronizer synchronizer = element.getHint(SynchronizationHints.HINT_SYNCHRONIZER); if (synchronizer != null) { CollectingModificationQueue queue = new CollectingModificationQueue(); synchronizer.synchronize(synchronizationContext, element); addModification(element, new CompositeModification(ModificationAdapter.LOW_PRIORITY, queue.getQueue())); } } @Override public void synchronizeElementOrder() { assertNotDisposed(); List snapshot = d.getSnapshot(); addModification(null, new ElementReorder(d, snapshot)); } @Override public void register(IElement element, Object object) { creation.put(element, (Resource) object); } @SuppressWarnings("unchecked") @Override public T backendObject(IElement element) { Object object = ElementUtils.getObject(element); if (object instanceof Resource) return (T) object; else return (T) creation.get(element); } } // <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< // GRAPH TO DIAGRAM SYCHRONIZATION LOGIC BEGIN // ------------------------------------------------------------------------ static class ConnectionData { ConnectionEntityImpl impl; List branchPoints = new ArrayList(); List segments = new ArrayList(); ConnectionData(ConnectionEntityImpl ce) { this.impl = ce; } void addBranchPoint(Resource bp) { branchPoints.add(bp); } void addSegment(EdgeResource seg) { segments.add(seg); } } class GraphToDiagramUpdater { DiagramContents lastContent; DiagramContents content; DiagramContentChanges changes; final List addedElements; final List removedElements; final List addedConnectionSegments; final List removedConnectionSegments; final List addedBranchPoints; final List removedBranchPoints; final Map addedElementMap; final Map addedConnectionMap; final Map addedConnectionEntities; final List removedConnectionEntities; final Map changedConnectionEntities; final Map addedRouteGraphConnectionMap; final List removedRouteGraphConnections; GraphToDiagramUpdater(DiagramContents lastContent, DiagramContents content, DiagramContentChanges changes) { this.lastContent = lastContent; this.content = content; this.changes = changes; this.addedElements = new ArrayList(changes.elements.size() + changes.branchPoints.size()); this.removedElements = new ArrayList(changes.elements.size() + changes.branchPoints.size()); this.addedConnectionSegments = new ArrayList(content.connectionSegments.size()); this.removedConnectionSegments = new ArrayList(content.connectionSegments.size()); this.addedBranchPoints = new ArrayList(content.branchPoints.size()); this.removedBranchPoints = new ArrayList(content.branchPoints.size()); this.addedElementMap = new HashMap(); this.addedConnectionMap = new HashMap(); this.addedConnectionEntities = new HashMap(); this.removedConnectionEntities = new ArrayList(changes.connections.size()); this.changedConnectionEntities = new HashMap(); this.addedRouteGraphConnectionMap = new HashMap(); this.removedRouteGraphConnections = new ArrayList(changes.routeGraphConnections.size()); } public void clear() { // Prevent DiagramContents leakage through DisposableListeners. lastContent = null; content = null; changes = null; this.addedElements.clear(); this.removedElements.clear(); this.addedConnectionSegments.clear(); this.removedConnectionSegments.clear(); this.addedBranchPoints.clear(); this.removedBranchPoints.clear(); this.addedElementMap.clear(); this.addedConnectionMap.clear(); this.addedConnectionEntities.clear(); this.removedConnectionEntities.clear(); this.changedConnectionEntities.clear(); this.addedRouteGraphConnectionMap.clear(); this.removedRouteGraphConnections.clear(); } void processNodes(AsyncReadGraph graph) { for (Map.Entry entry : changes.elements.entrySet()) { final Resource element = entry.getKey(); Change change = entry.getValue(); switch (change) { case ADDED: { IElement mappedElement = getMappedElement(element); if (mappedElement == null) { if (DebugPolicy.DEBUG_NODE_LOAD) graph.asyncRequest(new ReadRequest() { @Override public void run(ReadGraph graph) throws DatabaseException { System.out.println(" EXTERNALLY ADDED ELEMENT: " + NameUtils.getSafeName(graph, element) + " (" + element.getResourceId() + ")"); } }); if (content.connectionSet.contains(element)) { // TODO: Connection loading has no listening, changes :Connection will not be noticed by this code! Listener loadListener = new DisposableListener(canvasListenerSupport) { @Override public String toString() { return "Connection load listener for " + element; } @Override public void execute(IElement loaded) { // Invoked when the element has been loaded. if (DebugPolicy.DEBUG_CONNECTION_LISTENER) System.out.println("CONNECTION LoadListener for " + loaded); if (loaded == null) { disposeListener(); return; } Object data = loaded.getHint(ElementHints.KEY_OBJECT); // Logic for disposing listener if (!previousContent.connectionSet.contains(data)) { if (DebugPolicy.DEBUG_CONNECTION_LISTENER) System.out.println("CONNECTION LoadListener, connection not in current content: " + data + ". Disposing."); disposeListener(); return; } if (addedElementMap.containsKey(data)) { // This element was just loaded, in // which case its hints need to // uploaded to the real mapped // element immediately. IElement mappedElement = getMappedElement(data); if (DebugPolicy.DEBUG_CONNECTION_LISTENER) System.out.println("LOADED ADDED CONNECTION, currently mapped connection: " + mappedElement); if (mappedElement != null && (mappedElement instanceof Element)) { if (DebugPolicy.DEBUG_CONNECTION_LISTENER) { System.out.println(" mapped hints: " + mappedElement.getHints()); System.out.println(" loaded hints: " + loaded.getHints()); } updateMappedElement((Element) mappedElement, loaded); } } else { // This element was already loaded. // Just schedule an update some time // in the future. if (DebugPolicy.DEBUG_CONNECTION_LISTENER) System.out.println("PREVIOUSLY LOADED CONNECTION UPDATED, scheduling update into the future"); offerGraphUpdate( connectionUpdater(element, loaded) ); } } }; graph.asyncRequest(new ConnectionRequest(canvas, diagram, element, errorHandler, loadListener), new AsyncProcedure() { @Override public void execute(AsyncReadGraph graph, final IElement e) { if (e == null) return; //System.out.println("ConnectionRequestProcedure " + e); mapElement(element, e); synchronized (GraphToDiagramUpdater.this) { addedElements.add(e); addedElementMap.put(element, e); addedConnectionMap.put(element, e); } // Read connection type graph.forSingleType(element, br.DIA.Connection, new Procedure() { @Override public void exception(Throwable t) { error(t); } @Override public void execute(Resource connectionType) { synchronized (GraphToDiagramUpdater.this) { //System.out.println("new connection entity " + e); ConnectionEntityImpl entity = new ConnectionEntityImpl(element, connectionType, e); e.setHint(ElementHints.KEY_CONNECTION_ENTITY, entity); addedConnectionEntities.put(element, entity); } } }); } @Override public void exception(AsyncReadGraph graph, Throwable throwable) { error(throwable); } }); } else if (content.nodeSet.contains(element)) { Listener loadListener = new DisposableListener(canvasListenerSupport) { @Override public String toString() { return "Node load listener for " + element; } @Override public void execute(IElement loaded) { // Invoked when the element has been loaded. if (DebugPolicy.DEBUG_NODE_LISTENER) System.out.println("NODE LoadListener for " + loaded); if (loaded == null) { disposeListener(); return; } Object data = loaded.getHint(ElementHints.KEY_OBJECT); // Logic for disposing listener if (!previousContent.nodeSet.contains(data)) { if (DebugPolicy.DEBUG_NODE_LISTENER) System.out.println("NODE LoadListener, node not in current content: " + data + ". Disposing."); disposeListener(); return; } if (addedElementMap.containsKey(data)) { // This element was just loaded, in // which case its hints need to // uploaded to the real mapped // element immediately. IElement mappedElement = getMappedElement(data); if (DebugPolicy.DEBUG_NODE_LISTENER) System.out.println("LOADED ADDED ELEMENT, currently mapped element: " + mappedElement); if (mappedElement != null && (mappedElement instanceof Element)) { if (DebugPolicy.DEBUG_NODE_LISTENER) { System.out.println(" mapped hints: " + mappedElement.getHints()); System.out.println(" loaded hints: " + loaded.getHints()); } updateMappedElement((Element) mappedElement, loaded); } } else { // This element was already loaded. // Just schedule an update some time // in the future. if (DebugPolicy.DEBUG_NODE_LISTENER) System.out.println("PREVIOUSLY LOADED NODE UPDATED, scheduling update into the future"); offerGraphUpdate( nodeUpdater(element, loaded) ); } } }; //System.out.println("NODE REQUEST: " + element); graph.asyncRequest(new NodeRequest(canvas, diagram, element, loadListener), new AsyncProcedure() { @Override public void execute(AsyncReadGraph graph, IElement e) { if (e == null) return; // This is invoked before the element is actually loaded. //System.out.println("NodeRequestProcedure " + e); if (DebugPolicy.DEBUG_NODE_LOAD) System.out.println("MAPPING ADDED NODE: " + element + " -> " + e); mapElement(element, e); synchronized (GraphToDiagramUpdater.this) { addedElements.add(e); addedElementMap.put(element, e); } } @Override public void exception(AsyncReadGraph graph, Throwable throwable) { error(throwable); } }); } else { // warning("Diagram elements must be either elements or connections, " // + NameUtils.getSafeName(g, element) + " is neither", // new AssumptionException("")); } } break; } case REMOVED: { IElement e = getMappedElement(element); if (DebugPolicy.DEBUG_NODE_LOAD) graph.asyncRequest(new ReadRequest() { @Override public void run(ReadGraph graph) throws DatabaseException { System.out.println(" EXTERNALLY REMOVED ELEMENT: " + NameUtils.getSafeName(graph, element) + " (" + element.getResourceId() + ")"); } }); if (e != null) { removedElements.add(e); } break; } } } } void gatherChangedConnectionParts(Map changes) { for (Map.Entry entry : changes.entrySet()) { Object part = entry.getKey(); Change change = entry.getValue(); switch (change) { case ADDED: { synchronized (GraphToDiagramUpdater.this) { Resource connection = content.partToConnection.get(part); assert connection != null; IElement ce = getMappedElement(connection); if (ce == null) ce = addedElementMap.get(connection); if (ce != null) markConnectionChanged(ce); break; } } case REMOVED: { if (lastContent == null) break; Resource connection = lastContent.partToConnection.get(part); if (connection != null && content.connectionSet.contains(connection)) { markConnectionChanged(connection); } break; } } } } void markConnectionChanged(Resource connection) { // System.out.println("markConnectionChanged"); ConnectionEntityImpl ce = getMappedConnection(connection); if (ce != null) { markConnectionChanged(ce); return; } error("WARNING: marking connection entity " + connection + " changed, but the connection was not previously mapped", new Exception("created exception to get a stack trace")); } void markConnectionChanged(IElement connection) { ConnectionEntityImpl entity = connection.getHint(ElementHints.KEY_CONNECTION_ENTITY); if (entity != null) markConnectionChanged(entity); } void markConnectionChanged(ConnectionEntityImpl ce) { if (!changedConnectionEntities.containsKey(ce)) { changedConnectionEntities.put(ce, new ConnectionData(ce)); } } void processConnections() { // Find added/removed connection segments/branch points // in order to find all changed connection entities. gatherChangedConnectionParts(changes.connectionSegments); gatherChangedConnectionParts(changes.branchPoints); // Find removed connection entities for (Map.Entry entry : changes.connections.entrySet()) { Resource ce = entry.getKey(); Change change = entry.getValue(); switch (change) { case REMOVED: { removedConnectionEntities.add(ce); } } } // Generate update data of changed connection entities. // This ConnectionData will be applied in the canvas thread // diagram updater. for (ConnectionData cd : changedConnectionEntities.values()) { for (Object part : content.connectionToParts.getValuesUnsafe(cd.impl.connection)) { if (part instanceof Resource) { cd.branchPoints.add((Resource) part); } else if (part instanceof EdgeResource) { cd.segments.add((EdgeResource) part); } } } } void processRouteGraphConnections(AsyncReadGraph graph) { for (Map.Entry entry : changes.routeGraphConnections.entrySet()) { final Resource connection = entry.getKey(); Change change = entry.getValue(); switch (change) { case ADDED: { IElement mappedElement = getMappedElement(connection); if (mappedElement != null) continue; Listener loadListener = new DisposableListener(canvasListenerSupport) { @Override public String toString() { return "processRouteGraphConnections " + connection; } @Override public void execute(IElement loaded) { // Invoked when the element has been loaded. if (DebugPolicy.DEBUG_CONNECTION_LISTENER) System.out.println("ROUTE GRAPH CONNECTION LoadListener for " + loaded); if (loaded == null) { disposeListener(); return; } Object data = loaded.getHint(ElementHints.KEY_OBJECT); // Logic for disposing listener if (!previousContent.routeGraphConnectionSet.contains(data)) { if (DebugPolicy.DEBUG_CONNECTION_LISTENER) System.out.println("ROUTE GRAPH CONNECTION LoadListener, connection not in current content: " + data + ". Disposing."); disposeListener(); return; } if (addedElementMap.containsKey(data)) { // This element was just loaded, in // which case its hints need to // uploaded to the real mapped // element immediately. IElement mappedElement = getMappedElement(data); if (DebugPolicy.DEBUG_CONNECTION_LISTENER) System.out.println("LOADED ADDED ROUTE GRAPH CONNECTION, currently mapped connection: " + mappedElement); if (mappedElement instanceof Element) { if (DebugPolicy.DEBUG_CONNECTION_LISTENER) { System.out.println(" mapped hints: " + mappedElement.getHints()); System.out.println(" loaded hints: " + loaded.getHints()); } updateMappedElement((Element) mappedElement, loaded); } } else { // This element was already loaded. // Just schedule an update some time // in the future. if (DebugPolicy.DEBUG_CONNECTION_LISTENER) System.out.println("PREVIOUSLY LOADED ROUTE GRAPH CONNECTION UPDATED, scheduling update into the future: " + connection); Set dirtyNodes = new THashSet(4); IElement mappedElement = getMappedElement(connection); ConnectionEntity ce = mappedElement.getHint(ElementHints.KEY_CONNECTION_ENTITY); if (ce != null) { for (Connection conn : ce.getTerminalConnections(null)) { Object o = conn.node.getHint(ElementHints.KEY_OBJECT); if (o != null) { dirtyNodes.add(o); if (DebugPolicy.DEBUG_CONNECTION_LISTENER) System.out.println("Marked connectivity dirty for node: " + conn.node); } } } offerGraphUpdate( routeGraphConnectionUpdater(connection, loaded, dirtyNodes) ); } } }; graph.asyncRequest(new ConnectionRequest(canvas, diagram, connection, errorHandler, loadListener), new Procedure() { @Override public void execute(final IElement e) { if (e == null) return; //System.out.println("ConnectionRequestProcedure " + e); if (DebugPolicy.DEBUG_NODE_LOAD) System.out.println("MAPPING ADDED ROUTE GRAPH CONNECTION: " + connection + " -> " + e); mapElement(connection, e); synchronized (GraphToDiagramUpdater.this) { addedElements.add(e); addedElementMap.put(connection, e); addedRouteGraphConnectionMap.put(connection, e); } } @Override public void exception(Throwable throwable) { error(throwable); } }); break; } case REMOVED: { IElement e = getMappedElement(connection); if (e != null) removedRouteGraphConnections.add(e); break; } } } } ConnectionEntityImpl getConnectionEntity(Object connectionPart) { Resource connection = content.partToConnection.get(connectionPart); assert connection != null; ConnectionEntityImpl ce = addedConnectionEntities.get(connection); if (ce != null) return ce; return assertMappedConnection(connection); } void processBranchPoints(AsyncReadGraph graph) { for (Map.Entry entry : changes.branchPoints.entrySet()) { final Resource element = entry.getKey(); Change change = entry.getValue(); switch (change) { case ADDED: { IElement mappedElement = getMappedElement(element); if (mappedElement == null) { if (DebugPolicy.DEBUG_NODE_LOAD) graph.asyncRequest(new ReadRequest() { @Override public void run(ReadGraph graph) throws DatabaseException { System.out.println(" EXTERNALLY ADDED BRANCH POINT: " + NameUtils.getSafeName(graph, element) + " (" + element.getResourceId() + ")"); } }); Listener loadListener = new DisposableListener(canvasListenerSupport) { @Override public String toString() { return "processBranchPoints for " + element; } @Override public void execute(IElement loaded) { // Invoked when the element has been loaded. if (DebugPolicy.DEBUG_NODE_LISTENER) System.out.println("BRANCH POINT LoadListener for " + loaded); if (loaded == null) { disposeListener(); return; } Object data = loaded.getHint(ElementHints.KEY_OBJECT); if (addedElementMap.containsKey(data)) { // This element was just loaded, in // which case its hints need to // uploaded to the real mapped // element immediately. IElement mappedElement = getMappedElement(data); if (DebugPolicy.DEBUG_NODE_LISTENER) System.out.println("LOADED ADDED BRANCH POINT, currently mapped element: " + mappedElement); if (mappedElement != null && (mappedElement instanceof Element)) { if (DebugPolicy.DEBUG_NODE_LISTENER) { System.out.println(" mapped hints: " + mappedElement.getHints()); System.out.println(" loaded hints: " + loaded.getHints()); } updateMappedElement((Element) mappedElement, loaded); } } else { // This element was already loaded. // Just schedule an update some time // in the future. if (DebugPolicy.DEBUG_NODE_LISTENER) System.out.println("PREVIOUSLY LOADED BRANCH POINT UPDATED, scheduling update into the future"); offerGraphUpdate( nodeUpdater(element, loaded) ); } } }; graph.asyncRequest(new NodeRequest(canvas, diagram, element, loadListener), new AsyncProcedure() { @Override public void execute(AsyncReadGraph graph, IElement e) { if (e != null) { mapElement(element, e); synchronized (GraphToDiagramUpdater.this) { addedBranchPoints.add(e); addedElementMap.put(element, e); ConnectionEntityImpl ce = getConnectionEntity(element); e.setHint(ElementHints.KEY_CONNECTION_ENTITY, ce); e.setHint(ElementHints.KEY_PARENT_ELEMENT, ce.getConnectionElement()); } } } @Override public void exception(AsyncReadGraph graph, Throwable throwable) { error(throwable); } }); } break; } case REMOVED: { IElement e = getMappedElement(element); if (DebugPolicy.DEBUG_NODE_LOAD) graph.asyncRequest(new ReadRequest() { @Override public void run(ReadGraph graph) throws DatabaseException { System.out.println(" EXTERNALLY REMOVED BRANCH POINT: " + NameUtils.getSafeName(graph, element) + " (" + element.getResourceId() + ")"); } }); if (e != null) { removedBranchPoints.add(e); } break; } } } } void processConnectionSegments(AsyncReadGraph graph) { ConnectionSegmentAdapter adapter = connectionSegmentAdapter; for (Map.Entry entry : changes.connectionSegments.entrySet()) { final EdgeResource seg = entry.getKey(); Change change = entry.getValue(); switch (change) { case ADDED: { IElement mappedElement = getMappedElement(seg); if (mappedElement == null) { if (DebugPolicy.DEBUG_EDGE_LOAD) graph.asyncRequest(new ReadRequest() { @Override public void run(ReadGraph graph) throws DatabaseException { System.out.println(" EXTERNALLY ADDED CONNECTION SEGMENT: " + seg.toString() + " - " + seg.toString(graph)); } }); graph.asyncRequest(new EdgeRequest(canvas, errorHandler, canvasListenerSupport, diagram, adapter, seg), new AsyncProcedure() { @Override public void execute(AsyncReadGraph graph, IElement e) { if (DebugPolicy.DEBUG_EDGE_LOAD) System.out.println("ADDED EDGE LOADED: " + e); if (e != null) { mapElement(seg, e); synchronized (GraphToDiagramUpdater.this) { addedConnectionSegments.add(e); addedElementMap.put(seg, e); ConnectionEntityImpl ce = getConnectionEntity(seg); e.setHint(ElementHints.KEY_CONNECTION_ENTITY, ce); e.setHint(ElementHints.KEY_PARENT_ELEMENT, ce.getConnectionElement()); } } } @Override public void exception(AsyncReadGraph graph, Throwable throwable) { error(throwable); } }); } break; } case REMOVED: { final IElement e = getMappedElement(seg); if (DebugPolicy.DEBUG_EDGE_LOAD) graph.asyncRequest(new ReadRequest() { @Override public void run(ReadGraph graph) throws DatabaseException { System.out.println(" EXTERNALLY REMOVED CONNECTION SEGMENT: " + seg.toString() + " - " + seg.toString(graph) + " -> " + e); } }); if (e != null) { removedConnectionSegments.add(e); } break; } } } } void executeDeferredLoaders(ReadGraph graph) throws DatabaseException { // The rest of the diagram loading passes Deque q1 = new ArrayDeque(); Deque q2 = new ArrayDeque(); collectElementLoaders(q1, addedElements); while (!q1.isEmpty()) { //System.out.println("DEFFERED LOADERS: " + q1); for (IElement e : q1) { ElementLoader loader = e.removeHint(DiagramModelHints.KEY_ELEMENT_LOADER); //System.out.println("EXECUTING DEFFERED LOADER: " + loader); loader.load(graph, diagram, e); } collectElementLoaders(q2, q1); Deque qt = q1; q1 = q2; q2 = qt; q2.clear(); } } private void collectElementLoaders(Queue queue, Collection cs) { for (IElement e : cs) { ElementLoader loader = e.getHint(DiagramModelHints.KEY_ELEMENT_LOADER); if (loader != null) queue.add(e); } } public void process(ReadGraph graph) throws DatabaseException { // No changes? Do nothing. if (changes.isEmpty()) return; // NOTE: This order is important. Object task = Timing.BEGIN("processNodesConnections"); //System.out.println("---- PROCESS NODES & CONNECTIONS BEGIN"); if (!changes.elements.isEmpty()) { graph.syncRequest(new AsyncReadRequest() { @Override public void run(AsyncReadGraph graph) { processNodes(graph); } @Override public String toString() { return "processNodes"; } }); } //System.out.println("---- PROCESS NODES & CONNECTIONS END"); processConnections(); //System.out.println("---- PROCESS BRANCH POINTS BEGIN"); if (!changes.branchPoints.isEmpty()) { graph.syncRequest(new AsyncReadRequest() { @Override public void run(AsyncReadGraph graph) { processBranchPoints(graph); } @Override public String toString() { return "processBranchPoints"; } }); } //System.out.println("---- PROCESS BRANCH POINTS END"); Timing.END(task); task = Timing.BEGIN("processConnectionSegments"); //System.out.println("---- PROCESS CONNECTION SEGMENTS BEGIN"); if (!changes.connectionSegments.isEmpty()) { graph.syncRequest(new AsyncReadRequest() { @Override public void run(AsyncReadGraph graph) { processConnectionSegments(graph); } @Override public String toString() { return "processConnectionSegments"; } }); } //System.out.println("---- PROCESS CONNECTION SEGMENTS END"); Timing.END(task); task = Timing.BEGIN("processRouteGraphConnections"); if (!changes.routeGraphConnections.isEmpty()) { graph.syncRequest(new AsyncReadRequest() { @Override public void run(AsyncReadGraph graph) { processRouteGraphConnections(graph); } @Override public String toString() { return "processRouteGraphConnections"; } }); } Timing.END(task); //System.out.println("---- AFTER LOADING"); //for (IElement e : addedElements) // System.out.println(" ADDED ELEMENT: " + e); //for (IElement e : addedBranchPoints) // System.out.println(" ADDED BRANCH POINTS: " + e); task = Timing.BEGIN("executeDeferredLoaders"); executeDeferredLoaders(graph); Timing.END(task); } public boolean isEmpty() { return addedElements.isEmpty() && removedElements.isEmpty() && addedConnectionSegments.isEmpty() && removedConnectionSegments.isEmpty() && addedBranchPoints.isEmpty() && removedBranchPoints.isEmpty() && addedConnectionEntities.isEmpty() && removedConnectionEntities.isEmpty() && addedRouteGraphConnectionMap.isEmpty() && removedRouteGraphConnections.isEmpty() && !changes.elementOrderChanged; } class DefaultConnectionSegmentAdapter implements ConnectionSegmentAdapter { @Override public void getClass(AsyncReadGraph graph, EdgeResource edge, ConnectionInfo info, ListenerSupport listenerSupport, ICanvasContext canvas, IDiagram diagram, final AsyncProcedure procedure) { if (info.connectionType != null) { NodeClassRequest request = new NodeClassRequest(canvas, diagram, info.connectionType, true); graph.asyncRequest(request, new CacheListener(listenerSupport)); graph.asyncRequest(request, procedure); } else { procedure.execute(graph, null); } } @Override public void load(AsyncReadGraph graph, final EdgeResource edge, final ConnectionInfo info, ListenerSupport listenerSupport, ICanvasContext canvas, final IDiagram diagram, final IElement element) { graph.asyncRequest(new Read() { @Override public IElement perform(ReadGraph graph) throws DatabaseException { //ITask task = ThreadLogger.getInstance().begin("LoadSegment"); syncLoad(graph, edge, info, diagram, element); //task.finish(); return element; } @Override public String toString() { return "defaultConnectionSegmentAdapter"; } }, new DisposableListener(listenerSupport) { @Override public String toString() { return "DefaultConnectionSegmentAdapter listener for " + edge; } @Override public void execute(IElement loaded) { // Invoked when the element has been loaded. if (DebugPolicy.DEBUG_EDGE_LISTENER) System.out.println("EDGE LoadListener for " + loaded); if (loaded == null) { disposeListener(); return; } Object data = loaded.getHint(ElementHints.KEY_OBJECT); if (addedElementMap.containsKey(data)) { // This element was just loaded, in // which case its hints need to // uploaded to the real mapped // element immediately. IElement mappedElement = getMappedElement(data); if (DebugPolicy.DEBUG_EDGE_LISTENER) System.out.println("LOADED ADDED EDGE, currently mapped element: " + mappedElement); if (mappedElement != null && (mappedElement instanceof Element)) { if (DebugPolicy.DEBUG_EDGE_LISTENER) { System.out.println(" mapped hints: " + mappedElement.getHints()); System.out.println(" loaded hints: " + loaded.getHints()); } updateMappedElement((Element) mappedElement, loaded); } } else { // This element was already loaded. // Just schedule an update some time // in the future. if (DebugPolicy.DEBUG_EDGE_LISTENER) System.out.println("PREVIOUSLY LOADED EDGE UPDATED, scheduling update into the future"); offerGraphUpdate( edgeUpdater(element, loaded) ); } } }); } void syncLoad(ReadGraph graph, EdgeResource connectionSegment, ConnectionInfo info, IDiagram diagram, IElement element) throws DatabaseException { // Check that at least some data exists before continuing further. if (!graph.hasStatement(connectionSegment.first()) && !graph.hasStatement(connectionSegment.second())) { return; } // Validate that both ends of the segment are // part of the same connection before loading. // This will happen for connections that are // modified through splitting and joining of // connection segments. Resource c = ConnectionUtil.tryGetConnection(graph, connectionSegment); if (c == null) { // Ok, this segment is somehow invalid. Just don't load it. if (DebugPolicy.DEBUG_CONNECTION_LOAD) System.out.println("Skipping edge " + connectionSegment + ". Both segment ends are not part of the same connection."); return; } if (!info.isValid()) { // This edge must be somehow invalid, don't proceed with loading. if (DebugPolicy.DEBUG_CONNECTION_LOAD) warning("Cannot load edge " + connectionSegment + ". ConnectionInfo " + info + " is invalid.", new DebugException("execution trace")); return; } Element edge = (Element) element; edge.setHint(ElementHints.KEY_OBJECT, connectionSegment); // connectionSegment resources may currently be in a different // order than ConnectionInfo.firstEnd/secondEnd. Therefore the // segment ends must be resolved here. ConnectionSegmentEnd firstEnd = DiagramGraphUtil.resolveConnectionSegmentEnd(graph, connectionSegment.first()); ConnectionSegmentEnd secondEnd = DiagramGraphUtil.resolveConnectionSegmentEnd(graph, connectionSegment.second()); if (firstEnd == null || secondEnd == null) { if (DebugPolicy.DEBUG_CONNECTION_LOAD) warning("End attachments for edge " + connectionSegment + " are unresolved: (" + firstEnd + "," + secondEnd + ")", new DebugException("execution trace")); return; } if (DebugPolicy.DEBUG_CONNECTION_LOAD) System.out.println("CONNECTION INFO: " + connectionSegment + " - " + info); DesignatedTerminal firstTerminal = DiagramGraphUtil.findDesignatedTerminal( graph, diagram, connectionSegment.first(), firstEnd); DesignatedTerminal secondTerminal = DiagramGraphUtil.findDesignatedTerminal( graph, diagram, connectionSegment.second(), secondEnd); // Edges must be connected at both ends in order for edge loading to succeed. String err = validateConnectivity(graph, connectionSegment, firstTerminal, secondTerminal); if (err != null) { // Stop loading edge if the connectivity cannot be completely resolved. if (DebugPolicy.DEBUG_CONNECTION_LOAD) warning(err, null); return; } // NOTICE: Layer information is loaded from the connection entity resource // that is shared by all segments of the same connection. ElementFactoryUtil.loadLayersForElement(graph, layerManager, diagram, edge, info.connection, new AsyncProcedureAdapter() { @Override public void exception(AsyncReadGraph graph, Throwable t) { error("failed to load layers for connection segment", t); } }); edge.setHintWithoutNotification(KEY_CONNECTION_BEGIN_PLACEHOLDER, new PlaceholderConnection( EdgeEnd.Begin, firstTerminal.element.getHint(ElementHints.KEY_OBJECT), firstTerminal.terminal)); edge.setHintWithoutNotification(KEY_CONNECTION_END_PLACEHOLDER, new PlaceholderConnection( EdgeEnd.End, secondTerminal.element.getHint(ElementHints.KEY_OBJECT), secondTerminal.terminal)); IModelingRules modelingRules = diagram.getHint(DiagramModelHints.KEY_MODELING_RULES); if (modelingRules != null) { ConnectionVisualsLoader loader = diagram.getHint(DiagramModelHints.KEY_CONNECTION_VISUALS_LOADER); if (loader != null) loader.loadConnectionVisuals(graph, modelingRules, info.connection, diagram, edge, firstTerminal, secondTerminal); else DiagramGraphUtil.loadConnectionVisuals(graph, modelingRules, info.connection, diagram, edge, firstTerminal, secondTerminal); } } private String validateConnectivity(ReadGraph graph, EdgeResource edge, DesignatedTerminal firstTerminal, DesignatedTerminal secondTerminal) throws DatabaseException { boolean firstLoose = firstTerminal == null; boolean secondLoose = secondTerminal == null; boolean stray = firstLoose && secondLoose; if (firstTerminal == null || secondTerminal == null) { StringBuilder sb = new StringBuilder(); sb.append("encountered "); sb.append(stray ? "stray" : "loose"); sb.append(" connection segment, "); if (firstLoose) sb.append("first "); if (stray) sb.append("and "); if (secondLoose) sb.append("second "); sb.append("end disconnected: "); sb.append(edge.toString(graph)); sb.append(" - "); sb.append(edge.toString()); return sb.toString(); } return null; } } ConnectionSegmentAdapter connectionSegmentAdapter = new DefaultConnectionSegmentAdapter(); } private static final Double DIAGRAM_UPDATE_DIAGRAM_PRIORITY = 1d; private static final Double DIAGRAM_UPDATE_NODE_PRIORITY = 2d; private static final Double DIAGRAM_UPDATE_CONNECTION_PRIORITY = 3d; private static final Double DIAGRAM_UPDATE_EDGE_PRIORITY = 4d; interface DiagramUpdater extends Runnable { Double getPriority(); Comparator DIAGRAM_UPDATER_COMPARATOR = new Comparator() { @Override public int compare(DiagramUpdater o1, DiagramUpdater o2) { return o1.getPriority().compareTo(o2.getPriority()); } }; } interface GraphUpdateReactor { DiagramUpdater graphUpdate(ReadGraph graph) throws DatabaseException; } static abstract class AbstractDiagramUpdater implements DiagramUpdater, GraphUpdateReactor { protected final Double priority; protected final String runnerName; public AbstractDiagramUpdater(Double priority, String runnerName) { if (priority == null) throw new NullPointerException("null priority"); if (runnerName == null) throw new NullPointerException("null runner name"); this.priority = priority; this.runnerName = runnerName; } @Override public Double getPriority() { return priority; } @Override public AbstractDiagramUpdater graphUpdate(ReadGraph graph) { return this; } @Override public void run() { Object task = Timing.BEGIN(runnerName); forDiagram(); Timing.END(task); } protected void forDiagram() { } @Override public String toString() { return runnerName + "@" + System.identityHashCode(this) + " [" + priority + "]"; } } /** * @param content the new contents of the diagram, must not be * null. */ private GraphUpdateReactor diagramGraphUpdater(final DiagramContents content) { if (content == null) throw new NullPointerException("null diagram content"); return new GraphUpdateReactor() { @Override public String toString() { return "DiagramGraphUpdater@" + System.identityHashCode(this); } @Override public DiagramUpdater graphUpdate(ReadGraph graph) throws DatabaseException { // Never do anything here if the canvas has already been disposed. if (!GraphToDiagramSynchronizer.this.isAlive()) return null; // We must be prepared for the following changes in the diagram graph // model: // - the diagram has been completely removed // - elements have been added // - elements have been removed // // Element-specific changes are handled by the element query listeners: // - elements have been modified // - element position has changed // - element class (e.g. image) has changed diagramUpdateLock.lock(); try { if (DebugPolicy.DEBUG_DIAGRAM_UPDATE) System.out.println("In diagramGraphUpdater:"); // Find out what has changed since the last query. Object task = Timing.BEGIN("diagramContentDifference"); DiagramContents lastContent = previousContent; DiagramContentChanges changes = content.differenceFrom(previousContent); previousContent = content; Timing.END(task); if (DebugPolicy.DEBUG_DIAGRAM_UPDATE) System.out.println(" changes: " + changes); // Bail out if there are no changes to react to. if (changes.isEmpty()) return null; // Load everything that needs to be loaded from the graph, // but don't update the UI model in this thread yet. task = Timing.BEGIN("updater.process"); GraphToDiagramUpdater updater = new GraphToDiagramUpdater(lastContent, content, changes); GraphToDiagramSynchronizer.this.currentUpdater = updater; try { updater.process(graph); } finally { GraphToDiagramSynchronizer.this.currentUpdater = null; } Timing.END(task); if (updater.isEmpty()) return null; // Return an updater that will update the UI run-time model. return diagramUpdater(updater); } finally { diagramUpdateLock.unlock(); } } }; } DiagramUpdater diagramUpdater(final GraphToDiagramUpdater updater) { return new AbstractDiagramUpdater(DIAGRAM_UPDATE_DIAGRAM_PRIORITY, "updateDiagram") { @Override protected void forDiagram() { if (DebugPolicy.DEBUG_DIAGRAM_UPDATE) System.out.println("running diagram updater: " + this); // DiagramUtils.testDiagram(diagram); Set dirty = new HashSet(); Object task2 = Timing.BEGIN("Preprocess connection changes"); Map connectionChangeData = new HashMap(updater.changedConnectionEntities.size()); for (ConnectionData cd : updater.changedConnectionEntities.values()) { connectionChangeData.put(cd.impl, cd.impl.getConnectionChildren()); } Timing.END(task2); task2 = Timing.BEGIN("removeRouteGraphConnections"); for (IElement removedRouteGraphConnection : updater.removedRouteGraphConnections) { if (DebugPolicy.DEBUG_DIAGRAM_UPDATE_DETAIL) System.out.println("removing route graph connection: " + removedRouteGraphConnection); ConnectionEntity ce = removedRouteGraphConnection.getHint(ElementHints.KEY_CONNECTION_ENTITY); if (ce == null) continue; Object connectionData = ElementUtils.getObject(removedRouteGraphConnection); tempConnections.clear(); for (Connection conn : ce.getTerminalConnections(tempConnections)) { ((Element) conn.node).removeHintWithoutNotification(new TerminalKeyOf(conn.terminal, connectionData, Connection.class)); // To be sure the view will be up-to-date, mark the node // connected to the removed connection dirty. dirty.add(conn.node); } } Timing.END(task2); task2 = Timing.BEGIN("removeBranchPoints"); for (IElement removed : updater.removedBranchPoints) { if (DebugPolicy.DEBUG_DIAGRAM_UPDATE_DETAIL) System.out.println("removing branch point: " + removed); unmapElement(removed.getHint(ElementHints.KEY_OBJECT)); removeNodeTopologyHints((Element) removed); IElement connection = ElementUtils.getParent(removed); if (connection != null) { dirty.add(connection); } } Timing.END(task2); task2 = Timing.BEGIN("removeConnectionSegments"); for (IElement removed : updater.removedConnectionSegments) { if (DebugPolicy.DEBUG_DIAGRAM_UPDATE_DETAIL) System.out.println("removing segment: " + removed); unmapElement(removed.getHint(ElementHints.KEY_OBJECT)); removeEdgeTopologyHints((Element) removed); IElement connection = ElementUtils.getParent(removed); if (connection != null) { dirty.add(connection); } } Timing.END(task2); task2 = Timing.BEGIN("removeElements"); for (IElement removed : updater.removedElements) { if (DebugPolicy.DEBUG_DIAGRAM_UPDATE_DETAIL) System.out.println("removing element: " + removed); removed.setHint(KEY_REMOVE_RELATIONSHIPS, Boolean.TRUE); if (diagram.containsElement(removed)) { diagram.removeElement(removed); } unmapElement(removed.getHint(ElementHints.KEY_OBJECT)); removeNodeTopologyHints((Element) removed); // No use marking removed elements dirty. dirty.remove(removed); } Timing.END(task2); // TODO: get rid of this task2 = Timing.BEGIN("removeConnectionEntities"); for (Resource ce : updater.removedConnectionEntities) { unmapConnection(ce); } Timing.END(task2); task2 = Timing.BEGIN("setConnectionData"); for (ConnectionData cd : updater.changedConnectionEntities.values()) { cd.impl.setData(cd.segments, cd.branchPoints); } Timing.END(task2); // TODO: get rid of this task2 = Timing.BEGIN("addConnectionEntities"); for (Map.Entry entry : updater.addedConnectionEntities .entrySet()) { mapConnection(entry.getKey(), entry.getValue()); } Timing.END(task2); task2 = Timing.BEGIN("addBranchPoints"); for (IElement added : updater.addedBranchPoints) { if (DebugPolicy.DEBUG_DIAGRAM_UPDATE_DETAIL) System.out.println("adding branch point: " + added); mapElement(ElementUtils.getObject(added), added); IElement connection = ElementUtils.getParent(added); if (connection != null) { dirty.add(connection); } } Timing.END(task2); // Add new elements at end of diagram, element order will be synchronized later. task2 = Timing.BEGIN("addElements"); for(Resource r : updater.content.elements) { IElement added = updater.addedElementMap.get(r); if(added != null) { if (DebugPolicy.DEBUG_DIAGRAM_UPDATE_DETAIL) System.out.println("adding element: " + added); //Object task3 = BEGIN("mapElement " + added); Object task3 = Timing.BEGIN("mapElement"); mapElement(added.getHint(ElementHints.KEY_OBJECT), added); Timing.END(task3); //task3 = BEGIN("addElement " + added); task3 = Timing.BEGIN("addElement"); //System.out.println("diagram.addElement: " + added + " - " + diagram); diagram.addElement(added); dirty.add(added); Timing.END(task3); } } Timing.END(task2); // We've ensured that all nodes must have been and // mapped before reaching this. task2 = Timing.BEGIN("addConnectionSegments"); for (IElement added : updater.addedConnectionSegments) { if (DebugPolicy.DEBUG_DIAGRAM_UPDATE_DETAIL) System.out.println("adding segment: " + added); PlaceholderConnection cb = added.removeHint(GraphToDiagramSynchronizer.KEY_CONNECTION_BEGIN_PLACEHOLDER); PlaceholderConnection ce = added.removeHint(GraphToDiagramSynchronizer.KEY_CONNECTION_END_PLACEHOLDER); if (cb == null || ce == null) { if (DebugPolicy.DEBUG_DIAGRAM_UPDATE_DETAIL) warning("ignoring connection segment " + added + ", connectivity was not resolved (begin=" + cb + ", end=" + ce +")", null); continue; } mapElement(ElementUtils.getObject(added), added); IElement beginNode = assertMappedElement(cb.node); IElement endNode = assertMappedElement(ce.node); if (cb != null) connect(added, cb.end, beginNode, cb.terminal); if (ce != null) connect(added, ce.end, endNode, ce.terminal); IElement connection = ElementUtils.getParent(added); if (connection != null) { dirty.add(connection); } } Timing.END(task2); // We've ensured that all nodes must have been and // mapped before reaching this. task2 = Timing.BEGIN("handle dirty RouteGraph connections"); for (IElement addedRouteGraphConnection : updater.addedRouteGraphConnectionMap.values()) { updateDirtyRouteGraphConnection(addedRouteGraphConnection, dirty); } Timing.END(task2); // Prevent memory leaks tempConnections.clear(); // Make sure that the diagram element order matches that of the database. final TObjectIntHashMap orderMap = new TObjectIntHashMap(2 * updater.content.elements.size()); int i = 1; for (Resource r : updater.content.elements) { IElement e = getMappedElement(r); if (e != null) orderMap.put(e, i); ++i; } diagram.sort(new Comparator() { @Override public int compare(IElement e1, IElement e2) { int o1 = orderMap.get(e1); int o2 = orderMap.get(e2); return o1 - o2; } }); // TODO: consider removing this. The whole thing should work without it and // this "fix" will only be hiding the real problems. task2 = Timing.BEGIN("fixChangedConnections"); for (ConnectionData cd : updater.changedConnectionEntities.values()) { cd.impl.fix(); } Timing.END(task2); task2 = Timing.BEGIN("validateAndFix"); DiagramUtils.validateAndFix(diagram, dirty); Timing.END(task2); // This will fire connection entity change listeners task2 = Timing.BEGIN("Postprocess connection changes"); for (ConnectionData cd : updater.changedConnectionEntities.values()) { ConnectionChildren oldChildren = connectionChangeData.get(cd.impl); if (oldChildren != null) { ConnectionChildren currentChildren = cd.impl.getConnectionChildren(); cd.impl.fireListener(oldChildren, currentChildren); } } Timing.END(task2); task2 = Timing.BEGIN("setDirty"); for (IElement e : dirty) { if (DebugPolicy.DEBUG_DIAGRAM_UPDATE_DETAIL) System.out.println("MARKING ELEMENT DIRTY: " + e); e.setHint(Hints.KEY_DIRTY, Hints.VALUE_SG_DIRTY); } Timing.END(task2); // Signal about possible changes in the z-order of diagram elements. if (DebugPolicy.DEBUG_DIAGRAM_UPDATE_DETAIL) System.out.println("MARKING DIAGRAM DIRTY: " + diagram); diagram.setHint(Hints.KEY_DIRTY, Hints.VALUE_Z_ORDER_CHANGED); // Mark the updater as "processed". updater.clear(); // Inform listeners that the diagram has been updated. diagram.setHint(DiagramModelHints.KEY_DIAGRAM_CONTENTS_UPDATED, Boolean.TRUE); } }; } /** * @param connection * @param dirtySet */ private void updateDirtyRouteGraphConnection(IElement connection, Set dirtySet) { if (DebugPolicy.DEBUG_DIAGRAM_UPDATE_DETAIL) System.out.println("updating dirty route graph connection: " + connection); ConnectionEntity ce = connection.getHint(ElementHints.KEY_CONNECTION_ENTITY); if (ce == null) return; tempConnections.clear(); Object connectionData = ElementUtils.getObject(connection); for (Connection conn : ce.getTerminalConnections(tempConnections)) { ((Element) conn.node).setHintWithoutNotification( new TerminalKeyOf(conn.terminal, connectionData, Connection.class), conn); if (dirtySet != null) dirtySet.add(conn.node); } // Prevent memory leaks. tempConnections.clear(); } abstract class ElementUpdater extends AbstractDiagramUpdater { private final IElement newElement; public ElementUpdater(Double priority, String runnerName, IElement newElement) { super(priority, runnerName); if (newElement == null) throw new NullPointerException("null element"); this.newElement = newElement; } @Override public String toString() { return super.toString() + "[" + newElement + "]"; } @Override public void run() { // System.out.println("ElementUpdateRunner new=" + newElement); Object elementResource = newElement.getHint(ElementHints.KEY_OBJECT); // System.out.println("ElementUpdateRunner res=" + elementResource); final Element mappedElement = (Element) getMappedElement(elementResource); // System.out.println("ElementUpdateRunner mapped=" + mappedElement); if (mappedElement == null) { if (DebugPolicy.DEBUG_ELEMENT_LIFECYCLE) { System.out.println("SKIP DIAGRAM UPDATE " + this + " for element resource " + elementResource + ", no mapped element (newElement=" + newElement + ")"); } // Indicates the element has been removed from the graph. return; } Object task = Timing.BEGIN(runnerName); forMappedElement(mappedElement); Timing.END(task); } protected abstract void forMappedElement(Element mappedElement); } ElementUpdater nodeUpdater(final Resource resource, final IElement newElement) { return new ElementUpdater(DIAGRAM_UPDATE_NODE_PRIORITY, "updateNode", newElement) { Collection getTerminals(IElement e) { Collection ts = Collections.emptyList(); TerminalTopology tt = e.getElementClass().getAtMostOneItemOfClass(TerminalTopology.class); if (tt != null) { ts = new ArrayList(); tt.getTerminals(newElement, ts); } return ts; } @Override protected void forMappedElement(final Element mappedElement) { if (DebugPolicy.DEBUG_NODE_UPDATE) System.out.println("running node updater: " + this + " - new element: " + newElement); // Newly loaded node elements NEVER contain topology-related // hints, i.e. TerminalKeyOf hints. Instead all connections are // actually set into element hints when connection edges are // loaded. Collection oldTerminals = getTerminals(mappedElement); Collection newTerminals = getTerminals(newElement); if (!oldTerminals.equals(newTerminals)) { // Okay, there are differences in the terminals. Need to fix // the TerminalKeyOf hint values to use the new terminal // instances when correspondences exist. // If there is no correspondence for an old terminal, we // are simply forced to remove the hints related to this // connection. Map newTerminalMap = new HashMap(newTerminals.size()); for (Terminal t : newTerminals) { newTerminalMap.put(t, t); } for (Map.Entry entry : mappedElement.getHintsOfClass(TerminalKeyOf.class).entrySet()) { TerminalKeyOf key = entry.getKey(); Connection c = (Connection) entry.getValue(); if (c.node == mappedElement) { Terminal newTerminal = newTerminalMap.get(c.terminal); if (newTerminal != null) { c = new Connection(c.edge, c.end, c.node, newTerminal); ((Element) c.edge).setHintWithoutNotification(EndKeyOf.get(c.end), c); } else { mappedElement.removeHintWithoutNotification(key); } } } } updateMappedElement(mappedElement, newElement); mappedElement.setHint(Hints.KEY_DIRTY, Hints.VALUE_SG_DIRTY); } }; } ElementUpdater connectionUpdater(final Object data, final IElement newElement) { return new ElementUpdater(DIAGRAM_UPDATE_CONNECTION_PRIORITY, "updateConnection", newElement) { @Override public void forMappedElement(Element mappedElement) { if (DebugPolicy.DEBUG_CONNECTION_UPDATE) System.out.println("running connection updater: " + this + " - new element: " + newElement + " with data " + data); // This is kept up-to-date by GDS, make sure not to overwrite it // from the mapped element. newElement.removeHint(ElementHints.KEY_CONNECTION_ENTITY); updateMappedElement(mappedElement, newElement); mappedElement.setHint(Hints.KEY_DIRTY, Hints.VALUE_SG_DIRTY); } }; } ElementUpdater edgeUpdater(final Object data, final IElement newElement) { return new ElementUpdater(DIAGRAM_UPDATE_EDGE_PRIORITY, "updateEdge", newElement) { @Override public void forMappedElement(Element mappedElement) { if (DebugPolicy.DEBUG_EDGE_UPDATE) System.out.println("running edge updater: " + this + " - new element: " + newElement + " with data " + data); updateMappedElement(mappedElement, newElement); mappedElement.setHint(Hints.KEY_DIRTY, Hints.VALUE_SG_DIRTY); } }; } ElementUpdater routeGraphConnectionUpdater(final Object data, final IElement newElement, final Set dirtyNodes) { return new ElementUpdater(DIAGRAM_UPDATE_CONNECTION_PRIORITY, "updateRouteGraphConnection", newElement) { @Override public void forMappedElement(Element mappedElement) { if (DebugPolicy.DEBUG_CONNECTION_UPDATE) System.out.println("running route graph connection updater: " + this + " - new element: " + newElement + " with data " + data); // Remove all TerminalKeyOf hints from nodes that were // previously connected to the connection (mappedElement) // before updating mappedElement with new topology information. for (Object dirtyNodeObject : dirtyNodes) { Element dirtyNode = (Element) getMappedElement(dirtyNodeObject); if (dirtyNode == null) continue; if (DebugPolicy.DEBUG_DIAGRAM_UPDATE_DETAIL) System.out.println("preparing node with dirty connectivity: " + dirtyNode); for (Map.Entry entry : dirtyNode.getHintsOfClass(TerminalKeyOf.class).entrySet()) { Connection conn = (Connection) entry.getValue(); Object connectionNode = conn.edge.getHint(ElementHints.KEY_OBJECT); if (data.equals(connectionNode)) { dirtyNode.removeHintWithoutNotification(entry.getKey()); } } } // Update connection information, including topology updateMappedElement(mappedElement, newElement); // Reinstall TerminalKeyOf hints into nodes that are now connected // to mappedElement to keep diagram run-time model properly in sync // with the database. updateDirtyRouteGraphConnection(mappedElement, null); // TODO: should mark dirty nodes' scene graph dirty ? mappedElement.setHint(Hints.KEY_DIRTY, Hints.VALUE_SG_DIRTY); } }; } /** * Copies hints from newElement to mappedElement * asserting some validity conditions at the same time. * * @param mappedElement * @param newElement */ static void updateMappedElement(Element mappedElement, IElement newElement) { if (mappedElement == newElement) // Can't update anything if the two elements are the same. return; ElementClass oldClass = mappedElement.getElementClass(); ElementClass newClass = newElement.getElementClass(); Object mappedData = mappedElement.getHint(ElementHints.KEY_OBJECT); Object newData = newElement.getHint(ElementHints.KEY_OBJECT); assert mappedData != null; assert newData != null; assert mappedData.equals(newData); if (DebugPolicy.DEBUG_GENERAL_ELEMENT_UPDATE) { System.out.println("Updating mapped element, setting hints\n from: " + newElement + "\n into: " + mappedElement); } // TODO: consider if this equals check is a waste of time or does it pay // off due to having to reinitialize per-class caches for the new // ElementClass that are constructed on the fly? if (!newClass.equals(oldClass)) { if (DebugPolicy.DEBUG_GENERAL_ELEMENT_UPDATE) { System.out.println(" old element class: " + oldClass); System.out.println(" new element class: " + newClass); } mappedElement.setElementClass(newClass); } // Tuukka@2010-02-19: replaced with notifications for making // the graph synchronizer more transparent to the client. // Hint notifications will not work when this is used. //mappedElement.setHintsWithoutNotification(newElement.getHints()); Map discardableHints = mappedElement.getHintsOfClass(DiscardableKey.class); // Set all hints from newElement to mappedElement. // Leave any hints in mappedElement but not in newElement as is. Map hints = newElement.getHints(); Map newHints = new HashMap(); for (Map.Entry entry : hints.entrySet()) { Key key = entry.getKey(); Object newValue = entry.getValue(); Object oldValue = mappedElement.getHint(key); if (DebugPolicy.DEBUG_GENERAL_ELEMENT_UPDATE_DETAIL) { System.out.println(" hint " + key + " compare values: " + oldValue + " -> " + newValue); } if (!newValue.equals(oldValue)) { newHints.put(key, newValue); if (DebugPolicy.DEBUG_GENERAL_ELEMENT_UPDATE) { System.out.format(" %-42s : %64s -> %-64s\n", key, oldValue, newValue); } } else { // If the hint value has not changed but the hint still exists // we don't need to discard it even if it is considered // discardable. discardableHints.remove(key); } } // Set all hints at once and send notifications after setting the values. if (!discardableHints.isEmpty()) { if (DebugPolicy.DEBUG_GENERAL_ELEMENT_UPDATE) System.out.println("Discarding " + discardableHints.size() + " discardable hints:\n " + discardableHints); mappedElement.removeHints(discardableHints.keySet()); } if (!newHints.isEmpty()) { if (DebugPolicy.DEBUG_GENERAL_ELEMENT_UPDATE) { System.out.println("Updating mapped element, setting new hints:\n\t" + EString.implode(newHints.entrySet(), "\n\t") + "\nto replace old hints\n\t" + EString.implode(mappedElement.getHints().entrySet(), "\n\t")); } mappedElement.setHints(newHints); } if (DebugPolicy.DEBUG_GENERAL_ELEMENT_UPDATE) { System.out.println("All hints after update:\n\t" + EString.implode(mappedElement.getHints().entrySet(), "\n\t")); } } class TransactionListener extends SessionEventListenerAdapter { long startTime; @Override public void writeTransactionStarted() { startTime = System.nanoTime(); if (DebugPolicy.DEBUG_WRITE_TRANSACTIONS) System.out.println(GraphToDiagramSynchronizer.class.getSimpleName() + ".sessionEventListener.writeTransactionStarted"); inWriteTransaction.set(true); } @Override public void writeTransactionFinished() { long endTime = System.nanoTime(); if (DebugPolicy.DEBUG_WRITE_TRANSACTIONS) System.out.println(GraphToDiagramSynchronizer.class.getSimpleName() + ".sessionEventListener.writeTransactionFinished: " + (endTime - startTime)*1e-6 + " ms"); inWriteTransaction.set(false); scheduleGraphUpdates(); } }; Object graphUpdateLock = new Object(); TransactionListener sessionListener = null; AtomicBoolean inWriteTransaction = new AtomicBoolean(false); AtomicBoolean graphUpdateRequestScheduled = new AtomicBoolean(false); List queuedGraphUpdates = new ArrayList(); private void offerGraphUpdate(GraphUpdateReactor update) { if (DebugPolicy.DEBUG_GRAPH_UPDATE) System.out.println("offerGraphUpdate: " + update); boolean inWrite = inWriteTransaction.get(); synchronized (graphUpdateLock) { if (DebugPolicy.DEBUG_GRAPH_UPDATE) System.out.println("queueing graph update: " + update); queuedGraphUpdates.add(update); } if (!inWrite) { if (DebugPolicy.DEBUG_GRAPH_UPDATE) System.out.println("scheduling queued graph update immediately: " + update); scheduleGraphUpdates(); } } private Collection scrubGraphUpdates() { synchronized (graphUpdateLock) { if (queuedGraphUpdates.isEmpty()) return Collections.emptyList(); final List updates = queuedGraphUpdates; queuedGraphUpdates = new ArrayList(); return updates; } } private void scheduleGraphUpdates() { synchronized (graphUpdateLock) { if (queuedGraphUpdates.isEmpty()) return; if (!graphUpdateRequestScheduled.compareAndSet(false, true)) return; } if (DebugPolicy.DEBUG_GRAPH_UPDATE) System.out.println("scheduling " + queuedGraphUpdates.size() + " queued graph updates with "); session.asyncRequest(new ReadRequest() { @Override public void run(final ReadGraph graph) throws DatabaseException { Collection updates; synchronized (graphUpdateLock) { graphUpdateRequestScheduled.set(false); updates = scrubGraphUpdates(); } if (!GraphToDiagramSynchronizer.this.isAlive()) return; processGraphUpdates(graph, updates); } }, new ProcedureAdapter() { @Override public void exception(Throwable t) { error(t); } }); } private void processGraphUpdates(ReadGraph graph, final Collection graphUpdates) throws DatabaseException { final List diagramUpdates = new ArrayList(graphUpdates.size()); // Run GraphUpdaters and gather DiagramUpdaters. if (DebugPolicy.DEBUG_GRAPH_UPDATE) System.out.println("Running GRAPH updates: " + graphUpdates); for (GraphUpdateReactor graphUpdate : graphUpdates) { DiagramUpdater diagramUpdate = graphUpdate.graphUpdate(graph); if (diagramUpdate != null) { if (DebugPolicy.DEBUG_GRAPH_UPDATE) System.out.println(graphUpdate + " => " + diagramUpdate); diagramUpdates.add(diagramUpdate); } } if (diagramUpdates.isEmpty()) return; if (DebugPolicy.DEBUG_DIAGRAM_UPDATE) System.out.println("Diagram updates: " + diagramUpdates); Collections.sort(diagramUpdates, DiagramUpdater.DIAGRAM_UPDATER_COMPARATOR); if (DebugPolicy.DEBUG_DIAGRAM_UPDATE) System.out.println("Sorted diagram updates: " + diagramUpdates); ThreadUtils.asyncExec(canvas.getThreadAccess(), new StateRunnable() { @Override public void run() { if (GraphToDiagramSynchronizer.this.isAlive() && getState() != State.DISPOSED) safeRunInState(State.UPDATING_DIAGRAM, this); } @Override public void execute() throws InvocationTargetException { // Block out diagram write transactions. DiagramUtils.inDiagramTransaction(diagram, TransactionType.READ, new Runnable() { @Override public void run() { if (DebugPolicy.DEBUG_DIAGRAM_UPDATE) System.out.println("Running DIAGRAM updates: " + diagramUpdates); for (DiagramUpdater update : diagramUpdates) { if (DebugPolicy.DEBUG_DIAGRAM_UPDATE) System.out.println("Running DIAGRAM update: " + update); update.run(); } } }); setCanvasDirty(); } }); } private void attachSessionListener(Session session) { SessionEventSupport support = session.peekService(SessionEventSupport.class); if (support != null) { sessionListener = new TransactionListener(); support.addListener(sessionListener); } } private void detachSessionListener() { if (sessionListener != null) { session.getService(SessionEventSupport.class).removeListener(sessionListener); sessionListener = null; } } // ------------------------------------------------------------------------ // GRAPH TO DIAGRAM SYNCHRONIZATION LOGIC END // >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> // <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< // DIAGRAM CHANGE TRACKING, MAINLY VALIDATION PURPOSES. // This does not try to synchronize anything back into the graph. // ------------------------------------------------------------------------ IHintListener elementHintValidator = new HintListenerAdapter() { @Override public void hintChanged(IHintObservable sender, Key key, Object oldValue, Object newValue) { if (!(sender instanceof Element)) throw new IllegalStateException("invalid sender: " + sender); Element e = (Element) sender; if (newValue != null) { if (key instanceof TerminalKeyOf) { Connection c = (Connection) newValue; if (e != c.node) throw new IllegalStateException("TerminalKeyOf hint of node " + e + " refers to a different node " + c.node + ". Should be the same."); Object edgeObject = ElementUtils.getObject(c.edge); if (!(edgeObject instanceof EdgeResource)) throw new IllegalStateException("EndKeyOf hint of edge " + c.edge + " refers contains an invalid object: " + edgeObject); } else if (key instanceof EndKeyOf) { Connection c = (Connection) newValue; if (e != c.edge) throw new IllegalStateException("EndKeyOf hint of edge " + e + " refers to a different edge " + c.edge + ". Should be the same."); Object edgeObject = ElementUtils.getObject(c.edge); if (!(edgeObject instanceof EdgeResource)) throw new IllegalStateException("EndKeyOf hint of edge " + e + " refers contains an invalid object: " + edgeObject); } } } }; class DiagramListener implements CompositionListener, CompositionVetoListener { @Override public boolean beforeElementAdded(IDiagram d, IElement e) { // Make sure that MutatedElements NEVER get added to the diagram. if (d == diagram) { if (!(e instanceof Element)) { // THIS IS NOT GOOD! error("Attempting to add another implementation of IElement besides Element (=" + e.getElementClass().getClass().getName() + ") to the synchronized diagram which means that there is a bug somewhere! See stack trace to find out who is doing this!", new Exception("stacktrace")); System.err.println("Attempting to add another implementation of IElement besides Element (=" + e.getElementClass().getClass().getName() + ") to the synchronized diagram which means that there is a bug somewhere! See Error Log."); return false; } // Perform sanity checks that might veto the element addition. boolean pass = true; // Check that all elements added to the diagram are adaptable to Resource ElementClass ec = e.getElementClass(); Resource resource = ElementUtils.adapt(ec, Resource.class); if (resource == null) { pass = false; new Exception("Attempted to add an element to the diagram that is not adaptable to Resource: " + e + ", class: " + ec).printStackTrace(); } // Sanity check connection hints for (Map.Entry entry : e.getHintsOfClass(TerminalKeyOf.class).entrySet()) { Connection c = (Connection) entry.getValue(); Object edgeObject = ElementUtils.getObject(c.edge); if (e != c.node) { System.err.println("Invalid node in TerminalKeyOf hint: " + entry.getKey() + "=" + entry.getValue()); System.err.println("\tconnection.edge=" + c.edge); System.err.println("\tconnection.node=" + c.node); System.err.println("\tconnection.end=" + c.end); System.err.println("\telement=" + e); System.err.println("\telement class=" + e.getElementClass()); pass = false; } if (!(edgeObject instanceof EdgeResource)) { System.err.println("Invalid object in TerminalKeyOf hint edge: " + entry.getKey() + "=" + entry.getValue()); System.err.println("\tconnection.edge=" + c.edge); System.err.println("\tconnection.node=" + c.node); System.err.println("\tconnection.end=" + c.end); System.err.println("\telement=" + e); System.err.println("\telement class=" + e.getElementClass()); pass = false; } } return pass; } return true; } @Override public boolean beforeElementRemoved(IDiagram d, IElement e) { // Never veto diagram changes. return true; } @Override public void onElementAdded(IDiagram d, IElement e) { if (DebugPolicy.DEBUG_ELEMENT_LIFECYCLE) System.out.println("[" + d + "] element added: " + e); if (USE_ELEMENT_VALIDATING_LISTENERS) e.addHintListener(elementHintValidator); } @Override public void onElementRemoved(IDiagram d, IElement e) { if (DebugPolicy.DEBUG_ELEMENT_LIFECYCLE) System.out.println("[" + d + "] element removed: " + e); if (USE_ELEMENT_VALIDATING_LISTENERS) e.removeHintListener(elementHintValidator); if (e.containsHint(KEY_REMOVE_RELATIONSHIPS)) relationshipHandler.denyAll(diagram, e); } } DiagramListener diagramListener = new DiagramListener(); static void removeNodeTopologyHints(Element node) { Set terminalKeys = node.getHintsOfClass(TerminalKeyOf.class).keySet(); if (!terminalKeys.isEmpty()) { removeNodeTopologyHints(node, terminalKeys); } } static void removeNodeTopologyHints(Element node, Collection terminalKeys) { for (TerminalKeyOf key : terminalKeys) { Connection c = node.removeHintWithoutNotification(key); if (c != null) { removeEdgeTopologyHints((Element) c.edge); } } } static void removeEdgeTopologyHints(Element edge) { Object edgeData = edge.getHint(ElementHints.KEY_OBJECT); for (EndKeyOf key : EndKeyOf.KEYS) { Connection c = edge.removeHintWithoutNotification(key); if (c != null) { ((Element) c.node).removeHintWithoutNotification(new TerminalKeyOf(c.terminal, edgeData, Connection.class)); } } } // ------------------------------------------------------------------------ // DIAGRAM CHANGE TRACKING, MAINLY VALIDATION PURPOSES. // >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> // <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< // BACKEND TO DIAGRAM LOAD/LISTEN LOGIC BEGIN // ------------------------------------------------------------------------ void adaptDiagramClass(AsyncReadGraph graph, Resource diagram, final AsyncProcedure procedure) { graph.forAdapted(diagram, DiagramClass.class, new AsyncProcedure() { @Override public void exception(AsyncReadGraph graph, Throwable throwable) { procedure.exception(graph, throwable); } @Override public void execute(AsyncReadGraph graph, DiagramClass dc) { // To move TopologyImpl out of here, we need a separate // DiagramClassFactory that takes a canvas context as an argument. // DataElementMapImpl, ElementFactoryImpl and diagramLifeCycle can // safely stay here. procedure.execute(graph, dc.newClassWith( // This handler takes care of the topology of the diagram model. // It sets and fixes element hints related to describing the // connectivity of elements. diagramTopology, // TODO: not quite sure whether this can prove itself useful or not. elementFactory, // This map provides a bidirectional mapping between // IElement and back-end objects. dataElementMap, // This handler provides a facility to adapt an element class // to work properly with a diagram synchronized using this // GraphToDiagramSynchronizer. substituteElementClass, // These handlers provide a way to create simple identified // uni- and bidirectional relationships between any diagram // objects/elements. relationshipHandler)); } }); } static Connection connect(IElement edge, EdgeEnd end, IElement element, Terminal terminal) { Connection c = new Connection(edge, end, element, terminal); Object edgeData = edge.getHint(ElementHints.KEY_OBJECT); if (DebugPolicy.DEBUG_CONNECTION) { System.out.println("[connect](edge=" + edge + ", edgeData=" + edgeData + ", end=" + end + ", element=" + element + ", terminal=" + terminal + ")"); } TerminalKeyOf key = new TerminalKeyOf(terminal, edgeData, Connection.class); element.setHint(key, c); EndKeyOf key2 = EndKeyOf.get(end); edge.setHint(key2, c); return c; } static class ElementFactoryImpl implements ElementFactory { @Override public IElement spawnNew(ElementClass clazz) { IElement e = Element.spawnNew(clazz); return e; } } ElementFactoryImpl elementFactory = new ElementFactoryImpl(); public static final Object FIRST_TIME = new Object() { @Override public String toString() { return "FIRST_TIME"; } }; /** * A base for all listeners of graph requests performed internally by * GraphToDiagramSynchronizer. * * @param type of stored data element * @param query result type */ abstract class BaseListener implements AsyncListener { protected final T data; private Object oldResult = FIRST_TIME; protected boolean disposed = false; final ICanvasContext canvas; public BaseListener(T data) { this.canvas = GraphToDiagramSynchronizer.this.canvas; this.data = data; } @Override public void exception(AsyncReadGraph graph, Throwable throwable) { // Exceptions are always expected to mean that the listener should // be considered disposed once a query fails. disposed = true; } abstract void execute(AsyncReadGraph graph, Object oldResult, Object newResult); @Override public void execute(AsyncReadGraph graph, Result result) { if (DebugPolicy.DEBUG_LISTENER_BASE) System.out.println("BaseListener: " + result); if (disposed) { if (DebugPolicy.DEBUG_LISTENER_BASE) System.out.println("BaseListener: execute invoked although listener is disposed!"); return; } // A null result will permanently mark this listener disposed! if (result == null) { disposed = true; if (DebugPolicy.DEBUG_LISTENER_BASE) System.out.println(this + " null result, listener marked disposed"); } if (oldResult == FIRST_TIME) { oldResult = result; if (DebugPolicy.DEBUG_LISTENER_BASE) System.out.println(this + " first result computed: " + result); } else { if (DebugPolicy.DEBUG_LISTENER_BASE) System.out.println(this + " result changed from '" + oldResult + "' to '" + result + "'"); try { execute(graph, oldResult, result); } finally { oldResult = result; } } } @Override public boolean isDisposed() { if (disposed) return true; boolean alive = isAlive(); //System.out.println(getClass().getName() + ": isDisposed(" + resource.getResourceId() + "): canvas=" + canvas + ", isAlive=" + alive); if (!alive) return true; // If a mapping no longer exists for this element, dispose of this // listener. //IElement e = getMappedElement(resource); //System.out.println(getClass().getName() + ": isDisposed(" + resource.getResourceId() + "): canvas=" + canvas + ", element=" + e); //return e == null; return false; } // @Override // protected void finalize() throws Throwable { // System.out.println("finalize listener: " + this); // super.finalize(); // } } class DiagramClassRequest extends BaseRequest2 { public DiagramClassRequest(Resource resource) { super(GraphToDiagramSynchronizer.this.canvas, resource); } @Override public void perform(AsyncReadGraph graph, AsyncProcedure procedure) { adaptDiagramClass(graph, data, procedure); } } public class DiagramContentListener extends BaseListener { public DiagramContentListener(Resource resource) { super(resource); } @Override public void execute(final AsyncReadGraph graph, Object oldResult, Object newResult) { final DiagramContents newContent = (newResult == null) ? new DiagramContents() : (DiagramContents) newResult; // diagramGraphUpdater is called synchronously during // loading. The first result will not get updated through // this listener but through loadDiagram. if (DebugPolicy.DISABLE_DIAGRAM_UPDATES) { System.out.println("Skipped diagram content update: " + newResult); return; } if (DebugPolicy.DEBUG_DIAGRAM_LISTENER) System.out.println("diagram contents changed: " + oldResult + " => " + newResult); offerGraphUpdate( diagramGraphUpdater(newContent) ); } @Override public boolean isDisposed() { return !isAlive(); } @Override public void exception(AsyncReadGraph graph, Throwable t) { super.exception(graph, t); error("DiagramContentRequest failed", t); } } // ------------------------------------------------------------------------ // BACKEND TO DIAGRAM LOAD/LISTEN LOGIC END // >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> // <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< // GRAPH-CUSTOMIZED DIAGRAM TOPOLOGY HANDLER BEGIN // ------------------------------------------------------------------------ static class TopologyImpl implements Topology { @Override public Connection getConnection(IElement edge, EdgeEnd end) { Key key = EndKeyOf.get(end); Connection c = edge.getHint(key); if (c == null) return null; return c; } @Override public void getConnections(IElement node, Terminal terminal, Collection connections) { // IDiagram d = ElementUtils.getDiagram(node); for (Map.Entry entry : node.getHintsOfClass(TerminalKeyOf.class).entrySet()) { // First check that the terminal matches. TerminalKeyOf key = entry.getKey(); if (!key.getTerminal().equals(terminal)) continue; Connection c = (Connection) entry.getValue(); if (c != null) { connections.add(c); } } } @Override public void connect(IElement edge, EdgeEnd end, IElement node, Terminal terminal) { if (node != null && terminal != null) GraphToDiagramSynchronizer.connect(edge, end, node, terminal); if (DebugPolicy.DEBUG_CONNECTION) { if (end == EdgeEnd.Begin) System.out.println("Connection started from: " + edge + ", " + end + ", " + node + ", " + terminal); else System.out.println("Creating connection to: " + edge + ", " + end + ", " + node + ", " + terminal); } } @Override public void disconnect(IElement edge, EdgeEnd end, IElement node, Terminal terminal) { EndKeyOf edgeKey = EndKeyOf.get(end); Connection c = edge.getHint(edgeKey); if (c == null) throw new UnsupportedOperationException("cannot disconnect, no Connection in edge " + edge); for (Map.Entry entry : node.getHintsOfClass(TerminalKeyOf.class).entrySet()) { Connection cc = (Connection) entry.getValue(); if (c == cc) { node.removeHint(entry.getKey()); edge.removeHint(edgeKey); return; } } throw new UnsupportedOperationException("cannot disconnect, no connection between found between edge " + edge + " and node " + node); } } Topology diagramTopology = new TopologyImpl(); // ------------------------------------------------------------------------ // GRAPH-CUSTOMIZED DIAGRAM TOPOLOGY HANDLER END // >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> // <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< // DIAGRAM OBJECT RELATIONSHIP HANDLER BEGIN // ------------------------------------------------------------------------ RelationshipHandler relationshipHandler = new RelationshipHandler() { AssociativeMap map = new AssociativeMap(Associativity.of(true, false, false)); Object getPossibleObjectOrElement(Object o) { if (o instanceof IElement) { IElement e = (IElement) o; Object oo = e.getHint(ElementHints.KEY_OBJECT); return oo != null ? oo : e; } return o; } IElement getElement(Object o) { if (o instanceof IElement) return (IElement) o; return getMappedElement(o); } @Override public void claim(IDiagram diagram, Object subject, Relationship predicate, Object object) { Object sd = getPossibleObjectOrElement(subject); Object od = getPossibleObjectOrElement(object); Collection ts = null; Relationship inverse = predicate.getInverse(); if (inverse != null) ts = Arrays.asList(new Tuple(sd, predicate, od), new Tuple(od, inverse, sd)); else ts = Collections.singletonList(new Tuple(sd, predicate, od)); synchronized (this) { map.add(ts); } if (DebugPolicy.DEBUG_RELATIONSHIP) { new Exception().printStackTrace(); System.out.println("Claimed relationships:"); for (Tuple t : ts) System.out.println("\t" + t); } } private void doDeny(IDiagram diagram, Object subject, Relationship predicate, Object object) { Object sd = getPossibleObjectOrElement(subject); Object od = getPossibleObjectOrElement(object); if (sd == subject || od == object) { System.out .println("WARNING: denying relationship '" + predicate + "' between diagram element(s), not back-end object(s): " + sd + " -> " + od); } Collection ts = null; Relationship inverse = predicate.getInverse(); if (inverse != null) ts = Arrays.asList(new Tuple(sd, predicate, od), new Tuple(od, inverse, sd)); else ts = Collections.singleton(new Tuple(sd, predicate, od)); synchronized (this) { map.remove(ts); } if (DebugPolicy.DEBUG_RELATIONSHIP) { new Exception().printStackTrace(); System.out.println("Denied relationships:"); for (Tuple t : ts) System.out.println("\t" + t); } } @Override public void deny(IDiagram diagram, Object subject, Relationship predicate, Object object) { synchronized (this) { doDeny(diagram, subject, predicate, object); } } @Override public void deny(IDiagram diagram, Relation relation) { synchronized (this) { doDeny(diagram, relation.getSubject(), relation .getRelationship(), relation.getObject()); } } @Override public void denyAll(IDiagram diagram, Object element) { synchronized (this) { for (Relation relation : getRelations(diagram, element, null)) { doDeny(diagram, relation.getSubject(), relation .getRelationship(), relation.getObject()); } } } @Override public Collection getRelations(IDiagram diagram, Object element, Collection result) { if (DebugPolicy.DEBUG_GET_RELATIONSHIP) System.out.println("getRelations(" + element + ")"); Object e = getPossibleObjectOrElement(element); Collection tuples = null; synchronized (this) { tuples = map.get(new Tuple(e, null, null), null); } if (DebugPolicy.DEBUG_GET_RELATIONSHIP) { System.out.println("Result size: " + tuples.size()); for (Tuple t : tuples) System.out.println("\t" + t); } if (tuples.isEmpty()) return Collections.emptyList(); if (result == null) result = new ArrayList(tuples.size()); for (Tuple t : tuples) { Object obj = t.getField(2); IElement el = getElement(obj); Relationship r = (Relationship) t.getField(1); result.add(new Relation(element, r, el != null ? el : obj)); } return result; } }; // ------------------------------------------------------------------------ // DIAGRAM ELEMENT RELATIONSHIP HANDLER END // >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> }