DiagramUpdater.java

/**
 * Copyright (c) 2004-2025 Carnegie Mellon University and others. (see Contributors file).
 * All Rights Reserved.
 *
 * NO WARRANTY. ALL MATERIAL IS FURNISHED ON AN "AS-IS" BASIS. CARNEGIE MELLON UNIVERSITY MAKES NO WARRANTIES OF ANY
 * KIND, EITHER EXPRESSED OR IMPLIED, AS TO ANY MATTER INCLUDING, BUT NOT LIMITED TO, WARRANTY OF FITNESS FOR PURPOSE
 * OR MERCHANTABILITY, EXCLUSIVITY, OR RESULTS OBTAINED FROM USE OF THE MATERIAL. CARNEGIE MELLON UNIVERSITY DOES NOT
 * MAKE ANY WARRANTY OF ANY KIND WITH RESPECT TO FREEDOM FROM PATENT, TRADEMARK, OR COPYRIGHT INFRINGEMENT.
 *
 * This program and the accompanying materials are made available under the terms of the Eclipse Public License 2.0
 * which is available at https://www.eclipse.org/legal/epl-2.0/
 * SPDX-License-Identifier: EPL-2.0
 *
 * Created, in part, with funding and support from the United States Government. (see Acknowledgments file).
 *
 * This program includes and/or can make use of certain third party source code, object code, documentation and other
 * files ("Third Party Software"). The Third Party Software that is used by this program is dependent upon your system
 * configuration. By using this program, You agree to comply with any and all relevant Third Party Software terms and
 * conditions contained in any such Third Party Software or separate license file distributed with such Third Party
 * Software. The parties who own the Third Party Software ("Third Party Licensors") are intended third party benefici-
 * aries to this license with respect to the terms applicable to their Third Party Software. Third Party Software li-
 * censes only apply to the Third Party Software and not any other portion of this program or this program as a whole.
 */
package org.osate.ge.internal.diagram.runtime.updating;

import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;

import org.osate.ge.CanonicalBusinessObjectReference;
import org.osate.ge.DockingPosition;
import org.osate.ge.GraphicalConfiguration;
import org.osate.ge.RelativeBusinessObjectReference;
import org.osate.ge.aadl2.internal.model.PropertyValueGroup;
import org.osate.ge.businessobjecthandling.BusinessObjectHandler;
import org.osate.ge.graphics.Point;
import org.osate.ge.graphics.internal.AgeConnection;
import org.osate.ge.internal.diagram.runtime.AgeDiagram;
import org.osate.ge.internal.diagram.runtime.DiagramConfigurationBuilder;
import org.osate.ge.internal.diagram.runtime.DiagramElement;
import org.osate.ge.internal.diagram.runtime.DiagramElementPredicates;
import org.osate.ge.internal.diagram.runtime.DiagramModification;
import org.osate.ge.internal.diagram.runtime.DiagramNode;
import org.osate.ge.internal.diagram.runtime.DockArea;
import org.osate.ge.internal.model.EmbeddedBusinessObject;
import org.osate.ge.internal.services.ActionExecutor;
import org.osate.ge.internal.services.AgeAction;
import org.osate.ge.services.ReferenceBuilderService;
import org.osate.ge.services.ReferenceResolutionService;

/**
 * Updates the diagram's elements based on the diagram configuration.
 * The DiagramUpdater updates the diagram using information provided by objects passed into the constructor.
 */
public class DiagramUpdater {
	private final BusinessObjectTreeUpdater boTreeUpdater;
	private final DiagramElementInformationProvider infoProvider;
	private final ActionExecutor actionExecutor;
	private final ReferenceResolutionService referenceResolver;
	private final ReferenceBuilderService referenceBuilder;

	private final Map<DiagramNode, Map<RelativeBusinessObjectReference, DiagramElement>> containerToRelativeReferenceToGhostMap = new HashMap<>();

	// Holds information regarding diagram elements which have not been created. The DiagramNode is the parent of the new element.
	private final Map<DiagramNode, Map<RelativeBusinessObjectReference, FutureElementInfo>> futureElementInfoMap = new HashMap<>();

