SetBindingTool.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.tools;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;

import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.util.EcoreUtil;
import org.eclipse.jface.dialogs.IDialogConstants;
import org.eclipse.jface.dialogs.TitleAreaDialog;
import org.eclipse.jface.viewers.ArrayContentProvider;
import org.eclipse.jface.viewers.ComboViewer;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.LabelProvider;
import org.eclipse.jface.viewers.StructuredSelection;
import org.eclipse.jface.window.Window;
import org.eclipse.swt.SWT;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.xtext.util.Strings;
import org.osate.aadl2.Aadl2Package;
import org.osate.aadl2.ArrayRange;
import org.osate.aadl2.Classifier;
import org.osate.aadl2.ComponentClassifier;
import org.osate.aadl2.ComponentImplementation;
import org.osate.aadl2.ContainedNamedElement;
import org.osate.aadl2.ContainmentPathElement;
import org.osate.aadl2.Element;
import org.osate.aadl2.ListType;
import org.osate.aadl2.ListValue;
import org.osate.aadl2.MetaclassReference;
import org.osate.aadl2.ModalPropertyValue;
import org.osate.aadl2.NamedElement;
import org.osate.aadl2.Property;
import org.osate.aadl2.PropertyAssociation;
import org.osate.aadl2.ReferenceType;
import org.osate.aadl2.ReferenceValue;
import org.osate.ge.BusinessObjectContext;
import org.osate.ge.aadl2.internal.util.AgeAadlUtil;
import org.osate.ge.internal.diagram.runtime.AgeDiagram;
import org.osate.ge.internal.diagram.runtime.DiagramConfigurationBuilder;
import org.osate.ge.internal.services.AadlModificationService;
import org.osate.ge.internal.services.AadlModificationService.Modification;
import org.osate.ge.internal.services.AadlModificationService.SimpleModifier;
import org.osate.ge.internal.services.UiService;
import org.osate.ge.internal.ui.tools.ActivatedEvent;
import org.osate.ge.internal.ui.tools.DeactivatedEvent;
import org.osate.ge.internal.ui.tools.SelectionChangedEvent;
import org.osate.ge.internal.ui.tools.Tool;
import org.osate.ge.internal.ui.tools.ToolUtil;
import org.osate.ge.internal.ui.util.ContextHelpUtil;
import org.osate.ge.swt.SwtUtil;
import org.osate.xtext.aadl2.properties.util.DeploymentProperties;
import org.osate.xtext.aadl2.properties.util.GetProperties;

/**
 * The Set Binding Action presents a non-modal dialog to the user that allows create a property association for a standard AADL binding property.
 * Assumes that all selected elements are descendants of the same component implementation
 */
public class SetBindingTool implements Tool {
	public static final String setBindingIdentifier = "org.osate.ge.SetBinding";
	private SetBindingWindow currentWindow = null;

	@Override
	public void activated(final ActivatedEvent ctx) {
		final BusinessObjectContext[] selectedBocs = ctx.getSelectedBocs().toArray(new BusinessObjectContext[ctx.getSelectedBocs().size()]);
		final AgeDiagram diagram = ctx.getDiagram();
		final AadlModificationService aadlModService = ctx.getAadlModificatonService();
		final UiService uiService = ctx.getUiService();
		try {
			final BusinessObjectContext componentImplementationBoc = ToolUtil
					.findComponentImplementationBoc(selectedBocs[0]);

			// Open Dialog
			if (currentWindow == null && componentImplementationBoc != null) {
				currentWindow = new SetBindingWindow(Display.getCurrent().getActiveShell(), componentImplementationBoc,
						selectedBocs);
				if(currentWindow.open() == Window.OK) {
					// Ensure the diagram is configured to show the specified binding property
					if (!diagram.getConfiguration().getEnabledAadlPropertyNames()
							.contains(currentWindow.getSelectedProperty().getQualifiedName().toLowerCase())) {
						diagram.modify("Configure Diagram", m -> {
							m.setDiagramConfiguration(new DiagramConfigurationBuilder(diagram.getConfiguration()).
									addAadlProperty(currentWindow.getSelectedProperty().getQualifiedName()).
									build());
						});
					}

					// Create the property association
					createPropertyAssociations(aadlModService);
				}

				currentWindow = null;
			}
		} finally {
			uiService.deactivateActiveTool();
		}
	}

	@Override
	public void selectionChanged(SelectionChangedEvent ctx) {
		if (currentWindow != null && currentWindow.getShell() != null && !currentWindow.getShell().isDisposed()) {
			currentWindow.setTargetBusinessObjectContexts(
					ctx.getSelectedBocs().toArray(new BusinessObjectContext[ctx.getSelectedBocs().size()]));
		}
	}

