/******************************************************************************* * 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); } } }