	/**
	 * Creates an instance which will be used to update a diagram.
	 * @param boTreeUpdater the tree updater
	 * @param infoProvider the diagram element info provider
	 * @param actionExecutor the action executor to use to execute action
	 * @param referenceResolver the reference resolver. Must not be null but only used when a diagram has a diagram context.
	 * @param referenceBuilder the reference builder. Must not be null but only used when a diagram has a diagram context.
	 */
	public DiagramUpdater(final BusinessObjectTreeUpdater boTreeUpdater,
			final DiagramElementInformationProvider infoProvider, final ActionExecutor actionExecutor,
			final ReferenceResolutionService referenceResolver, final ReferenceBuilderService referenceBuilder) {
		this.boTreeUpdater = Objects.requireNonNull(boTreeUpdater, "boTreeUpdater must not be null");
		this.infoProvider = Objects.requireNonNull(infoProvider, "infoProvider must not be null"); // Adjust message after rename
		this.actionExecutor = Objects.requireNonNull(actionExecutor, "actionExecutor must not be null");
		this.referenceResolver = Objects.requireNonNull(referenceResolver, "referenceResolver must not be null");
		this.referenceBuilder = Objects.requireNonNull(referenceBuilder, "referenceBuilder must not be null");
	}

	/**
	 * Instructs the updater to create an element for the reference business object if the business object exists during the next
	 * update. Also allows settings additional information such as the initial position of the diagram element.
	 *
	 * This is the mechanism by which a position can be specified for a diagram element which doesn't exist yet.
	 * This is used to set the initial position of an element being created by the palette. When the diagram is updated the diagram element
	 * is created and then the position is set to the specified value. The specified values are cleared after each update.
	 * @param parentDiagramNode the diagram node which will be the parent of the new element
	 * @param ref the relative reference used to identify the element
	 * @param newElementInfo the information to use when initializing the diagram element. Must not be null.
	 */
	public void addToNextUpdate(final DiagramNode parentDiagramNode,
			final RelativeBusinessObjectReference ref,
			final FutureElementInfo newElementInfo) {
		Map<RelativeBusinessObjectReference, FutureElementInfo> m = futureElementInfoMap.get(parentDiagramNode);
		if(m == null) {
			m = new HashMap<>();
			futureElementInfoMap.put(parentDiagramNode, m);
		}
		m.put(ref, newElementInfo);
	}

	/**
	 * Updates the specified diagram.
	 * @param diagram the diagram to update. See {@link DiagramUpdater#updateDiagram(AgeDiagram, BusinessObjectNode)}
	 */
	public void updateDiagram(final AgeDiagram diagram) {
		// Create an updated business object tree based on the current state of the diagram and pending elements
		final BusinessObjectNode tree = DiagramToBusinessObjectTreeConverter.createBusinessObjectNode(diagram, futureElementInfoMap, containerToRelativeReferenceToGhostMap);
		updateDiagram(diagram, tree);
	}

	/**
	 * Updates a diagram
	 * @param diagram the diagram to update. Because the diagram updater is typically initialized with fields which are specific to a project,
	 * a diagram updater should only be used with diagrams for which it was created.
	 * @param inputTree is the input business object tree. The input tree is not modified.
	 */
	public void updateDiagram(final AgeDiagram diagram, final BusinessObjectNode inputTree) {
		// Create a tree by updating the input tree.
		final BusinessObjectNode tree = boTreeUpdater.updateTree(diagram.getConfiguration(), inputTree);
		final List<DiagramElement> connectionElements = new LinkedList<>();

		diagram.modify("Update Diagram", m -> {
			refreshDiagramContextReference(m, diagram);

			// Update the structure. By doing this in a separate pass, updateElements() will have access to the complete diagram structure.
			// However, connections will later be purged from the diagram if they do not refer to valid elements.
			updateStructure(m, diagram, tree.getChildren());

			updateElements(m, diagram, tree.getChildren(), connectionElements);
			removeInvalidConnections(m, connectionElements);
		});

		// Remove all entries from the future elements map regardless of whether they were created or not. This ensures that unused positions aren't retained indefinitely
		futureElementInfoMap.clear();
	}

	/**
	 * Rebuilds the diagram context's reference and updated the diagram context accordingly. This ensures the diagram is updated with the correct
	 * case.
	 * @param m
	 * @param diagram
	 */
	private void refreshDiagramContextReference(final DiagramModification m, final AgeDiagram diagram) {
		final CanonicalBusinessObjectReference diagramContextRef = diagram.getConfiguration().getContextBoReference();
		if (diagramContextRef != null) {
			final Object contextBo = referenceResolver.resolve(diagram.getConfiguration().getContextBoReference());
			if (contextBo != null) {
				final CanonicalBusinessObjectReference newDiagramContextRef = referenceBuilder
						.getCanonicalReference(contextBo);

				// If the new reference isn't equal to the old reference, then something isn't correct. Don't use the new reference.
				if (Objects.equals(diagramContextRef, newDiagramContextRef)) {
					m.setDiagramConfiguration(new DiagramConfigurationBuilder(diagram.getConfiguration())
							.contextBoReference(newDiagramContextRef).build());
				}
			}
		}
	}

