SavedDiagramIndex.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.indexing;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.eclipse.core.resources.IContainer;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.emf.common.util.URI;
import org.eclipse.emf.ecore.resource.Resource;
import org.eclipse.emf.ecore.resource.ResourceSet;
import org.eclipse.emf.ecore.resource.impl.ResourceSetImpl;
import org.eclipse.emf.ecore.util.EcoreUtil;
import org.eclipse.emf.ecore.xmi.XMLResource;
import org.osate.ge.BusinessObjectContext;
import org.osate.ge.CanonicalBusinessObjectReference;
import org.osate.ge.RelativeBusinessObjectReference;
import org.osate.ge.aadl2.internal.diagramtypes.CustomDiagramType;
import org.osate.ge.diagram.DiagramElement;
import org.osate.ge.diagram.DiagramNode;
import org.osate.ge.internal.GraphicalEditorException;
import org.osate.ge.internal.diagram.runtime.DiagramSerialization;
import org.osate.ge.internal.services.ProjectReferenceService;
import org.osate.ge.internal.services.ReferenceService;
import org.osate.ge.internal.util.BusinessObjectProviderHelper;
import org.osate.ge.internal.util.DiagramUtil;
import org.osate.ge.internal.util.Log;

import com.google.common.collect.ImmutableMap;

/**
 * Indexes saved diagram files.
 * Responsible for finding serialized diagrams and diagram elements matching requested criteria.
 *
 */
public class SavedDiagramIndex {
	/**
	 * Interface for saved diagram index entries
	 *
	 */
	public static interface DiagramIndexEntry {
		/**
		 * Returns the diagram file
		 * @return the diagram file
		 */
		IFile getDiagramFile();

		/**
		 * Returns the diagram type ID
		 * @return the diagram type ID
		 */
		String getDiagramTypeId();

		/**
		 * Returns the reference for the diagram's context
		 * @return the reference for the diagram's context. Returns null for contextless diagrams.
		 */
		CanonicalBusinessObjectReference getContext();
	}

	/**
	 * Index entry for {@link DiagramElement} elements
	 *
	 */
	public static class ElementIndexEntry {
		/**
		 * The file containing the element
		 */
		public final IFile diagramFile;

		/**
		 * Reference to the diagram element's business object
		 */
		public final CanonicalBusinessObjectReference reference;

		/**
		 * URI for the {@link DiagramElement}
		 */
		public final URI diagramElementUri;

		private ElementIndexEntry(final IFile diagramFile, final CanonicalBusinessObjectReference reference,
				final URI diagramElementUri) {
			this.diagramFile = Objects.requireNonNull(diagramFile, "diagramFile must not be null");
			this.reference = Objects.requireNonNull(reference, "reference must not be null");
			this.diagramElementUri = Objects.requireNonNull(diagramElementUri, "diagramElementUri must not be null");
		}
	}

	/**
	 * Used to store the structure of diagram files without keeping the entire diagram in memory. Used to allow partial-lazy creation
	 * of the reference to element URI mapping for diagram files without having to reload the diagram.
	 */
	private static class ReferenceNode {
		private RelativeBusinessObjectReference relativeReference;
		private URI diagramNodeUri;
		private List<ReferenceNode> children;

		public ReferenceNode(final RelativeBusinessObjectReference relativeReference, final URI diagramNodeUri,
				final List<ReferenceNode> children) {
			this.relativeReference = relativeReference;
			this.diagramNodeUri = Objects.requireNonNull(diagramNodeUri, "diagramNodeUri must not be null");
			this.children = Objects.requireNonNull(children, "children must not be null");
		}

		public final RelativeBusinessObjectReference getRelativeReference() {
			return relativeReference;
		}

		public final List<ReferenceNode> getChildren() {
			return children == null ? Collections.emptyList() : children;
		}

		public final URI getDiagramNodeUri() {
			return diagramNodeUri;
		}
	}

	private class DiagramFileIndex implements DiagramIndexEntry {
		public final ProjectDiagramIndex projectDiagramIndex;
		public final IFile diagramFile;
		private boolean metadataLoaded = false;
		private String diagramTypeId;
		private CanonicalBusinessObjectReference context;
		private ReferenceNode rootReferenceNode;
		private Map<CanonicalBusinessObjectReference, URI> refToDiagramElementUriMap;

		public DiagramFileIndex(final ProjectDiagramIndex projectDiagramIndex, final IFile diagramFile) {
			this.projectDiagramIndex = Objects.requireNonNull(projectDiagramIndex, "projectDiagramIndex must not be null");
			this.diagramFile = Objects.requireNonNull(diagramFile, "diagramFile must not be null");
		}

