GraphicalAnnexUtil.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.aadl2;

import java.util.Objects;
import java.util.Optional;
import java.util.stream.Stream;

import org.eclipse.emf.ecore.EClass;
import org.osate.aadl2.Aadl2Package;
import org.osate.aadl2.AadlPackage;
import org.osate.aadl2.AnnexLibrary;
import org.osate.aadl2.AnnexSubclause;
import org.osate.aadl2.Classifier;
import org.osate.aadl2.DefaultAnnexLibrary;
import org.osate.aadl2.DefaultAnnexSubclause;

/**
 * Class containing utility functions useful for implementing plugins adding annex support to the OSATE graphical editor.
 * @noextend
 * @since 2.0
 */
public final class GraphicalAnnexUtil {
	/**
	 * Source value to use when creating a new annex library or subclause
	 */
	private static final String DEFAULT_ANNEX_SOURCE = "{** **}";

	/**
	 * The token used for the start of an annex source block
	 */
	private static final String ANNEX_SOURCE_START = "{**";

	/**
	 * The token used for the end of an annex source block
	 */
	private static final String ANNEX_SOURCE_END = "**}";

	/**
	 * Private constructor to prevent instantiation.
	 */
	private GraphicalAnnexUtil() {
	}

	/**
	 * Finds and returns the parsed annex library for the first annex library in the package with the specified name. Creates a new annex
	 * library if one does not exist.
	 * An exception is thrown if an existing annex library is found but it does not have a parsed annex library of the expected type.
	 * @param <T> the type of the parsed annex library
	 * @param pkg is the package in which to look for the annex library
	 * @param annexName is the name of the annex library to look for
	 * @param parsedEType is the {@link EClass} of the parsed annex library created if it the annex library doesn't exist.
	 * @param parsedType is the java type that the parsed library is expected to be an instance of.
	 * @return the parsed annex library
	 */
	public static <T> T getOrCreateParsedAnnexLibrary(final AadlPackage pkg, final String annexName,
			final EClass parsedEType, final Class<T> parsedType) {
		// Get or create the DefaultAnnexLibrary
		final DefaultAnnexLibrary defaultLib = getFirstDefaultAnnexLibrary(pkg, annexName).orElseGet(() -> {
			// Create the public section of the package if it does not exist.
			if (pkg.getPublicSection() == null) {
				pkg.createOwnedPublicSection();
			}

			// Must create new annex
			final DefaultAnnexLibrary lib = (DefaultAnnexLibrary) pkg.getPublicSection()
					.createOwnedAnnexLibrary(Aadl2Package.eINSTANCE.getDefaultAnnexLibrary());
			lib.setName(annexName);
			lib.setSourceText(DEFAULT_ANNEX_SOURCE);
			return lib;
		});

		// Create the parsed library as needed
		final T result = getParsedAnnexLibrary(defaultLib, parsedType).orElse(null);
		if (result != null) {
			return result;
		}

		if (isEmptyAnnexSource(defaultLib.getSourceText())) {
			return parsedType.cast(defaultLib.createParsedAnnexLibrary(parsedEType));
		} else {
			throw new AadlGraphicalEditorException("Parsed annex library is null but source text is not empty");
		}
	}

	/**
	 * Finds and returns the parsed annex library for the first annex library in the package with the specified name.
	 * An exception is thrown if an annex library is found and it has a parsed annex library which is not an instance of the expected type.
	 * @param <T> the type of the parsed annex library
	 * @param pkg is the package in which to look for the annex library
	 * @param annexName is the name of the annex library to look for
	 * @param parsedType is the java type that the parsed library is expected to be an instance of.
	 * @return an optional containing the parsed annex library for the first annex with the specified name.
	 */
	public static <T> Optional<T> getFirstParsedAnnexLibrary(final AadlPackage pkg, final String annexName,
			final Class<T> parsedType) {
		return getFirstDefaultAnnexLibrary(pkg, annexName)
				.flatMap(defaultLib -> getParsedAnnexLibrary(defaultLib, parsedType));
	}

	/**
	 * Returns an optional containing the first default annex library with the specified name.
	 * @param pkg is the package in which to look for the annex library.
	 * @param annexName is the name to look for.
	 * @return the annex library. Returns empty if an annex library with the specified name does not exist in the package.
	 */
	private static Optional<DefaultAnnexLibrary> getFirstDefaultAnnexLibrary(final AadlPackage pkg,
			final String annexName) {
		if (pkg.getPublicSection() == null) {
			return Optional.empty();
		}

		for (final AnnexLibrary lib : pkg.getPublicSection().getOwnedAnnexLibraries()) {
			if (lib.getName().equalsIgnoreCase(annexName) && lib instanceof DefaultAnnexLibrary) {
				return Optional.of((DefaultAnnexLibrary) lib);
			}
		}

		return Optional.empty();
	}