	@Override
	public void deactivated(final DeactivatedEvent ctx) {
		if(currentWindow != null) {
			currentWindow.cancel();
			currentWindow = null;
		}
	}

	private static class SetBindingWindow extends TitleAreaDialog {
		private final BusinessObjectContext componentImplementationBoc;
		private final BusinessObjectContext[] bocsToBind;
		private final NamedElement[] elementsToBind;
		private ComboViewer bindingPropertyCombo;
		private Label selectionStatusLabel;
		private IStructuredSelection currentPropComboSel;
		private BusinessObjectContext[] targetBocs = new BusinessObjectContext[0];
		private final LabelProvider propertyLabelProvider = new LabelProvider() {
			@Override
			public String getText(final Object element) {
				final Property p = (Property)element;
				if(p == null) {
					return "";
				}

				return p.getName();
			}
		};

		public SetBindingWindow(final Shell parentShell,
				final BusinessObjectContext componentImplementationBoc,
				final BusinessObjectContext[] bocsToBind) {
			super(parentShell);
			this.componentImplementationBoc = Objects.requireNonNull(componentImplementationBoc, "componentImplementationBoc must not be null");
			this.bocsToBind = Objects.requireNonNull(bocsToBind, "bocsToBind must not be null");
			this.elementsToBind = Arrays.stream(bocsToBind).map(boc -> (NamedElement) boc.getBusinessObject())
					.toArray(size -> new NamedElement[size]);
			setShellStyle(SWT.RESIZE | SWT.CLOSE | SWT.MODELESS | SWT.BORDER | SWT.TITLE);
			setHelpAvailable(true);
		}

		@Override
		public void create() {
			super.create();

			setTitle("Select Elements");
			setMessage("Select a binding property and the elements from the diagram to bind " + Arrays
					.stream(elementsToBind).map(e -> Strings.emptyIfNull(e.getName())).collect(Collectors.joining(","))
					+ ".");
			validate();

			ContextHelpUtil.setHelp(getShell(), ContextHelpUtil.BINDING_TOOL);
		}

		@Override
		protected Control createDialogArea(Composite parent) {
			final Composite container = (Composite) super.createDialogArea(parent);

			final GridLayout layout = new GridLayout();
			layout.marginHeight = IDialogConstants.VERTICAL_MARGIN;
			layout.marginWidth = IDialogConstants.HORIZONTAL_MARGIN;
			layout.verticalSpacing = IDialogConstants.VERTICAL_SPACING;
			layout.horizontalSpacing = IDialogConstants.HORIZONTAL_SPACING;
			container.setLayout(layout);

			final List<Property> bindingProperties = new ArrayList<Property>();
			addPropertyIfApplicable(bindingProperties, GetProperties.lookupPropertyDefinition(elementsToBind[0],
					DeploymentProperties._NAME, DeploymentProperties.ACTUAL_CONNECTION_BINDING));
			addPropertyIfApplicable(bindingProperties, GetProperties.lookupPropertyDefinition(elementsToBind[0],
					DeploymentProperties._NAME, DeploymentProperties.ALLOWED_CONNECTION_BINDING));
			addPropertyIfApplicable(bindingProperties, GetProperties.lookupPropertyDefinition(elementsToBind[0],
					DeploymentProperties._NAME, DeploymentProperties.ACTUAL_FUNCTION_BINDING));
			addPropertyIfApplicable(bindingProperties, GetProperties.lookupPropertyDefinition(elementsToBind[0],
					DeploymentProperties._NAME, DeploymentProperties.ACTUAL_MEMORY_BINDING));
			addPropertyIfApplicable(bindingProperties, GetProperties.lookupPropertyDefinition(elementsToBind[0],
					DeploymentProperties._NAME, DeploymentProperties.ALLOWED_MEMORY_BINDING));
			addPropertyIfApplicable(bindingProperties, GetProperties.lookupPropertyDefinition(elementsToBind[0],
					DeploymentProperties._NAME, DeploymentProperties.ACTUAL_PROCESSOR_BINDING));
			addPropertyIfApplicable(bindingProperties, GetProperties.lookupPropertyDefinition(elementsToBind[0],
					DeploymentProperties._NAME, DeploymentProperties.ALLOWED_PROCESSOR_BINDING));

			// Create combo box for selection type
			bindingPropertyCombo = new ComboViewer(container, SWT.DROP_DOWN | SWT.READ_ONLY);
			bindingPropertyCombo.getControl().setLayoutData(new GridData(SWT.FILL, SWT.TOP, true, false));
			SwtUtil.setTestingId(bindingPropertyCombo.getControl(), setBindingIdentifier);
			bindingPropertyCombo.setContentProvider(new ArrayContentProvider());
			bindingPropertyCombo.setLabelProvider(propertyLabelProvider);
			bindingPropertyCombo.setInput(bindingProperties);
			bindingPropertyCombo.addSelectionChangedListener(event -> {
				setSelectedProperty((IStructuredSelection) bindingPropertyCombo.getSelection());
				validate();
			});

			selectionStatusLabel = new Label(container, SWT.WRAP);
			selectionStatusLabel.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));

