ErrorModelGeUtil.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.errormodel.util;

import java.util.List;
import java.util.Optional;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Stream;

import org.eclipse.emf.common.util.URI;
import org.eclipse.emf.ecore.util.EcoreUtil;
import org.osate.aadl2.AadlPackage;
import org.osate.aadl2.Classifier;
import org.osate.aadl2.ComponentClassifier;
import org.osate.aadl2.ComponentImplementation;
import org.osate.aadl2.ComponentType;
import org.osate.aadl2.NamedElement;
import org.osate.aadl2.Subcomponent;
import org.osate.ge.BusinessObjectContext;
import org.osate.ge.aadl2.AadlGraphicalEditorException;
import org.osate.ge.aadl2.GraphicalAnnexUtil;
import org.osate.ge.graphics.ArrowBuilder;
import org.osate.ge.graphics.ConnectionBuilder;
import org.osate.ge.graphics.Graphic;
import org.osate.ge.graphics.Style;
import org.osate.ge.graphics.StyleBuilder;
import org.osate.ge.operations.Operation;
import org.osate.ge.operations.StepResult;
import org.osate.xtext.aadl2.errormodel.errorModel.ErrorModelLibrary;
import org.osate.xtext.aadl2.errormodel.errorModel.ErrorModelPackage;
import org.osate.xtext.aadl2.errormodel.errorModel.ErrorModelSubclause;
import org.osate.xtext.aadl2.errormodel.util.EMV2Util;

/**
 * Utility class containing miscellaneous functions for working with the error model annex
 *
 */
public final class ErrorModelGeUtil {
	/**
	 * Private constructor to prevent instantiation.
	 */
	private ErrorModelGeUtil() {
	}

	private static ErrorModelLibrary getOrCreateErrorModelLibrary(final AadlPackage pkg) {
		return GraphicalAnnexUtil.getOrCreateParsedAnnexLibrary(pkg, EMV2Util.ErrorModelAnnexName,
				ErrorModelPackage.eINSTANCE.getErrorModelLibrary(), ErrorModelLibrary.class);
	}

	/**
	 * Returns the first parsed annex library for the specified package
	 * @param pkg the package for which to return the error model library
	 * @return the error model library
	 * @see GraphicalAnnexUtil#getFirstParsedAnnexLibrary
	 */
	public static Optional<ErrorModelLibrary> getErrorModelLibrary(final AadlPackage pkg) {
		return GraphicalAnnexUtil.getFirstParsedAnnexLibrary(pkg, EMV2Util.ErrorModelAnnexName,
				ErrorModelLibrary.class);
	}

	/**
	 * Gets the error model subclause for the specified classifier. If an error model subclause
	 * does not exist, it is created.
	 * @param classifier the classifier for which to get or create the error model subclause
	 * @return the error model subclause
	 * @see GraphicalAnnexUtil#getOrCreateParsedAnnexSubclause
	 */
	public static ErrorModelSubclause getOrCreateErrorModelSubclause(final Classifier classifier) {
		return GraphicalAnnexUtil.getOrCreateParsedAnnexSubclause(classifier, EMV2Util.ErrorModelAnnexName,
				ErrorModelPackage.eINSTANCE.getErrorModelSubclause(), ErrorModelSubclause.class);
	}

	/**
	 * Returns the first error model subclause for the specified classifier
	 * @param classifier the classifier for which to return the subclause
	 * @return the error model subclause
	 * @see GraphicalAnnexUtil#getFirstParsedAnnexSubclause
	 */
	public static Optional<ErrorModelSubclause> getFirstErrorModelSubclause(final Classifier classifier) {
		return GraphicalAnnexUtil.getFirstParsedAnnexSubclause(classifier, EMV2Util.ErrorModelAnnexName,
				ErrorModelSubclause.class);
	}

	/**
	 * Returns all the error model subclauses owned by the specified classifier. This method does not
	 * return subclauses contained in extended or implemented classifiers
	 * @param classifier the classifier for which to return the subclauses
	 * @return the error model subclauses owned by the classifier
	 */
	public static Stream<ErrorModelSubclause> getAllErrorModelSubclauses(final Classifier classifier) {
		return GraphicalAnnexUtil.getAllParsedAnnexSubclauses(classifier, EMV2Util.ErrorModelAnnexName,
				ErrorModelSubclause.class);
	}

	/**
	 * Returns a stream containing the error model subclauses contained in the classifier. This method includes
	 * subclauses contained in extended and implemented classifiers
	 * @param classifier the classifier for which to return the subclauses
	 * @return the error model subclauses
	 */
	public static Stream<ErrorModelSubclause> getAllInheritedErrorModelSubclauses(final Classifier classifier) {
		if (classifier == null) {
			return Stream.empty();
		}

		Stream<Classifier> classifiers = classifier.getSelfPlusAllExtended().stream();
		if (classifier instanceof ComponentImplementation) {
			final ComponentType ct = ((ComponentImplementation) classifier).getType();
			if (ct != null) {
				classifiers = Stream.concat(classifiers, ct.getSelfPlusAllExtended().stream());
			}
		}
		return classifiers.flatMap(tmpClassifier -> getAllErrorModelSubclauses(tmpClassifier));
	}

