X-Git-Url: https://gerrit.simantics.org/r/gitweb?p=simantics%2Fplatform.git;a=blobdiff_plain;f=bundles%2Forg.simantics.modeling%2Fsrc%2Forg%2Fsimantics%2Fmodeling%2Ftypicals%2FSyncTypicalTemplatesToInstances.java;h=09545ada8b473a45c49a2a54a1d5fc4a13b0cb6c;hp=96a33393aeac5bab1c972d995cd1e0754a33ae71;hb=bd27aa4cd3ca0ce7d6e7d28bc3d86c381478ef63;hpb=0580ea8b675c868685993b0780c9ecc31010f681 diff --git a/bundles/org.simantics.modeling/src/org/simantics/modeling/typicals/SyncTypicalTemplatesToInstances.java b/bundles/org.simantics.modeling/src/org/simantics/modeling/typicals/SyncTypicalTemplatesToInstances.java index 96a33393a..09545ada8 100644 --- a/bundles/org.simantics.modeling/src/org/simantics/modeling/typicals/SyncTypicalTemplatesToInstances.java +++ b/bundles/org.simantics.modeling/src/org/simantics/modeling/typicals/SyncTypicalTemplatesToInstances.java @@ -1,1151 +1,1305 @@ -/******************************************************************************* - * Copyright (c) 2007, 2012 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.modeling.typicals; - -import java.awt.geom.Point2D; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.Date; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Set; - -import org.eclipse.core.runtime.IProgressMonitor; -import org.simantics.Simantics; -import org.simantics.databoard.Bindings; -import org.simantics.db.ReadGraph; -import org.simantics.db.Resource; -import org.simantics.db.Statement; -import org.simantics.db.WriteGraph; -import org.simantics.db.common.CommentMetadata; -import org.simantics.db.common.NamedResource; -import org.simantics.db.common.primitiverequest.Adapter; -import org.simantics.db.common.procedure.adapter.TransientCacheListener; -import org.simantics.db.common.request.ObjectsWithType; -import org.simantics.db.common.request.PossibleIndexRoot; -import org.simantics.db.common.request.WriteRequest; -import org.simantics.db.common.uri.UnescapedChildMapOfResource; -import org.simantics.db.common.utils.CommonDBUtils; -import org.simantics.db.common.utils.NameUtils; -import org.simantics.db.exception.DatabaseException; -import org.simantics.db.layer0.adapter.Instances; -import org.simantics.db.layer0.request.ActiveModels; -import org.simantics.db.layer0.util.RemoverUtil; -import org.simantics.diagram.content.ConnectionUtil; -import org.simantics.diagram.handler.CopyPasteStrategy; -import org.simantics.diagram.handler.ElementObjectAssortment; -import org.simantics.diagram.handler.PasteOperation; -import org.simantics.diagram.handler.Paster; -import org.simantics.diagram.stubs.DiagramResource; -import org.simantics.diagram.synchronization.CollectingModificationQueue; -import org.simantics.diagram.synchronization.CopyAdvisor; -import org.simantics.diagram.synchronization.SynchronizationHints; -import org.simantics.diagram.synchronization.graph.GraphSynchronizationContext; -import org.simantics.diagram.ui.DiagramModelHints; -import org.simantics.document.DocumentResource; -import org.simantics.g2d.canvas.ICanvasContext; -import org.simantics.g2d.diagram.DiagramClass; -import org.simantics.g2d.diagram.IDiagram; -import org.simantics.g2d.diagram.impl.Diagram; -import org.simantics.layer0.Layer0; -import org.simantics.modeling.ModelingResources; -import org.simantics.modeling.ModelingUtils; -import org.simantics.modeling.mapping.ModelingSynchronizationHints; -import org.simantics.modeling.typicals.rules.AuxKeys; -import org.simantics.modeling.typicals.rules.FlagRule; -import org.simantics.modeling.typicals.rules.InstanceOfRule; -import org.simantics.modeling.typicals.rules.LabelRule; -import org.simantics.modeling.typicals.rules.MonitorRule; -import org.simantics.modeling.typicals.rules.NameRule; -import org.simantics.modeling.typicals.rules.ProfileMonitorRule; -import org.simantics.modeling.typicals.rules.SVGElementRule; -import org.simantics.modeling.typicals.rules.TransformRule; -import org.simantics.scenegraph.g2d.events.command.Commands; -import org.simantics.scl.runtime.function.Function4; -import org.simantics.structural.stubs.StructuralResource2; -import org.simantics.utils.datastructures.MapSet; -import org.simantics.utils.strings.AlphanumComparator; -import org.simantics.utils.strings.EString; -import org.simantics.utils.ui.ErrorLogger; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import gnu.trove.map.hash.THashMap; -import gnu.trove.set.hash.THashSet; - -/** - * A write request that synchronizes typical master templates and their - * instances as specified. - * - *

