X-Git-Url: https://gerrit.simantics.org/r/gitweb?p=simantics%2Fplatform.git;a=blobdiff_plain;f=bundles%2Forg.simantics.diagram%2Fsrc%2Forg%2Fsimantics%2Fdiagram%2Fadapter%2FGraphToDiagramSynchronizer.java;h=aa35c63342c28e8d952a7989f2cc5edd26f83f21;hp=a947a1d215633b2f6b73f8bb2ddbedaad35ab651;hb=881a82d0707953038b327d334560ac12ac3e5ea9;hpb=969bd23cab98a79ca9101af33334000879fb60c5 diff --git a/bundles/org.simantics.diagram/src/org/simantics/diagram/adapter/GraphToDiagramSynchronizer.java b/bundles/org.simantics.diagram/src/org/simantics/diagram/adapter/GraphToDiagramSynchronizer.java index a947a1d21..aa35c6334 100644 --- a/bundles/org.simantics.diagram/src/org/simantics/diagram/adapter/GraphToDiagramSynchronizer.java +++ b/bundles/org.simantics.diagram/src/org/simantics/diagram/adapter/GraphToDiagramSynchronizer.java @@ -1,3938 +1,3989 @@ -/******************************************************************************* - * 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 - // >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> - -} +/******************************************************************************* + * Copyright (c) 2007, 2018 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 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.procedure.adapter.TransientCacheAsyncListener; +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; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import gnu.trove.map.hash.TObjectIntHashMap; +import gnu.trove.set.hash.THashSet; + +/** + * 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 { + + private static final Logger LOGGER = LoggerFactory.getLogger(GraphToDiagramSynchronizer.class); + + /** + * 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, false).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. + LOGGER.error("Dispose interrupted!", e); + } 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, TransientCacheAsyncListener.instance()); + //System.err.println("contents: " + contents); + 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)); + + g.getSession().asyncRequest(new AsyncReadRequest() { + @Override + public void run(AsyncReadGraph graph) { + ProfileObserver po = profileObserver; + if (po != null) + po.listen(graph, GraphToDiagramSynchronizer.this); + else + LOGGER.info("profileObserver has been disposed already!"); + } + }); + + 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(ReadGraph graph) throws DatabaseException { + + 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.syncRequest(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) { + + boolean firstTime = true; + + @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; + } + + if (firstTime) { + + mapElement(element, loaded); + synchronized (GraphToDiagramUpdater.this) { + addedElements.add(loaded); + addedElementMap.put(element, loaded); + addedConnectionMap.put(element, loaded); + } + + firstTime = false; + + } + + 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.syncRequest(new ConnectionRequest(canvas, diagram, element, errorHandler, loadListener), new AsyncProcedure() { + @Override + public void execute(AsyncReadGraph graph, final IElement 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) { + + boolean firstTime = true; + + @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; + } + + if (firstTime) { + + // 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 + " -> " + loaded); + mapElement(element, loaded); + synchronized (GraphToDiagramUpdater.this) { + addedElements.add(loaded); + addedElementMap.put(element, loaded); + } + + firstTime = false; + + } + + 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.syncRequest(new NodeRequest(canvas, diagram, element, loadListener), new AsyncProcedure() { + @Override + public void execute(AsyncReadGraph graph, IElement 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.syncRequest(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; + } + default: + } + } + } + + 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; + } + default: + } + } + } + + 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); + } + default: + } + } + + // 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(ReadGraph graph) throws DatabaseException { + 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) { + + boolean firstTime = true; + + @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; + } + + if(firstTime) { + if (DebugPolicy.DEBUG_NODE_LOAD) + System.out.println("MAPPING ADDED ROUTE GRAPH CONNECTION: " + connection + " -> " + loaded); + mapElement(connection, loaded); + synchronized (GraphToDiagramUpdater.this) { + addedElements.add(loaded); + addedElementMap.put(connection, loaded); + addedRouteGraphConnectionMap.put(connection, loaded); + } + firstTime = false; + } + + 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.syncRequest(new ConnectionRequest(canvas, diagram, connection, errorHandler, loadListener), new Procedure() { + @Override + public void execute(final IElement e) { + } + @Override + public void exception(Throwable throwable) { + error(throwable); + } + }); + break; + } + case REMOVED: { + IElement e = getMappedElement(connection); + if (e != null) + removedRouteGraphConnections.add(e); + break; + } + default: + } + } + } + + 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(ReadGraph graph) throws DatabaseException { + 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.syncRequest(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) { + + boolean firstTime = true; + + @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; + } + + if (firstTime) { + + mapElement(element, loaded); + synchronized (GraphToDiagramUpdater.this) { + addedBranchPoints.add(loaded); + addedElementMap.put(element, loaded); + ConnectionEntityImpl ce = getConnectionEntity(element); + loaded.setHint(ElementHints.KEY_CONNECTION_ENTITY, ce); + loaded.setHint(ElementHints.KEY_PARENT_ELEMENT, ce.getConnectionElement()); + } + + firstTime = false; + + } + + 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.syncRequest(new NodeRequest(canvas, diagram, element, loadListener), new AsyncProcedure() { + @Override + public void execute(AsyncReadGraph graph, IElement e) { + } + + @Override + public void exception(AsyncReadGraph graph, Throwable throwable) { + error(throwable); + } + }); + } + break; + } + case REMOVED: { + IElement e = getMappedElement(element); + if (DebugPolicy.DEBUG_NODE_LOAD) + graph.syncRequest(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; + } + default: + } + } + } + + void processConnectionSegments(ReadGraph graph) throws DatabaseException { + 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.syncRequest(new ReadRequest() { + @Override + public void run(ReadGraph graph) throws DatabaseException { + System.out.println(" EXTERNALLY ADDED CONNECTION SEGMENT: " + seg.toString() + + " - " + seg.toString(graph)); + } + }); + + graph.syncRequest(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.syncRequest(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; + } + default: + } + } + } + + 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 ReadRequest() { + @Override + public void run(ReadGraph graph) throws DatabaseException { + 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 ReadRequest() { + @Override + public void run(ReadGraph graph) throws DatabaseException { + 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 ReadRequest() { + @Override + public void run(ReadGraph graph) throws DatabaseException { + 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 ReadRequest() { + @Override + public void run(ReadGraph graph) throws DatabaseException { + 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; + } + + graph.syncRequest(new AsyncReadRequest() { + @Override + public void run(AsyncReadGraph graph) { + // 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; + LOGGER.error("", new Exception("Attempted to add an element to the diagram that is not adaptable to Resource: " + e + ", class: " + ec)); + } + + // 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 + // >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> + +}