	/**
	 * Retrieves the parsed annex library from a {@link DefaultAnnexLibrary} instance.
	 * Throws an exception if a parsed library was found but was of an unexpected type.
	 * @param <T> is the type of the parsed annex library
	 * @param defaultLib is the annex library to return the parsed library for
	 * @param parsedType is the java type that the parsed library is expected to be an instance of.
	 * @return an optional describing the library. Empty if the parsed library is null.
	 */
	private static <T> Optional<T> getParsedAnnexLibrary(final DefaultAnnexLibrary defaultLib,
			final Class<T> parsedType) {
		final AnnexLibrary parsedAnnexLibrary = defaultLib.getParsedAnnexLibrary();
		if (parsedAnnexLibrary == null) {
			return Optional.empty();
		}

		if (parsedType.isInstance(parsedAnnexLibrary)) {
			return Optional.of(parsedType.cast(parsedAnnexLibrary));
		} else {
			throw new IllegalArgumentException(
					"Invalid parsed type. Parsed annex library is not of specified type. Specified: "
							+ parsedType.getName() + ". Actual: " + parsedAnnexLibrary.getClass().getName());
		}
	}

	/**
	 * Finds and returns the parsed annex subclause for the first annex subclause in the classifier with the specified name.
	 * Creates a new annex subclause if one does not exist.
	 * An exception is thrown if an existing annex subclause is found but it does not have a parsed annex subclause of the expected type.
	 * @param <T> the type of the parsed annex subclause
	 * @param classifier is the classifier in which to look for the annex subclause
	 * @param annexName is the name of the annex subclause to look for
	 * @param parsedEType is the {@link EClass} of the parsed annex subclause created if the annex subclause doesn't exist.
	 * @param parsedType is the java type that the parsed subclause is expected to be an instance of.
	 * @return the parsed annex subclause
	 */
	public static <T> T getOrCreateParsedAnnexSubclause(final Classifier classifier, final String annexName,
			final EClass parsedEType, final Class<T> parsedType) {
		// Get or create the DefaultAnnexSubclause
		final DefaultAnnexSubclause defaultSubclause = getAllDefaultAnnexSubclauses(classifier, annexName).findFirst()
				.orElseGet(() -> createAnnexSubclause(classifier, annexName));

		// Create the parsed subclause as needed
		final T result = getParsedAnnexSubclause(defaultSubclause, parsedType).orElse(null);
		if (result != null) {
			return result;
		}

		if (isEmptyAnnexSource(defaultSubclause.getSourceText())) {
			return parsedType.cast(defaultSubclause.createParsedAnnexSubclause(parsedEType));
		} else {
			throw new AadlGraphicalEditorException("Parsed annex subclause is null but source text is not empty");
		}
	}

	/**
	 * Creates a new {@link DefaultAnnexSubclause} instance which contains default source text. The default source text only contains an empty annex
	 * source block.
	 * @param classifier the classifier to which to add the subclause
	 * @param annexName the name of the annex
	 * @return the new annex subclause
	 */
	private static DefaultAnnexSubclause createAnnexSubclause(final Classifier classifier, final String annexName) {
		final DefaultAnnexSubclause subclause = classifier.createOwnedAnnexSubclause();
		subclause.setName(annexName);
		subclause.setSourceText(DEFAULT_ANNEX_SOURCE);
		return subclause;
	}

	/**
	 * Creates a new annex subclause.
	 * @param <T> the type of the parsed annex subclause.
	 * @param classifier is the owner of the annex subclause.
	 * @param annexName is the name of the new annex subclause.
	 * @param parsedEType is the {@link EClass} of the parsed annex subclause created.
	 * @param parsedType is the java type that the parsed subclause is expected to be an instance of.
	 * @return the parsed annex subclause.
	 * @since 2.1
	 */
	public static <T> T createParsedAnnexSubclause(final Classifier classifier, final String annexName,
			final EClass parsedEType, final Class<T> parsedType) {
		// Must create new annex
		final DefaultAnnexSubclause defaultSubclause = createAnnexSubclause(classifier, annexName);
		return parsedType.cast(defaultSubclause.createParsedAnnexSubclause(parsedEType));
	}