- * Use {@link #SyncTypicalTemplatesToInstances(Resource[], MapSet)} to - * synchronize all instances of specified templates. Use - * {@link #syncSingleInstance(Resource)} to synchronize a single typical - * instance with its master template. - * - * @author Tuukka Lehtonen - * - * @see ReadTypicalInfo - * @see TypicalInfo - * @see TypicalSynchronizationMetadata - */ -public class SyncTypicalTemplatesToInstances extends WriteRequest { - private static final Logger LOGGER = LoggerFactory.getLogger(SyncTypicalTemplatesToInstances.class); - - /** - * A constant used as the second argument to - * {@link #SyncTemplates(Resource[], MapSet)} for stating that all specified - * templates should be fully synchronized to their instances. - * - * This is useful for forcing complete synchronization and unit testing. - */ - public static final EmptyMapSet ALL = EmptyMapSet.INSTANCE; - - public static class EmptyMapSet extends MapSet { - - public static final EmptyMapSet INSTANCE = new EmptyMapSet(); - - public EmptyMapSet() { - this.sets = Collections.emptyMap(); - } - - @Override - protected Set getOrCreateSet(Resource key) { - throw new UnsupportedOperationException("immutable constant instance"); - } - - }; - - protected static final boolean DEBUG = false; - - // Input - - final private IProgressMonitor monitor; - /** - * Typical diagram rules to apply - */ - protected Set selectedRules; - - /** - * Typical diagram templates to synchronize with their instances. - */ - protected Resource[] templates; - - /** - * Typical diagram instances to synchronize with their templates. - */ - protected Resource[] instances; - - /** - * For each template diagram in {@link #templates}, shall contain a set of - * elements that have changed and should be synchronized into the instance - * diagrams. Provided as an argument by the client. Allows optimizing - * real-time synchronization by not processing everything all the time. - * - * If the value is {@link #ALL}, all elements of the template shall be fully - * synchronized. - */ - protected MapSet changedElementsByDiagram; - - // Temporary data - - protected Layer0 L0; - protected StructuralResource2 STR; - protected DiagramResource DIA; - protected ModelingResources MOD; - - /** - * Needed for using {@link Paster} in - * {@link #addMissingElements(WriteGraph, TypicalInfo, Resource, Resource, Set)} - */ - protected GraphSynchronizationContext syncCtx; - - /** - * For collecting commit metadata during the processing of this request. - */ - protected TypicalSynchronizationMetadata metadata; - - /** - * Necessary for using {@link CopyPasteStrategy} and {@link PasteOperation} - * for now. Will be removed in the future once IDiagram is removed from - * PasteOperation. - */ - protected IDiagram temporaryDiagram; - - protected ConnectionUtil cu; - - /** - * Maps source -> target connection route nodes, i.e. connectors and - * interior route nodes (route lines). Inverse mapping of {@link #t2s}. - */ - protected Map s2t; - - /** - * Maps target -> source connection route nodes, i.e. connectors and - * interior route nodes (route lines). Inverse mapping of {@link #s2t}. - */ - protected Map t2s; - - /** - * An auxiliary resource map for extracting the correspondences between - * originals and copied resource when diagram contents are copied from - * template to instance. - */ - protected Map copyMap; - - final private Map> messageLogs = new HashMap>(); - - public List logs = new ArrayList(); - - private boolean writeLog; - - /** - * For SCL API. - * - * @param graph - * @param selectedRules - * @param templates - * @param instances - * @throws DatabaseException - */ - public static void syncTypicals(WriteGraph graph, boolean log, List templates, List instances) throws DatabaseException { - graph.syncRequest( - new SyncTypicalTemplatesToInstances( - null, - templates.toArray(Resource.NONE), - instances.toArray(Resource.NONE), - ALL, - null) - .logging(log)); - } - - /** - * @param templates typical diagram templates to completely synchronize with - * their instances - */ - public SyncTypicalTemplatesToInstances(Set selectedRules, Resource... templates) { - this(selectedRules, templates, null, ALL, null); - } - - /** - * @param templates typical diagram templates to partially synchronize with - * their instances - * @param changedElementsByDiagram see {@link #changedElementsByDiagram} - */ - public SyncTypicalTemplatesToInstances(Set selectedRules, Resource[] templates, MapSet changedElementsByDiagram) { - this(selectedRules, templates, null, changedElementsByDiagram, null); - } - - /** - * Return a write request that completely synchronizes the specified - * instance diagram with its template. - * - * @param instance - * @return - */ - public static SyncTypicalTemplatesToInstances syncSingleInstance(Set selectedRules, Resource instance) { - return new SyncTypicalTemplatesToInstances(selectedRules, null, new Resource[] { instance }, ALL, null); - } - - /** - * @param templates typical diagram templates to synchronize with their instances - * @param instances typical diagram instances to synchronize with their templates - * @param changedElementsByDiagram see {@link #changedElementsByDiagram} - */ - private SyncTypicalTemplatesToInstances(Set selectedRules, Resource[] templates, Resource[] instances, MapSet changedElementsByDiagram, IProgressMonitor monitor) { - this.selectedRules = selectedRules; - this.templates = templates; - this.instances = instances; - this.changedElementsByDiagram = changedElementsByDiagram; - this.monitor = monitor; - } - - public SyncTypicalTemplatesToInstances logging(boolean writeLog) { - this.writeLog = writeLog; - return this; - } - - private Resource getDiagramNameResource(ReadGraph graph, Resource diagram) throws DatabaseException { - Resource composite = graph.getPossibleObject(diagram, MOD.DiagramToComposite); - if(composite != null) return composite; - else return diagram; - } - - private Resource getElementNameResource(ReadGraph graph, Resource element) throws DatabaseException { - Resource corr = ModelingUtils.getPossibleElementCorrespondendence(graph, element); - if(corr != null) return corr; - else return element; - } - - private List getLog(ReadGraph graph, Resource diagram) throws DatabaseException { - Resource indexRoot = graph.syncRequest(new PossibleIndexRoot(diagram)); - if(indexRoot == null) throw new DatabaseException("FATAL: Diagram is not under any index root."); - List log = messageLogs.get(indexRoot); - if(log == null) { - log = new ArrayList(); - messageLogs.put(indexRoot, log); - } - return log; - } - - private String elementName(ReadGraph graph, Resource element) throws DatabaseException { - - StringBuilder b = new StringBuilder(); - b.append(safeNameAndType(graph, element)); - - int spaces = 60-b.length(); - for(int i=0;i(); - - this.temporaryDiagram = Diagram.spawnNew(DiagramClass.DEFAULT); - this.temporaryDiagram.setHint(SynchronizationHints.CONTEXT, syncCtx); - - this.cu = new ConnectionUtil(graph); - - if (templates != null) { - // Look for typical template instances from the currently active models only. - Collection activeModels = graph.syncRequest(new ActiveModels(Simantics.getProjectResource())); - if (!activeModels.isEmpty()) { - for (Resource template : templates) { - syncTemplate(graph, template, activeModels); - } - } - } - if (instances != null) { - for (Resource instance : instances) { - syncInstance(graph, instance); - } - } - - if (writeLog) { - for(Map.Entry> entry : messageLogs.entrySet()) { - - Resource indexRoot = entry.getKey(); - List messageLog = entry.getValue(); - - Layer0 L0 = Layer0.getInstance(graph); - DocumentResource DOC = DocumentResource.getInstance(graph); - - Collection libs = graph.syncRequest(new ObjectsWithType(indexRoot, L0.ConsistsOf, DOC.DocumentLibrary)); - if(libs.isEmpty()) continue; - - List nrs = new ArrayList(); - for(Resource lib : libs) nrs.add(new NamedResource(NameUtils.getSafeName(graph, lib), lib)); - Collections.sort(nrs, AlphanumComparator.CASE_INSENSITIVE_COMPARATOR); - Resource library = nrs.iterator().next().getResource(); - - CommonDBUtils.selectClusterSet(graph, library); - - String text = "--- Created: " + new Date().toString() + " ---\n"; - text += EString.implode(messageLog); - - Resource log = graph.newResource(); - graph.claim(log, L0.InstanceOf, null, DOC.PlainTextDocument); - graph.claimLiteral(log, L0.HasName, L0.String, "Typical Sync " + new Date().toString()); - graph.claim(library, L0.ConsistsOf, L0.PartOf, log); - graph.claimLiteral(log, DOC.PlainTextDocument_text, L0.String, text); - logs.add(log); - - } - } - - if (!metadata.getTypicals().isEmpty()) { - graph.addMetadata(metadata); - - // Add comment to change set. - CommentMetadata cm = graph.getMetadata(CommentMetadata.class); - graph.addMetadata( cm.add("Synchronized " + metadata.getTypicals().size() + " typical diagram instances (" + metadata.getTypicals() + ") with their templates.") ); - } - - temporaryDiagram = null; - syncCtx = null; - } - - private Collection findInstances(ReadGraph graph, Resource ofType, Collection indexRoots) throws DatabaseException { - Instances index = graph.adapt(ofType, Instances.class); - Set instances = new HashSet<>(); - for (Resource indexRoot : indexRoots) - instances.addAll( index.find(graph, indexRoot) ); - return instances; - } - - private void syncTemplate(WriteGraph graph, Resource template, Collection indexRoots) throws DatabaseException { - Resource templateType = graph.getPossibleType(template, DIA.Diagram); - if (templateType == null) - return; - - Collection instances = findInstances(graph, templateType, indexRoots); - // Do not include the template itself as it is also an instance of templateType - instances.remove(template); - if (instances.isEmpty()) - return; - - Set templateElements = new THashSet( graph.syncRequest( - new ObjectsWithType(template, L0.ConsistsOf, DIA.Element) ) ); - - try { - for (Resource instance : instances) { - this.temporaryDiagram.setHint(DiagramModelHints.KEY_DIAGRAM_RESOURCE, instance); - syncInstance(graph, template, instance, templateElements); - } - } catch (Exception e) { - LOGGER.error("Template synchronization failed.", e); - } finally { - this.temporaryDiagram.removeHint(DiagramModelHints.KEY_DIAGRAM_RESOURCE); - } - } - - private void syncInstance(WriteGraph graph, Resource instance) throws DatabaseException { - Resource template = graph.getPossibleObject(instance, MOD.HasDiagramSource); - if (template == null) - return; - - Set templateElements = new THashSet( graph.syncRequest( - new ObjectsWithType(template, L0.ConsistsOf, DIA.Element) ) ); - - try { - this.temporaryDiagram.setHint(DiagramModelHints.KEY_DIAGRAM_RESOURCE, instance); - syncInstance(graph, template, instance, templateElements); - } finally { - this.temporaryDiagram.removeHint(DiagramModelHints.KEY_DIAGRAM_RESOURCE); - } - } - - private Resource findInstanceCounterpart(ReadGraph graph, Resource instanceDiagram, Resource templateElement) throws DatabaseException { - Map children = graph.syncRequest(new UnescapedChildMapOfResource(instanceDiagram)); - for(Resource child : children.values()) { - if(graph.hasStatement(child, MOD.HasElementSource, templateElement)) return child; - } - return null; - } - - private boolean isSynchronizedConnector(ReadGraph graph, Resource templateConnection, Resource instanceConnector) throws DatabaseException { - DiagramResource DIA = DiagramResource.getInstance(graph); - Resource instanceConnection = graph.getPossibleObject(instanceConnector, DIA.IsConnectorOf); - return graph.hasStatement(instanceConnection, MOD.HasElementSource, templateConnection) - // If the master connection has been removed, this is all that's left - // to identify a connection that at least was originally synchronized - // from the typical master to this instance. - || graph.hasStatement(instanceConnection, MOD.IsTemplatized); - } - - /** - * Perform the following synchronization steps for the instance diagram: - *

    - *
  1. remove such templatized elements from the instance diagram whose - * template counterpart no longer exists
  2. - *
  3. add elements to the instance diagram that are only in the template
  4. - *
  5. synchronize elements of the instance diagram that have been deemed - * changed
  6. - *
- * - * @param graph database write access - * @param template the synchronization source diagram - * @param instance the synchronization target diagram - * @param currentTemplateElements the set of all elements currently in the - * template diagram - * @throws DatabaseException if anything goes wrong - */ - private void syncInstance(WriteGraph graph, Resource template, Resource instance, Set currentTemplateElements) throws DatabaseException { - - List messageLog = getLog(graph, instance); - - messageLog.add("Synchronization of changed typical template: " + SyncTypicalTemplatesToInstances.safeNameAndType(graph, getDiagramNameResource(graph, template))); - messageLog.add("----\n\ttypical instance: " + safeNameAndType(graph, getDiagramNameResource(graph, instance))); - - CommonDBUtils.selectClusterSet(graph, instance); - - // Form instance element <-> template element bijection - TypicalInfoBean typicalInfoBean = graph.syncRequest( - new ReadTypicalInfo(instance), - TransientCacheListener. instance()); - // Must be able to modify the typicalInfo structure, - // therefore clone the query result. - typicalInfoBean = (TypicalInfoBean) typicalInfoBean.clone(); - typicalInfoBean.templateElements = currentTemplateElements; - typicalInfoBean.auxiliary = new HashMap(1); - - TypicalInfo info = new TypicalInfo(); - info.monitor = monitor; - info.messageLog = messageLog; - info.bean = typicalInfoBean; - - // Resolve naming function for this typical instance. - Resource compositeInstance = graph.getPossibleObject(instance, MOD.DiagramToComposite); - if (compositeInstance != null) { - Function4 namingFunction = TypicalUtil.getTypicalNamingFunction(graph, compositeInstance); - if (namingFunction != null) - typicalInfoBean.auxiliary.put(AuxKeys.KEY_TYPICAL_NAMING_FUNCTION, namingFunction); - } - - int dSizeAbs = Math.abs(typicalInfoBean.instanceElements.size() - currentTemplateElements.size()); - - if(DEBUG) - System.out.println("typical <-> template mapping: " + typicalInfoBean.instanceToTemplate); - - // Find elements to be removed from instance by looking for all - // instance elements that do not have a MOD.HasElementSource - // relation but have a MOD.IsTemplatized tag. - Set instanceElementsRemovedFromTemplate = findInstanceElementsRemovedFromTemplate( - graph, info, new THashSet(dSizeAbs)); - - // Find elements in template that do not yet exist in the instance - Set templateElementsAddedToTemplate = findTemplateElementsMissingFromInstance( - graph, currentTemplateElements, info, - new THashSet(dSizeAbs)); - - Set changedTemplateElements = changedElementsByDiagram.removeValues(template); - - if(DEBUG) - System.out.println("ADDED: " + templateElementsAddedToTemplate.size() + ", REMOVED: " + instanceElementsRemovedFromTemplate.size() + ", CHANGED: " + changedTemplateElements.size()); - - // Validate - for(Resource templateElement : graph.getObjects(template, L0.ConsistsOf)) { - if(graph.isInstanceOf(templateElement, DIA.RouteGraphConnection)) { - for(Resource connector : graph.getObjects(templateElement, DIA.HasConnector)) { - for(Statement elementStm : graph.getStatements(connector, STR.Connects)) { - Resource otherElement = elementStm.getObject(); - if(!otherElement.equals(templateElement)) { - Resource counterPartElement = findInstanceCounterpart(graph, instance, otherElement); - if(counterPartElement != null) { - Resource diagramConnectionPoint = graph.getInverse(elementStm.getPredicate()); - Resource connectionPoint = graph.getPossibleObject(diagramConnectionPoint, MOD.DiagramConnectionRelationToConnectionRelation); - if(connectionPoint != null) { - Statement stm = graph.getPossibleStatement(counterPartElement, diagramConnectionPoint); - if(stm != null) { - if(graph.isInstanceOf(connectionPoint, L0.FunctionalRelation)) { - if(!isSynchronizedConnector(graph, templateElement, stm.getObject())) { - messageLog.add("\t\tABORTED: tried to connect to an already connected terminal " + NameUtils.getSafeName(graph, counterPartElement) + " " + NameUtils.getSafeName(graph, connectionPoint)); - return; - } - } - } - } - } - } - } - } - } - } - - // Perform changes - boolean changed = false; - changed |= synchronizeDiagramChanges(graph, info, template, instance); - changed |= removeElements(graph, info, instanceElementsRemovedFromTemplate); - changed |= addMissingElements(graph, info, template, instance, templateElementsAddedToTemplate); - changed |= synchronizeChangedElements(graph, info, template, instance, changedTemplateElements, templateElementsAddedToTemplate, changedElementsByDiagram == ALL); - - if (changed) - metadata.addTypical(instance); - } - - /** - * Synchronize any configurable aspects of the typical diagram instance itself. - * Every rule executed here comes from the ontology, nothing is fixed. - * - * @param graph - * @param typicalInfo - * @param template - * @param instance - * @return if any changes were made. - * @throws DatabaseException - */ - private boolean synchronizeDiagramChanges( - WriteGraph graph, - TypicalInfo typicalInfo, - Resource template, - Resource instance) - throws DatabaseException - { - boolean changed = false; - for (Resource rule : graph.getObjects(template, MOD.HasTypicalSynchronizationRule)) { - if (selectedRules != null && !selectedRules.contains(rule)) - continue; - ITypicalSynchronizationRule r = graph.getPossibleAdapter(rule, ITypicalSynchronizationRule.class); - if (r != null) - changed |= r.synchronize(graph, template, instance, typicalInfo); - } - return changed; - } - - /** - * Add elements from template that do not yet exist in the instance. - * - * @param graph - * @param template - * @param instance - * @param elementsAddedToTemplate - * @return true if changes were made to the instance - * @throws DatabaseException - */ - private boolean addMissingElements(WriteGraph graph, TypicalInfo typicalInfo, Resource template, - Resource instance, Set elementsAddedToTemplate) - throws DatabaseException { - if (elementsAddedToTemplate.isEmpty()) - return false; - - CopyAdvisor copyAdvisor = graph.syncRequest(new Adapter(instance, CopyAdvisor.class)); - this.temporaryDiagram.setHint(SynchronizationHints.COPY_ADVISOR, copyAdvisor); - - ElementObjectAssortment assortment = new ElementObjectAssortment(graph, elementsAddedToTemplate); - if (copyMap == null) - copyMap = new THashMap(); - else - copyMap.clear(); - - if (DEBUG) - System.out.println("ADD MISSING ELEMENTS: " + assortment); - - // initialCopyMap argument is needed for copying just connections - // when their end-points are not copied at the same time. - - PasteOperation pasteOp = new PasteOperation(Commands.COPY, - (ICanvasContext) null, template, instance, temporaryDiagram, - assortment, false, new Point2D.Double(0, 0), - typicalInfo.bean.templateToInstance, copyMap) - .options(PasteOperation.ForceCopyReferences.INSTANCE); - - new Paster(graph.getSession(), pasteOp).perform(graph); - - boolean changed = false; - - if(!elementsAddedToTemplate.isEmpty()) - typicalInfo.messageLog.add("\tadded elements"); - - for (Resource addedElement : elementsAddedToTemplate) { - Resource copyElement = (Resource) copyMap.get(addedElement); - if (copyElement != null) { - graph.claim(copyElement, MOD.IsTemplatized, MOD.IsTemplatized, copyElement); - graph.claim(copyElement, MOD.HasElementSource, MOD.ElementHasInstance, addedElement); - - typicalInfo.bean.instanceElements.add(copyElement); - typicalInfo.bean.instanceToTemplate.put(copyElement, addedElement); - typicalInfo.bean.templateToInstance.put(addedElement, copyElement); - - typicalInfo.messageLog.add("\t\t" + safeNameAndType(graph, copyElement)); - - changed = true; - } - } - - ModelingResources MOD = ModelingResources.getInstance(graph); - Resource instanceComposite = graph.getPossibleObject(instance, MOD.DiagramToComposite); - List instanceComponents = new ArrayList(elementsAddedToTemplate.size()); - - // Post-process added elements after typicalInfo has been updated and - // template mapping statements are in place. - for (Resource addedElement : elementsAddedToTemplate) { - Resource copyElement = (Resource) copyMap.get(addedElement); - if (copyElement != null) { - postProcessAddedElement(graph, addedElement, copyElement, typicalInfo); - - if (instanceComponents != null) { - // Gather all instance typical components for applying naming - // strategy on them. - Resource component = graph.getPossibleObject(copyElement, MOD.ElementToComponent); - if (component != null) - instanceComponents.add(component); - } - } - } - - if (instanceComposite != null) - TypicalUtil.applySelectedModuleNames(graph, instanceComposite, instanceComponents); - - return changed; - } - - private void postProcessAddedElement(WriteGraph graph, - Resource addedTemplateElement, Resource addedInstanceElement, - TypicalInfo typicalInfo) throws DatabaseException { - if (graph.isInstanceOf(addedInstanceElement, DIA.Monitor)) { - postProcessAddedMonitor(graph, addedTemplateElement, addedInstanceElement, typicalInfo); - } - } - - private void postProcessAddedMonitor(WriteGraph graph, - Resource addedTemplateMonitor, Resource addedInstanceMonitor, - TypicalInfo typicalInfo) throws DatabaseException { - Resource monitor = addedInstanceMonitor; - Resource monitoredComponent = graph.getPossibleObject(monitor, DIA.HasMonitorComponent); - if (monitoredComponent != null) { - Resource monitoredTemplateElement = graph.getPossibleObject(monitoredComponent, MOD.ComponentToElement); - if (monitoredTemplateElement != null) { - Resource monitoredInstanceElement = typicalInfo.bean.templateToInstance.get(monitoredTemplateElement); - if (monitoredInstanceElement != null) { - Resource monitoredInstanceComponent = graph.getPossibleObject(monitoredInstanceElement, MOD.ElementToComponent); - if (monitoredInstanceComponent != null) { - // Ok, the monitor refers to a component within the - // template composite. Change it to refer to the - // instance composite. - graph.deny(monitor, DIA.HasMonitorComponent); - graph.claim(monitor, DIA.HasMonitorComponent, monitoredInstanceComponent); - } - } - } - } - } - - private boolean removeElements(WriteGraph graph, TypicalInfo typicalInfo, Set elementsRemovedFromTemplate) throws DatabaseException { - if (elementsRemovedFromTemplate.isEmpty()) - return false; - - // Remove mapped elements from instance that are removed from the template. - boolean changed = false; - - if(!elementsRemovedFromTemplate.isEmpty()) - typicalInfo.messageLog.add("\tremoved elements"); - - for (Resource removedElement : elementsRemovedFromTemplate) { - typicalInfo.messageLog.add("\t\t" + safeNameAndType(graph, removedElement)); - - RemoverUtil.remove(graph, removedElement); - - typicalInfo.bean.instanceElements.remove(removedElement); - Resource template = typicalInfo.bean.instanceToTemplate.remove(removedElement); - if (template != null) - typicalInfo.bean.templateToInstance.remove(template); - - changed = true; - } - return changed; - } - - private Set findTemplateElementsMissingFromInstance( - WriteGraph graph, Collection currentTemplateElements, - TypicalInfo typicalInfo, THashSet result) - throws DatabaseException { - for (Resource templateElement : currentTemplateElements) { - Resource instanceElement = typicalInfo.bean.templateToInstance.get(templateElement); - if (instanceElement == null) { - if(DEBUG) - System.out.println("No instance correspondence for template element " + NameUtils.getSafeName(graph, templateElement, true) + " => add"); - result.add(templateElement); - } - } - return result; - } - - public Set findInstanceElementsRemovedFromTemplate( - ReadGraph graph, TypicalInfo typicalInfo, - THashSet result) throws DatabaseException { - for (Resource instanceElement : typicalInfo.bean.instanceElements) { - if (!typicalInfo.bean.instanceToTemplate.containsKey(instanceElement)) { - if (typicalInfo.bean.isTemplatized.contains(instanceElement)) { - if(DEBUG) - System.out.println("Templatized typical instance element " + NameUtils.getSafeName(graph, instanceElement, true) + " has no correspondence in template => remove"); - result.add(instanceElement); - } - } - } - return result; - } - - /** - * Synchronize basic visual aspects of changed elements. For all elements, - * transform and label are synchronized. Otherwise synchronization is - * type-specific for connections, flags, monitors and svg elements. - * - * @param graph - * @param typicalInfo - * @param template - * @param instance - * @param changedTemplateElements - * @param addedElements - * elements that have been added and thus need not be - * synchronized - * @param synchronizeAllElements - * @return - * @throws DatabaseException - */ - private boolean synchronizeChangedElements(WriteGraph graph, - TypicalInfo typicalInfo, Resource template, Resource instance, - Collection changedTemplateElements, - Set addedElements, - boolean synchronizeAllElements) throws DatabaseException { - - if (synchronizeAllElements) { - // For unit testing purposes. - changedTemplateElements = graph.syncRequest(new ObjectsWithType(template, L0.ConsistsOf, DIA.Element)); - } - - if (changedTemplateElements.isEmpty()) - return false; - - boolean changed = false; - - typicalInfo.messageLog.add("\telement change analysis"); - int analysisLogPosition = typicalInfo.messageLog.size(); - - for (Resource changedTemplateElement : changedTemplateElements) { - // Skip synchronization of elements that were just added and are - // thus already synchronized. - if (addedElements.contains(changedTemplateElement)) - continue; - - Resource instanceElement = typicalInfo.bean.templateToInstance.get(changedTemplateElement); - if (instanceElement == null) { - // There's an earlier problem in the sync process if this happens. - typicalInfo.messageLog.add("SKIPPING SYNC OF CHANGED TEMPLATE ELEMENT DUE TO MISSING INSTANCE: " + safeNameAndType(graph, getElementNameResource(graph, changedTemplateElement))); - continue; - } - - typicalInfo.messageLog.add("\t\t" + elementName(graph, changedTemplateElement)); - int currentLogSize = typicalInfo.messageLog.size(); - - changed |= InstanceOfRule.INSTANCE.synchronize(graph, changedTemplateElement, instanceElement, typicalInfo); - changed |= NameRule.INSTANCE.synchronize(graph, changedTemplateElement, instanceElement, typicalInfo); - changed |= TransformRule.INSTANCE.synchronize(graph, changedTemplateElement, instanceElement, typicalInfo); - changed |= LabelRule.INSTANCE.synchronize(graph, changedTemplateElement, instanceElement, typicalInfo); - - Collection types = graph.getTypes(changedTemplateElement); - if (types.contains(DIA.RouteGraphConnection)) { - changed |= synchronizeConnection(graph, changedTemplateElement, instanceElement, typicalInfo); - } else if (types.contains(DIA.Flag)) { - changed |= FlagRule.INSTANCE.synchronize(graph, changedTemplateElement, instanceElement, typicalInfo); - } else if (types.contains(DIA.Monitor)) { - changed |= MonitorRule.INSTANCE.synchronize(graph, changedTemplateElement, instanceElement, typicalInfo); - } else if (types.contains(DIA.SVGElement)) { - changed |= SVGElementRule.INSTANCE.synchronize(graph, changedTemplateElement, instanceElement, typicalInfo); - } - - changed |= ProfileMonitorRule.INSTANCE.synchronize(graph, changedTemplateElement, instanceElement, typicalInfo); - - for (Resource rule : graph.getObjects(changedTemplateElement, MOD.HasTypicalSynchronizationRule)) { - if(selectedRules != null && !selectedRules.contains(rule)) continue; - ITypicalSynchronizationRule r = graph.getPossibleAdapter(rule, ITypicalSynchronizationRule.class); - if (r != null) - changed |= r.synchronize(graph, changedTemplateElement, instanceElement, typicalInfo); - } - - // Show element only if something has happened - if(currentLogSize == typicalInfo.messageLog.size()) - typicalInfo.messageLog.remove(typicalInfo.messageLog.size()-1); - - } - - if (s2t != null) - s2t.clear(); - if (t2s != null) - t2s.clear(); - - // Show analysis header only if something has happened - if(analysisLogPosition == typicalInfo.messageLog.size()) - typicalInfo.messageLog.remove(typicalInfo.messageLog.size()-1); - - return changed; - } - - /** - * Synchronizes two route graph connection topologies if and only if the - * destination connection is not attached to any node elements besides - * the ones that exist in the source. This means that connections that - * have instance-specific connections to non-template nodes are ignored - * here. - * - * @param graph - * @param sourceConnection - * @param targetConnection - * @param typicalInfo - * @return true if changes were made - * @throws DatabaseException - */ - private boolean synchronizeConnection(WriteGraph graph, Resource sourceConnection, Resource targetConnection, TypicalInfo typicalInfo) - throws DatabaseException { - - if(DEBUG) - System.out.println("connection " + NameUtils.getSafeName(graph, sourceConnection, true) + " to target connection " + NameUtils.getSafeName(graph, targetConnection, true)); - - boolean changed = false; - - // Initialize utilities and data maps - s2t = newOrClear(s2t); - t2s = newOrClear(t2s); - - if (cu == null) - cu = new ConnectionUtil(graph); - - // 0.1. find mappings between source and target connection connectors - Collection targetConnectors = graph.getObjects(targetConnection, DIA.HasConnector); - for (Resource targetConnector : targetConnectors) { - Statement toNode = cu.getConnectedComponentStatement(targetConnection, targetConnector); - if (toNode == null) { - // Corrupted target connection! - ErrorLogger.defaultLogError("Encountered corrupted typical template connection " + NameUtils.getSafeName(graph, targetConnection, true) + " with a stray DIA.Connector instance " + NameUtils.getSafeName(graph, targetConnector, true), new Exception("trace")); - return false; - } - - // Check that the target connections does not connect to - // non-templatized elements before syncing. - if (!graph.hasStatement(toNode.getObject(), MOD.IsTemplatized)) - return false; - - //Resource templateNode = typicalInfo.instanceToTemplate.get(toNode.getObject()); - Resource templateNode = graph.getPossibleObject(toNode.getObject(), MOD.HasElementSource); - if (templateNode != null) { - Resource isConnectedTo = graph.getPossibleInverse(toNode.getPredicate()); - if (isConnectedTo != null) { - Resource templateConnector = graph.getPossibleObject(templateNode, isConnectedTo); - if (templateConnector != null) { - Resource connectionOfTemplateConnector = ConnectionUtil.tryGetConnection(graph, templateConnector); - if (sourceConnection.equals(connectionOfTemplateConnector)) { - s2t.put(templateConnector, targetConnector); - t2s.put(targetConnector, templateConnector); - - if (DEBUG) - System.out.println("Mapping connector " - + NameUtils.getSafeName(graph, templateConnector, true) - + " to " + NameUtils.getSafeName(graph, targetConnector, true)); - } - } - } - } - } - - // 0.2. find mapping between source and target route lines - Collection sourceInteriorRouteNodes = graph.getObjects(sourceConnection, DIA.HasInteriorRouteNode); - Collection targetInteriorRouteNodes = graph.getObjects(targetConnection, DIA.HasInteriorRouteNode); - Map sourceToRouteLine = new THashMap(); - Map targetToRouteLine = new THashMap(); - - for (Resource source : sourceInteriorRouteNodes) - sourceToRouteLine.put(source, Paster.readRouteLine(graph, source)); - for (Resource target : targetInteriorRouteNodes) - targetToRouteLine.put(target, Paster.readRouteLine(graph, target)); - - nextSourceLine: - for (Iterator> sourceIt = sourceToRouteLine.entrySet().iterator(); !targetToRouteLine.isEmpty() && sourceIt.hasNext();) { - Map.Entry sourceEntry = sourceIt.next(); - Paster.RouteLine sourceLine = sourceEntry.getValue(); - for (Iterator> targetIt = targetToRouteLine.entrySet().iterator(); targetIt.hasNext();) { - Map.Entry targetEntry = targetIt.next(); - if (sourceLine.equals(targetEntry.getValue())) { - s2t.put(sourceEntry.getKey(), targetEntry.getKey()); - t2s.put(targetEntry.getKey(), sourceEntry.getKey()); - sourceIt.remove(); - targetIt.remove(); - - if (DEBUG) - System.out.println("Mapping routeline " - + NameUtils.getSafeName(graph, sourceEntry.getKey(), true) - + " - " + sourceEntry.getValue() - + " to " + NameUtils.getSafeName(graph, targetEntry.getKey(), true) - + " - " + targetEntry.getValue()); - - continue nextSourceLine; - } - } - } - - if (DEBUG) { - System.out.println("Take 1: Source to target route nodes map : " + s2t); - System.out.println("Take 1: Target to source route nodes map : " + t2s); - } - - // 1.1 remove excess connectors - for (Resource targetConnector : targetConnectors) { - if (!t2s.containsKey(targetConnector)) { - typicalInfo.messageLog.add("\t\t\tremove excess connector from target connection: " + NameUtils.getSafeName(graph, targetConnector)); - cu.removeConnectionPart(targetConnector); - changed = true; - } - } - - // 1.2 add missing connectors to target - Collection sourceConnectors = graph.getObjects(sourceConnection, DIA.HasConnector); - for (Resource sourceConnector : sourceConnectors) { - if (!s2t.containsKey(sourceConnector)) { - Statement sourceIsConnectorOf = graph.getSingleStatement(sourceConnector, DIA.IsConnectorOf); - Statement connects = cu.getConnectedComponentStatement(sourceConnection, sourceConnector); - if (connects == null) { - // TODO: serious error! - throw new DatabaseException("ERROR: connector is astray, i.e. not connected to a node element: " + safeNameAndType(graph, sourceConnector)); - } - Resource connectsInstanceElement = typicalInfo.bean.templateToInstance.get(connects.getObject()); - if (connectsInstanceElement == null) { - // TODO: serious error! - throw new DatabaseException("ERROR: could not find instance element to which template element " + safeNameAndType(graph, connects.getObject()) + " is connected to"); - } - Resource hasConnector = graph.getInverse(sourceIsConnectorOf.getPredicate()); - - Resource newTargetConnector = cu.newConnector(targetConnection, hasConnector); - graph.claim(newTargetConnector, connects.getPredicate(), connectsInstanceElement); - changed = true; - - s2t.put(sourceConnector, newTargetConnector); - t2s.put(newTargetConnector, sourceConnector); - - typicalInfo.messageLog.add("\t\t\tadd new connector to target connection: " + NameUtils.getSafeName(graph, newTargetConnector) + " to map to source connector " + NameUtils.getSafeName(graph, sourceConnector)); - } - } - - // 2. sync route lines and their connectivity: - // 2.1. assign correspondences in target for each source route line - // by reusing excess routelines in target and by creating new - // route lines. - - Resource[] targetRouteLines = targetToRouteLine.keySet().toArray(Resource.NONE); - int targetRouteLine = targetRouteLines.length - 1; - - for (Iterator> sourceIt = sourceToRouteLine.entrySet().iterator(); sourceIt.hasNext();) { - Map.Entry sourceEntry = sourceIt.next(); - Resource source = sourceEntry.getKey(); - Paster.RouteLine sourceLine = sourceEntry.getValue(); - - typicalInfo.messageLog.add("\t\t\tassign an instance-side routeline complement for " + NameUtils.getSafeName(graph, source, true) + " - " + sourceLine); - - // Assign target route line for source - Resource target = null; - if (targetRouteLine < 0) { - // by creating new route lines - target = cu.newRouteLine(targetConnection, sourceLine.getPosition(), sourceLine.isHorizontal()); - typicalInfo.messageLog.add("\t\t\tcreate new route line " + NameUtils.getSafeName(graph, target)); - changed = true; - } else { - // by reusing existing route line - target = targetRouteLines[targetRouteLine--]; - copyRouteLine(graph, source, target); - cu.disconnectFromAllRouteNodes(target); - typicalInfo.messageLog.add("\t\t\treused existing route line " + NameUtils.getSafeName(graph, target)); - changed = true; - } - s2t.put(source, target); - t2s.put(target, source); - - typicalInfo.messageLog.add("\t\t\tmapped source route line " + NameUtils.getSafeName(graph, source) + " to target route line " + NameUtils.getSafeName(graph, target)); - } - - if (targetRouteLine >= 0) { - typicalInfo.messageLog.add("\t\t\tremove excess route lines (" + (targetRouteLine + 1) + ") from target connection"); - for (; targetRouteLine >= 0; targetRouteLine--) { - typicalInfo.messageLog.add("\t\t\t\tremove excess route line: " + NameUtils.getSafeName(graph, targetRouteLines[targetRouteLine], true)); - cu.removeConnectionPart(targetRouteLines[targetRouteLine]); - } - } - - if (DEBUG) { - System.out.println("Take 2: Source to target route nodes map : " + s2t); - System.out.println("Take 2: Target to source route nodes map : " + t2s); - } - - // 2.2. Synchronize target connection topology (DIA.AreConnected) - changed |= connectRouteNodes(graph, typicalInfo, sourceInteriorRouteNodes); - changed |= connectRouteNodes(graph, typicalInfo, sourceConnectors); - - // 3. remove excess routelines & connectors from target connection - changed |= cu.removeExtraInteriorRouteNodes(targetConnection) > 0; - changed |= cu.removeUnusedConnectors(targetConnection) > 0; - - return changed; - } - - private boolean connectRouteNodes(WriteGraph graph, TypicalInfo typicalInfo, Collection sourceRouteNodes) throws DatabaseException { - boolean changed = false; - for (Resource src : sourceRouteNodes) { - Resource dst = s2t.get(src); - if (dst == null) { - throw new DatabaseException("TARGET ROUTE NODE == NULL FOR SRC: " + NameUtils.getSafeName(graph, src)); - } - - Collection connectedToSrcs = graph.getObjects(src, DIA.AreConnected); - Collection connectedToDsts = graph.getObjects(dst, DIA.AreConnected); - - // Remove excess statements - for (Resource connectedToDst : connectedToDsts) { - Resource connectedToSrc = t2s.get(connectedToDst); - if (connectedToSrc == null) { - throw new DatabaseException("CONNECTED TO SRC == NULL FOR DST: " + NameUtils.getSafeName(graph, connectedToDst)); - } - if (connectedToSrc == null || !graph.hasStatement(src, DIA.AreConnected, connectedToSrc)) { - graph.deny(dst, DIA.AreConnected, DIA.AreConnected, connectedToDst); - changed = true; - typicalInfo.messageLog.add("\t\t\tdisconnected route nodes (" + NameUtils.getSafeName(graph, dst) + ", " + NameUtils.getSafeName(graph, connectedToDst) + ")"); - } - } - - // Add necessary statements - for (Resource connectedToSrc : connectedToSrcs) { - Resource connectedToDst = s2t.get(connectedToSrc); - if (connectedToDst == null) { - throw new DatabaseException("CONNECTED TO DST == NULL FOR SRC: " + NameUtils.getSafeName(graph, connectedToSrc)); - } - if (!graph.hasStatement(dst, DIA.AreConnected, connectedToDst)) { - graph.claim(dst, DIA.AreConnected, DIA.AreConnected, connectedToDst); - changed = true; - typicalInfo.messageLog.add("\t\t\tconnected route nodes (" + NameUtils.getSafeName(graph, dst) + ", " + NameUtils.getSafeName(graph, connectedToDst) + ")"); - } - } - } - return changed; - } - - private void copyRouteLine(WriteGraph graph, Resource src, Resource tgt) throws DatabaseException { - Double pos = graph.getPossibleRelatedValue(src, DIA.HasPosition, Bindings.DOUBLE); - Boolean hor = graph.getPossibleRelatedValue(src, DIA.IsHorizontal, Bindings.BOOLEAN); - if (pos == null) - pos = 0.0; - if (hor == null) - hor = Boolean.TRUE; - graph.claimLiteral(tgt, DIA.HasPosition, L0.Double, pos, Bindings.DOUBLE); - graph.claimLiteral(tgt, DIA.IsHorizontal, L0.Boolean, hor, Bindings.BOOLEAN); - } - - private static String safeNameAndType(ReadGraph graph, Resource r) throws DatabaseException { - StringBuilder sb = new StringBuilder(); - sb.append(NameUtils.getSafeName(graph, r, true)); - sb.append(" : ["); - boolean first = true; - for (Resource type : graph.getPrincipalTypes(r)) { - if (!first) - sb.append(","); - first = false; - sb.append(NameUtils.getSafeName(graph, type, true)); - } - sb.append("]"); - return sb.toString(); - } - - private static Map newOrClear(Map current) { - if (current == null) - return new THashMap(); - current.clear(); - return current; - } - +/******************************************************************************* + * Copyright (c) 2007, 2012 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.modeling.typicals; + +import java.awt.geom.Point2D; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.eclipse.core.runtime.IProgressMonitor; +import org.simantics.Simantics; +import org.simantics.databoard.Bindings; +import org.simantics.db.ReadGraph; +import org.simantics.db.Resource; +import org.simantics.db.Statement; +import org.simantics.db.WriteGraph; +import org.simantics.db.common.CommentMetadata; +import org.simantics.db.common.NamedResource; +import org.simantics.db.common.primitiverequest.Adapter; +import org.simantics.db.common.procedure.adapter.TransientCacheListener; +import org.simantics.db.common.request.ObjectsWithType; +import org.simantics.db.common.request.PossibleIndexRoot; +import org.simantics.db.common.request.WriteRequest; +import org.simantics.db.common.uri.UnescapedChildMapOfResource; +import org.simantics.db.common.utils.CommonDBUtils; +import org.simantics.db.common.utils.NameUtils; +import org.simantics.db.exception.DatabaseException; +import org.simantics.db.layer0.adapter.Instances; +import org.simantics.db.layer0.request.ActiveModels; +import org.simantics.db.layer0.util.RemoverUtil; +import org.simantics.diagram.content.ConnectionUtil; +import org.simantics.diagram.handler.CopyPasteStrategy; +import org.simantics.diagram.handler.ElementObjectAssortment; +import org.simantics.diagram.handler.PasteOperation; +import org.simantics.diagram.handler.Paster; +import org.simantics.diagram.handler.Paster.RouteLine; +import org.simantics.diagram.stubs.DiagramResource; +import org.simantics.diagram.synchronization.CollectingModificationQueue; +import org.simantics.diagram.synchronization.CopyAdvisor; +import org.simantics.diagram.synchronization.SynchronizationHints; +import org.simantics.diagram.synchronization.graph.GraphSynchronizationContext; +import org.simantics.diagram.ui.DiagramModelHints; +import org.simantics.document.DocumentResource; +import org.simantics.g2d.canvas.ICanvasContext; +import org.simantics.g2d.diagram.DiagramClass; +import org.simantics.g2d.diagram.IDiagram; +import org.simantics.g2d.diagram.impl.Diagram; +import org.simantics.layer0.Layer0; +import org.simantics.modeling.ModelingResources; +import org.simantics.modeling.ModelingUtils; +import org.simantics.modeling.mapping.ModelingSynchronizationHints; +import org.simantics.modeling.typicals.rules.AuxKeys; +import org.simantics.modeling.typicals.rules.FlagRule; +import org.simantics.modeling.typicals.rules.InstanceOfRule; +import org.simantics.modeling.typicals.rules.LabelRule; +import org.simantics.modeling.typicals.rules.MonitorRule; +import org.simantics.modeling.typicals.rules.NameRule; +import org.simantics.modeling.typicals.rules.ProfileMonitorRule; +import org.simantics.modeling.typicals.rules.SVGElementRule; +import org.simantics.modeling.typicals.rules.TransformRule; +import org.simantics.scenegraph.g2d.events.command.Commands; +import org.simantics.scl.runtime.function.Function4; +import org.simantics.structural.stubs.StructuralResource2; +import org.simantics.utils.datastructures.MapSet; +import org.simantics.utils.strings.AlphanumComparator; +import org.simantics.utils.strings.EString; +import org.simantics.utils.ui.ErrorLogger; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import gnu.trove.map.hash.THashMap; +import gnu.trove.set.hash.THashSet; + +/** + * A write request that synchronizes typical master templates and their + * instances as specified. + * + *

+ * Use {@link #SyncTypicalTemplatesToInstances(Resource[], MapSet)} to + * synchronize all instances of specified templates. Use + * {@link #syncSingleInstance(Resource)} to synchronize a single typical + * instance with its master template. + * + * @author Tuukka Lehtonen + * + * @see ReadTypicalInfo + * @see TypicalInfo + * @see TypicalSynchronizationMetadata + */ +public class SyncTypicalTemplatesToInstances extends WriteRequest { + private static final Logger LOGGER = LoggerFactory.getLogger(SyncTypicalTemplatesToInstances.class); + + /** + * A constant used as the second argument to + * {@link #SyncTemplates(Resource[], MapSet)} for stating that all specified + * templates should be fully synchronized to their instances. + * + * This is useful for forcing complete synchronization and unit testing. + */ + public static final EmptyMapSet ALL = EmptyMapSet.INSTANCE; + + public static class EmptyMapSet extends MapSet { + + public static final EmptyMapSet INSTANCE = new EmptyMapSet(); + + public EmptyMapSet() { + this.sets = Collections.emptyMap(); + } + + @Override + protected Set getOrCreateSet(Resource key) { + throw new UnsupportedOperationException("immutable constant instance"); + } + + }; + + protected static final boolean DEBUG = false; + + // Input + + final private IProgressMonitor monitor; + /** + * Typical diagram rules to apply + */ + protected Set selectedRules; + + /** + * Typical diagram templates to synchronize with their instances. + */ + protected Resource[] templates; + + /** + * Typical diagram instances to synchronize with their templates. + */ + protected Resource[] instances; + + /** + * For each template diagram in {@link #templates}, shall contain a set of + * elements that have changed and should be synchronized into the instance + * diagrams. Provided as an argument by the client. Allows optimizing + * real-time synchronization by not processing everything all the time. + * + * If the value is {@link #ALL}, all elements of the template shall be fully + * synchronized. + */ + protected MapSet changedElementsByDiagram; + + // Temporary data + + protected Layer0 L0; + protected StructuralResource2 STR; + protected DiagramResource DIA; + protected ModelingResources MOD; + + /** + * Needed for using {@link Paster} in + * {@link #addMissingElements(WriteGraph, TypicalInfo, Resource, Resource, Set)} + */ + protected GraphSynchronizationContext syncCtx; + + /** + * For collecting commit metadata during the processing of this request. + */ + protected TypicalSynchronizationMetadata metadata; + + /** + * Necessary for using {@link CopyPasteStrategy} and {@link PasteOperation} + * for now. Will be removed in the future once IDiagram is removed from + * PasteOperation. + */ + protected IDiagram temporaryDiagram; + + protected ConnectionUtil cu; + + /** + * Maps source -> target connection route nodes, i.e. connectors and + * interior route nodes (route lines). Inverse mapping of {@link #t2s}. + */ + protected Map s2t; + + /** + * Maps target -> source connection route nodes, i.e. connectors and + * interior route nodes (route lines). Inverse mapping of {@link #s2t}. + */ + protected Map t2s; + + /** + * An auxiliary resource map for extracting the correspondences between + * originals and copied resource when diagram contents are copied from + * template to instance. + */ + protected Map copyMap; + + final private Map> messageLogs = new HashMap<>(); + + public List logs = new ArrayList<>(); + + private boolean writeLog; + + /** + * For SCL API. + * + * @param graph + * @param selectedRules + * @param templates + * @param instances + * @throws DatabaseException + */ + public static void syncTypicals(WriteGraph graph, boolean log, List templates, List instances) throws DatabaseException { + graph.syncRequest( + new SyncTypicalTemplatesToInstances( + null, + templates.toArray(Resource.NONE), + instances.toArray(Resource.NONE), + ALL, + null) + .logging(log)); + } + + /** + * @param templates typical diagram templates to completely synchronize with + * their instances + */ + public SyncTypicalTemplatesToInstances(Set selectedRules, Resource... templates) { + this(selectedRules, templates, null, ALL, null); + } + + /** + * @param templates typical diagram templates to partially synchronize with + * their instances + * @param changedElementsByDiagram see {@link #changedElementsByDiagram} + */ + public SyncTypicalTemplatesToInstances(Set selectedRules, Resource[] templates, MapSet changedElementsByDiagram) { + this(selectedRules, templates, null, changedElementsByDiagram, null); + } + + /** + * Return a write request that completely synchronizes the specified + * instance diagram with its template. + * + * @param instance + * @return + */ + public static SyncTypicalTemplatesToInstances syncSingleInstance(Set selectedRules, Resource instance) { + return new SyncTypicalTemplatesToInstances(selectedRules, null, new Resource[] { instance }, ALL, null); + } + + /** + * @param templates typical diagram templates to synchronize with their instances + * @param instances typical diagram instances to synchronize with their templates + * @param changedElementsByDiagram see {@link #changedElementsByDiagram} + */ + private SyncTypicalTemplatesToInstances(Set selectedRules, Resource[] templates, Resource[] instances, MapSet changedElementsByDiagram, IProgressMonitor monitor) { + this.selectedRules = selectedRules; + this.templates = templates; + this.instances = instances; + this.changedElementsByDiagram = changedElementsByDiagram; + this.monitor = monitor; + } + + public SyncTypicalTemplatesToInstances logging(boolean writeLog) { + this.writeLog = writeLog; + return this; + } + + private Resource getDiagramNameResource(ReadGraph graph, Resource diagram) throws DatabaseException { + Resource composite = graph.getPossibleObject(diagram, MOD.DiagramToComposite); + if(composite != null) return composite; + else return diagram; + } + + private Resource getElementNameResource(ReadGraph graph, Resource element) throws DatabaseException { + Resource corr = ModelingUtils.getPossibleElementCorrespondendence(graph, element); + if(corr != null) return corr; + else return element; + } + + private List getLog(ReadGraph graph, Resource diagram) throws DatabaseException { + Resource indexRoot = graph.syncRequest(new PossibleIndexRoot(diagram)); + if(indexRoot == null) throw new DatabaseException("FATAL: Diagram is not under any index root."); + List log = messageLogs.get(indexRoot); + if(log == null) { + log = new ArrayList<>(); + messageLogs.put(indexRoot, log); + } + return log; + } + + private String elementName(ReadGraph graph, Resource element) throws DatabaseException { + + StringBuilder b = new StringBuilder(); + b.append(safeNameAndType(graph, element)); + + int spaces = 60-b.length(); + for(int i=0;i(); + + this.temporaryDiagram = Diagram.spawnNew(DiagramClass.DEFAULT); + this.temporaryDiagram.setHint(SynchronizationHints.CONTEXT, syncCtx); + + this.cu = new ConnectionUtil(graph); + + if (templates != null) { + // Look for typical template instances from the currently active models only. + Collection activeModels = graph.syncRequest(new ActiveModels(Simantics.getProjectResource())); + if (!activeModels.isEmpty()) { + for (Resource template : templates) { + syncTemplate(graph, template, activeModels); + } + } + } + if (instances != null) { + for (Resource instance : instances) { + syncInstance(graph, instance); + } + } + + if (writeLog) { + for(Map.Entry> entry : messageLogs.entrySet()) { + + Resource indexRoot = entry.getKey(); + List messageLog = entry.getValue(); + + Layer0 L0 = Layer0.getInstance(graph); + DocumentResource DOC = DocumentResource.getInstance(graph); + + Collection libs = graph.syncRequest(new ObjectsWithType(indexRoot, L0.ConsistsOf, DOC.DocumentLibrary)); + if(libs.isEmpty()) continue; + + List nrs = new ArrayList<>(); + for(Resource lib : libs) nrs.add(new NamedResource(NameUtils.getSafeName(graph, lib), lib)); + Collections.sort(nrs, AlphanumComparator.CASE_INSENSITIVE_COMPARATOR); + Resource library = nrs.iterator().next().getResource(); + + CommonDBUtils.selectClusterSet(graph, library); + + String text = "--- Created: " + new Date().toString() + " ---\n"; + text += EString.implode(messageLog); + + Resource log = graph.newResource(); + graph.claim(log, L0.InstanceOf, null, DOC.PlainTextDocument); + graph.claimLiteral(log, L0.HasName, L0.String, "Typical Sync " + new Date().toString()); + graph.claim(library, L0.ConsistsOf, L0.PartOf, log); + graph.claimLiteral(log, DOC.PlainTextDocument_text, L0.String, text); + logs.add(log); + + } + } + + if (!metadata.getTypicals().isEmpty()) { + graph.addMetadata(metadata); + + // Add comment to change set. + CommentMetadata cm = graph.getMetadata(CommentMetadata.class); + graph.addMetadata( cm.add("Synchronized " + metadata.getTypicals().size() + " typical diagram instances (" + metadata.getTypicals() + ") with their templates.") ); + } + + temporaryDiagram = null; + syncCtx = null; + } + + private Collection findInstances(ReadGraph graph, Resource ofType, Collection indexRoots) throws DatabaseException { + Instances index = graph.adapt(ofType, Instances.class); + Set instances = new HashSet<>(); + for (Resource indexRoot : indexRoots) + instances.addAll( index.find(graph, indexRoot) ); + return instances; + } + + private void syncTemplate(WriteGraph graph, Resource template, Collection indexRoots) throws DatabaseException { + Resource templateType = graph.getPossibleType(template, DIA.Diagram); + if (templateType == null) + return; + + Collection instances = findInstances(graph, templateType, indexRoots); + // Do not include the template itself as it is also an instance of templateType + instances.remove(template); + if (instances.isEmpty()) + return; + + Set templateElements = new THashSet<>( graph.syncRequest( + new ObjectsWithType(template, L0.ConsistsOf, DIA.Element) ) ); + + try { + for (Resource instance : instances) { + this.temporaryDiagram.setHint(DiagramModelHints.KEY_DIAGRAM_RESOURCE, instance); + syncInstance(graph, template, instance, templateElements); + } + } catch (Exception e) { + LOGGER.error("Template synchronization failed.", e); + } finally { + this.temporaryDiagram.removeHint(DiagramModelHints.KEY_DIAGRAM_RESOURCE); + } + } + + private void syncInstance(WriteGraph graph, Resource instance) throws DatabaseException { + Resource template = graph.getPossibleObject(instance, MOD.HasDiagramSource); + if (template == null) + return; + + Set templateElements = new THashSet<>( graph.syncRequest( + new ObjectsWithType(template, L0.ConsistsOf, DIA.Element) ) ); + + try { + this.temporaryDiagram.setHint(DiagramModelHints.KEY_DIAGRAM_RESOURCE, instance); + syncInstance(graph, template, instance, templateElements); + } finally { + this.temporaryDiagram.removeHint(DiagramModelHints.KEY_DIAGRAM_RESOURCE); + } + } + + private Resource findInstanceCounterpart(ReadGraph graph, Resource instanceDiagram, Resource templateElement) throws DatabaseException { + Map children = graph.syncRequest(new UnescapedChildMapOfResource(instanceDiagram)); + for(Resource child : children.values()) { + if(graph.hasStatement(child, MOD.HasElementSource, templateElement)) return child; + } + return null; + } + + private boolean isSynchronizedConnector(ReadGraph graph, Resource templateConnection, Resource instanceConnector) throws DatabaseException { + DiagramResource DIA = DiagramResource.getInstance(graph); + Resource instanceConnection = graph.getPossibleObject(instanceConnector, DIA.IsConnectorOf); + if (instanceConnection == null) + return false; + return graph.hasStatement(instanceConnection, MOD.HasElementSource, templateConnection) + // If the master connection has been removed, this is all that's left + // to identify a connection that at least was originally synchronized + // from the typical master to this instance. + || graph.hasStatement(instanceConnection, MOD.IsTemplatized); + } + + /** + * Perform the following synchronization steps for the instance diagram: + *

    + *
  1. remove such templatized elements from the instance diagram whose + * template counterpart no longer exists
  2. + *
  3. add elements to the instance diagram that are only in the template
  4. + *
  5. synchronize elements of the instance diagram that have been deemed + * changed
  6. + *
+ * + * @param graph database write access + * @param template the synchronization source diagram + * @param instance the synchronization target diagram + * @param currentTemplateElements the set of all elements currently in the + * template diagram + * @throws DatabaseException if anything goes wrong + */ + private void syncInstance(WriteGraph graph, Resource template, Resource instance, Set currentTemplateElements) throws DatabaseException { + + List messageLog = getLog(graph, instance); + + messageLog.add("Synchronization of changed typical template: " + SyncTypicalTemplatesToInstances.safeNameAndType(graph, getDiagramNameResource(graph, template))); + messageLog.add("----\n\ttypical instance: " + safeNameAndType(graph, getDiagramNameResource(graph, instance))); + + CommonDBUtils.selectClusterSet(graph, instance); + + // Form instance element <-> template element bijection + TypicalInfoBean typicalInfoBean = graph.syncRequest( + new ReadTypicalInfo(instance), + TransientCacheListener. instance()); + // Must be able to modify the typicalInfo structure, + // therefore clone the query result. + typicalInfoBean = (TypicalInfoBean) typicalInfoBean.clone(); + typicalInfoBean.templateElements = currentTemplateElements; + typicalInfoBean.auxiliary = new HashMap<>(1); + + TypicalInfo info = new TypicalInfo(); + info.monitor = monitor; + info.messageLog = messageLog; + info.bean = typicalInfoBean; + + // Resolve naming function for this typical instance. + Resource compositeInstance = graph.getPossibleObject(instance, MOD.DiagramToComposite); + if (compositeInstance != null) { + Function4 namingFunction = TypicalUtil.getTypicalNamingFunction(graph, compositeInstance); + if (namingFunction != null) + typicalInfoBean.auxiliary.put(AuxKeys.KEY_TYPICAL_NAMING_FUNCTION, namingFunction); + } + + int dSizeAbs = Math.abs(typicalInfoBean.instanceElements.size() - currentTemplateElements.size()); + + if(DEBUG) + System.out.println("typical <-> template mapping: " + typicalInfoBean.instanceToTemplate); + + // Find elements to be removed from instance by looking for all + // instance elements that do not have a MOD.HasElementSource + // relation but have a MOD.IsTemplatized tag. + Set instanceElementsRemovedFromTemplate = findInstanceElementsRemovedFromTemplate( + graph, info, new THashSet<>(dSizeAbs)); + + // Find elements in template that do not yet exist in the instance + Set templateElementsAddedToTemplate = findTemplateElementsMissingFromInstance( + graph, currentTemplateElements, info, + new THashSet<>(dSizeAbs)); + + Set changedTemplateElements = changedElementsByDiagram.removeValues(template); + + if(DEBUG) + System.out.println("ADDED: " + templateElementsAddedToTemplate.size() + ", REMOVED: " + instanceElementsRemovedFromTemplate.size() + ", CHANGED: " + changedTemplateElements.size()); + + // Validate + for(Resource templateElement : graph.getObjects(template, L0.ConsistsOf)) { + if(graph.isInstanceOf(templateElement, DIA.RouteGraphConnection)) { + for(Resource connector : graph.getObjects(templateElement, DIA.HasConnector)) { + for(Statement elementStm : graph.getStatements(connector, STR.Connects)) { + Resource otherElement = elementStm.getObject(); + if(!otherElement.equals(templateElement)) { + Resource counterPartElement = findInstanceCounterpart(graph, instance, otherElement); + if(counterPartElement != null) { + Resource diagramConnectionPoint = graph.getInverse(elementStm.getPredicate()); + Resource connectionPoint = graph.getPossibleObject(diagramConnectionPoint, MOD.DiagramConnectionRelationToConnectionRelation); + if(connectionPoint != null) { + Statement stm = graph.getPossibleStatement(counterPartElement, diagramConnectionPoint); + if(stm != null) { + if(graph.isInstanceOf(connectionPoint, L0.FunctionalRelation)) { + if(!isSynchronizedConnector(graph, templateElement, stm.getObject())) { + messageLog.add("\t\tWARNING: skipping addition of template connection " + NameUtils.getSafeName(graph, templateElement, true) + " into instance."); + messageLog.add("\t\t\ttried to connect to an already connected terminal " + NameUtils.getSafeName(graph, counterPartElement, true) + " " + NameUtils.getSafeName(graph, connectionPoint)); + templateElementsAddedToTemplate.remove(templateElement); + } + } + } + } + } + } + } + } + } + } + + // Perform changes + boolean changed = false; + changed |= synchronizeDiagramChanges(graph, info, template, instance); + changed |= removeElements(graph, info, instanceElementsRemovedFromTemplate); + changed |= addMissingElements(graph, info, template, instance, templateElementsAddedToTemplate); + changed |= synchronizeChangedElements(graph, info, template, instance, changedTemplateElements, templateElementsAddedToTemplate, changedElementsByDiagram == ALL); + + if (changed) + metadata.addTypical(instance); + } + + /** + * Synchronize any configurable aspects of the typical diagram instance itself. + * Every rule executed here comes from the ontology, nothing is fixed. + * + * @param graph + * @param typicalInfo + * @param template + * @param instance + * @return if any changes were made. + * @throws DatabaseException + */ + private boolean synchronizeDiagramChanges( + WriteGraph graph, + TypicalInfo typicalInfo, + Resource template, + Resource instance) + throws DatabaseException + { + boolean changed = false; + for (Resource rule : graph.getObjects(template, MOD.HasTypicalSynchronizationRule)) { + if (selectedRules != null && !selectedRules.contains(rule)) + continue; + ITypicalSynchronizationRule r = graph.getPossibleAdapter(rule, ITypicalSynchronizationRule.class); + if (r != null) + changed |= r.synchronize(graph, template, instance, typicalInfo); + } + return changed; + } + + /** + * Add elements from template that do not yet exist in the instance. + * + * @param graph + * @param template + * @param instance + * @param elementsAddedToTemplate + * @return true if changes were made to the instance + * @throws DatabaseException + */ + private boolean addMissingElements(WriteGraph graph, TypicalInfo typicalInfo, Resource template, + Resource instance, Set elementsAddedToTemplate) + throws DatabaseException { + if (elementsAddedToTemplate.isEmpty()) + return false; + + CopyAdvisor copyAdvisor = graph.syncRequest(new Adapter(instance, CopyAdvisor.class)); + this.temporaryDiagram.setHint(SynchronizationHints.COPY_ADVISOR, copyAdvisor); + + ElementObjectAssortment assortment = new ElementObjectAssortment(graph, elementsAddedToTemplate); + if (copyMap == null) + copyMap = new THashMap<>(); + else + copyMap.clear(); + + if (DEBUG) + System.out.println("ADD MISSING ELEMENTS: " + assortment); + + // initialCopyMap argument is needed for copying just connections + // when their end-points are not copied at the same time. + + PasteOperation pasteOp = new PasteOperation(Commands.COPY, + (ICanvasContext) null, template, instance, temporaryDiagram, + assortment, false, new Point2D.Double(0, 0), + typicalInfo.bean.templateToInstance, copyMap) + .options(PasteOperation.ForceCopyReferences.INSTANCE); + + new Paster(graph.getSession(), pasteOp).perform(graph); + + boolean changed = false; + + if(!elementsAddedToTemplate.isEmpty()) + typicalInfo.messageLog.add("\tadded elements"); + + for (Resource addedElement : elementsAddedToTemplate) { + Resource copyElement = (Resource) copyMap.get(addedElement); + if (copyElement != null) { + graph.claim(copyElement, MOD.IsTemplatized, MOD.IsTemplatized, copyElement); + graph.claim(copyElement, MOD.HasElementSource, MOD.ElementHasInstance, addedElement); + + typicalInfo.bean.instanceElements.add(copyElement); + typicalInfo.bean.instanceToTemplate.put(copyElement, addedElement); + typicalInfo.bean.templateToInstance.put(addedElement, copyElement); + + typicalInfo.messageLog.add("\t\t" + safeNameAndType(graph, copyElement)); + + changed = true; + } + } + + ModelingResources MOD = ModelingResources.getInstance(graph); + Resource instanceComposite = graph.getPossibleObject(instance, MOD.DiagramToComposite); + List instanceComponents = new ArrayList<>(elementsAddedToTemplate.size()); + + // Post-process added elements after typicalInfo has been updated and + // template mapping statements are in place. + for (Resource addedElement : elementsAddedToTemplate) { + Resource copyElement = (Resource) copyMap.get(addedElement); + if (copyElement != null) { + postProcessAddedElement(graph, addedElement, copyElement, typicalInfo); + + if (instanceComponents != null) { + // Gather all instance typical components for applying naming + // strategy on them. + Resource component = graph.getPossibleObject(copyElement, MOD.ElementToComponent); + if (component != null) + instanceComponents.add(component); + } + } + } + + if (instanceComposite != null) + TypicalUtil.applySelectedModuleNames(graph, instanceComposite, instanceComponents); + + return changed; + } + + private void postProcessAddedElement(WriteGraph graph, + Resource addedTemplateElement, Resource addedInstanceElement, + TypicalInfo typicalInfo) throws DatabaseException { + if (graph.isInstanceOf(addedInstanceElement, DIA.Monitor)) { + postProcessAddedMonitor(graph, addedTemplateElement, addedInstanceElement, typicalInfo); + } + } + + private void postProcessAddedMonitor(WriteGraph graph, + Resource addedTemplateMonitor, Resource addedInstanceMonitor, + TypicalInfo typicalInfo) throws DatabaseException { + Resource monitor = addedInstanceMonitor; + Resource monitoredComponent = graph.getPossibleObject(monitor, DIA.HasMonitorComponent); + if (monitoredComponent != null) { + Resource monitoredTemplateElement = graph.getPossibleObject(monitoredComponent, MOD.ComponentToElement); + if (monitoredTemplateElement != null) { + Resource monitoredInstanceElement = typicalInfo.bean.templateToInstance.get(monitoredTemplateElement); + if (monitoredInstanceElement != null) { + Resource monitoredInstanceComponent = graph.getPossibleObject(monitoredInstanceElement, MOD.ElementToComponent); + if (monitoredInstanceComponent != null) { + // Ok, the monitor refers to a component within the + // template composite. Change it to refer to the + // instance composite. + graph.deny(monitor, DIA.HasMonitorComponent); + graph.claim(monitor, DIA.HasMonitorComponent, monitoredInstanceComponent); + } + } + } + } + } + + private boolean removeElements(WriteGraph graph, TypicalInfo typicalInfo, Set elementsRemovedFromTemplate) throws DatabaseException { + if (elementsRemovedFromTemplate.isEmpty()) + return false; + + // Remove mapped elements from instance that are removed from the template. + boolean changed = false; + + if(!elementsRemovedFromTemplate.isEmpty()) + typicalInfo.messageLog.add("\tremoved elements"); + + for (Resource removedElement : elementsRemovedFromTemplate) { + typicalInfo.messageLog.add("\t\t" + safeNameAndType(graph, removedElement)); + + RemoverUtil.remove(graph, removedElement); + + typicalInfo.bean.instanceElements.remove(removedElement); + Resource template = typicalInfo.bean.instanceToTemplate.remove(removedElement); + if (template != null) + typicalInfo.bean.templateToInstance.remove(template); + + changed = true; + } + return changed; + } + + private Set findTemplateElementsMissingFromInstance( + WriteGraph graph, Collection currentTemplateElements, + TypicalInfo typicalInfo, THashSet result) + throws DatabaseException { + for (Resource templateElement : currentTemplateElements) { + Resource instanceElement = typicalInfo.bean.templateToInstance.get(templateElement); + if (instanceElement == null) { + if(DEBUG) + System.out.println("No instance correspondence for template element " + NameUtils.getSafeName(graph, templateElement, true) + " => add"); + result.add(templateElement); + } + } + return result; + } + + public Set findInstanceElementsRemovedFromTemplate( + ReadGraph graph, TypicalInfo typicalInfo, + THashSet result) throws DatabaseException { + for (Resource instanceElement : typicalInfo.bean.instanceElements) { + if (!typicalInfo.bean.instanceToTemplate.containsKey(instanceElement)) { + if (typicalInfo.bean.isTemplatized.contains(instanceElement)) { + if(DEBUG) + System.out.println("Templatized typical instance element " + NameUtils.getSafeName(graph, instanceElement, true) + " has no correspondence in template => remove"); + result.add(instanceElement); + } + } + } + return result; + } + + /** + * Synchronize basic visual aspects of changed elements. For all elements, + * transform and label are synchronized. Otherwise synchronization is + * type-specific for connections, flags, monitors and svg elements. + * + * @param graph + * @param typicalInfo + * @param template + * @param instance + * @param changedTemplateElements + * @param addedElements + * elements that have been added and thus need not be + * synchronized + * @param synchronizeAllElements + * @return + * @throws DatabaseException + */ + private boolean synchronizeChangedElements(WriteGraph graph, + TypicalInfo typicalInfo, Resource template, Resource instance, + Collection changedTemplateElements, + Set addedElements, + boolean synchronizeAllElements) throws DatabaseException { + + if (synchronizeAllElements) { + // For unit testing purposes. + changedTemplateElements = graph.syncRequest(new ObjectsWithType(template, L0.ConsistsOf, DIA.Element)); + } + + if (changedTemplateElements.isEmpty()) + return false; + + boolean changed = false; + + typicalInfo.messageLog.add("\telement change analysis"); + int analysisLogPosition = typicalInfo.messageLog.size(); + + for (Resource changedTemplateElement : changedTemplateElements) { + // Skip synchronization of elements that were just added and are + // thus already synchronized. + if (addedElements.contains(changedTemplateElement)) + continue; + + Resource instanceElement = typicalInfo.bean.templateToInstance.get(changedTemplateElement); + if (instanceElement == null) { + // There's an earlier problem in the sync process if this happens. + typicalInfo.messageLog.add("\t\tSKIPPING SYNC OF CHANGED TEMPLATE ELEMENT DUE TO MISSING INSTANCE: " + safeNameAndType(graph, getElementNameResource(graph, changedTemplateElement))); + continue; + } + + typicalInfo.messageLog.add("\t\t" + elementName(graph, changedTemplateElement)); + int currentLogSize = typicalInfo.messageLog.size(); + + changed |= InstanceOfRule.INSTANCE.synchronize(graph, changedTemplateElement, instanceElement, typicalInfo); + changed |= NameRule.INSTANCE.synchronize(graph, changedTemplateElement, instanceElement, typicalInfo); + changed |= TransformRule.INSTANCE.synchronize(graph, changedTemplateElement, instanceElement, typicalInfo); + changed |= LabelRule.INSTANCE.synchronize(graph, changedTemplateElement, instanceElement, typicalInfo); + + Collection types = graph.getTypes(changedTemplateElement); + if (types.contains(DIA.RouteGraphConnection)) { + changed |= synchronizeConnection(graph, changedTemplateElement, instanceElement, typicalInfo); + } else if (types.contains(DIA.Flag)) { + changed |= FlagRule.INSTANCE.synchronize(graph, changedTemplateElement, instanceElement, typicalInfo); + } else if (types.contains(DIA.Monitor)) { + changed |= MonitorRule.INSTANCE.synchronize(graph, changedTemplateElement, instanceElement, typicalInfo); + } else if (types.contains(DIA.SVGElement)) { + changed |= SVGElementRule.INSTANCE.synchronize(graph, changedTemplateElement, instanceElement, typicalInfo); + } + + changed |= ProfileMonitorRule.INSTANCE.synchronize(graph, changedTemplateElement, instanceElement, typicalInfo); + + for (Resource rule : graph.getObjects(changedTemplateElement, MOD.HasTypicalSynchronizationRule)) { + if(selectedRules != null && !selectedRules.contains(rule)) continue; + ITypicalSynchronizationRule r = graph.getPossibleAdapter(rule, ITypicalSynchronizationRule.class); + if (r != null) + changed |= r.synchronize(graph, changedTemplateElement, instanceElement, typicalInfo); + } + + // Show element only if something has happened + if(currentLogSize == typicalInfo.messageLog.size()) + typicalInfo.messageLog.remove(typicalInfo.messageLog.size()-1); + + } + + if (s2t != null) + s2t.clear(); + if (t2s != null) + t2s.clear(); + + // Show analysis header only if something has happened + if(analysisLogPosition == typicalInfo.messageLog.size()) + typicalInfo.messageLog.remove(typicalInfo.messageLog.size()-1); + + return changed; + } + + private static class Connector { + public final Resource attachmentRelation; + public final Resource connector; + public RouteLine attachedTo; + + public Connector(Resource attachmentRelation, Resource connector) { + this.attachmentRelation = attachmentRelation; + this.connector = connector; + } + } + + /** + * Synchronizes two route graph connection topologies if and only if the + * destination connection is not attached to any node elements besides + * the ones that exist in the source. This means that connections that + * have instance-specific connections to non-template nodes are ignored + * here. + * + * @param graph + * @param sourceConnection + * @param targetConnection + * @param typicalInfo + * @return true if changes were made + * @throws DatabaseException + */ + private boolean synchronizeConnection(WriteGraph graph, Resource sourceConnection, Resource targetConnection, TypicalInfo typicalInfo) + throws DatabaseException { + + if(DEBUG) + System.out.println("connection " + NameUtils.getSafeName(graph, sourceConnection, true) + " to target connection " + NameUtils.getSafeName(graph, targetConnection, true)); + + boolean changed = false; + + // Initialize utilities and data maps + s2t = newOrClear(s2t); + t2s = newOrClear(t2s); + + if (cu == null) + cu = new ConnectionUtil(graph); + + // 0.1. find mappings between source and target connection connectors + Collection toTargetConnectors = graph.getStatements(targetConnection, DIA.HasConnector); + Map targetConnectors = new THashMap<>(toTargetConnectors.size()); + for (Statement toTargetConnector : toTargetConnectors) { + Resource targetConnector = toTargetConnector.getObject(); + targetConnectors.put(targetConnector, new Connector(toTargetConnector.getPredicate(), targetConnector)); + Statement toNode = cu.getConnectedComponentStatement(targetConnection, targetConnector); + if (toNode == null) { + // Corrupted target connection! + ErrorLogger.defaultLogError("Encountered corrupted typical template connection " + + NameUtils.getSafeName(graph, targetConnection, true) + " with a stray DIA.Connector instance " + + NameUtils.getSafeName(graph, targetConnector, true) + " that is not attached to any element.", + new Exception("trace")); + return false; + } + if (!graph.hasStatement(targetConnector, DIA.AreConnected)) { + // Corrupted target connection! + ErrorLogger.defaultLogError("Encountered corrupted typical template connection " + + NameUtils.getSafeName(graph, targetConnection, true) + " with a stray DIA.Connector instance " + + NameUtils.getSafeName(graph, targetConnector, true) + " that is not connected to any other route node.", + new Exception("trace")); + return false; + } + + //Resource templateNode = typicalInfo.instanceToTemplate.get(toNode.getObject()); + Resource templateNode = graph.getPossibleObject(toNode.getObject(), MOD.HasElementSource); + if (templateNode != null) { + Resource isConnectedTo = graph.getPossibleInverse(toNode.getPredicate()); + if (isConnectedTo != null) { + Resource templateConnector = graph.getPossibleObject(templateNode, isConnectedTo); + if (templateConnector != null) { + Resource connectionOfTemplateConnector = ConnectionUtil.tryGetConnection(graph, templateConnector); + if (sourceConnection.equals(connectionOfTemplateConnector)) { + s2t.put(templateConnector, targetConnector); + t2s.put(targetConnector, templateConnector); + + if (DEBUG) + debug(typicalInfo, "Mapping connector " + + NameUtils.getSafeName(graph, templateConnector, true) + + " to " + NameUtils.getSafeName(graph, targetConnector, true)); + } + } + } + } + } + + // 0.2. find mapping between source and target route lines + Collection sourceInteriorRouteNodes = graph.getObjects(sourceConnection, DIA.HasInteriorRouteNode); + Collection targetInteriorRouteNodes = graph.getObjects(targetConnection, DIA.HasInteriorRouteNode); + Map sourceToRouteLine = new THashMap<>(); + Map targetToRouteLine = new THashMap<>(); + + for (Resource source : sourceInteriorRouteNodes) + sourceToRouteLine.put(source, Paster.readRouteLine(graph, source)); + for (Resource target : targetInteriorRouteNodes) + targetToRouteLine.put(target, Paster.readRouteLine(graph, target)); + + Map originalSourceToRouteLine = new THashMap<>(sourceToRouteLine); + Map originalTargetToRouteLine = new THashMap<>(targetToRouteLine); + + nextSourceLine: + for (Iterator> sourceIt = sourceToRouteLine.entrySet().iterator(); !targetToRouteLine.isEmpty() && sourceIt.hasNext();) { + Map.Entry sourceEntry = sourceIt.next(); + Paster.RouteLine sourceLine = sourceEntry.getValue(); + for (Iterator> targetIt = targetToRouteLine.entrySet().iterator(); targetIt.hasNext();) { + Map.Entry targetEntry = targetIt.next(); + if (sourceLine.equals(targetEntry.getValue())) { + s2t.put(sourceEntry.getKey(), targetEntry.getKey()); + t2s.put(targetEntry.getKey(), sourceEntry.getKey()); + sourceIt.remove(); + targetIt.remove(); + + if (DEBUG) + debug(typicalInfo, "Mapping routeline " + + NameUtils.getSafeName(graph, sourceEntry.getKey(), true) + + " - " + sourceEntry.getValue() + + " to " + NameUtils.getSafeName(graph, targetEntry.getKey(), true) + + " - " + targetEntry.getValue()); + + continue nextSourceLine; + } + } + } + + if (DEBUG) { + debug(typicalInfo, "Take 1: Source to target route nodes map : " + s2t); + debug(typicalInfo, "Take 1: Target to source route nodes map : " + t2s); + } + + // 1.1. Temporarily disconnect instance-specific connectors from the the connection . + // They will be added back to the connection after the templatized parts of the + // connection have been synchronized. + + // Stores diagram connectors that are customizations in the synchronized instance. + List instanceOnlyConnectors = null; + + for (Connector connector : targetConnectors.values()) { + if (!t2s.containsKey(connector.connector)) { + typicalInfo.messageLog.add("\t\tencountered instance-specific diagram connector in target connection: " + NameUtils.getSafeName(graph, connector.connector)); + + // Find the RouteLine this connectors is connected to. + for (Resource rl : graph.getObjects(connector.connector, DIA.AreConnected)) { + connector.attachedTo = originalTargetToRouteLine.get(rl); + if (connector.attachedTo != null) + break; + } + + // Disconnect connector from connection + graph.deny(targetConnection, connector.attachmentRelation, connector.connector); + graph.deny(connector.connector, DIA.AreConnected); + + // Keep track of the disconnected connector + if (instanceOnlyConnectors == null) + instanceOnlyConnectors = new ArrayList<>(targetConnectors.size()); + instanceOnlyConnectors.add(connector); + } + } + + // 1.2. add missing connectors to target + Collection sourceConnectors = graph.getObjects(sourceConnection, DIA.HasConnector); + for (Resource sourceConnector : sourceConnectors) { + if (!s2t.containsKey(sourceConnector)) { + Statement sourceIsConnectorOf = graph.getSingleStatement(sourceConnector, DIA.IsConnectorOf); + Statement connects = cu.getConnectedComponentStatement(sourceConnection, sourceConnector); + if (connects == null) { + // TODO: serious error! + throw new DatabaseException("ERROR: connector is astray, i.e. not connected to a node element: " + safeNameAndType(graph, sourceConnector)); + } + Resource connectsInstanceElement = typicalInfo.bean.templateToInstance.get(connects.getObject()); + if (connectsInstanceElement == null) { + // TODO: serious error! + throw new DatabaseException("ERROR: could not find instance element to which template element " + safeNameAndType(graph, connects.getObject()) + " is connected to"); + } + Resource hasConnector = graph.getInverse(sourceIsConnectorOf.getPredicate()); + + Resource newTargetConnector = cu.newConnector(targetConnection, hasConnector); + graph.claim(newTargetConnector, connects.getPredicate(), connectsInstanceElement); + changed = true; + + s2t.put(sourceConnector, newTargetConnector); + t2s.put(newTargetConnector, sourceConnector); + + typicalInfo.messageLog.add("\t\t\tadd new connector to target connection: " + NameUtils.getSafeName(graph, newTargetConnector) + " to map to source connector " + NameUtils.getSafeName(graph, sourceConnector)); + } + } + + // 2. sync route lines and their connectivity: + // 2.1. assign correspondences in target for each source route line + // by reusing excess route lines in target and by creating new + // route lines. + + Resource[] targetRouteLines = targetToRouteLine.keySet().toArray(Resource.NONE); + int targetRouteLine = targetRouteLines.length - 1; + + for (Iterator> sourceIt = sourceToRouteLine.entrySet().iterator(); sourceIt.hasNext();) { + Map.Entry sourceEntry = sourceIt.next(); + Resource source = sourceEntry.getKey(); + Paster.RouteLine sourceLine = sourceEntry.getValue(); + + typicalInfo.messageLog.add("\t\t\tassign an instance-side routeline counterpart for " + NameUtils.getSafeName(graph, source, true) + " - " + sourceLine); + + // Assign target route line for source + Resource target = null; + if (targetRouteLine < 0) { + // by creating new route lines + target = cu.newRouteLine(targetConnection, sourceLine.getPosition(), sourceLine.isHorizontal()); + typicalInfo.messageLog.add("\t\t\tcreate new route line " + NameUtils.getSafeName(graph, target)); + changed = true; + } else { + // by reusing existing route line + target = targetRouteLines[targetRouteLine--]; + copyRouteLine(graph, source, target); + cu.disconnectFromAllRouteNodes(target); + typicalInfo.messageLog.add("\t\t\treused existing route line " + NameUtils.getSafeName(graph, target)); + changed = true; + } + s2t.put(source, target); + t2s.put(target, source); + + typicalInfo.messageLog.add("\t\t\tmapped source route line " + NameUtils.getSafeName(graph, source) + " to target route line " + NameUtils.getSafeName(graph, target)); + } + + if (targetRouteLine >= 0) { + typicalInfo.messageLog.add("\t\t\tremove excess route lines (" + (targetRouteLine + 1) + ") from target connection"); + for (; targetRouteLine >= 0; targetRouteLine--) { + typicalInfo.messageLog.add("\t\t\t\tremove excess route line: " + NameUtils.getSafeName(graph, targetRouteLines[targetRouteLine], true)); + cu.removeConnectionPart(targetRouteLines[targetRouteLine]); + } + } + + if (DEBUG) { + debug(typicalInfo, "Take 2: Source to target route nodes map : " + s2t); + debug(typicalInfo, "Take 2: Target to source route nodes map : " + t2s); + } + + // 2.2. Synchronize target connection topology (DIA.AreConnected) + changed |= connectRouteNodes(graph, typicalInfo, sourceInteriorRouteNodes); + changed |= connectRouteNodes(graph, typicalInfo, sourceConnectors); + + // 3. remove excess routelines & connectors from target connection + changed |= cu.removeExtraInteriorRouteNodes(targetConnection) > 0; + changed |= cu.removeUnusedConnectors(targetConnection) > 0; + + // 3.1. Ensure that all mapped route nodes in the target connection + // are tagged with MOD.IsTemplatized. Future synchronization + // can then take advantage of this information to more easily + // decide which parts of the connection are originated from + // the template and which are not. + changed |= markMappedRouteNodesTemplatized(graph, s2t.values()); + + // 4. Add temporarily disconnected instance-specific connectors + // back to the synchronized connection. The route line to attach + // to is based on a simple heuristic. + if (instanceOnlyConnectors != null) { + if (originalSourceToRouteLine.isEmpty()) { + // If there are 0 route lines in the template connection, + // then one must be added to the instance connection. + // This can only happen if the template connection is + // simple, i.e. just between two terminals without any + // custom routing. + + // Attach all target connection connectors to the newly created route line + Resource rl = cu.newRouteLine(targetConnection, null, null); + for (Resource sourceConnector : sourceConnectors) { + Resource targetConnector = s2t.get(sourceConnector); + graph.deny(targetConnector, DIA.AreConnected); + graph.claim(targetConnector, DIA.AreConnected, DIA.AreConnected, rl); + } + + // Copy orientation and position for new route line from original target route lines. + // This is a simplification that will attach any amount of route lines in the original + // target connection into just one route line. There is room for improvement here + // but it will require a more elaborate algorithm to find and cut the non-templatized + // route lines as well as connectors out of the connection before synchronizing it. + // + // TODO: This implementation chooses the added route line position at random if + // there are multiple route lines in the target connection. + if (!originalTargetToRouteLine.isEmpty()) { + RouteLine originalRl = originalTargetToRouteLine.values().iterator().next(); + setRouteLine(graph, rl, originalRl); + } + + // Attach the instance specific connectors also to the only route line + for (Connector connector : instanceOnlyConnectors) { + graph.claim(targetConnection, connector.attachmentRelation, connector.connector); + graph.claim(connector.connector, DIA.AreConnected, DIA.AreConnected, rl); + } + + changed = true; + } else { + for (Connector connector : instanceOnlyConnectors) { + // Find the route line that most closely matches the original + // route line that the connector was connected to. + Resource closestMatch = null; + double closestDistance = Double.MAX_VALUE; + if (connector.attachedTo != null) { + for (Map.Entry sourceLine : originalSourceToRouteLine.entrySet()) { + double dist = distance(sourceLine.getValue(), connector.attachedTo); + if (dist < closestDistance) { + closestMatch = s2t.get(sourceLine.getKey()); + closestDistance = dist; + } + } + } else { + closestMatch = originalSourceToRouteLine.keySet().iterator().next(); + } + graph.claim(targetConnection, connector.attachmentRelation, connector.connector); + graph.claim(connector.connector, DIA.AreConnected, DIA.AreConnected, closestMatch); + if (closestDistance > 0) + changed = true; + typicalInfo.messageLog.add("\t\t\treattached instance-specific connector " + + NameUtils.getSafeName(graph, connector.connector) + " to nearest existing route line " + + NameUtils.getSafeName(graph, closestMatch) + " with distance " + closestDistance); + } + } + } + + return changed; + } + + private boolean markMappedRouteNodesTemplatized(WriteGraph graph, Iterable routeNodes) throws DatabaseException { + boolean changed = false; + for (Resource rn : routeNodes) { + if (!graph.hasStatement(rn, MOD.IsTemplatized)) { + graph.claim(rn, MOD.IsTemplatized, MOD.IsTemplatized, rn); + changed = true; + } + } + return changed; + } + + private static double distance(RouteLine l1, RouteLine l2) { + double dist = Math.abs(l2.getPosition() - l1.getPosition()); + dist *= l2.isHorizontal() == l1.isHorizontal() ? 1 : 1000; + return dist; + } + + private boolean connectRouteNodes(WriteGraph graph, TypicalInfo typicalInfo, Collection sourceRouteNodes) throws DatabaseException { + boolean changed = false; + for (Resource src : sourceRouteNodes) { + Resource dst = s2t.get(src); + if (dst == null) { + throw new DatabaseException("TARGET ROUTE NODE == NULL FOR SRC: " + NameUtils.getSafeName(graph, src)); + } + + Collection connectedToSrcs = graph.getObjects(src, DIA.AreConnected); + Collection connectedToDsts = graph.getObjects(dst, DIA.AreConnected); + + // Remove excess statements + for (Resource connectedToDst : connectedToDsts) { + Resource connectedToSrc = t2s.get(connectedToDst); + if (connectedToSrc == null) { + throw new DatabaseException("CONNECTED TO SRC == NULL FOR DST: " + NameUtils.getSafeName(graph, connectedToDst)); + } + if (connectedToSrc == null || !graph.hasStatement(src, DIA.AreConnected, connectedToSrc)) { + graph.deny(dst, DIA.AreConnected, DIA.AreConnected, connectedToDst); + changed = true; + typicalInfo.messageLog.add("\t\t\tdisconnected route nodes (" + NameUtils.getSafeName(graph, dst) + ", " + NameUtils.getSafeName(graph, connectedToDst) + ")"); + } + } + + // Add necessary statements + for (Resource connectedToSrc : connectedToSrcs) { + Resource connectedToDst = s2t.get(connectedToSrc); + if (connectedToDst == null) { + throw new DatabaseException("CONNECTED TO DST == NULL FOR SRC: " + NameUtils.getSafeName(graph, connectedToSrc)); + } + if (!graph.hasStatement(dst, DIA.AreConnected, connectedToDst)) { + graph.claim(dst, DIA.AreConnected, DIA.AreConnected, connectedToDst); + changed = true; + typicalInfo.messageLog.add("\t\t\tconnected route nodes (" + NameUtils.getSafeName(graph, dst) + ", " + NameUtils.getSafeName(graph, connectedToDst) + ")"); + } + } + } + return changed; + } + + private void setRouteLine(WriteGraph graph, Resource line, double position, boolean horizontal) throws DatabaseException { + graph.claimLiteral(line, DIA.HasPosition, L0.Double, position, Bindings.DOUBLE); + graph.claimLiteral(line, DIA.IsHorizontal, L0.Boolean, horizontal, Bindings.BOOLEAN); + } + + private void setRouteLine(WriteGraph graph, Resource line, RouteLine rl) throws DatabaseException { + setRouteLine(graph, line, rl.getPosition(), rl.isHorizontal()); + } + + private void copyRouteLine(WriteGraph graph, Resource src, Resource tgt) throws DatabaseException { + Double pos = graph.getPossibleRelatedValue(src, DIA.HasPosition, Bindings.DOUBLE); + Boolean hor = graph.getPossibleRelatedValue(src, DIA.IsHorizontal, Bindings.BOOLEAN); + if (pos == null) + pos = 0.0; + if (hor == null) + hor = Boolean.TRUE; + graph.claimLiteral(tgt, DIA.HasPosition, L0.Double, pos, Bindings.DOUBLE); + graph.claimLiteral(tgt, DIA.IsHorizontal, L0.Boolean, hor, Bindings.BOOLEAN); + } + + private static String safeNameAndType(ReadGraph graph, Resource r) throws DatabaseException { + StringBuilder sb = new StringBuilder(); + sb.append(NameUtils.getSafeName(graph, r, true)); + sb.append(" : ["); + boolean first = true; + for (Resource type : graph.getPrincipalTypes(r)) { + if (!first) + sb.append(","); + first = false; + sb.append(NameUtils.getSafeName(graph, type, true)); + } + sb.append("]"); + return sb.toString(); + } + + private static Map newOrClear(Map current) { + if (current == null) + return new THashMap<>(); + current.clear(); + return current; + } + + private void debug(TypicalInfo typicalInfo, String message) { + if (DEBUG) { + System.out.println(message); + typicalInfo.messageLog.add(message); + } + } + } \ No newline at end of file