ToolUtil.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.ui.tools;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.TreeSet;
import java.util.function.Function;
import java.util.stream.Collector;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.eclipse.core.resources.IProject;
import org.eclipse.emf.common.util.BasicDiagnostic;
import org.eclipse.emf.common.util.Diagnostic;
import org.eclipse.emf.common.util.URI;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.resource.ResourceSet;
import org.eclipse.emf.ecore.util.Diagnostician;
import org.eclipse.emf.ecore.util.EcoreUtil;
import org.eclipse.xtext.resource.XtextResource;
import org.eclipse.xtext.validation.FeatureBasedDiagnostic;
import org.osate.aadl2.AadlPackage;
import org.osate.aadl2.ComponentImplementation;
import org.osate.aadl2.Context;
import org.osate.aadl2.Element;
import org.osate.aadl2.NamedElement;
import org.osate.ge.BusinessObjectContext;
import org.osate.ge.ProjectUtil;
import org.osate.ge.aadl2.ui.AadlModelAccessUtil;

import com.google.common.base.Predicates;

/**
 * Contains utility functions for working with {@link Tool} instances
 *
 */
public final class ToolUtil {
	/**
	 * Private constructor to prevent instantiation.
	 */
	private ToolUtil() {
	}

	/**
	 * Looks at the specified {@link BusinessObjectContext} and ancestors and returns the first one which is associated with
	 * a {@link ComponentImplementation}
	 * @param boc the business object ocntext at which to begin the search.
	 * @return the component implementation. Returns null if any ancestor is not associated with a NamedElement.
	 */
	public static BusinessObjectContext findComponentImplementationBoc(final BusinessObjectContext boc) {
		BusinessObjectContext tmp = boc;
		while (tmp != null) {
			if (tmp.getBusinessObject() instanceof ComponentImplementation) {
				return tmp;
			} else if (!(tmp.getBusinessObject() instanceof NamedElement)) {
				return null;
			}

			tmp = tmp.getParent();
		}

		return null;
	}

	private static BusinessObjectContext findContextAncestorBoc(final BusinessObjectContext boc) {
		BusinessObjectContext tmp = boc.getParent();
		while (tmp != null) {
			if (tmp.getBusinessObject() instanceof Context) {
				return tmp;
			}

			tmp = tmp.getParent();
		}

		return null;
	}

	/**
	 * Finds a {@link Context} by starting with the parent of the specified {@link BusinessObjectContext} and walking up until one is found.
	 * @param boc the business object context whose parent will be the first {@link BusinessObjectContext} to check
	 * @return the {@link Context} business object. Returns null if one could not be found.
	 */
	public static Context findContext(final BusinessObjectContext boc) {
		final BusinessObjectContext contextBoc = findContextAncestorBoc(boc);
		return contextBoc == null ? null : (Context) contextBoc.getBusinessObject();
	}

	/**
	 * Finds a context. If the context {@link BusinessObjectContext} is the specified owner {@link BusinessObjectContext}, null is returned.
	 * It is expected that the owner's business object will be a valid context and is a container
	 * of the specified BOC. In such cases the function will return a context only if it is inside the owner business object context.
	 * @param boc the object to use to find the {@link Context}
	 * @param ownerBoc the business context from which not return the context
	 * @return the {@link Context} business object. Returns null if one could not be found or if the business object context was the specified
	 * owner business object context
	 */
	public static Context findContextExcludeOwner(final BusinessObjectContext boc,
			final BusinessObjectContext ownerBoc) {
		final BusinessObjectContext contextBoc = findContextAncestorBoc(boc);
		return contextBoc == null || contextBoc == ownerBoc ? null : (Context) contextBoc.getBusinessObject();
	}

	/**
	 * Get the errors and warnings for the AADL packages whose contents are in the same tree as the specified business object context
	 * @param boc the business object context whose tree will be used.
	 * @return the errors and warnings.
	 */
	public static Set<Diagnostic> getAllReferencedPackageDiagnostics(final BusinessObjectContext boc) {
		// Find root to get all referenced package diagnostics
		final BusinessObjectContext root = findRoot(boc).orElseThrow(() -> new RuntimeException("Cannot find root"));

		// Get referenced packages of root boc
		final Set<AadlPackage> packages = getReferencedPackages(root);

		// Collect errors and warnings for referenced AADL packages
		final List<DiagnosticBuilder> diagnosticBuilders = new ArrayList<>();
		for (final AadlPackage pkg : packages) {
			ProjectUtil.getProjectForBo(pkg).ifPresent(project -> {
				final ResourceSet resourceSet = AadlModelAccessUtil.getLiveResourceSet(project);
				final DiagnosticBuilder diagnosticBuilder = new DiagnosticBuilder(pkg);
				// Model error and warning diagnostics
				populateDiagnostics(diagnosticBuilder, pkg, resourceSet);

				diagnosticBuilders.add(diagnosticBuilder);
			});
		}

		return diagnosticBuilders.stream().flatMap(DiagnosticBuilder::getDiagnostics).collect(toDiagnosticTreeSet());
	}