	/**
	 * Style which places labels at the top and horizontal center.
	 */
	public static final Style TOP_CENTERED_LABEL_STYLE = StyleBuilder.create()
			.labelsTop()
			.labelsHorizontalCenter()
			.build();

	/**
	 * Style which places labels at the horizontal and vertical center
	 */
	public static final Style CENTERED_STYLE = StyleBuilder.create().labelsCenter().build();

	/**
	 * Graphic used by transition connections
	 */
	public static final Graphic TRANSITION_CONNECTION_GRAPHIC = ConnectionBuilder.create()
			.destinationTerminator(ArrowBuilder.create().small().filled().build())
			.build();

	/**
	 * Creates an operation which allows prompting the user and then modifies the error model library of a package.
	 * @param <P> is a type of result that is returned by the prompter function.
	 * @param packageBoc is the business object context of the package to be modified.
	 * @param prompter is a function that will return results of prompting the user. If it returns an empty optional,
	 * 	the modification portion of the operation will not be executed.
	 * @param modifier a function that is executed to perform the model operation.
	 * @return an optional describing the operation. If the business object context does not have an {@link AadlPackage}
	 * as a business object, an empty optional will be returned.
	 */
	public static <P> Optional<Operation> createErrorModelLibraryPromptAndModifyOperation(
			final BusinessObjectContext packageBoc, final Supplier<Optional<P>> prompter,
			BiFunction<ErrorModelLibrary, P, StepResult<?>> modifier) {
		return packageBoc.getBusinessObject(AadlPackage.class)
				.map(readonlyPkg -> Operation.createWithBuilder(b -> b
						.supply(() -> prompter.get().map(v -> StepResult.forValue(v)).orElseGet(StepResult::abort))
						.modifyModel(null, (tag, promptResult) -> {
							// Select the object to modify. If an error model library exists, modify it. Otherwise, modify the package.
							final Optional<ErrorModelLibrary> readonlyLibrary = getErrorModelLibrary(readonlyPkg);
							if (readonlyLibrary.isPresent()) {
								return readonlyLibrary.get();
							} else {
								return readonlyPkg;
							}
						}, (tag, boToModify, promptResult) -> {
							// Get or create the error model library as needed
							final ErrorModelLibrary lib;
							if (boToModify instanceof AadlPackage) {
								lib = getOrCreateErrorModelLibrary((AadlPackage) boToModify);
							} else {
								lib = (ErrorModelLibrary) boToModify;
							}

							// Perform the modification
							return modifier.apply(lib, promptResult);
						})));
	}

	/**
	 * Creates an operation to modify the error model library of a package. If the specified {@link BusinessObjectContext} doesn't contain a
	 * {@link AadlPackage} business object, an empty optional is returned. If the error model library doesn't exist, it is created.
	 * @param packageBoc the business object being modified. Must be the business object context for a package.
	 * @param modifier is the function that will modify the error model library
	 * @return an optional containing the operation.
	 */
	public static Optional<Operation> createErrorModelLibraryModifyOperation(final BusinessObjectContext packageBoc,
			final Function<ErrorModelLibrary, StepResult<?>> modifier) {
		return createErrorModelLibraryPromptAndModifyOperation(packageBoc, () -> Optional.of(true),
				(lib, promptResult) -> modifier.apply(lib));
	}

