AadlOperationBuilder.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;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.function.Predicate;

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.FeatureGroup;
import org.osate.aadl2.FeatureGroupType;
import org.osate.aadl2.NamedElement;
import org.osate.aadl2.Subcomponent;
import org.osate.ge.aadl2.AadlGraphicalEditorException;
import org.osate.ge.aadl2.ui.internal.AadlUiUtil;
import org.osate.ge.operations.OperationBuilder;
import org.osate.ge.operations.StepResult;
import org.osate.ge.palette.PaletteCommand;

/**
 * Helper class for building an operation or determining whether a valid operation for modifying the business object can be created.
 * Supports filtering.
 *
 * This class is useful when implementing {@link PaletteCommand}. For a given target, an operation can be built which will modify the desired
 * type of object. If such a model element is not available or ambiguous, an error message or prompt may be shown by the operation based
 * on the builder's configuration.
 *
 * The target business object is not necessarily the object meant to be modified. For example, if the target object is a subcomponent or feature group,
 * the referenced classifier or extended classifier may be edited.
 * @param <T> is the type of AADL element to be modified
 *
 * @since 2.0
 * @see org.osate.ge.operations.Operation
 * @see OperationBuilder
 */
public final class AadlOperationBuilder<T> {
	private final Class<T> elementType;
	private Predicate<T> filter = c -> true; // Filter for selecting those from the classifier. These filters are and'ed together
	private boolean includeAllWhenBoIsMatch = false;
	private String allowedClassifierDescription = "classifier";
	private boolean showErrorForSubcomponentWithoutClassifier = false;
	private boolean showErrorForFeatureGroupWithoutClassifier = false;

	/**
	 * Private constructor.
	 * @param elementType is the type of AADL element to be modified.
	 */
	private AadlOperationBuilder(final Class<T> elementType) {
		this.elementType = Objects.requireNonNull(elementType, "elementType must not be null");
	}

	/**
	 * Creates an instance for modifying an {@link AadlPackage}
	 * @return the new instance
	 */
	public static AadlOperationBuilder<AadlPackage> packages() {
		return new AadlOperationBuilder<>(AadlPackage.class);
	}

	/**
	 * Creates an instance for modifying a {@link Classifier}.
	 * Will allowing building an operation which will show an error if the target is a subcomponent or feature group without a classifier
	 * @return the new instance
	 */
	public static AadlOperationBuilder<Classifier> classifiers() {
		return new AadlOperationBuilder<>(Classifier.class).showErrorForSubcomponentWithoutClassifier(true)
				.showErrorForFeatureGroupWithoutClassifier(true);
	}

	/**
	 * Creates an instance for modifying a {@link ComponentImplementation}.
	 * Will allowing building an operation which will show an error if the target is a subcomponent without a classifier
	 * @return the new instance
	 */
	public static AadlOperationBuilder<ComponentImplementation> componentImplementations() {
		return new AadlOperationBuilder<>(ComponentImplementation.class)
				.allowedClassifierDescription("component implementation")
				.showErrorForSubcomponentWithoutClassifier(true);
	}

	/**
	 * Creates an instance for modifying a {@link ComponentType}.
	 * Will allowing building an operation which will show an error if the target is a subcomponent without a classifier
	 * @return the new instance
	 */
	public static AadlOperationBuilder<ComponentType> componentTypes() {
		return new AadlOperationBuilder<>(ComponentType.class).allowedClassifierDescription("component type")
				.showErrorForSubcomponentWithoutClassifier(true);
	}

	/**
	 * Creates an instance for modifying a {@link AadlPackage} or {@link ComponentClassifier}.
	 * Will allowing building an operation which will show an error if the target is a subcomponent without a classifier
	 * @return the new instance
	 */
	public static AadlOperationBuilder<NamedElement> packagesAndComponentClassifiers() {
		return new AadlOperationBuilder<>(NamedElement.class)
				.filter(c -> c instanceof AadlPackage || c instanceof ComponentClassifier)
				.allowedClassifierDescription("component classifier")
				.showErrorForSubcomponentWithoutClassifier(true);
	}

	/**
	 * Creates an instance for modifying a {@link ComponentClassifier}.
	 * Will allowing building an operation which will show an error if the target is a subcomponent without a classifier
	 * @return the new instance
	 */
	public static AadlOperationBuilder<ComponentClassifier> componentClassifiers() {
		return new AadlOperationBuilder<>(ComponentClassifier.class)
				.allowedClassifierDescription("component classifier")
				.showErrorForSubcomponentWithoutClassifier(true);
	}

	/**
	 * Creates an instance for modifying a {@link FeatureGroupType}}.
	 * Will allowing building an operation which will show an error if the target is a feature group without a classifier
	 * @return the new instance
	 */
	public static AadlOperationBuilder<FeatureGroupType> featureGroupTypes() {
		return new AadlOperationBuilder<>(FeatureGroupType.class).allowedClassifierDescription("feature group")
				.showErrorForFeatureGroupWithoutClassifier(true);
	}

	/**
	 * Creates an instance for modifying a {@link ComponentType} or {@link FeatureGroupType}.
	 * Will allowing building an operation which will show an error if the target is a subcomponent or feature group without a classifier
	 * @return the new instance
	 */
	public static AadlOperationBuilder<Classifier> classifierTypes() {
		return new AadlOperationBuilder<>(Classifier.class)
				.filter(c -> c instanceof ComponentType || c instanceof FeatureGroupType)
				.allowedClassifierDescription("component or feature group type")
				.showErrorForSubcomponentWithoutClassifier(true)
				.showErrorForFeatureGroupWithoutClassifier(true);
	}