	/**
	 * Search a boc's descendants for elements and collect each element's root element that are an AadlPackage.
	 * @param rootBoc the boc that contains descendants to iterate
	 * @return a set of AadlPackages that are referenced within a root boc
	 */
	private static Set<AadlPackage> getReferencedPackages(final BusinessObjectContext rootBoc) {
		return rootBoc.getAllDescendants().map(queryable -> {
			final Object bo = queryable.getBusinessObject();
			if (bo instanceof Element) {
				final Element element = (Element) bo;
				final NamedElement root = element.getElementRoot();
				return root instanceof AadlPackage ? (AadlPackage) root : null;
			}

			return null;
		}).filter(Predicates.notNull()).collect(Collectors.toCollection(HashSet::new));
	}

	// Find the editor root boc
	private static Optional<BusinessObjectContext> findRoot(final BusinessObjectContext boc) {
		BusinessObjectContext tmp = boc;
		while (tmp.getParent() != null) {
			tmp = tmp.getParent();
		}

		return Optional.ofNullable(tmp);
	}

	/**
	 * Modifies an eObject and returns error and warning diagnostics
	 * @param elementToModify is the element to modify
	 * @param getModifiedObject performs the modification and returns modified eObject
	 * @return a set of error and warning diagnostics found during validation
	 */
	public static Set<Diagnostic> getModificationDiagnostics(final Element elementToModify,
			final Function<ResourceSet, EObject> getModifiedObject) {
		final IProject project = ProjectUtil.getProjectForBoOrThrow(elementToModify);
		final ResourceSet resourceSet = AadlModelAccessUtil.getLiveResourceSet(project);
		// Make modification
		final EObject modifiedObject = getModifiedObject.apply(resourceSet);

		final List<DiagnosticBuilder> diagnosticBuilders = new ArrayList<>();
		final NamedElement root = elementToModify.getElementRoot();
		if (root instanceof AadlPackage) {
			final DiagnosticBuilder diagnosticBuilder = new DiagnosticBuilder((AadlPackage) root);
			// Model error and warning diagnostics
			populateDiagnostics(diagnosticBuilder, modifiedObject, resourceSet);
			diagnosticBuilders.add(diagnosticBuilder);
		}

		return diagnosticBuilders.stream().flatMap(DiagnosticBuilder::getDiagnostics).collect(toDiagnosticTreeSet());
	}

	/**
	 * Converts a stream to a tree set of diagnostics sorted by severity and message.
	 * Diagnostics that have a severity of error Diagnostic.ERROR
	 * are grouped to the start of the tree set in alphabetical order.  Diagnostics that
	 * have another severity are grouped to the end of the tree set in alphabetical order.
	 * @return returns a tree set of diagnostics that are sorted by severity and message
	 */
	private static Collector<Diagnostic, ?, TreeSet<Diagnostic>> toDiagnosticTreeSet() {
		return Collectors.toCollection(() -> new TreeSet<Diagnostic>((d1, d2) -> {
			// Convert severity into a sorting value based on whether it is an error.
			final int severity1 = d1.getSeverity() == Diagnostic.ERROR ? 0 : 1;
			final int severity2 = d2.getSeverity() == Diagnostic.ERROR ? 0 : 1;
			int result = Integer.compare(severity1, severity2);

			// Compare messages for equal severity
			if (result == 0) {
				result = d1.getMessage().compareToIgnoreCase(d2.getMessage());
			}

			return result;
		}));
	}

	// Get error and warning diagnostics
	private static void populateDiagnostics(final DiagnosticBuilder diagnosticsBuilder, final EObject eObjectToValidate,
			final ResourceSet resourceSet) {
		// Serialize
		final Optional<String> serializedSrc = getSerializedSource(eObjectToValidate);
		if (!serializedSrc.isPresent()) {
			diagnosticsBuilder.addDiagnostic(Diagnostic.ERROR, "Serialization Error");
			return;
		}

		final XtextResource resource = getOrCreateXtextResource(resourceSet, eObjectToValidate.eResource().getURI());
		loadResource(resource, serializedSrc.get());

		// Concrete Syntax Validation
		resource.validateConcreteSyntax()
		.stream()
		.filter(diagnostic -> isErrorOrWarning(diagnostic))
		.forEach(diagnostic -> diagnosticsBuilder.addDiagnostic(diagnostic));

		final EObject serializedObject = resourceSet.getEObject(EcoreUtil.getURI(eObjectToValidate), true);
		final Diagnostic validationDiagnostic = Diagnostician.INSTANCE.validate(serializedObject,
				Collections.singletonMap(Diagnostician.VALIDATE_RECURSIVELY, true));
		if (isErrorOrWarning(validationDiagnostic)) {
			// Validation Diagnostics
			getDiagnosticDescendants(validationDiagnostic)
			.filter(diagnostic -> diagnostic instanceof FeatureBasedDiagnostic)
			.forEach(diagnostic -> diagnosticsBuilder.addDiagnostic(diagnostic));
		}

		// Errors
		getResourceDiagnostics(resource.getErrors(), diagnosticsBuilder, Diagnostic.ERROR);

		// Warnings
		getResourceDiagnostics(resource.getWarnings(), diagnosticsBuilder, Diagnostic.WARNING);
	}