	/**
	 * Updates the structure of the diagram based on the business object tree.
	 * Creates/Unghosts elements to match the business object tree. Ghosts diagram elements which are not in the diagram element tree.
	 * @param m
	 * @param container
	 * @param bos
	 */
	private void updateStructure(final DiagramModification m, final DiagramNode container, final Collection<BusinessObjectNode> bos) {
		for(final BusinessObjectNode n : bos) {
			// Get existing element if it exists.
			DiagramElement element = container.getChildByRelativeReference(n.getRelativeReference());

			// Create the element if it does not exist
			if(element == null) {
				final DiagramElement removedGhost = removeGhost(container, n.getRelativeReference());
				if(removedGhost == null) {
					final BusinessObjectHandler boh = infoProvider
							.getApplicableBusinessObjectHandler(n.getBusinessObject());
					if(boh == null) {
						// Ignore the object
						continue;
					}

					element = new DiagramElement(container, n.getBusinessObject(), boh, n.getRelativeReference(),
							n.getId());
				} else {
					element = removedGhost;
					m.updateBusinessObject(element, n.getBusinessObject(), n.getRelativeReference());
				}

				m.addElement(element);
			} else {
				// Update the business object and relative reference. Although the reference matches. The business object may be new and the
				// relative reference may have case differences.
				m.updateBusinessObject(element, n.getBusinessObject(),
						n.getRelativeReference());
			}

			// Set the business object handler if it is null
			if(element.getBusinessObjectHandler() == null) {
				final BusinessObjectHandler boh = infoProvider
						.getApplicableBusinessObjectHandler(n.getBusinessObject());
				if(boh == null) {
					ghostAndRemove(m, element);
					continue;
				} else {
					m.setBusinessObjectHandler(element, boh);
				}
			}

			// Update the element's children
			updateStructure(m, element, n.getChildren());
		}

		// Remove elements whose business objects are not in the business object tree
		// At this point, it is assumed that there is a diagram element for each business object. Elements that are incomplete may be pruned later.
		// If the collections are the same size, there is nothing to remove
		if (bos.size() != container.getChildren().size()) {
			// Build Set of Relative References of All the Objects in the Business Object Tree
			final Set<RelativeBusinessObjectReference> boTreeRelativeReferenceSet = bos.stream().map((n) -> n.getRelativeReference()).collect(Collectors.toCollection(HashSet::new));
			Iterator<DiagramElement> childrenIt = container.getChildren().iterator();
			while(childrenIt.hasNext()) {
				final DiagramElement child = childrenIt.next();
				if(!boTreeRelativeReferenceSet.contains(child.getRelativeReference())) {
					ghostAndRemove(m, child);
				}
			}
		}
	}

	/**
	 * Ghosts and removes an element. Updates the parent's complete state.
	 * @param m
	 * @param e
	 */
	private void ghostAndRemove(final DiagramModification m, final DiagramElement e) {
		addGhost(e);
		m.removeElement(e);

		// Ignore property result groups and embedded business objects when determining if an element completeness
		if (e.getParent() instanceof DiagramElement && !(e.getBusinessObject() instanceof PropertyValueGroup)
				&& (!(e.getBusinessObject() instanceof EmbeddedBusinessObject))) {
			m.setCompleteness((DiagramElement)e.getParent(), Completeness.INCOMPLETE);
		}
	}