		public final boolean isValid() {
			return getDiagramTypeId() != null;
		}

		@Override
		public IFile getDiagramFile() {
			return diagramFile;
		}

		/**
		 *
		 * @return may return null if diagram was invalid.
		 */
		@Override
		public String getDiagramTypeId() {
			ensureMetadataLoaded();
			return diagramTypeId;
		}

		@Override
		public CanonicalBusinessObjectReference getContext() {
			ensureMetadataLoaded();
			return context;
		}

		public Map<CanonicalBusinessObjectReference, URI> getReferenceToDiagramElementUriMap() {
			ensureRefToDiagramElementUriMapValid();
			return refToDiagramElementUriMap;
		}

		// Ensure valid. Populate fields as necessary
		private void ensureMetadataLoaded() {
			if(!metadataLoaded) {
				metadataLoaded = true;
				context = null;

				// Index the diagram file
				final ResourceSet rs = new ResourceSetImpl();
				final URI diagramUri = URI.createPlatformResourceURI(diagramFile.getFullPath().toString(), true);
				final Resource diagramResource = rs.createResource(diagramUri);
				try {
					diagramResource.load(Collections.singletonMap(XMLResource.OPTION_RECORD_UNKNOWN_FEATURE, true));
					if(diagramResource.getContents().size() == 1) {
						if(diagramResource.getContents().get(0) instanceof org.osate.ge.diagram.Diagram) {
							final org.osate.ge.diagram.Diagram mmDiagram = (org.osate.ge.diagram.Diagram)diagramResource.getContents().get(0);
							final org.osate.ge.diagram.DiagramConfiguration config = mmDiagram.getConfig();

							// Set the diagram type id
							diagramTypeId = config == null || config.getType() == null ? CustomDiagramType.ID
									: config.getType();

							// Get the context reference
							if(config != null) {
								context = DiagramSerialization.convert(config.getContext());
							}

							rootReferenceNode = createReferenceNode(mmDiagram, null);
						}
					}
				} catch (IOException e) {
					// Ignore.
				} finally {
					// Unload the resource
					if(diagramResource.isLoaded()) {
						diagramResource.unload();
					}
				}
			}
		}

		private ReferenceNode createReferenceNode(final DiagramElement element) {
			final RelativeBusinessObjectReference ref = DiagramSerialization.convert(element.getBo());
			if (ref == null) {
				return null;
			}

			return createReferenceNode(element, ref);
		}

		private ReferenceNode createReferenceNode(final DiagramNode node,
				final RelativeBusinessObjectReference relRef) {
			final URI diagramElementUri = EcoreUtil.getURI(node);
			if (diagramElementUri == null) {
				return null;
			}

			final List<ReferenceNode> childRefNodes = createChildReferenceNodes(node);

			return new ReferenceNode(relRef, diagramElementUri, childRefNodes);
		}

		private List<ReferenceNode> createChildReferenceNodes(final DiagramNode node) {
			if (node.getElement().size() > 0) {
				final List<ReferenceNode> childRefNodes = new ArrayList<>(node.getElement().size());
				for (final DiagramElement child : node.getElement()) {
					final ReferenceNode childRefNode = createReferenceNode(child);
					if (childRefNode != null) {
						childRefNodes.add(childRefNode);
					}
				}
				return Collections.unmodifiableList(childRefNodes);
			} else {
				return Collections.emptyList();
			}
		}

		private void ensureRefToDiagramElementUriMapValid() {
			if (refToDiagramElementUriMap == null) {
				ensureMetadataLoaded();

				refToDiagramElementUriMap = new HashMap<>();

				final ProjectReferenceService projectReferenceService = referenceService
						.getProjectReferenceService(projectDiagramIndex.project);

				// Get the business object for the context reference.
				Object contextBo = null;
				if (context != null) {
					contextBo = projectReferenceService.resolve(context);
				}

				if ((context == null || contextBo != null) && rootReferenceNode != null) {
					final BusinessObjectContext rootBoc;
					final Collection<Object> potentialChildBusinessObjects;
					if (context == null) {
						// Contextless diagrams can have multiple top level elements.
						rootBoc = new SimpleUnqueryableBusinessObjectContext(null, projectDiagramIndex.project);
						potentialChildBusinessObjects = bopHelper.getChildBusinessObjects(rootBoc);
					} else {
						rootBoc = new SimpleUnqueryableBusinessObjectContext(null, null);
						potentialChildBusinessObjects = Collections.singleton(contextBo);
					}

					indexChildElements(this, rootReferenceNode, rootBoc, potentialChildBusinessObjects, bopHelper,
							projectReferenceService);
				}
			}
		}