	/**
	 * Diagnostic builder to hold the diagnostics that contain an error or warning
	 * severity and a message created for an AadlPackage
	 *
	 * Possible severity values:
	 * 0x2 = Diagnostic.WARNING
	 * 0x4 = Diagnostic.ERROR
	 */
	private static class DiagnosticBuilder {
		private final Stream.Builder<Diagnostic> diagnostics = Stream.builder();
		private final String msgPrefix;

		public DiagnosticBuilder(final AadlPackage pkg) {
			this.msgPrefix = getMessagePrefix(pkg.getName());
		}

		private static String getMessagePrefix(final String pkgName) {
			return new StringBuilder("Package ").append(pkgName).append(": ").toString();
		}

		public Stream<Diagnostic> getDiagnostics() {
			return diagnostics.build();
		}

		public void addDiagnostic(final Diagnostic diagnostic) {
			addDiagnostic(diagnostic.getSeverity(), diagnostic.getMessage());
		}

		public void addDiagnostic(final int severity,
				final org.eclipse.emf.ecore.resource.Resource.Diagnostic diagnostic) {
			addDiagnostic(severity, diagnostic.getMessage());
		}

		// Only support error and warning severity levels
		public void addDiagnostic(final int severity, final String msg) {
			if (severity != Diagnostic.ERROR && severity != Diagnostic.WARNING) {
				throw new RuntimeException("Severity must be an error or warning");
			}
			diagnostics.add(new BasicDiagnostic(severity, "", 0, getDetailedDiagnosticMessage(msg), null));
		}

		private String getDetailedDiagnosticMessage(final String message) {
			return new StringBuilder(this.msgPrefix).append(message).toString();
		}
	}

	/**
	 * Diagnostics contain a severity level that can be Diagnostic.OK(0x0), Diagnostic.INFO(0x1),
	 * Diagnostic.WARNING(0x2), Diagnostic.ERROR(0x4), or Diagnostic.CANCEL(0x8).
	 * This returns true if the diagnostic's severity level is Diagnstic.ERROR or Diagnostic.WARNING
	 * @param diagnostic the diagnostic that contains the severity to check
	 * @return true if the diagnostic's severity is an error or warning
	 */
	private static boolean isErrorOrWarning(final Diagnostic diagnostic) {
		final int severity = diagnostic.getSeverity();
		return severity == Diagnostic.ERROR || severity == Diagnostic.WARNING;
	}

	private static void getResourceDiagnostics(
			final List<org.eclipse.emf.ecore.resource.Resource.Diagnostic> diagnostics,
			final DiagnosticBuilder diagnosticBuilder, final int severity) {
		diagnostics.stream().forEach(diagnostic -> diagnosticBuilder.addDiagnostic(severity, diagnostic));
	}

	/**
	 * Recursively collects a diagnostic and its descendants
	 * @param diagnostic the diagnostic to get descendants from
	 * @return a stream of diagnostics that contains the diagnostic its descendants
	 */
	private static Stream<Diagnostic> getDiagnosticDescendants(final Diagnostic diagnostic) {
		if (diagnostic.getChildren().isEmpty()) {
			return Stream.of(diagnostic);
		}

		return Stream.concat(Stream.of(diagnostic),
				diagnostic.getChildren()
				.stream()
				.flatMap(childDiagnostic -> getDiagnosticDescendants(childDiagnostic)));
	}

	private static Optional<String> getSerializedSource(final EObject modifiedObject) {
		String serializedSrc = null;
		try {
			serializedSrc = ((XtextResource) modifiedObject.eResource()).getSerializer()
					.serialize(modifiedObject.eResource().getContents().get(0));
		} catch (final RuntimeException e) {
			// Error serializing modified object
		}

		return Optional.ofNullable(serializedSrc);
	}

	private static void loadResource(final XtextResource resource, final String src) {
		try {
			resource.unload();
			resource.load(new ByteArrayInputStream(src.getBytes()), null);
		} catch (final IOException e) {
			throw new RuntimeException("Serialized source cannot be loaded");
		}
	}

	private static XtextResource getOrCreateXtextResource(final ResourceSet resourceSet, final URI uri) {
		final XtextResource resource = (XtextResource) resourceSet.getResource(uri, true);
		return resource != null ? resource : (XtextResource) resourceSet.createResource(uri);
	}

}