AadlUiUtil.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.ui.internal;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;

import org.eclipse.core.resources.IProject;
import org.eclipse.emf.ecore.EClass;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.resource.ResourceSet;
import org.eclipse.emf.ecore.util.EcoreUtil;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.window.Window;
import org.eclipse.swt.widgets.Display;
import org.eclipse.xtext.resource.IEObjectDescription;
import org.osate.aadl2.Aadl2Package;
import org.osate.aadl2.AadlPackage;
import org.osate.aadl2.Classifier;
import org.osate.aadl2.ComponentCategory;
import org.osate.aadl2.ComponentClassifier;
import org.osate.aadl2.ComponentImplementation;
import org.osate.aadl2.ComponentType;
import org.osate.aadl2.Element;
import org.osate.aadl2.FeatureGroup;
import org.osate.aadl2.FeatureGroupType;
import org.osate.aadl2.NamedElement;
import org.osate.aadl2.Subcomponent;
import org.osate.ge.aadl2.internal.util.AadlClassifierUtil;
import org.osate.ge.aadl2.ui.AadlModelAccessUtil;
import org.osate.ge.aadl2.ui.internal.dialogs.ElementSelectionDialog;
import org.osate.ge.operations.OperationBuilder;
import org.osate.ge.operations.StepResult;

public class AadlUiUtil {
	private AadlUiUtil() {
	}

	public static Set<IEObjectDescription> getEditablePackages(final IProject project) {
		return AadlModelAccessUtil.getAllEObjectsByType(project, Aadl2Package.eINSTANCE.getAadlPackage()).stream()
				.filter(od -> od.getEObjectURI() != null && !od.getEObjectURI().isPlatformPlugin())
				.collect(Collectors.toSet());
	}

	public static <T extends Element> Optional<AadlPackage> getCommonPackage(final Collection<T> elements) {
		return Optional.ofNullable(elements.stream()
				.map(e -> e.getElementRoot() instanceof AadlPackage ? (AadlPackage) e.getElementRoot() : null)
				.reduce(null, (p1, p2) -> p1 == p2 ? p1 : null));
	}

	/**
	 * Return a set of IEObjectDescriptions for classifiers that would be valid "base" classifier for a new classifier of the specified component category.
	 * A "base" classifier is one that will be implemented or extended.
	 */
	public static Set<IEObjectDescription> getValidBaseClassifierDescriptions(final IProject project,
			final ComponentCategory componentCategory, boolean includeImplementations) {
		final EClass classifierClass = AadlClassifierUtil.componentCategoryToClassifierEClass(componentCategory);
		final Set<IEObjectDescription> objectDescriptions = new HashSet<IEObjectDescription>();
		for (final IEObjectDescription desc : AadlModelAccessUtil.getAllEObjectsByType(project,
				Aadl2Package.eINSTANCE.getComponentClassifier())) {
			// Add objects that have are either types or implementations of the same category as the classifier type
			if (classifierClass.isSuperTypeOf(desc.getEClass()) && (includeImplementations
					|| !Aadl2Package.eINSTANCE.getComponentImplementation().isSuperTypeOf(desc.getEClass()))) {
				objectDescriptions.add(desc);
			}
		}

		return objectDescriptions;
	}

	public static final <ClassifierType extends Classifier> OperationBuilder<ClassifierType> selectClassifier(
			final OperationBuilder<?> operation, final List<ClassifierType> potentialClassifiers) {
		return operation.supply(() -> {
			// Determine which classifier should own the new element
			final ClassifierType selectedClassifier = (ClassifierType) getBusinessObjectToModify(potentialClassifiers);
			if (selectedClassifier == null) {
				return StepResult.abort();
			}

			return StepResult.forValue(selectedClassifier);
		});
	}