		private void indexChildElements(final DiagramFileIndex diagramFileIndex,
				final ReferenceNode referenceNode,
				final BusinessObjectContext parentBoc,
				final Collection<Object> potentialBusinessObjects,
				final BusinessObjectProviderHelper bopHelper,
				final ProjectReferenceService projectReferenceService) {

			// Build a mapping from relative business object references to business objects. Do not include null references.
			final Map<RelativeBusinessObjectReference, Object> relativeReferenceToPotentialBo = new HashMap<>();
			for (final Object bo : potentialBusinessObjects) {
				final RelativeBusinessObjectReference relRef = projectReferenceService.getRelativeReference(bo);
				if (relRef != null) {
					relativeReferenceToPotentialBo.put(relRef, bo);
				}
			}

			// Process children
			for (final ReferenceNode child : referenceNode.getChildren()) {
				final Object childBo = relativeReferenceToPotentialBo.get(child.getRelativeReference());
				if(childBo == null) {
					continue;
				}

				final CanonicalBusinessObjectReference childCanonicalRef = projectReferenceService.getCanonicalReference(childBo);
				if(childCanonicalRef == null) {
					continue;
				}

				// Store the element's URI in the map
				diagramFileIndex.refToDiagramElementUriMap.put(childCanonicalRef, child.getDiagramNodeUri());

				final BusinessObjectContext childBoc = new SimpleUnqueryableBusinessObjectContext(parentBoc, childBo);
				final Collection<Object> potentialChildBusinessObjects = bopHelper.getChildBusinessObjects(childBoc);
				indexChildElements(diagramFileIndex, child, childBoc, potentialChildBusinessObjects, bopHelper, projectReferenceService);
			}
		}
	}

	private class ProjectDiagramIndex {
		public final IProject project;
		public ImmutableMap<IFile, DiagramFileIndex> fileToIndexMap;

		public ProjectDiagramIndex(final IProject project) {
			this.project = Objects.requireNonNull(project, "project must not be null");
		}

		public ImmutableMap<IFile, DiagramFileIndex> getOrCreateFileToIndexMap() {
			if(fileToIndexMap == null) {
				final ImmutableMap.Builder<IFile, DiagramFileIndex> builder = ImmutableMap.builder();

				// Create diagram references as appropriate
				for (final IFile diagramFile : findDiagramFiles(project, null)) {
					builder.put(diagramFile, new DiagramFileIndex(this, diagramFile));
				}

				fileToIndexMap = builder.build();
			}

			return fileToIndexMap;
		}

		/**
		 * Finds all files with the diagram extension in the specified container and its children
		 * @param container the container in which to look for diagrams
		 * @param diagramFiles a list to which to add the results. Optional.
		 * @return diagramFiles if specified otherwise, a newly created list containing the results
		 */
		private List<IFile> findDiagramFiles(final IContainer container, List<IFile> diagramFiles) {
			if(diagramFiles == null) {
				diagramFiles = new ArrayList<IFile>();
			}

			try {
				if(container.isAccessible()) {
					for(final IResource resource : container.members()) {
						if (resource instanceof IContainer) {
							findDiagramFiles((IContainer)resource, diagramFiles);
						} else if (resource instanceof IFile) {
							final IFile file = (IFile) resource;
							if (DiagramUtil.isDiagram(file)) {
								diagramFiles.add(file);
							}
						}
					}
				}
			} catch (CoreException e) {
				Log.error("Error finding diagrams", e);
				throw new GraphicalEditorException(e);
			}

			return diagramFiles;
		}
	}

	private final Map<IProject, ProjectDiagramIndex> projectToIndexMap = new HashMap<>();
	private final ReferenceService referenceService;
	private final BusinessObjectProviderHelper bopHelper;

	/**
	 * Creates a new instance
	 * @param referenceService the reference service
	 * @param bopHelper the business object provider helper
	 */
	public SavedDiagramIndex(final ReferenceService referenceService,
			BusinessObjectProviderHelper bopHelper) {
		this.referenceService = Objects.requireNonNull(referenceService, "referenceService must not be null");
		this.bopHelper = Objects.requireNonNull(bopHelper, "bopHelper must not be null");
	}

	/**
	 * Returns the diagrams in the specified projects
	 * @param projects the projects for which to return diagrams
	 * @return the diagrams in the specified projects
	 */
	public synchronized List<DiagramIndexEntry> getDiagramsByProject(final Stream<IProject> projects) {
		Objects.requireNonNull(projects, "projects must not be null");

		return projects.flatMap(p -> getOrCreateProjectIndex(p).getOrCreateFileToIndexMap().entrySet().stream())
				.map(e -> e.getValue()).collect(Collectors.toList());
	}