			return container;
		}

		// Returns whether a property is applicable to the element to bind
		private boolean isApplicable(final Property property) {
			return Arrays.stream(elementsToBind).allMatch(e -> isApplicableForElement(property, e));
		}

		private static boolean isApplicableForElement(final Property property, final NamedElement element) {
			for (final MetaclassReference mcr : property.getAppliesToMetaclasses()) {
				if (mcr.getMetaclass() != null && mcr.getMetaclass().isSuperTypeOf(element.eClass())) {
					return true;
				}
			}

			return false;
		}

		// Adds a property to the collection if it is applicable to the element to bind
		private void addPropertyIfApplicable(final Collection<Property> properties, final Property property) {
			if (isApplicable(property)) {
				properties.add(property);
			}
		}

		@SuppressWarnings("unchecked")
		private void validate() {
			boolean validationSuccessful = false;
			if (((List<Property>) bindingPropertyCombo.getInput()).size() == 0) {
				selectionStatusLabel.setText("No applicable binding properties.");
			} else {
				selectionStatusLabel.setText("Elements selected: " + targetBocs.length);
				final Property selectedProperty = getSelectedProperty();
				if (selectedProperty == null) {
					selectionStatusLabel.setText("Select a binding property.");
				} else {
					final ListType listType = (ListType) selectedProperty.getPropertyType();
					final ReferenceType refType = (ReferenceType) listType.getElementType();

					// Check target business object context
					validationSuccessful = true;
					for (final BusinessObjectContext targetBoc : targetBocs) {
						boolean boIsValid = false;
						if(ToolUtil.findComponentImplementationBoc(targetBoc) == componentImplementationBoc) {
							final Element bo = (Element)targetBoc.getBusinessObject();
							if (bo != null) {
								// The root element can not be a target element
								if (!(bo instanceof Classifier)) {
									for (final MetaclassReference mcr : refType.getNamedElementReferences()) {
										if (mcr.getMetaclass() != null && mcr.getMetaclass().isSuperTypeOf(bo.eClass())) {
											boIsValid = true;
											break;
										}
									}
								}
							}
						}

						// Show an error message if the BO is not valid
						if (!boIsValid) {
							validationSuccessful = false;
							selectionStatusLabel.setText("One or more of the selected target elements are not valid.");
							break;
						}

					}
				}
			}

			final Button okButton = getButton(IDialogConstants.OK_ID);
			okButton.setEnabled(validationSuccessful);
		}

		@Override
		protected void configureShell(final Shell newShell) {
			super.configureShell(newShell);
			newShell.setText("Bind");
			newShell.setSize(400, 285);
			newShell.setMinimumSize(400, 280);
		}

		public void cancel() {
			setReturnCode(CANCEL);
			close();
		}

		@Override
		public boolean close() {
			return super.close();
		}

		public BusinessObjectContext getComponentImplementationBoc() {
			return componentImplementationBoc;
		}

		public BusinessObjectContext[] getBocsToBind() {
			return bocsToBind;
		}

		public Property getSelectedProperty() {
			if (currentPropComboSel instanceof StructuredSelection) {
				final StructuredSelection selection = (StructuredSelection) currentPropComboSel;
				return (Property) selection.getFirstElement();
			}

			return null;
		}

		public void setSelectedProperty(final IStructuredSelection propComboSelection) {
			currentPropComboSel = propComboSelection;
		}

		public void setTargetBusinessObjectContexts(final BusinessObjectContext[] value) {
			targetBocs = value;
			if(getShell() != null && !getShell().isDisposed()) {
				validate();
			}
		}

		public BusinessObjectContext[] getTargetBocs() {
			return targetBocs;
		}
	};

	private void createPropertyAssociations(final AadlModificationService aadlModService) {
		final BusinessObjectContext ciBoc = currentWindow.getComponentImplementationBoc();
		final ComponentImplementation ci = (ComponentImplementation)ciBoc.getBusinessObject();
		aadlModService.modify(Modification.create(ci, new SimpleModifier<ComponentClassifier>() {
			@Override
			public void modify(final ComponentClassifier ci) {
				for (final BusinessObjectContext bocToBind : currentWindow.getBocsToBind()) {
					final PropertyAssociation newPa = AgeAadlUtil.getAadl2Factory().createPropertyAssociation();

					// Set property
					newPa.setProperty(currentWindow.getSelectedProperty());

					// Set applies to
					if (ciBoc != bocToBind) {
						setContainedNamedElementPath(newPa.createAppliesTo(), ciBoc, bocToBind);
					}

					// Create owned values
					final ModalPropertyValue pv = newPa.createOwnedValue();
					final ListValue lv = (ListValue) pv.createOwnedValue(Aadl2Package.eINSTANCE
							.getListValue());

					for (final BusinessObjectContext targetBoc : currentWindow.getTargetBocs()) {
						// Ignore diagram selections
						final ReferenceValue rv = (ReferenceValue) lv.createOwnedListElement(
								Aadl2Package.eINSTANCE.getReferenceValue());
						setContainedNamedElementPath(rv, ciBoc, targetBoc);
					}

					removeOldPropertyAssociation(ci, newPa);

					// Add the property association
					ci.setNoProperties(false);
					ci.getOwnedPropertyAssociations().add(newPa);
				}
			}

			// Deletes any existing property associations/removes the bound element from any property associations that matches the property association
			// being created.
			private void removeOldPropertyAssociation(final ComponentClassifier cc, final PropertyAssociation newPa) {
				final List<PropertyAssociation> propertyAssociationsToDelete = new ArrayList<PropertyAssociation>();
				for (final PropertyAssociation existingPa : cc.getOwnedPropertyAssociations()) {
					// If Property matches
					if (newPa.getProperty().getFullName().equals(existingPa.getProperty().getFullName())) {
						if (existingPa.getAppliesTos().size() == 0 || newPa.getAppliesTos().size() == 0) {
							if (existingPa.getAppliesTos().size() == 0 && newPa.getAppliesTos().size() == 0) {
								propertyAssociationsToDelete.add(existingPa);
							}
						} else {
							final List<ContainedNamedElement> containedElementsToDelete = new ArrayList<ContainedNamedElement>();
							for (final ContainedNamedElement existingContainedElement : existingPa.getAppliesTos()) {
								if (containmentPathsMatch(existingContainedElement.getPath(), newPa.getAppliesTos()
										.get(0).getPath())) {
									// Add the contained element to the list of objects to delete
									containedElementsToDelete.add(existingContainedElement);
								}
							}

							// Delete objects
							for (final EObject ce : containedElementsToDelete) {
								EcoreUtil.delete(ce);
							}

							// Delete the property association if it was the last element in the applies to clause
							if (existingPa.getAppliesTos().size() == 0) {
								propertyAssociationsToDelete.add(existingPa);
							}

						}
					}
				}

				// Delete property association(s) that are being replaced
				for (final PropertyAssociation pa : propertyAssociationsToDelete) {
					EcoreUtil.delete(pa);
				}
			}
		}));
	}

	private boolean containmentPathsMatch(ContainmentPathElement cp1, ContainmentPathElement cp2) {
		if (cp1 == cp2) {
			return true;
		}

		if (cp1 == null || cp2 == null) {
			return false;
		}

		while (cp1 != null) {
			// Compare Named Elements
			if (cp1.getNamedElement().getName() != null
					&& !cp1.getNamedElement().getName().equalsIgnoreCase(cp2.getNamedElement().getName())) {
				return false;
			}

			// Compare Array Ranges
			if (cp1.getArrayRanges().size() != cp2.getArrayRanges().size()) {
				return false;
			}

			for (int i = 0; i < cp1.getArrayRanges().size(); i++) {
				final ArrayRange ar1 = cp1.getArrayRanges().get(i);
				final ArrayRange ar2 = cp2.getArrayRanges().get(i);
				if (ar1.getUpperBound() != ar2.getUpperBound() || ar1.getLowerBound() != ar2.getLowerBound()) {
					return false;
				}
			}

			// Annex Name
			if (cp1.getAnnexName() != cp2.getAnnexName()
					&& (cp1.getAnnexName() != null && !cp1.getAnnexName().equalsIgnoreCase(cp2.getAnnexName()))) {
				return false;
			}

			cp1 = cp1.getPath();
			cp2 = cp2.getPath();
		}

		return cp1 == cp2; // Both should be null
	}

	private ContainmentPathElement setContainedNamedElementPath(final ContainedNamedElement c, final BusinessObjectContext ciBoc, BusinessObjectContext bocToAdd) {
		if (bocToAdd == null || bocToAdd == ciBoc) {
			return null;
		}

		// Create the path element for the container
		ContainmentPathElement pathElement = setContainedNamedElementPath(c, ciBoc, bocToAdd.getParent());

		// Create the path element for the business object context
		final NamedElement bo = (NamedElement)bocToAdd.getBusinessObject();
		if (bo != null) {
			pathElement = (pathElement == null) ? c.createPath() : pathElement.createPath();
			pathElement.setNamedElement(bo);
		}

		return pathElement;
	}
}