/*******************************************************************************
* 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, 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.
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, Change> changes) {
for (Map.Entry, Change> 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
// >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
}