	/**
	 * Finds and returns the parsed annex subclause for the first annex subclause in the classifier with the specified name.
	 * An exception is thrown if an annex subclause is found and it has a parsed annex subclause which is not an instance of the expected type.
	 * @param <T> the type of the parsed annex subclause
	 * @param classifier is the classifier in which to look for the annex subclause
	 * @param annexName is the name of the annex subclause to look for
	 * @param parsedType is the java type that the parsed subclause is expected to be an instance of.
	 * @return an optional containing the parsed annex subclause for the first annex with the specified name.
	 */
	public static <T> Optional<T> getFirstParsedAnnexSubclause(final Classifier classifier, final String annexName,
			final Class<T> parsedType) {
		return getAllParsedAnnexSubclauses(classifier, annexName, parsedType).findFirst();
	}

	/**
	 * Returns a stream of all the {@link DefaultAnnexSubclause} instances in the classifier with a name matching a specified annex name.
	 * @param classifier is the classifier in which to look for the annex subclauses.
	 * @param annexName is the name of the annex subclauses to look for
	 * @return a stream containing the matching subclauses
	 */
	private static Stream<DefaultAnnexSubclause> getAllDefaultAnnexSubclauses(final Classifier classifier,
			final String annexName) {
		return classifier.getOwnedAnnexSubclauses()
				.stream()
				.filter(subclause -> annexName.equalsIgnoreCase(subclause.getName())
						&& subclause instanceof DefaultAnnexSubclause)
				.map(DefaultAnnexSubclause.class::cast);
	}

	/**
	 * Returns a stream of all the non-null parsed annex subclauses of the annex subclauses in the specified classifier with the
	 * specified annex name.
	 * Throws an exception if a parsed annex subclause is non-null but is not of the expected type.
	 * @param <T> is the type of the parsed annex subclause
	 * @param classifier is the classifier in which to look for the annex subclauses.
	 * @param annexName is the name of the annex subclauses to look for.
	 * @param parsedType is the java type that the parsed subclause is expected to be an instance of.
	 * @return a stream containing the matching subclauses.
	 */
	public static <T> Stream<T> getAllParsedAnnexSubclauses(final Classifier classifier, final String annexName,
			final Class<T> parsedType) {
		return getAllDefaultAnnexSubclauses(classifier, annexName)
				.map(subclause -> getParsedAnnexSubclause(subclause, parsedType).orElse(null))
				.filter(Objects::nonNull);
	}

	/**
	 * Retrieves the parsed annex subclause from a {@link DefaultAnnexSubclause} instance.
	 * Throws an exception if a parsed subclause was found but was of an unexpected type.
	 * @param <T> is the type of the parsed annex subclause
	 * @param defaultSubclause is the annex subclause to return the parsed subclause for
	 * @param parsedType is the java type that the parsed subclause is expected to be an instance of.
	 * @return an optional describing the subclause. Empty if the parsed subclause is null.
	 */
	private static <T> Optional<T> getParsedAnnexSubclause(final DefaultAnnexSubclause defaultSubclause,
			final Class<T> parsedType) {
		final AnnexSubclause parsedSubclause = defaultSubclause.getParsedAnnexSubclause();
		if (parsedSubclause == null) {
			return Optional.empty();
		}

		if (parsedType.isInstance(parsedSubclause)) {
			return Optional.of(parsedType.cast(parsedSubclause));
		} else {
			throw new IllegalArgumentException(
					"Invalid parsed type. Parsed annex subclause is not of specified type. Specified: "
							+ parsedType.getName() + ". Actual: " + parsedSubclause.getClass().getName());
		}
	}

	/**
	 * Returns true if the specified text is an annex source block which is empty except for the starting and ending tokens.
	 * @param src is the source text to check.
	 * @return true if the source represents an empty annex source block. Otherwise, false.
	 */
	private static boolean isEmptyAnnexSource(final String src) {
		final String trimmedSrc = src.trim();
		if (trimmedSrc.startsWith(ANNEX_SOURCE_START) && trimmedSrc.endsWith(ANNEX_SOURCE_END)) {
			final String innerSrc = trimmedSrc
					.substring(ANNEX_SOURCE_START.length(), trimmedSrc.length() - ANNEX_SOURCE_END.length())
					.trim();
			return innerSrc.isEmpty();
		}

		return false;
	}
}