	/**
	 * Returns a list of classifiers for editing based on a specified business object.
	 * If the specified object is of the specified type and passes the filter, only it is returned.
	 * @param bo
	 * @return
	 */
	public static <ClassifierType> List<ClassifierType> getPotentialClassifiersForEditing(final Object bo,
			final Class<ClassifierType> classifierClass, final Predicate<ClassifierType> filter,
			final boolean includeAllWhenBoIsMatch) {
		if (!includeAllWhenBoIsMatch) {
			if (matches(bo, classifierClass, filter)) {
				return Collections.singletonList(classifierClass.cast(bo));
			}
		}

		final List<ClassifierType> results = new ArrayList<>();

		if (bo instanceof ComponentType || bo instanceof FeatureGroupType) {
			addSelfAndExtended((Classifier) bo, classifierClass, results);
		} else if (bo instanceof ComponentImplementation) {
			final ComponentImplementation ci = (ComponentImplementation) bo;
			addSelfAndExtended(ci, classifierClass, results);
			addSelfAndExtended(ci.getType(), classifierClass, results);
		} else if (bo instanceof Subcomponent) {
			final ComponentClassifier subcomponentClassifier = ((Subcomponent) bo).getAllClassifier();
			addSelfAndExtended(subcomponentClassifier, classifierClass, results);
			if (subcomponentClassifier instanceof ComponentImplementation) {
				addSelfAndExtended(((ComponentImplementation) subcomponentClassifier).getType(), classifierClass,
						results);
			}
		} else if (bo instanceof FeatureGroup) {
			final FeatureGroupType fgType = ((FeatureGroup) bo).getAllFeatureGroupType();
			addSelfAndExtended(fgType, classifierClass, results);
		}

		return results.stream().filter(c -> classifierClass.isInstance(c)).map(classifierClass::cast).filter(filter)
				.collect(Collectors.toList());
	}

	public static <ClassifierType> List<ClassifierType> getPotentialClassifiersForEditing(
			final Object bo, final Class<ClassifierType> classifierClass, final Predicate<ClassifierType> filter) {
		return getPotentialClassifiersForEditing(bo, classifierClass, filter, false);
	}

	/**
	 * Returns a list of component types and feature group types for editing based on a specified business object.
	 * If the specified object is a classifier type, only it is returned.
	 * @param bo
	 * @return
	 */
	public static List<Classifier> getPotentialClassifierTypesForEditing(final Object bo,
			final Predicate<Classifier> filter) {
		if (bo instanceof ComponentType || bo instanceof FeatureGroupType && filter.test((Classifier) bo)) {
			return Collections.singletonList((Classifier) bo);
		}

		final Predicate<Classifier> classifierTypeFilter = c -> c instanceof ComponentType
				|| c instanceof FeatureGroupType;
		return getPotentialClassifiersForEditing(bo, Classifier.class, classifierTypeFilter.and(filter));
	}

	public static List<Classifier> getPotentialClassifierTypesForEditing(final Object bo) {
		return getPotentialClassifierTypesForEditing(bo, c -> true);
	}

	private static <ClassifierType> void addSelfAndExtended(final Classifier c,
			final Class<ClassifierType> classifierClass, final List<ClassifierType> results) {
		if (c != null) {
			for (final Classifier tmpClassifier : c.getSelfPlusAllExtended()) {
				if (classifierClass.isInstance(tmpClassifier)) {
					results.add(classifierClass.cast(tmpClassifier));
				}
			}
		}
	}

	public static List<ComponentClassifier> getPotentialComponentClassifiers(final Object bo,
			final Predicate<ComponentClassifier> filter) {
		return getPotentialClassifiersForEditing(bo, ComponentClassifier.class, filter);
	}

	public static List<ComponentClassifier> getPotentialComponentClassifiers(final Object bo) {
		return getPotentialComponentClassifiers(bo, cc -> true);
	}

	public static <BusinessObjectType> BusinessObjectType getBusinessObjectToModify(
			final List<BusinessObjectType> potentialBusinessObjects) {
		return getBusinessObjectToModify(potentialBusinessObjects, false);
	}