	/**
	 * Configures the builder to add an additional filter which restricts the allowed modified business objects.
	 * @param value the new filter
	 * @return this builder to allow method chaining.
	 */
	public AadlOperationBuilder<T> filter(final Predicate<T> value) {
		this.filter = this.filter.and(value);
		return this;
	}

	/**
	 * Configures the builder to use the specified label when describing the type of classifier that is missing from a
	 * subcomponent or feature group.
	 * @param value the label
	 * @return this builder to allow method chaining.
	 */
	private AadlOperationBuilder<T> allowedClassifierDescription(final String value) {
		this.allowedClassifierDescription = Objects.requireNonNull(value, "value must not be null");
		return this;
	}

	/**
	 * Configures the builder to allow building an operation when targeting a subcomponent without a classifier.
	 * @param value whether to allow building an operation when the target is a subcomponent without a classifier.
	 * @return this builder to allow method chaining.
	 */
	private AadlOperationBuilder<T> showErrorForSubcomponentWithoutClassifier(final boolean value) {
		this.showErrorForSubcomponentWithoutClassifier = value;
		return this;
	}

	/**
	 * Configures the builder to allow building an operation when targeting a feature group without a classifier.
	 * @param value whether to allow building an operation when the target is a feature group without a classifier.
	 * @return this builder to allow method chaining.
	 */
	private AadlOperationBuilder<T> showErrorForFeatureGroupWithoutClassifier(final boolean value) {
		this.showErrorForFeatureGroupWithoutClassifier = value;
		return this;
	}

	/**
	 * The classifiers are filtered to determine which ones are applicable. Typically if the target business object is applicable,
	 * then only it is used. Calling this method will result in all applicable classifiers to be considered when selecting the classifier to modify.
	 * @return this builder to allow method chaining.
	 */
	public AadlOperationBuilder<T> allowAnyMatchingClassifier() {
		includeAllWhenBoIsMatch = true;
		return this;
	}

	/**
	 * Returns true if {@link #buildOperation(OperationBuilder, Object)} will be return a valid operation.
	 * @param targetBo is the target for the operation
	 * @return whether an operation can be built.
	 */
	public boolean canBuildOperation(final Object targetBo) {
		if (AadlUiUtil.matches(targetBo, elementType, filter)) {
			return true;
		}

		// Return true for subcomponents / feature groups which do not have a classifier assigned so that the operation can show an explanation.
		if (showErrorForSubcomponentWithoutClassifier && AadlUiUtil.isSubcomponentWithoutClassifier(targetBo)) {
			return true;
		}

		if (showErrorForFeatureGroupWithoutClassifier && AadlUiUtil.isSubcomponentOrFeatureGroupWithoutClassifier(targetBo)) {
			return true;
		}

		// Check if there is at least one potential classifier for editing
		if ((showErrorForSubcomponentWithoutClassifier && targetBo instanceof Subcomponent)
				|| (showErrorForFeatureGroupWithoutClassifier && targetBo instanceof FeatureGroup)
				|| targetBo instanceof Classifier) {
			return !AadlUiUtil.getPotentialClassifiersForEditing(targetBo, elementType, filter).isEmpty();
		}

		return false;
	}

	/**
	 * Creates an operation builder which will either abort or supply a business object of the requested type.
	 * The built operation is not guaranteed to supply a business object. The operation may prompt the user or display an error depending on the configuration
	 * of the {@link AadlOperationBuilder}.
	 * Must not be called unless {@link #canBuildOperation(Object)} would return true.
	 * @param operation is the operation builder to use to build the operation.
	 * @param targetBo is the target of the operation.
	 * @return an operation builder which is configured to allow building the requested object type.
	 */
	public OperationBuilder<T> buildOperation(final OperationBuilder<?> operation, final Object targetBo) {
		return operation.supply(() -> {
			final List<T> potentialBusinessObjects = getPotentialBusinessObjects(targetBo);

			// Determine which classifier should own the new element
			if (!potentialBusinessObjects.isEmpty()) {
				final T selectedBo = AadlUiUtil.getBusinessObjectToModify(potentialBusinessObjects);
				if (selectedBo == null) {
					return StepResult.abort();
				}

				return StepResult.forValue(selectedBo);
			} else {
				if ((showErrorForSubcomponentWithoutClassifier || showErrorForFeatureGroupWithoutClassifier)
						&& AadlUiUtil.showMessageIfSubcomponentOrFeatureGroupWithoutClassifier(targetBo,
								"Set a " + allowedClassifierDescription + ".")) {
					return StepResult.abort();
				}
			}

			// Should mean that canBuildOperation() would return false.
			throw new AadlGraphicalEditorException("Unable to determine business object to modify");
		});
	}

	/**
	 * Returns a list of business objects which meet the configured criteria.
	 * @param targetBo the target of the operation.
	 * @return the list of modifiable business objects
	 */
	private List<T> getPotentialBusinessObjects(final Object targetBo) {
		final boolean boMatches = AadlUiUtil.matches(targetBo, elementType, filter);
		final List<T> potentialBusinessObjects = new ArrayList<>();

		// Avoid multiple entries by not including matching classifiers because that will be handled when processing classifiers
		if (!(targetBo instanceof Classifier) && boMatches) {
			potentialBusinessObjects.add(elementType.cast(targetBo));
		}

		if (potentialBusinessObjects.isEmpty() || includeAllWhenBoIsMatch) {
			potentialBusinessObjects.addAll(AadlUiUtil.getPotentialClassifiersForEditing(targetBo, elementType, filter,
					includeAllWhenBoIsMatch));
		}

		return potentialBusinessObjects;
	}
}