package org.simantics.diagram.handler; import static org.simantics.diagram.handler.Paster.ComposedCutProcedure.compose; import gnu.trove.map.hash.THashMap; import gnu.trove.set.hash.THashSet; import java.awt.geom.AffineTransform; import java.awt.geom.Point2D; import java.util.ArrayDeque; import java.util.Collection; import java.util.Deque; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Queue; import java.util.Set; import org.simantics.databoard.Bindings; import org.simantics.db.ReadGraph; import org.simantics.db.Resource; import org.simantics.db.Session; import org.simantics.db.Statement; import org.simantics.db.WriteGraph; import org.simantics.db.common.CommentMetadata; import org.simantics.db.common.request.IndexRoot; import org.simantics.db.common.request.PossibleTypedParent; import org.simantics.db.common.request.WriteRequest; import org.simantics.db.common.utils.CommonDBUtils; import org.simantics.db.common.utils.NameUtils; import org.simantics.db.common.utils.OrderedSetUtils; import org.simantics.db.exception.DatabaseException; import org.simantics.db.layer0.util.RemoverUtil; import org.simantics.db.request.Write; import org.simantics.diagram.content.ConnectionUtil; import org.simantics.diagram.flag.DiagramFlagPreferences; import org.simantics.diagram.flag.FlagLabelingScheme; import org.simantics.diagram.flag.FlagUtil; import org.simantics.diagram.flag.IOTableUtil; import org.simantics.diagram.flag.IOTablesInfo; import org.simantics.diagram.handler.PasteOperation.ForceCopyReferences; import org.simantics.diagram.internal.DebugPolicy; import org.simantics.diagram.stubs.DiagramResource; import org.simantics.diagram.synchronization.CopyAdvisor; import org.simantics.diagram.synchronization.ErrorHandler; import org.simantics.diagram.synchronization.IModifiableSynchronizationContext; import org.simantics.diagram.synchronization.StatementEvaluation; import org.simantics.diagram.synchronization.SynchronizationException; import org.simantics.diagram.synchronization.SynchronizationHints; import org.simantics.diagram.synchronization.graph.AddConnection; import org.simantics.diagram.synchronization.graph.AddElement; import org.simantics.diagram.synchronization.graph.CopyAdvisorUtil; import org.simantics.diagram.synchronization.graph.DiagramGraphUtil; import org.simantics.diagram.synchronization.graph.GraphSynchronizationHints; import org.simantics.diagram.synchronization.graph.layer.GraphLayerManager; import org.simantics.g2d.element.IElement; import org.simantics.layer0.Layer0; import org.simantics.modeling.ModelingResources; import org.simantics.operation.Layer0X; import org.simantics.scl.runtime.tuple.Tuple2; import org.simantics.scl.runtime.tuple.Tuple3; import org.simantics.structural.stubs.StructuralResource2; import org.simantics.structural2.modelingRules.CPTerminal; import org.simantics.structural2.modelingRules.ConnectionJudgement; import org.simantics.structural2.modelingRules.IConnectionPoint; import org.simantics.utils.datastructures.BinaryFunction; import org.simantics.utils.datastructures.map.Tuple; /** * @author Tuukka Lehtonen */ public class Paster { private final boolean DEBUG = DebugPolicy.DEBUG_COPY_PASTE | true; private final Session session; private final PasteOperation op; private final Resource sourceDiagram; private final Resource targetDiagram; private final IModifiableSynchronizationContext targetContext; /** * Set for the duration of the write. */ private WriteGraph graph; private ConnectionUtil cu; private Layer0 L0; private Layer0X L0X; private DiagramResource DIA; private ModelingResources MOD; private StructuralResource2 STR; /** * A translating affine transform that can be pre-concatenated to existing * affine transforms to move them around according to the paste operation * offset specification. */ private AffineTransform offsetTransform; /** * A node map for post-processing copied resources */ private NodeMap nodeMap; private Resource sourceRoot; private Resource targetRoot; private String sourceRootUri; private String targetRootUri; private boolean operateWithinSameRoot; /** * @param session * @param op * @throws DatabaseException */ public Paster(Session session, PasteOperation op) throws DatabaseException { this.session = session; this.op = op; this.sourceDiagram = op.sourceDiagram; this.targetDiagram = op.targetDiagram; if (this.sourceDiagram == null) throw new IllegalArgumentException("source diagram has no resource"); if (this.targetDiagram == null) throw new IllegalArgumentException("target diagram has no resource"); this.targetContext = (IModifiableSynchronizationContext) op.target.getHint(SynchronizationHints.CONTEXT); if (this.targetContext == null) throw new IllegalArgumentException("target diagram has no synchronization context"); this.offsetTransform = AffineTransform.getTranslateInstance(op.offset.getX(), op.offset.getY()); } private String toString(PasteOperation op) { StringBuilder sb = new StringBuilder(); sb.append("Diagram paste "); sb.append(op.ea.all.size()); sb.append(" element(s) "); if (op.cut) sb.append("cut"); else sb.append("copied"); sb.append(" from "); sb.append(op.sourceDiagram); sb.append(" to "); sb.append(op.targetDiagram); return sb.toString(); } public void perform() throws DatabaseException { final String comment = toString(op); Write request = new WriteRequest() { @Override public void perform(WriteGraph graph) throws DatabaseException { graph.markUndoPoint(); Paster.this.perform(graph); // Add comment to change set. CommentMetadata cm = graph.getMetadata(CommentMetadata.class); graph.addMetadata(cm.add(comment)); } }; session.syncRequest(request); } public void perform(WriteGraph graph) throws DatabaseException { L0 = Layer0.getInstance(graph); L0X = Layer0X.getInstance(graph); STR = StructuralResource2.getInstance(graph); DIA = DiagramResource.getInstance(graph); MOD = ModelingResources.getInstance(graph); this.graph = graph; this.cu = new ConnectionUtil(graph); this.sourceRoot = graph.sync(new IndexRoot(sourceDiagram)); this.targetRoot = graph.sync(new IndexRoot(targetDiagram)); this.sourceRootUri = graph.getURI(sourceRoot); this.targetRootUri = graph.getURI(targetRoot); this.operateWithinSameRoot = sourceRoot.equals(targetRoot); try { if (op.cut) cut(); else copy(); } catch (DatabaseException e) { throw e; } catch (Exception e) { throw new DatabaseException(e); } finally { this.cu = null; this.graph = null; } } private void onFinish() { final CopyAdvisor advisor = op.target.getHint(SynchronizationHints.COPY_ADVISOR); if (advisor != null) { try { targetContext.set(GraphSynchronizationHints.READ_TRANSACTION, graph); targetContext.set(GraphSynchronizationHints.WRITE_TRANSACTION, graph); advisor.onFinish(targetContext); } catch (SynchronizationException e) { ErrorHandler eh = targetContext.get(SynchronizationHints.ERROR_HANDLER); eh.error(e.getMessage(), e); } finally { targetContext.set(GraphSynchronizationHints.READ_TRANSACTION, null); targetContext.set(GraphSynchronizationHints.WRITE_TRANSACTION, null); } } } // ------------------------------------------------------------------------ // SIMPLIFICATIONS // ------------------------------------------------------------------------ interface Procedure { void execute(Resource resource) throws Exception; } public void forEachResourceElement(String description, Collection elements, Procedure procedure) throws Exception { for (Object object : elements) { if (object instanceof Resource) { procedure.execute((Resource) object); } else { if (DEBUG) { System.out.println("[" + description + "] Skipping non-resource element: " + object); } } } } private void applyPasteOffset(Resource forResource) throws DatabaseException { applyOffset(forResource, op.offset); } private void applyOffset(Resource forResource, Point2D offset) throws DatabaseException { AffineTransform at = DiagramGraphUtil.getTransform(graph, forResource); at.preConcatenate(AffineTransform.getTranslateInstance(offset.getX(), offset.getY())); DiagramGraphUtil.setTransform(graph, forResource, at); } private void applyPasteOffsetToRouteLine(Resource routeLine) throws DatabaseException { Boolean isHorizontal = graph.getPossibleRelatedValue(routeLine, DIA.IsHorizontal, Bindings.BOOLEAN); Double pos = graph.getPossibleRelatedValue(routeLine, DIA.HasPosition, Bindings.DOUBLE); if (pos == null) pos = 0.0; if (Boolean.TRUE.equals(isHorizontal)) pos += op.offset.getY(); else pos += op.offset.getX(); graph.claimLiteral(routeLine, DIA.HasPosition, pos, Bindings.DOUBLE); } // ------------------------------------------------------------------------ // CUT LOGIC // ------------------------------------------------------------------------ Resource parentElement(Resource resource, Resource referenceRelation) throws DatabaseException { // Only allow cutting if reference element has a parent and it is selected for cutting also. Resource referencedParentComponent = graph.getPossibleObject(resource, referenceRelation); if (referencedParentComponent == null) return null; return graph.getPossibleObject(referencedParentComponent, MOD.ComponentToElement); } boolean parentIsIncludedInCut(Resource resource, Resource referenceRelation, boolean noParentElementReturnValue) throws DatabaseException { Resource referencedElement = parentElement(resource, referenceRelation); if (referencedElement != null) return op.ea.all.contains(referencedElement); return noParentElementReturnValue; } private void cut() throws Exception { final GraphLayerManager glm = targetContext.get(GraphSynchronizationHints.GRAPH_LAYER_MANAGER); final THashSet cutElements = new THashSet(); final CutProcedure registerNames = new CutProcedure() { void postCut(Resource resource, Object cutResult) throws Exception { String name = graph.getPossibleRelatedValue(resource, L0.HasName, Bindings.STRING); if (name != null) { cutElements.add(resource); } } }; final CutProcedure nodeCutProcedure = new CutProcedure() { @Override void postCut(Resource resource, Object cutResult) throws Exception { if (cutResult != null) { applyPasteOffset(resource); if (glm != null) { glm.removeFromAllLayers(graph, resource); glm.putElementOnVisibleLayers(op.target, graph, resource); } } } }; CutProcedure flagCutProcedure = new CutProcedure() { @Override boolean preCut(Resource resource) throws Exception { return nodeCutProcedure.preCut(resource); } @Override void postCut(Resource resource, Object cutResult) throws Exception { nodeCutProcedure.postCut(resource, cutResult); if (FlagUtil.isJoinedInSingleDiagram(graph, resource)) { FlagLabelingScheme scheme = DiagramFlagPreferences.getActiveFlagLabelingScheme(graph); String commonLabel = scheme.generateLabel(graph, targetDiagram); graph.claimLiteral(resource, L0.HasLabel, DIA.FlagLabel, commonLabel); for (Resource otherFlag : FlagUtil.getCounterparts(graph, resource)) graph.claimLiteral(otherFlag, L0.HasLabel, DIA.FlagLabel, commonLabel, Bindings.STRING); } IOTablesInfo ioTablesInfo = IOTableUtil.getIOTablesInfo(graph, op.targetDiagram); double[] transform = graph.getRelatedValue(resource, DIA.HasTransform, Bindings.DOUBLE_ARRAY); ioTablesInfo.updateBinding(graph, DIA, resource, transform[4], transform[5]); // #11077: fix STR.JoinsComposite relations from joins related to moved flags. // Otherwise the JoinsComposite relations will be wrong after the cut-operation. for (Resource join : graph.getObjects(resource, DIA.FlagIsJoinedBy)) fixConnectionJoin(join); } Set flagComposites = new HashSet<>(); Set joinedComposites = new HashSet<>(); Set invalidJoinedComposites = new HashSet<>(); void fixConnectionJoin(Resource join) throws DatabaseException { Collection flags = graph.getObjects(join, DIA.JoinsFlag); if (flags.size() < 2) { // Broken join, remove it. Joins that have // < 2 flags attached to it shouldn't exist. graph.deny(join); } else { flagComposites.clear(); possibleCompositesOfElements(flags, flagComposites); joinedComposites.clear(); joinedComposites.addAll( graph.getObjects(join, STR.JoinsComposite) ); // Calculate which JoinsComposites need to be added and which removed. invalidJoinedComposites.clear(); invalidJoinedComposites.addAll(joinedComposites); invalidJoinedComposites.removeAll(flagComposites); flagComposites.removeAll(joinedComposites); if (!invalidJoinedComposites.isEmpty()) { for (Resource invalidComposite : invalidJoinedComposites) graph.deny(join, STR.JoinsComposite, invalidComposite); } if (!flagComposites.isEmpty()) { for (Resource joinedComposite : flagComposites) graph.claim(join, STR.JoinsComposite, joinedComposite); } } } Set possibleCompositesOfElements(Collection elements, Set result) throws DatabaseException { for (Resource e : elements) { Resource composite = possibleCompositeOfElement(e); if (composite != null) result.add(composite); } return result; } Resource possibleCompositeOfElement(Resource element) throws DatabaseException { Resource diagram = graph.getPossibleObject(element, L0.PartOf); return diagram != null ? graph.getPossibleObject(diagram, MOD.DiagramToComposite) : null; } }; CutProcedure monitorCutProcedure = new CutProcedure() { @Override void postCut(Resource resource, Object cutResult) throws DatabaseException { if (cutResult != null) { Resource parentElement = parentElement(resource, DIA.HasMonitorComponent); if (parentElement == null) { applyPasteOffset(resource); } else if (!op.ea.all.contains(parentElement)) { Point2D offset = op.offset; if (!op.sameDiagram()) { Resource parentDiagram = graph.sync(new PossibleTypedParent(parentElement, DIA.Diagram)); AffineTransform monitoredComponentTr = DiagramGraphUtil.getWorldTransform(graph, parentElement); if (op.targetDiagram.equals(parentDiagram)) { // Monitor is moved back to the parent element diagram. // Must make monitor position relative to the parent position. offset = new Point2D.Double( op.offset.getX() - monitoredComponentTr.getTranslateX(), op.offset.getY() - monitoredComponentTr.getTranslateY()); } else { // Monitor is moved to another diagram than the parent element diagram. // Must make monitor position absolute. offset = new Point2D.Double( op.offset.getX() + monitoredComponentTr.getTranslateX(), op.offset.getY() + monitoredComponentTr.getTranslateY()); } } applyOffset(resource, offset); } if (glm != null) { glm.removeFromAllLayers(graph, resource); glm.putElementOnVisibleLayers(op.target, graph, resource); } } } }; CutProcedure referenceElementCutProcedure = new CutProcedure() { @Override boolean preCut(Resource resource) throws DatabaseException { // Only allow cutting if reference element has a parent and it is selected for cutting also. return parentIsIncludedInCut(resource, MOD.HasParentComponent, true); } @Override void postCut(Resource resource, Object cutResult) throws Exception { if (cutResult != null) { if (!parentIsIncludedInCut(resource, MOD.HasParentComponent, false)) { applyPasteOffset(resource); } if (glm != null) { glm.removeFromAllLayers(graph, resource); glm.putElementOnVisibleLayers(op.target, graph, resource); } } } }; CutProcedure connectionCutProcedure = new CutProcedure() { @Override void postCut(Resource resource, Object cutResult) throws Exception { if (cutResult != null) { for (Resource rn : graph.getObjects(resource, DIA.HasInteriorRouteNode)) { if (graph.isInstanceOf(rn, DIA.BranchPoint)) applyPasteOffset(rn); else if (graph.isInstanceOf(rn, DIA.RouteLine)) applyPasteOffsetToRouteLine(rn); } if (glm != null) { glm.removeFromAllLayers(graph, resource); glm.putElementOnVisibleLayers(op.target, graph, resource); } } } }; // Before cutting, disconnect all nodes from connections that are not in // the cut connection set. final Set selectedConnections = new HashSet(); forEachResourceElement("Gather connections", op.ea.connections, new Procedure() { @Override public void execute(Resource resource) throws Exception { selectedConnections.add(resource); } }); Set affectedConnections = new HashSet(); disconnectExcludedConnections("Disconnect Nodes", op.ea.nodeList, selectedConnections, affectedConnections); disconnectExcludedConnections("Disconnect Flags", op.ea.flags, selectedConnections, affectedConnections); for (Resource connection : affectedConnections) { // Leave the connection only if it has: // - at least one truly connected :DIA.Connector // - at least 1 route/branch points int connectedConnectors = cu.getConnectedConnectors(connection, null).size(); int branchPoints = cu.getBranchPoints(connection, null).size(); if (connectedConnectors > 0 && branchPoints > 0) continue; // Remove the whole connection. cu.removeConnection(connection); } cut("Cut Nodes", op.ea.nodeList, compose(nodeCutProcedure, registerNames)); cut("Cut Others", op.ea.others, compose(nodeCutProcedure, registerNames)); // Cut reference elements after nodes so that parent relationships can be restored // but before connections so that connections to the reference elements can be copied. cut("Cut References", op.ea.references, compose(referenceElementCutProcedure, registerNames)); cut("Cut Flags", op.ea.flags, compose(flagCutProcedure, registerNames)); cut("Cut Connections", op.ea.connections, compose(connectionCutProcedure, registerNames)); cut("Cut Monitors", op.ea.monitors, compose(monitorCutProcedure, registerNames)); // Make sure that all the pasted nodes have unique names in their new namespace. // Element names are only diagram-locally unique so this must be done after cut-paste. for (Resource element : cutElements) AddElement.claimFreshElementName(graph, targetDiagram, element); onFinish(); } /** * @param description * @param nodes * @param affectedConnections * @return * @throws Exception */ private Set disconnectExcludedConnections(String description, Collection nodes, final Set selectedConnections, final Set affectedConnections) throws Exception { final StructuralResource2 str = StructuralResource2.getInstance(graph); // Disconnect each connection that is not a part of selectedConnections // but is attached to the listed nodes. forEachResourceElement(description, nodes, new Procedure() { @Override public void execute(Resource resource) throws Exception { for (Resource connector : graph.getObjects(resource, str.IsConnectedTo)) { Resource connection = ConnectionUtil.tryGetConnection(graph, connector); if (connection == null) { // This is a stray connector that has no purpose and should be removed. cu.removeConnectionPart(connector); continue; } if (selectedConnections.contains(connection)) continue; cu.removeConnectionPart(connector); affectedConnections.add(connection); } } }); return affectedConnections; } /** * @param description * @param elements * @param cutProcedure custom pre- and post-cut processing, may be null * @throws Exception */ private void cut(final String description, Collection elements, final CutProcedure cutProcedure) throws Exception { final CopyAdvisor advisor = op.target.getHint(SynchronizationHints.COPY_ADVISOR); forEachResourceElement(description, elements, new Procedure() { @Override public void execute(Resource resource) throws Exception { if (DEBUG) System.out.println("[" + description + "] " + NameUtils.getSafeName(graph, resource, true)); if (cutProcedure != null && !cutProcedure.preCut(resource)) { if (DEBUG) System.out.println("[" + description + "] ignoring element cut for " + NameUtils.getSafeName(graph, resource, true)); return; } Object result = CopyAdvisorUtil.cut(targetContext, graph, advisor, resource, sourceDiagram, targetDiagram); if (DEBUG) System.out.println("[" + description + "] RESULT: " + result); if (cutProcedure != null) cutProcedure.postCut(resource, result); } }); } static class CutProcedure { boolean preCut(Resource resource) throws Exception { return true; } void postCut(Resource resource, Object cutResult) throws Exception {} } static class ComposedCutProcedure extends CutProcedure { private final CutProcedure[] procedures; public static ComposedCutProcedure compose(CutProcedure... procedures) { return new ComposedCutProcedure(procedures); } public ComposedCutProcedure(CutProcedure... procedures) { this.procedures = procedures; } boolean preCut(Resource resource) throws Exception { for (CutProcedure proc : procedures) if (!proc.preCut(resource)) return false; return true; } void postCut(Resource resource, Object cutResult) throws Exception { for (CutProcedure proc : procedures) proc.postCut(resource, cutResult); } } // ------------------------------------------------------------------------ // COPY LOGIC SUPPORT CLASSES // ------------------------------------------------------------------------ static class IdentifiedElement extends Tuple { public IdentifiedElement(Resource object, IElement element) { super(object, element); } public Resource getObject() { return (Resource) getField(0); } public IElement getElement() { return (IElement) getField(1); } } static public class NodeMap { Map resourceMap = new HashMap(); Map elementMap = new HashMap(); public void put(Resource sourceResource, IElement sourceElement, IdentifiedElement dst) { if (sourceResource == null) throw new NullPointerException("null source resource"); resourceMap.put(sourceResource, dst); if (sourceElement != null) elementMap.put(sourceElement, dst); } public IdentifiedElement get(Resource source) { return resourceMap.get(source); } public IdentifiedElement get(IElement source) { return elementMap.get(source); } public Set allResources() { return resourceMap.keySet(); } public Resource getResource(Resource source) { IdentifiedElement ie = resourceMap.get(source); if(ie != null) return ie.getObject(); else return null; } public Resource getResource(IElement source) { IdentifiedElement ie = elementMap.get(source); if(ie != null) return ie.getObject(); else return null; } } static class ResourceMap extends HashMap { private static final long serialVersionUID = 687528035082504835L; } static class StatementMap extends HashMap { private static final long serialVersionUID = 8520092255776208395L; } static class MapQueue { Map> map = new HashMap>(); public void offer(K key, V value) { Deque deque = map.get(key); if (deque == null) map.put(key, deque = new ArrayDeque()); deque.offer(value); } public V poll(K key) { Deque deque = map.get(key); if (deque == null) return null; V value = deque.poll(); if (deque.isEmpty()) map.remove(key); return value; } } // ------------------------------------------------------------------------ // COPY LOGIC // ------------------------------------------------------------------------ /** * This is necessary to have DIA.Flag type copied over to the copied flag. * Diagram mapping will have problems and potentially break the * configuration if the type is not the same as in the source. */ BinaryFunction statementAdvisor = new BinaryFunction() { @Override public StatementEvaluation call(ReadGraph graph, Statement stm) { if (DIA.HasFlagType.equals(stm.getPredicate())) return StatementEvaluation.INCLUDE; return StatementEvaluation.USE_DEFAULT; } }; CopyProcedure nodeCopyProcedure = new CopyProcedure() { Resource copy(Resource source) throws Exception { Layer0 L0 = Layer0.getInstance(graph); Resource copy = null; final CopyAdvisor advisor = op.target.getHint(SynchronizationHints.COPY_ADVISOR); if (advisor != null) { Resource sourceComposite = graph.getPossibleObject(source, L0.PartOf); if (sourceComposite == null || !graph.isInstanceOf(source, DIA.Composite)) { DiagramResource DIA = DiagramResource.getInstance(graph); sourceComposite = OrderedSetUtils.getSingleOwnerList(graph, source, DIA.Composite); } copy = CopyAdvisorUtil.copy(targetContext, graph, advisor, source, sourceComposite, op.targetDiagram); } if (copy == null) { copy = CopyAdvisorUtil.copy2(graph, source, statementAdvisor); } graph.deny(copy, MOD.IsTemplatized, copy); // Add comment to change set. CommentMetadata cm = graph.getMetadata(CommentMetadata.class); graph.addMetadata(cm.add("Copied element " + source + " to " + copy)); // Add the new element to the diagram composite OrderedSetUtils.add(graph, op.targetDiagram, copy); // Give running name to element and increment the counter attached to the diagram. AddElement.claimFreshElementName(graph, op.targetDiagram, copy); // Make the diagram consist of the new element graph.claim(op.targetDiagram, L0.ConsistsOf, copy); // Put the element on all the currently active layers if possible. GraphLayerManager glm = targetContext.get(GraphSynchronizationHints.GRAPH_LAYER_MANAGER); if (glm != null) { glm.removeFromAllLayers(graph, copy); glm.putElementOnVisibleLayers(op.target, graph, copy); } return copy; } @Override void postCopy(Resource source, Resource copy) throws Exception { CopyPasteUtil.copyElementPosition(graph, op.ctx, source, copy, op.offset); } }; private void copy() throws Exception { nodeMap = new NodeMap(); CommonDBUtils.selectClusterSet(graph, targetDiagram); // Fill nodeMap with initial Resource->Resource mappings if (op.initialNodeMap != null) { for (Map.Entry entry : op.initialNodeMap.entrySet()) { nodeMap.put(entry.getKey(), null, new IdentifiedElement(entry.getValue(), null)); } } // Perform copies in a suitable order copyNodes( nodeMap ); // Copy reference elements after nodes so that parent relationships can be restored // but before connections so that connections to the reference elements can be copied. copyReferences( nodeMap ); copyFlags( nodeMap ); copyConnections( nodeMap ); // Copy monitors last since their parents must have been copied already. copyMonitors( nodeMap ); onFinish(); } private NodeMap copyNodes(final NodeMap nodeMap) throws Exception { copy("Copy Others", op.ea.others, nodeMap, nodeCopyProcedure); copy("Copy Nodes", op.ea.nodeList, nodeMap, nodeCopyProcedure); return nodeMap; } private NodeMap copyReferences(final NodeMap nodeMap) throws Exception { final boolean forceCopyReferences = op.hasOption(ForceCopyReferences.class); copy("Copy References", op.ea.references, nodeMap, new CopyProcedure() { @Override Resource copy(Resource source) throws Exception { // Don't copy unless the parent component is copied too. Resource sourceParentComponent = graph.getPossibleObject(source, MOD.HasParentComponent); if (sourceParentComponent == null) return null; Resource sourceParentElement = graph.getPossibleObject(sourceParentComponent, MOD.ComponentToElement); if (sourceParentElement != null) { if (!forceCopyReferences && !op.ea.all.contains(sourceParentElement)) return null; // Find copied component IdentifiedElement copiedParentElement = nodeMap.get(sourceParentElement); if (copiedParentElement == null) return null; Resource copiedParentComponent = graph.getPossibleObject(copiedParentElement.getObject(), MOD.ElementToComponent); if (copiedParentComponent == null) return null; return copyReference(source, copiedParentComponent); } else { // Check that the component is part of a diagramless composite before proceeding Resource partOf = graph.getPossibleObject(sourceParentComponent, L0.PartOf); if (partOf == null || graph.hasStatement(partOf, MOD.CompositeToDiagram)) return null; // Resolve the matching parent component from the target context. Resource targetParentComponent = resolveTargetComponent(sourceParentComponent); if (targetParentComponent == null) return null; return copyReference(source, targetParentComponent); } } private Resource resolveTargetComponent(Resource sourceParentComponent) throws DatabaseException { if (operateWithinSameRoot) return sourceParentComponent; // Directly map relative source component URI into target root namespace. String sourceUri = graph.getURI(sourceParentComponent); String targetUri = sourceUri.replace(sourceRootUri, targetRootUri); Resource targetParentComponent = graph.getPossibleResource(targetUri); return targetParentComponent; } private Resource copyReference(Resource source, Resource parentComponent) throws Exception { Resource referenceRelation = graph.getPossibleObject(source, MOD.HasReferenceRelation); if (referenceRelation == null) return null; Resource relationCopy = CopyAdvisorUtil.copy4(graph, referenceRelation); if (relationCopy == null) return null; Resource copy = nodeCopyProcedure.copy(source); // WORKAROUND: The result consists of a badly copied reference relation. // Remove it. How the relation is copied depends on whether the copy target // is the same model or not. If it is, the relation is copied, but invalidly // and if the target is not the same model, the relation is simply referenced // with a uni-directional L0.ConsistsOf relation. for (Resource o : graph.getObjects(copy, L0.ConsistsOf)) { boolean ownedByCopy = graph.hasStatement(o, L0.PartOf, copy); if (ownedByCopy) { graph.deny(copy, L0.ConsistsOf, o); RemoverUtil.remove(graph, o); } else { graph.deny(copy, L0.ConsistsOf, o); } } // The element the copied reference is attached to was also copied. // This means that we must attach the copied reference to its // original component's copy. graph.deny(copy, MOD.HasParentComponent); if(parentComponent != null) graph.claim(copy, MOD.HasParentComponent, MOD.HasParentComponent_Inverse, parentComponent); // Attach reference relation graph.claim(copy, L0.ConsistsOf, L0.PartOf, relationCopy); graph.claim(copy, MOD.HasReferenceRelation, MOD.HasReferenceRelation_Inverse, relationCopy); return copy; } @Override void postCopy(Resource source, Resource copy) throws Exception { // Must fix element position if the copied reference element // doesn't have a visible parent element. Resource parentComponent = graph.getPossibleObject(source, MOD.HasParentComponent); if (parentComponent == null) return; Resource parentElement = graph.getPossibleObject(parentComponent, MOD.ComponentToElement); if (parentElement == null) CopyPasteUtil.copyElementPosition(graph, op.ctx, source, copy, op.offset); } }); return nodeMap; } private NodeMap copyFlags(NodeMap nodeMap) throws Exception { final Layer0 l0 = Layer0.getInstance(graph); final DiagramResource dia = DiagramResource.getInstance(graph); class FlagCopy { private final Map selectedFlags = new HashMap(); private final Map flagSelectedCounterpart = new HashMap(); /** * Analyze which flag pairs are selected * * @throws DatabaseException */ private void analyzeFlagSelection() throws DatabaseException { for (Resource flag : op.ea.flags) { selectedFlags.put(flag, flag); } for (Resource flag : selectedFlags.keySet()) { boolean external = FlagUtil.isExternal(graph, flag); boolean inSingleDiagram = FlagUtil.isJoinedInSingleDiagram(graph, flag); if (!external && inSingleDiagram) { // FIXME: this doesn't take into account local merged flags, which is a corner case but still possible Resource counterpart = FlagUtil.getPossibleCounterpart(graph, flag); if (selectedFlags.containsKey(counterpart)) { flagSelectedCounterpart.put(flag, counterpart); flagSelectedCounterpart.put(counterpart, flag); } } } } /** * Reconnect copied flag pairs. * @throws DatabaseException */ private void reconnectLocalFlagPairs(NodeMap nodeMap) throws DatabaseException { FlagLabelingScheme scheme = DiagramFlagPreferences.getActiveFlagLabelingScheme(graph); Resource diagram = op.targetDiagram; Set visited = new HashSet(); ArrayDeque queue = new ArrayDeque(flagSelectedCounterpart.values()); while (!queue.isEmpty()) { Resource flag = queue.poll(); Resource counterpart = flagSelectedCounterpart.get(flag); if (!visited.add(flag) || !visited.add(counterpart) || counterpart == null) continue; // Get copies Resource flagSourceElement = selectedFlags.get(flag); Resource counterpartSourceElement = selectedFlags.get(counterpart); IdentifiedElement flagCopy = nodeMap.get(flagSourceElement); IdentifiedElement counterpartCopy = nodeMap.get(counterpartSourceElement); FlagUtil.join(graph, flagCopy.getObject(), counterpartCopy.getObject()); // Provide fresh labeling for connected flags if possible if (scheme != null) { String label = scheme.generateLabel(graph, diagram); if (label != null) { graph.claimLiteral(flagCopy.getObject(), l0.HasLabel, dia.FlagLabel, label, Bindings.STRING); graph.claimLiteral(counterpartCopy.getObject(), l0.HasLabel, dia.FlagLabel, label, Bindings.STRING); } } } } public void perform(NodeMap nodeMap) throws Exception { analyzeFlagSelection(); copy("Copy Flags", op.ea.flags, nodeMap, new CopyProcedure() { @Override Resource copy(Resource source) throws Exception { return nodeCopyProcedure.copy(source); } @Override public void postCopy(Resource source, Resource copy) throws Exception { AffineTransform at = CopyPasteUtil.copyElementPosition(graph, op.ctx, source, copy, op.offset); // Update flag table binding IOTablesInfo ioTablesInfo = IOTableUtil.getIOTablesInfo(graph, op.targetDiagram); ioTablesInfo.updateBinding(graph, DIA, copy, at.getTranslateX(), at.getTranslateY()); // All label properties must be removed from // the copied flags. Disconnected flags are // not supposed to have labels, and the right // place to reset the labels is when the flags // are reconnected to their respective // counterparts. graph.denyValue(copy, l0.HasLabel); } }); reconnectLocalFlagPairs(nodeMap); } } new FlagCopy().perform( nodeMap ); return nodeMap; } private NodeMap copyMonitors(final NodeMap nodeMap) throws Exception { copy("Copy Monitors", op.ea.monitors, nodeMap, new CopyProcedure() { @Override Resource copy(Resource source) throws Exception { // Don't copy monitors if they are copied without // their parent element into another root (model). if (!operateWithinSameRoot) { Resource monitorComponent = graph.getPossibleObject(source, DIA.HasMonitorComponent); if (monitorComponent != null) { Resource monitorElement = graph.getPossibleObject(monitorComponent, MOD.ComponentToElement); if (monitorElement == null || !op.ea.all.contains(monitorElement)) return null; } } Resource copy = nodeCopyProcedure.copy(source); return copy; } @Override void postCopy(Resource source, Resource copy) throws Exception { // Find the component and diagram element the source monitor is // connected to. Resource monitorElement = null; Resource monitorComponent = graph.getPossibleObject(source, DIA.HasMonitorComponent); if (monitorComponent != null) { monitorElement = graph.getPossibleObject(monitorComponent, MOD.ComponentToElement); } if (monitorElement != null && op.ea.all.contains(monitorElement)) { // The element the copied monitor is attached was also copied. // This means that we must attach the copied monitor to its // original components copy. // Remove old association graph.deny(copy, DIA.HasMonitorComponent); // Associate to copied component IdentifiedElement parent = nodeMap.get(monitorElement); if (parent != null) { monitorComponent = graph.getPossibleObject(parent.getObject(), MOD.ElementToComponent); if (monitorComponent != null) graph.claim(copy, DIA.HasMonitorComponent, monitorComponent); } else { //throw new PasteException("no parent could be found for monitored element " + monitoredElement); } } else { // The element the copied monitor is attached was not copied // or there is no element for the monitored component. // This means that the copied monitor must be kept attached // to the same component no matter where it is in the model, // unless the copy is done into another model. if (operateWithinSameRoot && monitorComponent != null) graph.claim(copy, DIA.HasMonitorComponent, monitorComponent); Point2D offset = op.offset; if (!op.sameDiagram()) { if (monitorElement != null) { // Monitor doesn't have a diagram parent element any // more, must recalculate its offset. AffineTransform monitoredComponentTr = DiagramGraphUtil.getWorldTransform(graph, monitorElement); offset = new Point2D.Double( op.offset.getX() + monitoredComponentTr.getTranslateX(), op.offset.getY() + monitoredComponentTr.getTranslateY()); } } CopyPasteUtil.copyElementPosition(graph, op.ctx, source, copy, offset); } // Copy monitor suffix from original to copy. String monitorSuffix = graph.getPossibleRelatedValue(source, DIA.HasMonitorSuffix, Bindings.STRING); if (monitorSuffix != null) graph.claimLiteral(copy, DIA.HasMonitorSuffix, monitorSuffix, Bindings.STRING); // Copy used property obtains for monitor template data. graph.deny(copy, L0X.ObtainsProperty); for (Statement stm : graph.getStatements(source, L0X.ObtainsProperty)) { graph.claim(copy, stm.getPredicate(), null, stm.getObject()); } } }); return nodeMap; } /** * @param description * @param elements * @param nodeMap * @param copyProcedure * @throws Exception */ private void copy(final String description, Collection elements, final NodeMap nodeMap, final CopyProcedure copyProcedure) throws Exception { if (copyProcedure == null) throw new IllegalArgumentException("null copy procedure"); forEachResourceElement(description, elements, new Procedure() { @Override public void execute(Resource resource) throws Exception { if (DEBUG) System.out.println("[" + description + "] " + NameUtils.getSafeName(graph, resource, true)); Resource copy = copyProcedure.copy(resource); if (copy != null) { if (DEBUG) System.out.println("[" + description + "] " + NameUtils.getSafeName(graph, resource, true) + " copied as " + NameUtils.getSafeName(graph, copy, true)); nodeMap.put(resource, null, new IdentifiedElement(copy, null)); if (op.copyMap != null) op.copyMap.put(resource, copy); copyProcedure.postCopy(resource, copy); } } }); } public static class RouteLine extends Tuple2 { public RouteLine(Double position, Boolean horizontal) { super(position, horizontal); } public double getPosition() { Double pos = (Double) get(0); return pos != null ? pos : 0.0; } public boolean isHorizontal() { return Boolean.TRUE.equals(get(1)); } } public static class BranchPoint extends Tuple3 { public BranchPoint(AffineTransform at, Boolean horizontal, Boolean vertical) { super(at, horizontal, vertical); } public AffineTransform getTransform() { return (AffineTransform) get(0); } } public static RouteLine readRouteLine(ReadGraph graph, Resource src) throws DatabaseException { DiagramResource DIA = DiagramResource.getInstance(graph); Double pos = graph.getPossibleRelatedValue(src, DIA.HasPosition, Bindings.DOUBLE); Boolean hor = graph.getPossibleRelatedValue(src, DIA.IsHorizontal, Bindings.BOOLEAN); return new RouteLine(pos, hor); } public static BranchPoint readBranchPoint(ReadGraph graph, Resource src) throws DatabaseException { DiagramResource DIA = DiagramResource.getInstance(graph); AffineTransform at = DiagramGraphUtil.getTransform(graph, src); boolean hor = graph.hasStatement(src, DIA.Horizontal); boolean ver = graph.hasStatement(src, DIA.Vertical); return new BranchPoint(at, hor, ver); } /** * @param nodeMap * @return * @throws Exception */ private NodeMap copyConnections(final NodeMap nodeMap) throws Exception { final StructuralResource2 STR = StructuralResource2.getInstance(graph); final DiagramResource DIA = DiagramResource.getInstance(graph); // final IModelingRules rules = graph.syncRequest(DiagramRequests.getModelingRules(op.sourceDiagram, null)); // if (rules == null) // throw new IllegalArgumentException("source diagram offers no modeling rules"); final CopyAdvisor ca = op.target.getHint(SynchronizationHints.COPY_ADVISOR); if (ca == null) throw new UnsupportedOperationException("Cannot copy connections, no copy advisor available for diagram " + op.target); forEachResourceElement("Copy Connections", op.ea.connections, new Procedure() { @Override public void execute(Resource sourceObject) throws DatabaseException { copyConnection(sourceObject); } private void copyConnection(Resource sourceObject) throws DatabaseException { // For associating source<->destination connection parts final Map resourceMap = new THashMap(); // For associating source connectors to source nodes final StatementMap connectorToNode = new StatementMap(); // 1. copy connection // - This will also copy interior route nodes // - But will leave out the DIA.AreConnected relations between route nodes Resource sourceDiagram = graph.getPossibleObject(sourceObject, Layer0.getInstance(graph).PartOf); if (sourceDiagram == null) sourceDiagram = OrderedSetUtils.getSingleOwnerList(graph, sourceObject, DIA.Diagram); Resource copy = CopyAdvisorUtil.copy(targetContext, graph, ca, sourceObject, sourceDiagram, op.targetDiagram, resourceMap); if (copy == null) throw new UnsupportedOperationException("Could not copy connection " + sourceObject); OrderedSetUtils.addFirst(graph, op.targetDiagram, copy); graph.deny(copy, MOD.IsTemplatized, copy); AddElement.claimFreshElementName(graph, op.targetDiagram, copy); AddConnection.copyConnectionType(graph, sourceObject, copy); GraphLayerManager glm = targetContext.get(GraphSynchronizationHints.GRAPH_LAYER_MANAGER); if (glm != null) { glm.removeFromAllLayers(graph, copy); glm.putElementOnVisibleLayers(op.target, graph, copy); } nodeMap.put(sourceObject, null, new IdentifiedElement(copy, null)); if (op.copyMap != null) op.copyMap.put(sourceObject, copy); // WORKAROUND: CopyAdvisorUtil.copy(..., resourceMap) // implementations do not all support filling the resource map. // Thus we resort to the old logic if resourceMap is empty at this point. final boolean mapResources = resourceMap.isEmpty(); // 2. associate source connection parts to destination connection parts // Connectors Collection sourceHasConnectors = graph.getStatements(sourceObject, DIA.HasConnector); MapQueue connectorsByType = new MapQueue(); for (Statement hasConnector : sourceHasConnectors) { connectorsByType.offer(hasConnector.getPredicate(), hasConnector.getObject()); for (Statement connects : graph.getStatements(hasConnector.getObject(), STR.Connects)) { if (!sourceObject.equals(connects.getObject())) { connectorToNode.put(hasConnector.getObject(), connects); break; } } } if (mapResources) { for (Statement hasConnector : graph.getStatements(copy, DIA.HasConnector)) { Resource srcConnector = connectorsByType.poll(hasConnector.getPredicate()); resourceMap.put(srcConnector, hasConnector.getObject()); } } // 2.2. Offset interior route nodes Collection sourceInteriorRouteNodes = graph.getObjects(sourceObject, DIA.HasInteriorRouteNode); if (mapResources) { // WORKAROUND: for cases where resourceMap was not filled by // the copy operation. Still needed because TG copying does // not output this information. Queue branchPoints = new ArrayDeque(sourceInteriorRouteNodes.size()); Queue routeLines = new ArrayDeque(sourceInteriorRouteNodes.size()); for (Resource dst : graph.getObjects(copy, DIA.HasInteriorRouteNode)) { if (graph.isInstanceOf(dst, DIA.BranchPoint)) branchPoints.offer(dst); else if (graph.isInstanceOf(dst, DIA.RouteLine)) routeLines.offer(dst); } for (Resource src : sourceInteriorRouteNodes) { if (graph.isInstanceOf(src, DIA.BranchPoint)) { Resource dst = branchPoints.poll(); resourceMap.put(src, dst); BranchPoint bp = readBranchPoint(graph, src); AffineTransform at = bp.getTransform(); at.preConcatenate(offsetTransform); DiagramGraphUtil.setTransform(graph, dst, at); } else if (graph.isInstanceOf(src, DIA.RouteLine)) { Resource dst = routeLines.poll(); resourceMap.put(src, dst); RouteLine rl = readRouteLine(graph, src); double newPos = rl.getPosition() + (rl.isHorizontal() ? op.offset.getY() : op.offset.getX()); graph.claimLiteral(dst, DIA.HasPosition, newPos, Bindings.DOUBLE); } } } else { for (Resource src : sourceInteriorRouteNodes) { Resource dst = (Resource) resourceMap.get(src); if (dst != null) { if (graph.isInstanceOf(src, DIA.BranchPoint)) { BranchPoint bp = readBranchPoint(graph, src); AffineTransform at = bp.getTransform(); at.preConcatenate(offsetTransform); DiagramGraphUtil.setTransform(graph, dst, at); } else if (graph.isInstanceOf(src, DIA.RouteLine)) { RouteLine rl = readRouteLine(graph, src); double newPos = rl.getPosition() + (rl.isHorizontal() ? op.offset.getY() : op.offset.getX()); graph.claimLiteral(dst, DIA.HasPosition, newPos, Bindings.DOUBLE); } } } } // 3. Connect connection parts according to how the source is connected for (Resource src : sourceInteriorRouteNodes) { Resource dst = (Resource) resourceMap.get(src); for (Resource connectedToSrc : graph.getObjects(src, DIA.AreConnected)) { Resource connectedToDst = (Resource) resourceMap.get(connectedToSrc); if (connectedToDst != null) { graph.claim(dst, DIA.AreConnected, DIA.AreConnected, connectedToDst); } else { throw new DatabaseException("Connection copying failed due to an invalid DIA.AreConnected link between source resources " + src + " <-> " + connectedToSrc); } } } for (Statement hasConnector : sourceHasConnectors) { Resource srcConnector = hasConnector.getObject(); Resource dstConnector = (Resource) resourceMap.get(srcConnector); Statement srcConnects = connectorToNode.get(srcConnector); // Connect to copied nodes IdentifiedElement dstNode = nodeMap.get(srcConnects.getObject()); if (dstNode == null) throw new DatabaseException("Source element " + NameUtils.getURIOrSafeNameInternal(graph, srcConnects.getObject()) + " not copied causing copying of connection " + NameUtils.getURIOrSafeNameInternal(graph, sourceObject) +" to fail."); graph.claim(dstConnector, srcConnects.getPredicate(), dstNode.getObject()); // Connect to other copied route nodes for (Resource connectedToSrc : graph.getObjects(srcConnector, DIA.AreConnected)) { Resource connectedToDst = (Resource) resourceMap.get(connectedToSrc); graph.claim(dstConnector, DIA.AreConnected, DIA.AreConnected, connectedToDst); } } // 4. Make sure MOD.ConnectorToComponent relations are copied as well. // Otherwise diagram mapping will do bad things on the model. Resource sourceComponent = graph.getPossibleObject(sourceObject, MOD.ElementToComponent); if (sourceComponent != null) { for (Statement hasConnector : sourceHasConnectors) { Resource sourceConnector = hasConnector.getObject(); Resource targetConnector = (Resource) resourceMap.get(sourceConnector); // Should have been defined back in steps 1-2. assert targetConnector != null; Statement sourceConnectorToComponent = graph.getPossibleStatement(sourceConnector, MOD.ConnectorToComponent); if (sourceConnectorToComponent == null) continue; if (!sourceConnectorToComponent.getObject().equals(sourceComponent)) continue; Resource targetComponent = graph.getPossibleObject(copy, MOD.ElementToComponent); if (targetComponent == null) continue; graph.claim(targetConnector, sourceConnectorToComponent.getPredicate(), targetComponent); // #6190 & apros:#11435: Ensure that MOD.HasConnectionMappingSpecification is added to target for (Resource connectionMappingSpec : graph.getObjects(sourceConnector, MOD.HasConnectionMappingSpecification)) graph.claim(targetConnector, MOD.HasConnectionMappingSpecification, connectionMappingSpec); } } } }); return nodeMap; } class CopyProcedure { Resource copy(Resource source) throws Exception { throw new UnsupportedOperationException(); } void postCopy(Resource source, Resource copy) throws Exception {} } /** * @param judgment null if no judgement is available in which * case defaultValue is always returned * @param connectionPoint * @param defaultValue * @return * @throws DatabaseException */ @SuppressWarnings("unused") private static Resource getAttachmentRelation(ReadGraph graph, ConnectionJudgement judgment, IConnectionPoint connectionPoint, Resource defaultValue) throws DatabaseException { if (judgment == null || !(connectionPoint instanceof CPTerminal) || judgment.attachmentRelations == null) return defaultValue; Resource attachment = judgment.attachmentRelations.get(graph, (CPTerminal) connectionPoint); return attachment != null ? attachment : defaultValue; } /** * Get node map of copied variables. Map contains original and new resources. * * @return NodeMap of copied resources or null if copy has not been performed */ public NodeMap getNodeMap() { return nodeMap; } }