	/**
	 *
	 * @param container is the container for which to update the elements
	 * @param bos
	 * @param connectionElements is a collection to populate with connection elements.
	 */
	private void updateElements(final DiagramModification m, final DiagramNode container, final Collection<BusinessObjectNode> bos, final Collection<DiagramElement> connectionElements) {
		for(final BusinessObjectNode n : bos) {
			// Get existing element. The updateStructure() pass should have ensured that it exists if a valid element could be created.
			final DiagramElement element = container.getChildByRelativeReference(n.getRelativeReference());
			if(element == null) {
				continue;
			}

			// Set fields
			m.setCompleteness(element, n.getCompleteness());

			// Set name fields
			m.setLabelName(element, infoProvider.getLabelName(element));
			m.setUserInterfaceName(element, infoProvider.getUserInterfaceName(element));

			// Set the graphical Configuration
			final GraphicalConfiguration graphicalConfiguration = infoProvider.getGraphicalConfiguration(element);
			if(graphicalConfiguration == null) {
				ghostAndRemove(m, element);
			} else {
				// Reset position of flow indicators if the start element has changed. This can occur when feature groups are expanded for example.
				if (element.hasPosition() && DiagramElementPredicates.isFlowIndicator(element)
						&& graphicalConfiguration.getConnectionSource() != element.getStartElement()) {
					m.setPosition(element, null);
				}

				m.setGraphicalConfiguration(element, graphicalConfiguration);

				// Set the dock area based on the default docking position
				final DockingPosition defaultDockingPosition = graphicalConfiguration.getDefaultDockingPosition();
				final boolean dockable = defaultDockingPosition != DockingPosition.NOT_DOCKABLE;
				if(dockable) {
					// If parent is docked, the child should use the group docking area
					if(container instanceof DiagramElement && ((DiagramElement) container).getDockArea() != null) {
						m.setDockArea(element, DockArea.GROUP);
					} else if(element.getDockArea() == null) {
						m.setDockArea(element, DockArea.fromDockingPosition(defaultDockingPosition));
					}
				} else {
					// Ensure the dock area is null
					m.setDockArea(element, null);
				}

				// Set the initial position if there is a value in the future element position map
				// Set the position after the dock area so that setPosition() will know whether the element is dockable.
				final Map<RelativeBusinessObjectReference, FutureElementInfo> futureElementInfos = futureElementInfoMap
						.get(container);
				final FutureElementInfo futureElementInfo = futureElementInfos == null ? null
						: futureElementInfos.get(n.getRelativeReference());
				final Point initialPosition = futureElementInfo != null ? futureElementInfo.position
						: null;
				if(initialPosition != null) {
					m.setPosition(element, initialPosition);
				}

				if(element.getGraphic() instanceof AgeConnection) {
					// Add connection elements to the list so that they can be access later.
					connectionElements.add(element);
				}

				// Update the element's children
				updateElements(m, element, n.getChildren(), connectionElements);
			}
		}
	}

	/**
	 * Removes invalid connections.
	 */
	private void removeInvalidConnections(final DiagramModification m, final Collection<DiagramElement> connectionElements) {
		// Build Collection of All Invalid Connections
		final Set<DiagramElement> invalidConnectionElements = new HashSet<>();
		Iterator<DiagramElement> connectionElementsIt = connectionElements.iterator();
		while(connectionElementsIt.hasNext()) {
			final DiagramElement e = connectionElementsIt.next();

			if(e.getStartElement() == null || (e.getEndElement() == null && !((AgeConnection)e.getGraphic()).isFlowIndicator)) {
				invalidConnectionElements.add(e);

				// Remove the connection from the connection collection and the diagram
				connectionElementsIt.remove();
				ghostAndRemove(m, e);
			}
		}

		// Loop through the connections repeatedly until there are no longer any invalid connections referenced.
		for(int lastSize = 0; (invalidConnectionElements.size() - lastSize) > 0; lastSize = invalidConnectionElements.size()) {
			connectionElementsIt = connectionElements.iterator();
			while(connectionElementsIt.hasNext()) {
				final DiagramElement e = connectionElementsIt.next();
				if(invalidConnectionElements.contains(e.getStartElement()) || invalidConnectionElements.contains(e.getEndElement())) {
					invalidConnectionElements.add(e);

					// Remove the connection from the connection collection and the diagram
					removeConnectionAndDescendantConnections(e, connectionElements);
					ghostAndRemove(m, e);
				}
			}
		}
	}

	private static void removeConnectionAndDescendantConnections(DiagramElement e, final Collection<DiagramElement> connections) {
		if(e.getGraphic() instanceof AgeConnection) {
			connections.remove(e);
		}

		for(final DiagramElement child : connections) {
			removeConnectionAndDescendantConnections(child, connections);
		}
	}

	// Ghosting
	/**
	 * Clear ghosts. Ghosts are elements which have been removed from the diagram but which are retained until ghosts are cleared. They are
	 * retained to allow the diagram element to be restored if the element was removed due to a transient issue. An example of that would
	 * be a broken reference. One way ghosts can be restored is by using {@link org.osate.ge.internal.ui.handlers.RestoreMissingDiagramElementsHandler}.
	 */
	public void clearGhosts() {
		containerToRelativeReferenceToGhostMap.clear();
	}

