TransientView.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.gef.ui.transientviews;

import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.UUID;

import org.eclipse.e4.core.contexts.EclipseContextFactory;
import org.eclipse.e4.core.contexts.IEclipseContext;
import org.osate.ge.DiagramType;
import org.osate.ge.businessobjecthandling.BusinessObjectHandler;
import org.osate.ge.businessobjecthandling.IsApplicableContext;
import org.osate.ge.gef.DiagramRootNode;
import org.osate.ge.gef.ui.diagram.GefAgeDiagram;
import org.osate.ge.gef.ui.services.GefDiagramExportService;
import org.osate.ge.internal.businessobjecthandlers.BusinessObjectHandlerProvider;
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.DiagramNode;
import org.osate.ge.internal.diagram.runtime.layout.DiagramElementLayoutUtil;
import org.osate.ge.internal.diagram.runtime.layout.LayoutOptionsBuilder;
import org.osate.ge.internal.diagram.runtime.updating.BusinessObjectNode;
import org.osate.ge.internal.diagram.runtime.updating.BusinessObjectNodeFactory;
import org.osate.ge.internal.diagram.runtime.updating.BusinessObjectTreeUpdater;
import org.osate.ge.internal.diagram.runtime.updating.Completeness;
import org.osate.ge.internal.diagram.runtime.updating.DefaultDiagramElementGraphicalConfigurationProvider;
import org.osate.ge.internal.diagram.runtime.updating.DiagramUpdater;
import org.osate.ge.internal.services.ExtensionRegistryService;
import org.osate.ge.internal.services.ReferenceService;
import org.osate.ge.internal.services.impl.DefaultColoringService;
import org.osate.ge.internal.services.impl.DefaultColoringService.StyleRefresher;
import org.osate.ge.internal.services.impl.DefaultReferenceService;
import org.osate.ge.internal.services.impl.SimpleActionExecutor;
import org.osate.ge.services.QueryService;
import org.osate.ge.services.impl.DefaultQueryService;
import org.osgi.framework.FrameworkUtil;

import com.google.common.collect.BiMap;
import com.google.common.collect.HashBiMap;
import com.google.common.collect.ImmutableCollection;
import com.google.common.collect.ImmutableSet;

import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.scene.Node;
import javafx.scene.Scene;

/**
 * A transient view is a visualization of a business object tree which is not persisted. Views match the
 * structure and appearance of editable diagrams.
 *
 * The purpose of this class is to allow plugins to create JavaFX nodes for a non-persistent diagram from a tree of business objects.
 *
 * @since 1.1
 */
public final class TransientView {
	/**
	 * Diagram type used internally when creating a diagram. All diagrams must have a diagram type.
	 */
	private static class TransientViewDiagramType implements DiagramType {
		public static final String ID = "gef.transientView";

		@Override
		public String getId() {
			return ID;
		}

		@Override
		public String getName() {
			return "Transient View";
		}

		@Override
		public boolean isApplicableToContext(final Object contextBo) {
			return contextBo == null;
		}

		@Override
		public ImmutableSet<String> getDefaultContentFilters(final Object bo) {
			return ImmutableSet.of();
		}

		@Override
		public ImmutableCollection<String> getDefaultAadlPropertyNames() {
			return ImmutableSet.of();
		}
	}

	/**
	 * The object which manages creating an updating the scene graph for the diagram
	 */
	private final GefAgeDiagram gefDiagram;

	/**
	 * Bidirectional mapping between the tree nodes specified in the constructor and the diagram nodes.
	 */
	private final BiMap<TransientViewTreeNode, DiagramNode> transientViewNodeToDiagramNodeMap;

	/**
	 * Creates a new instance which contains the business objects in the specified tree.
	 * @param tree the tree containing the business objects to visualize
	 * @see TransientView#TransientView(TransientViewTreeNode, Collection)
	 */
	public TransientView(final TransientViewTreeNode tree) {
		this(tree, Collections.emptyList());
	}