	/**
	 * Returns the diagrams in the specified projects and which have the specified context business object
	 * @param projects the projects for which to return diagrams
	 * @param context the reference to the context business object for which to return diagrams
	 * @return the diagrams in the specified projects and which have the specified context business object
	 */
	public synchronized List<DiagramIndexEntry> getDiagramsByContext(final Stream<IProject> projects,
			final CanonicalBusinessObjectReference context) {
		return getDiagramsByContexts(projects, Collections.singleton(context));
	}

	/**
	 * Returns the diagrams in the specified projects and which have one of the specified contexts.
	 * @param projects the projects for which to return diagrams
	 * @param contexts the reference to the context business objects for which to return diagrams
	 * @return the diagrams in the specified projects and which have the specified context business object
	 */
	public synchronized List<DiagramIndexEntry> getDiagramsByContexts(final Stream<IProject> projects,
			final Set<CanonicalBusinessObjectReference> contexts) {
		Objects.requireNonNull(projects, "projects must not be null");
		Objects.requireNonNull(contexts, "contexts must not be null");

		return projects.flatMap(p -> getOrCreateProjectIndex(p).getOrCreateFileToIndexMap().entrySet().stream()).
				filter(e -> contexts.contains(e.getValue().getContext())).filter(e -> e.getValue().isValid())
				.map(e -> e.getValue()).collect(Collectors.toList());
	}

	/**
	 * Returns the index entries for serialized diagram elements with a business object matching the specified business objects
	 * @param projects the projects for which to return serialized diagram elements
	 * @param refs the references of business objects for which to return diagram elements
	 * @return the serialized diagram element index entries
	 */
	public synchronized List<ElementIndexEntry> getDiagramElementUrisByReferences(final Stream<IProject> projects,
			final Set<CanonicalBusinessObjectReference> refs) {
		Objects.requireNonNull(projects, "projects must not be null");
		Objects.requireNonNull(refs, "refs must not be null");
		return projects.flatMap(p -> getOrCreateProjectIndex(p).getOrCreateFileToIndexMap().
				entrySet().
				stream().
				flatMap(i -> i.getValue()
						.getReferenceToDiagramElementUriMap()
						.
						entrySet().
						stream().
						filter(e -> refs.contains(e.getKey())).
						map(e -> new ElementIndexEntry(i.getKey(), e.getKey(), e.getValue()))
						)
				).collect(Collectors.toList());
	}

	/**
	 * Removes index entries for the specified project
	 * @param project the project for which to remove index entries
	 */
	public synchronized void remove(final IProject project) {
		projectToIndexMap.remove(project);
	}

	/**
	 * Removes index entries for the specified diagram file
	 * @param diagramFile the diagram file for which to remove index entries
	 */
	public synchronized void remove(final IFile diagramFile) {
		final ProjectDiagramIndex projectDiagramIndex = projectToIndexMap.get(diagramFile.getProject());
		if (projectDiagramIndex != null && projectDiagramIndex.fileToIndexMap != null) {
			// Build a new immutable map without the file
			final ImmutableMap.Builder<IFile, DiagramFileIndex> builder = ImmutableMap.builder();
			projectDiagramIndex.fileToIndexMap.entrySet().stream().filter(e -> !Objects.equals(e.getKey(), diagramFile))
			.forEachOrdered(e -> {
				builder.put(e);
			});

			// Add the file to the builder if it still exists
			if (diagramFile.exists()) {
				builder.put(diagramFile, new DiagramFileIndex(projectDiagramIndex, diagramFile));
			}

			projectDiagramIndex.fileToIndexMap = builder.build();
		}
	}

	private ProjectDiagramIndex getOrCreateProjectIndex(final IProject project) {
		Objects.requireNonNull(project, "project must not be null");

		ProjectDiagramIndex projectDiagramIndex = projectToIndexMap.get(project);
		if(projectDiagramIndex == null) {
			projectDiagramIndex = new ProjectDiagramIndex(project);
			projectToIndexMap.put(project, projectDiagramIndex);
		}

		return projectDiagramIndex;
	}

	private static class SimpleUnqueryableBusinessObjectContext implements BusinessObjectContext {
		private final BusinessObjectContext parent;
		private final Object bo;

		public SimpleUnqueryableBusinessObjectContext(final BusinessObjectContext parent,
				final Object bo) {
			this.parent = parent;
			this.bo = bo;
		}

		@Override
		public Collection<? extends BusinessObjectContext> getChildren() {
			// This should not be called since business object providers are not given access to the business object context's children
			throw new GraphicalEditorException("Not supported");
		}

		@Override
		public BusinessObjectContext getParent() {
			return parent;
		}

		@Override
		public Object getBusinessObject() {
			return bo;
		}
	}
}