	private void addGhost(final DiagramElement ghost) {
		actionExecutor.execute("Add Ghost", ActionExecutor.ExecutionMode.NORMAL, new AddGhostAction(ghost));
	}

	private DiagramElement removeGhost(final DiagramNode container, final RelativeBusinessObjectReference relativeReference) {
		final RemoveGhostAction action = new RemoveGhostAction(container, relativeReference);
		actionExecutor.execute("Remove Ghost", ActionExecutor.ExecutionMode.NORMAL, action);
		return action.removedGhost;
	}

	/**
	 * Wrapper for a ghosted DiagramElement. Intended to prevent corrupting containerToRelativeReferenceToGhostMap caused by changing the element's relative reference directly.
	 *
	 */
	public class GhostedElement {
		private final DiagramElement element;

		/**
		 * Creates an instance
		 * @param element the diagram element which was ghosted
		 */
		public GhostedElement(final DiagramElement element) {
			this.element = Objects.requireNonNull(element, "element mustnot be null");
		}

		/**
		 * Return the relative reference of the ghosted diagram element
		 * @return the relative reference of the ghosted diagram element
		 */
		public RelativeBusinessObjectReference getRelativeReference() {
			return element.getRelativeReference();
		}

		/**
		 * Return the parent of the ghosted diagram element
		 * @return the parent of the ghosted diagram element
		 */
		public DiagramNode getParent() {
			return element.getParent();
		}

		/**
		 * Update the business object and relative reference of the ghosted element. The ghosted element map will be updated with the new mapping.
		 * @param m the modification to use to update the ghost
		 * @param bo the new business object
		 * @param newRelativeReference the relative reference for the business object
		 */
		public void updateBusinessObject(final DiagramModification m, final Object bo,
				final RelativeBusinessObjectReference newRelativeReference) {
			removeGhost(element.getParent(), getRelativeReference());
			m.updateBusinessObject(element, bo, newRelativeReference);
			addGhost(element);
		}
	}

	/**
	 * Returns a collection containing the ghosted children for the specified node.
	 * @param node the node for which to retrieve the ghosted children
	 * @return a collection containing the ghosted children for the specified node.
	 */
	public Collection<GhostedElement> getGhosts(final DiagramNode node) {
		return containerToRelativeReferenceToGhostMap.getOrDefault(node, Collections.emptyMap()).values().stream()
				.map(e -> new GhostedElement(e)).collect(Collectors.toList());
	}

	private class AddGhostAction implements AgeAction {
		private final DiagramElement ghost;

		public AddGhostAction(final DiagramElement ghost) {
			this.ghost = Objects.requireNonNull(ghost, "ghost must not be null");
		}

		@Override
		public boolean canExecute() {
			return true;
		}

		@Override
		public AgeAction execute() {
			final DiagramNode container = ghost.getParent();

			// Get the mapping from relative reference to the ghost for the container
			Map<RelativeBusinessObjectReference, DiagramElement> relativeReferenceToGhostMap = containerToRelativeReferenceToGhostMap
					.get(container);
			if (relativeReferenceToGhostMap == null) {
				relativeReferenceToGhostMap = new HashMap<>();
				containerToRelativeReferenceToGhostMap.put(container, relativeReferenceToGhostMap);
			}

			// Add the ghost to the map
			relativeReferenceToGhostMap.put(ghost.getRelativeReference(), ghost);

			return new RemoveGhostAction(container, ghost.getRelativeReference());
		}
	}

	private class RemoveGhostAction implements AgeAction {
		private final DiagramNode container;
		private final RelativeBusinessObjectReference relativeReference;

		private DiagramElement removedGhost;

		public RemoveGhostAction(final DiagramNode container, final RelativeBusinessObjectReference relativeReference) {
			this.container = container;
			this.relativeReference = relativeReference;
		}

		@Override
		public boolean canExecute() {
			return true;
		}

		@Override
		public AgeAction execute() {
			final Map<RelativeBusinessObjectReference, DiagramElement> relativeReferenceToGhostMap = containerToRelativeReferenceToGhostMap
					.get(container);
			if (relativeReferenceToGhostMap == null) {
				return null;
			}

			removedGhost = relativeReferenceToGhostMap.remove(relativeReference);

			return removedGhost == null ? null : new AddGhostAction(removedGhost);
		}
	}
}