	/**
	 * Creates a new instance which contains the business objects in the specified tree and which uses the specified
	 * business object handlers in addition to the registered business object handlers.
	 * In cases where both a registered and a specified business object handler is applicable, the one specified is
	 * used.
	 * If a business object is encountered that does not have a business object handler available, an exception will be thrown.
	 * @param tree the tree containing the business objects to visualize
	 * @param additionalBusinessObjectHandlers additional business object handlers which should be used for the transient view.
	 */
	public TransientView(final TransientViewTreeNode tree,
			final Collection<BusinessObjectHandler> additionalBusinessObjectHandlers) {
		Objects.requireNonNull(tree, "treeRoot must not be null");
		Objects.requireNonNull(additionalBusinessObjectHandlers, "additionalBusinessObjectHandlers must not be null");

		// Create the business object provider which delegates the the extension registry if none of the
		// specified business object handlers are applicable
		final IEclipseContext eclipseContext = EclipseContextFactory
				.getServiceContext(FrameworkUtil.getBundle(GefDiagramExportService.class).getBundleContext());
		final ExtensionRegistryService extensionRegistry = Objects.requireNonNull(
				eclipseContext.get(ExtensionRegistryService.class), "Unable to retrieve extension registry");
		final BusinessObjectHandlerProvider bohProvider = bo -> {
			final IsApplicableContext ctx = new IsApplicableContext(bo);
			for (final BusinessObjectHandler boh : additionalBusinessObjectHandlers) {
				if (boh.isApplicable(ctx)) {
					return boh;
				}
			}
			return extensionRegistry.getApplicableBusinessObjectHandler(bo);
		};

		// Create a reference service which uses the business object handler and instantiate remaining objects which are needed
		// to create the diagram
		final ReferenceService referenceService = new DefaultReferenceService(bohProvider);
		final SimpleActionExecutor actionExecutor = new SimpleActionExecutor();
		final QueryService queryService = new DefaultQueryService(referenceService);
		final BusinessObjectNodeFactory nodeFactory = new BusinessObjectNodeFactory(referenceService);

		// Create the business object node tree needed by the diagram updater to build the diagram using the specified tree
		final Map<BusinessObjectNode, TransientViewTreeNode> bonToTransientNodeMap = new HashMap<>();
		final BusinessObjectNode boTree = transientViewNodeToBusinessObjectNode(null, tree, nodeFactory,
				bonToTransientNodeMap);

		// Create the diagram and diagram updater
		final AgeDiagram diagram = new AgeDiagram();
		final BusinessObjectTreeUpdater boTreeUpdater = (configuration, oldTree) -> boTree;
		final DefaultDiagramElementGraphicalConfigurationProvider deInfoProvider = new DefaultDiagramElementGraphicalConfigurationProvider(
				queryService, () -> diagram, bohProvider);
		final DiagramUpdater diagramUpdater = new DiagramUpdater(boTreeUpdater, deInfoProvider, actionExecutor,
				(bo) -> null, referenceService);

		// Initialize the diagram.
		diagram.modify("Configure and Update Diagram", m -> {
			m.setDiagramConfiguration(new DiagramConfigurationBuilder(new TransientViewDiagramType(), true).build());
			diagramUpdater.updateDiagram(diagram);
		});

		// Build the mapping between transient tree nodes and diagram nodes
		this.transientViewNodeToDiagramNodeMap = buildTransientViewNodeToDiagramNodeMap(boTree, diagram,
				bonToTransientNodeMap, HashBiMap.create());

		// Set the style of the diagram elements to match the styles specified in tree
		diagram.modify("Set Style", m -> {
			for (final Entry<TransientViewTreeNode, DiagramNode> e : transientViewNodeToDiagramNodeMap.entrySet()) {
				if (e.getValue() instanceof DiagramElement) {
					m.setStyle((DiagramElement) e.getValue(), e.getKey().getStyle());
				}
			}
		});

		// Create the GEF Age Diagram. This will create the scene nodes and will layout the diagram when it is added
		// to a Java FX scene graph
		this.gefDiagram = new GefAgeDiagram(diagram, new DefaultColoringService(new StyleRefresher() {
			@Override
			public void refreshDiagramColoring() {
				// No-op. Handling coloring service refresh requests is not required.
			}

			@Override
			public void refreshColoring(final Collection<DiagramElement> diagramElements) {
				// No-op. Handling coloring service refresh requests is not required.
			}
		}));

		// Register a listener which will layout the diagram when it is attached to the scene.
		// This listener is unregistered after the first execution.
		final ChangeListener<Scene> sceneSetListener = new ChangeListener<Scene>() {
			@Override
			public void changed(final ObservableValue<? extends Scene> observable, final Scene oldValue,
					final Scene newValue) {
				if (newValue != null) {
					gefDiagram.updateDiagramFromSceneGraph(false);

					DiagramElementLayoutUtil.layout("Layout", diagram, gefDiagram, new LayoutOptionsBuilder().build());

					// Remove the listener
					gefDiagram.getSceneNode().sceneProperty().removeListener(this);
				}
			}
		};


		gefDiagram.getSceneNode().sceneProperty().addListener(sceneSetListener);
	}