	/**
	 * Creates an operation which allows prompt the user and then modifies the first error model subclause of a classifier or subcomponent.
	 * If the specified {@link BusinessObjectContext} doesn't contain an appropriate business object, an empty optional is returned.
	 * If a error model subclause doesn't exist in the classifier, it is created.
	 * @param <P> the result of prompting the user
	 * @param boc the business object being modified. Must be the business object context for a classifier or subcomponent.
	 * @param prompter is a function that will return results of prompting the user. If it returns an empty optional,
	 * 	the modification portion of the operation will not be executed.
	 * @param modifier a function that is executed to perform the model operation.
	 * @return an optional describing the operation. If the business object context does not have a classifier or subcomponent
	 * as a business object, an empty optional will be returned.
	 */
	public static <P> Optional<Operation> createErrorModelSubclausePromptAndModifyOperation(
			final BusinessObjectContext boc, final Supplier<Optional<P>> prompter,
			BiFunction<ErrorModelSubclause, P, StepResult<?>> modifier) {
		final Object readonlyBo = boc.getBusinessObject();
		final Classifier readonlyClassifier;
		if (readonlyBo instanceof Classifier) {
			readonlyClassifier = (Classifier) readonlyBo;
		} else if (readonlyBo instanceof Subcomponent) {
			readonlyClassifier = ((Subcomponent) readonlyBo).getAllClassifier();
		} else {
			readonlyClassifier = null;
		}

		if (readonlyClassifier == null) {
			return Optional.empty();
		}

		return Optional.of(Operation.createWithBuilder(
				b -> b.supply(() -> prompter.get().map(v -> StepResult.forValue(v)).orElseGet(StepResult::abort))
						.modifyModel(null, (tag, promptResult) -> {
							// Select the object to modify. If an error model subclause exists, modify it. Otherwise, modify the classifier.
							final Optional<ErrorModelSubclause> readonlySubclause = getFirstErrorModelSubclause(
									readonlyClassifier);
							if (readonlySubclause.isPresent()) {
								return readonlySubclause
										.filter(subclause -> subclause.getContainingClassifier() != null)
										.orElse(null);
							} else {
								return readonlyClassifier;
							}
						}, (tag, boToModify, promptResult) -> {
							// Get or create the error model subclause as needed
							final ErrorModelSubclause subclause;
							if (boToModify instanceof Classifier) {
								subclause = getOrCreateErrorModelSubclause((Classifier) boToModify);
							} else {
								subclause = (ErrorModelSubclause) boToModify;
							}

							// Perform the modification
							return modifier.apply(subclause, promptResult);
						})));
	}

	/**
	 * Creates an operation to modify the first error model subclause of a classifier or subcomponent.
	 * If the specified {@link BusinessObjectContext} doesn't contain an appropriate business object, an empty optional is returned.
	 * If a error model subclause doesn't exist in the classifier, it is created.
	 * @param boc the business object being modified. Must be the business object context for a classifier or subcomponent.
	 * @param modifier is the function that will modify the error model subclause
	 * @return an optional containing the operation.
	 */
	public static Optional<Operation> createErrorModelSubclauseModifyOperation(final BusinessObjectContext boc,
			final Function<ErrorModelSubclause, StepResult<?>> modifier) {
		return createErrorModelSubclausePromptAndModifyOperation(boc, () -> Optional.of(true),
				(subclause, promptResult) -> modifier.apply(subclause));
	}

	/**
	 * Walks up the business object context tree and returns the classifier for the first classifier or subcomponent.
	 * @param boc the first business object context to check
	 * @return the classifier
	 */
	public static Optional<Classifier> getClassifier(BusinessObjectContext boc) {
		return getClassifierSourceBoc(boc).flatMap(t -> {
			final Object bo = t.getBusinessObject();
			return getClassifierFromBusinessObject(bo);
		});
	}

	/**
	 * Returns the classifier associated with the specified business object. In order to return a classifier, the business object must be a
	 * subcomponent or a classifier
	 * @param bo the business object for which to retrieve the classifier.
	 * @return the classifier
	 */
	public static Optional<Classifier> getClassifierFromBusinessObject(Object bo) {
		if (bo instanceof Classifier) {
			return Optional.of((Classifier) bo);
		} else if (bo instanceof Subcomponent) {
			final ComponentClassifier cc = ((Subcomponent) bo).getAllClassifier();
			return Optional.ofNullable(cc);
		} else {
			return Optional.empty();
		}
	}

	/**
	 * Walks up the business object context tree and returns the first business object context which references a classifier
	 * or subcomponent.
	 * @param boc the first business object context to check
	 * @return the classifier
	 */
	public static Optional<BusinessObjectContext> getClassifierSourceBoc(BusinessObjectContext boc) {
		while (boc != null) {
			final Object bo = boc.getBusinessObject();
			if (bo instanceof Classifier || bo instanceof Subcomponent) {
				return Optional.of(boc);
			}
			boc = boc.getParent();
		}

		return Optional.empty();
	}

	/**
	 * Creates a list containing the URIs of named elements between the specified root(exclusive) the specified boc(inclusive).
	 * @param boc the business object context to which to return the path.
	 * @param root the root of the path.
	 * @param result the list to populate. Must not be null.
	 * @return the result.
	 */
	public static List<URI> createQualifiedPropagationPointPath(final BusinessObjectContext boc,
			final BusinessObjectContext root, List<URI> result) {
		if (boc.getParent() != root) {
			result = createQualifiedPropagationPointPath(boc.getParent(), root, result);
		}

		if (result != null) {
			final Object targetBo = boc.getBusinessObject();
			if (targetBo instanceof NamedElement) {
				result.add(EcoreUtil.getURI((NamedElement) targetBo));
			} else {
				throw new AadlGraphicalEditorException("Unexpected business object: " + targetBo);
			}
		}

		return result;
	}
}