	/**
	 * Returns the business object from the specified list which should be modified. If there are multiple objects in the specified list,
	 * the user will be prompted to select one. The first element will be the default. Must be called from the UI thread.
	 * @param potentialBusinessObjects must have at least one element. If empty, an exception will be thrown.
	 * @return the object to modify. Will return null if the user cancels the selection prompt.
	 */
	@SuppressWarnings("unchecked")
	public static <BusinessObjectType> BusinessObjectType getBusinessObjectToModify(
			final List<BusinessObjectType> potentialBusinessObjects, final boolean forcePrompt) {
		if (potentialBusinessObjects.isEmpty()) {
			throw new RuntimeException("potentialBusinessObjects is empty");
		}

		// Determine which business object should be modified
		final BusinessObjectType selectedBo;
		if (forcePrompt || potentialBusinessObjects.size() > 1) {
			// Prompt the user for the classifier
			final ElementSelectionDialog dlg = new ElementSelectionDialog(Display.getCurrent().getActiveShell(),
					"Select an Element to Modify", "Select an element to modify.", potentialBusinessObjects);
			dlg.setInitialSelections(new Object[] { potentialBusinessObjects.get(0) });
			if (dlg.open() == Window.CANCEL) {
				return null;
			}

			selectedBo = (BusinessObjectType) dlg.getFirstSelectedElement();
		} else {
			selectedBo = potentialBusinessObjects.get(0);
		}

		return selectedBo;
	}

	public static boolean isSubcomponentWithoutClassifier(final Object bo) {
		if (bo instanceof Subcomponent) {
			return ((Subcomponent) bo).getAllClassifier() == null;
		}

		return false;
	}

	public static boolean isFeatureGroupWithoutClassifier(final Object bo) {
		if (bo instanceof FeatureGroup) {
			return ((FeatureGroup) bo).getAllClassifier() == null;
		}

		return false;
	}

	public static boolean isSubcomponentOrFeatureGroupWithoutClassifier(final Object bo) {
		return isSubcomponentWithoutClassifier(bo) || isFeatureGroupWithoutClassifier(bo);
	}

	/**
	 * Returns true if the element was a subcomponent or feature group without classifier and the error was shown.
	 * @param bo
	 * @return
	 */
	public static boolean showMessageIfSubcomponentOrFeatureGroupWithoutClassifier(final Object bo,
			final String secondaryMsg) {
		final boolean showMsg = isSubcomponentOrFeatureGroupWithoutClassifier(bo);
		if (showMsg) {
			final String targetDescription = bo instanceof NamedElement
					? ("The element '" + ((NamedElement) bo).getQualifiedName() + "'")
							: "The target element";
			MessageDialog.openError(Display.getDefault().getActiveShell(), "Classifier Not Set",
					targetDescription + " does not have a classifier. " + secondaryMsg);
		}

		return showMsg;
	}

	/**
	 * Returns true if the specified object is of the specified class and passes the specified filter.
	 * @param bo
	 * @param targetClass
	 * @param filter
	 * @return
	 */
	public static <BusinessObjectType> boolean matches(final Object bo, final Class<BusinessObjectType> targetClass,
			final Predicate<BusinessObjectType> filter) {
		if (!targetClass.isInstance(bo)) {
			return false;
		}

		final BusinessObjectType targetBo = targetClass.cast(bo);
		return filter.test(targetBo);
	}

	/**
	 * Returning an optional describing the resource set in which the object's resource is contained. Returns empty optional if the object, the resource, or the resource set is null.
	 */
	private static Optional<ResourceSet> getResourceSet(final EObject obj) {
		return Optional.ofNullable(obj).flatMap(eobj -> Optional.ofNullable(eobj.eResource()))
				.flatMap(r -> Optional.ofNullable(r.getResourceSet()));
	}

	/**
	 * Returns true if and only if the specified list contains at least one business object and all business objects are contained in the the same valid resource set.
	 */
	public static boolean allHaveSameValidResourceSet(final List<? extends EObject> bos) {
		if (bos.size() == 0) {
			return false;
		}

		final Optional<ResourceSet> firstResourceSet = getResourceSet(bos.get(0));
		if (!firstResourceSet.isPresent()) {
			return false;
		}

		return bos.stream().allMatch(bo -> getResourceSet(bo).equals(firstResourceSet));
	}

	/**
	 * Attempts to return a resolved version of the specified EObject.
	 * If the specified EObject is not a proxy, it returns the EObject. If the specified EObject is a proxy,
	 * it attempts to resolve the proxy using a live resource set for the specified project and returns the result.
	 */
	public static EObject resolveWithLiveResourceSetIfProject(final EObject obj, final IProject project) {
		if (obj.eIsProxy()) {
			final ResourceSet liveResourceSet = AadlModelAccessUtil.getLiveResourceSet(project);
			return EcoreUtil.resolve(obj, liveResourceSet);
		} else {
			return obj;
		}
	}
}