	/**
	 * Builds a mapping between transient view nodes and diagram nodes by using the business object node tree that was created
	 * from the transient view nodes.
	 * @param bon the business object node
	 * @param dn the corresponding diagram node
	 * @param bonToTransientViewNodeMap a mapping between business object nodes and transient view tree nodes
	 * @param result the map being populated.
	 * @return the result
	 */
	private static BiMap<TransientViewTreeNode, DiagramNode> buildTransientViewNodeToDiagramNodeMap(
			final BusinessObjectNode bon, final DiagramNode dn,
			final Map<BusinessObjectNode, TransientViewTreeNode> bonToTransientViewNodeMap,
			final BiMap<TransientViewTreeNode, DiagramNode> result) {
		result.put(bonToTransientViewNodeMap.get(bon), dn);
		for (final BusinessObjectNode bonChild : bon.getChildren()) {
			final DiagramNode dnChild = dn.getChildByRelativeReference(bonChild.getRelativeReference());
			if (dnChild != null) {
				buildTransientViewNodeToDiagramNodeMap(bonChild, dnChild, bonToTransientViewNodeMap, result);
			}
		}
		return result;
	}

	/**
	 * Creates a {@link BusinessObjectNode} from a {@link TransientViewTreeNode}.
	 * @param parent the parent of the new {@link BusinessObjectNode}
	 * @param n the {@link TransientViewTreeNode} from which the {@link BusinessObjectNode} is being created
	 * @param nodeFactory the factory used to create the {@link BusinessObjectNode}
	 * @param bonToTransientNodeMap a map to populate with the mapping between nodes
	 * @return the new {@link BusinessObjectNode}
	 */
	private static BusinessObjectNode transientViewNodeToBusinessObjectNode(final BusinessObjectNode parent,
			final TransientViewTreeNode n,
			final BusinessObjectNodeFactory nodeFactory,
			final Map<BusinessObjectNode, TransientViewTreeNode> bonToTransientNodeMap) {
		final BusinessObjectNode newNode = nodeFactory.create(parent, UUID.randomUUID(), n.getBusinessObject(),
				Completeness.COMPLETE);
		bonToTransientNodeMap.put(newNode, n);
		for (final var child : n.getChildren()) {
			transientViewNodeToBusinessObjectNode(newNode, child, nodeFactory, bonToTransientNodeMap);
		}
		return newNode;
	}

	/**
	 * Returns the root JavaFX scene node for the view. This is the node that should be added to the scene graph.
	 * @return the root scene node for the view.
	 */
	public DiagramRootNode getRootSceneNode() {
		return gefDiagram.getSceneNode();
	}

	/**
	 * Returns the scene node for the transient view tree node which was used to create it.
	 * The returned scene node is a descendant of the root scene node for the view.
	 * @param treeNode the tree node for which to return the scene node.
	 * @return the scene node. Returns null if a scene node was not created for the specified tree node.
	 * @see #getRootSceneNode()
	 */
	public Node getSceneNode(final TransientViewTreeNode treeNode) {
		final DiagramNode dn = this.transientViewNodeToDiagramNodeMap.get(treeNode);
		return gefDiagram.getSceneNode(dn);
	}

	/**
	 * Returns the transient view tree node which was used to create the specified scene node.
	 * If the node does not have a tree node associated with it, the scene node's ancestors are checked
	 * until a tree node is found.
	 * @param sceneNode the scene node for which to return the tree node.
	 * @return the tree node which was used to created the specified node. Returns null if
	 * the transient tree node was not found.
	 */
	public TransientViewTreeNode getTransientViewTreeNode(final Node sceneNode) {
		// Walk up scene graph as necessary and find the transient view tree node
		for (Node tmp = sceneNode; tmp != null; tmp = tmp.getParent()) {
			final DiagramElement de = gefDiagram.getDiagramElement(tmp);
			final var tn = transientViewNodeToDiagramNodeMap.inverse().get(de);
			if (tn != null) {
				return tn;
			}
		}

		return null;
	}
}