DeleteHandler.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.handlers;

import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.function.Consumer;

import org.eclipse.core.commands.AbstractHandler;
import org.eclipse.core.commands.ExecutionEvent;
import org.eclipse.core.commands.ExecutionException;
import org.eclipse.emf.common.util.URI;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.resource.Resource;
import org.eclipse.emf.ecore.util.EcoreUtil;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.swt.widgets.Display;
import org.eclipse.ui.IEditorPart;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.handlers.HandlerUtil;
import org.osate.aadl2.AnnexLibrary;
import org.osate.aadl2.AnnexSubclause;
import org.osate.aadl2.DefaultAnnexSubclause;
import org.osate.aadl2.NamedElement;
import org.osate.ge.CanonicalBusinessObjectReference;
import org.osate.ge.EmfContainerProvider;
import org.osate.ge.businessobjecthandling.BusinessObjectHandler;
import org.osate.ge.businessobjecthandling.CanDeleteContext;
import org.osate.ge.businessobjecthandling.CustomDeleteContext;
import org.osate.ge.businessobjecthandling.CustomDeleter;
import org.osate.ge.businessobjecthandling.RawDeleteContext;
import org.osate.ge.businessobjecthandling.RawDeleter;
import org.osate.ge.internal.diagram.runtime.AgeDiagram;
import org.osate.ge.internal.diagram.runtime.DiagramElement;
import org.osate.ge.internal.model.EmbeddedBusinessObject;
import org.osate.ge.internal.services.AadlModificationService;
import org.osate.ge.internal.services.AadlModificationService.Modification;
import org.osate.ge.internal.services.ActionExecutor.ExecutionMode;
import org.osate.ge.internal.ui.editor.InternalDiagramEditor;
import org.osate.ge.internal.ui.handlers.AgeHandlerUtil;
import org.osate.ge.internal.util.DiagramElementUtil;
import org.osate.ge.services.ReferenceBuilderService;

import com.google.common.base.Predicates;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ListMultimap;

public class DeleteHandler extends AbstractHandler {
	@Override
	public void setEnabled(final Object evaluationContext) {
		final boolean enabled = calculateEnabled(evaluationContext);
		setBaseEnabled(enabled);
	}

	private boolean calculateEnabled(final Object evaluationContext) {
		final IEditorPart activeEditor = AgeHandlerUtil.getActiveEditorFromContext(evaluationContext);
		if (!(activeEditor instanceof InternalDiagramEditor)) {
			return false;
		}

		final List<DiagramElement> selectedDiagramElements = AgeHandlerUtil.getSelectedDiagramElements();

		return canExecute(selectedDiagramElements);
	}

	private boolean canExecute(final List<DiagramElement> selectedDiagramElements) {
		if (selectedDiagramElements.size() == 0) {
			return false;
		}

		// Don't allow deleting multiple objects if one of the objects is inside an annex
		if (selectedDiagramElements.size() > 1 && anyIsInAnnex(selectedDiagramElements)) {
			return false;
		}

		// If DeleteRaw is going to be used, then only allow deleting a single element at a time.
		if (anyRequiresRawDelete(selectedDiagramElements) && selectedDiagramElements.size() != 1) {
			return false;
		}

		for (final DiagramElement de : selectedDiagramElements) {
			if (!canDelete(de)) {
				return false;
			}
		}

		return true;
	}

	private boolean canDelete(final DiagramElement de) {
		final Object bo = de.getBusinessObject();

		final BusinessObjectHandler boHandler = de.getBusinessObjectHandler();
		if (boHandler == null) {
			return false;
		}

		// Don't allow proxies.
		if (bo instanceof EObject) {
			final EObject eobj = ((EObject) bo);
			if (eobj.eIsProxy()) {
				return false;
			}

			// Prevent deletion of resources which are part of plugins
			final Resource res = eobj.eResource();
			if (res != null && res.getURI().isPlatformPlugin()) {
				return false;
			}
		}

		return boHandler.canDelete(new CanDeleteContext(bo));
	}

	@Override
	public Object execute(final ExecutionEvent event) throws ExecutionException {
		final IEditorPart activeEditor = HandlerUtil.getActiveEditor(event);
		if (!(activeEditor instanceof InternalDiagramEditor)) {
			throw new RuntimeException("Unexpected editor: " + activeEditor);
		}

		final InternalDiagramEditor ageEditor = (InternalDiagramEditor) activeEditor;

		final ReferenceBuilderService refBuilder = Objects.requireNonNull(
				(ReferenceBuilderService) ageEditor.getAdapter(ReferenceBuilderService.class),
				"Unable to retrieve reference builder service");
		final AadlModificationService aadlModificationService = Objects.requireNonNull(
				(AadlModificationService) ageEditor.getAdapter(AadlModificationService.class),
				"Unable to retrieve modification service");

		// Get diagram and selected elements
		final List<DiagramElement> selectedDiagramElements = AgeHandlerUtil.getSelectedDiagramElements();
		if (!canExecute(selectedDiagramElements)) {
			throw new RuntimeException("canExecute() returned false");
		}

		if (!confirmDelete(selectedDiagramElements)) {
			return null;
		}

		final boolean boIsContext = anyBoIsDiagramContext(selectedDiagramElements, ageEditor.getDiagram(),
				refBuilder);

		ageEditor.getActionExecutor().execute("Delete", ExecutionMode.NORMAL, () -> {
			if (anyRequiresRawDelete(selectedDiagramElements)) {
				if (selectedDiagramElements.size() != 1) {
					throw new RuntimeException("Deleting multiple elements when using DeleteRaw is not supported");
				}

				final DiagramElement deToDelete = selectedDiagramElements.get(0);

				// This is safe because we have already check that at least one business object requires a raw deletion and there is exactly one
				// business object being deleted.
				final RawDeleter deleter = (RawDeleter) deToDelete.getBusinessObjectHandler();
				deleter.delete(new RawDeleteContext(deToDelete.getBusinessObject()));
			} else if (anyIsInAnnex(selectedDiagramElements)) {
				if (selectedDiagramElements.size() != 1) {
					throw new RuntimeException(
							"Deleting multiple elements when deleting an element inside of an annex is not supported");
				}

				// Handle annex specially because we need to modify the annex itself instead of the root of the model.
				// Only a single annex element can be modified at a time because modifying annex elements as part of a
				// group is not supported.
				final BusinessObjectRemoval modInfo = createBusinessObjectRemovalOrRemoveDiagramElement(
						selectedDiagramElements.get(0));
				if (modInfo != null) {
					aadlModificationService.modify(modInfo.staleBoToModify, (boToModify) -> {
						modInfo.remover.accept(boToModify);
					});
				}
			} else {
				// Group elements to be removed by resource. All the elements will be removed as part of the same modification.
				// This ensures that the appropriate element is retrieved regardless of the order in the model or the URI scheme.
				final ListMultimap<Resource, BusinessObjectRemoval> removals = ArrayListMultimap.create();
				for (final DiagramElement de : selectedDiagramElements) {
					final BusinessObjectRemoval removal = createBusinessObjectRemovalOrRemoveDiagramElement(de);
					if (removal != null) {
						removals.put(removal.staleBoToModify.eResource(), removal);
					}
				}

				// Perform the modifications. One modification will be performed for each resource.
				final List<AadlModificationService.Modification<?, ?>> modifications = new ArrayList<>();
				for (final Entry<Resource, Collection<BusinessObjectRemoval>> entry : removals.asMap().entrySet()) {
					final Resource resource = entry.getKey();
					final EObject root = Objects.requireNonNull(resource.getContents().get(0),
							"unable to retrieve root element");
					final Modification<EObject, EObject> mod = AadlModificationService.Modification.create(root,
							(rootToModify) -> {
								// Store objects which should be modified. This is performed before any modifications to the resource
								// so that the correct object will be removed regardless of URI scheme. Otherwise, the object's URI
								// could change in between removals and an incorrect element will be removed.
								for (final BusinessObjectRemoval removal : entry.getValue()) {
									final URI uri = EcoreUtil.getURI(removal.staleBoToModify);
									Objects.requireNonNull(uri,
											"unable to retrieve uri for " + removal.staleBoToModify);
									removal.boToModify = rootToModify.eResource().getResourceSet().getEObject(uri,
											true);
								}

								// Remove the business object using the stored business object to modify
								for (final BusinessObjectRemoval removal : entry.getValue()) {
									removal.remover.accept(removal.boToModify);
								}
							});
					modifications.add(mod);
				}

				if (!modifications.isEmpty()) {
					aadlModificationService.modify(modifications);
				}
			}

			return null;
		});

		if (boIsContext) {
			// Close the editor if the context was deleted
			Display.getDefault().syncExec(() -> ageEditor.closeEditor());
		} else {
			ageEditor.clearSelection();
		}

		return null;
	}

	/**
	 * Stores information regarding business objects being removed.
	 *
	 */
	private static class BusinessObjectRemoval {
		public final EObject staleBoToModify; // The non-live version of the business object which should be passed to the remover.
		public final Consumer<EObject> remover; // Performs the removal by modifying the passed in business object.
		public EObject boToModify; // Populated during the modification process. The live version of the owner business object.

		public BusinessObjectRemoval(final EObject staleBoToModify, final Consumer<EObject> remover) {
			this.staleBoToModify = Objects.requireNonNull(staleBoToModify, "staleBoToModify must not be null");
			this.remover = Objects.requireNonNull(remover, "remover must not be null");
		}
	}

	/**
	 * Creates a BusinessObjectRemoval object which can be used to remove the business object for the diagram element.
	 * If the diagram element's business object is an embedded business object, remove the element.
	 * @param de
	 * @return
	 */
	private static BusinessObjectRemoval createBusinessObjectRemovalOrRemoveDiagramElement(final DiagramElement de) {
		// Remove the EObject from the model
		final Object bo = de.getBusinessObject();

		final Object boHandler = de.getBusinessObjectHandler();
		if (bo instanceof EObject) {
			EObject boEObj = (EObject) bo;

			if (boHandler instanceof CustomDeleter) {
				final CustomDeleter deleter = (CustomDeleter) boHandler;
				final EObject ownerBo = boEObj.eContainer();
				return new BusinessObjectRemoval(ownerBo, (boToModify) -> {
					deleter.delete(new CustomDeleteContext(boToModify, bo));
				});
			}

			// When deleting AnnexSubclauses, the deletion must executed on the container DefaultAnnexSubclause
			if (boEObj instanceof AnnexSubclause && boEObj.eContainer() instanceof DefaultAnnexSubclause) {
				boEObj = boEObj.eContainer();
			}

			return new BusinessObjectRemoval(boEObj, (boToModify) -> EcoreUtil.remove(boToModify));
		} else if (bo instanceof EmfContainerProvider) {
			if(!(boHandler instanceof CustomDeleter)) {
				throw new RuntimeException("Business object handler '" + boHandler + "' for "
						+ EmfContainerProvider.class.getName() + " based business object must implement "
						+ CustomDeleter.class.getCanonicalName() + ".");
			}

			final CustomDeleter deleter = (CustomDeleter) boHandler;
			final EObject ownerBo = ((EmfContainerProvider) bo).getEmfContainer();

			return new BusinessObjectRemoval(ownerBo, (boToModify) -> {
				deleter.delete(new CustomDeleteContext(boToModify, bo));
			});
		} else if (bo instanceof EmbeddedBusinessObject) {
			// For embedded business objects, there isn't a model from which to remove the business object.
			// Instead, we remove the diagram element and return null.
			final AgeDiagram diagram = DiagramElementUtil.getDiagram(de);
			diagram.modify("Delete Element", m -> m.removeElement(de));
			return null;
		} else {
			// canDelete() should have returned false in this case
			throw new RuntimeException("Unhandled case: " + bo);
		}
	}

	private static boolean confirmDelete(final List<DiagramElement> diagramElements) {
		if (diagramElements.size() == 1) {
			final Object bo = diagramElements.get(0).getBusinessObject();
			if (bo == null) {
				return false;
			}

			final String elementName = (bo instanceof NamedElement) ? ((NamedElement) bo).getQualifiedName() : null;
			final String msg = (elementName != null)
					? MessageFormat.format("Are you sure you want to delete ''{0}''?", elementName)
							: "Are you sure you want to delete this element?";
					if (!MessageDialog.openQuestion(PlatformUI.getWorkbench().getActiveWorkbenchWindow().getShell(),
							"Confirm Delete", msg)) {
						return false;
					}
		} else {
			final String msg = MessageFormat.format("Are you sure you want to delete {0} elements?",
					diagramElements.size());
			if (!MessageDialog.openQuestion(PlatformUI.getWorkbench().getActiveWorkbenchWindow().getShell(),
					"Confirm Delete", msg)) {
				return false;
			}
		}

		return true;
	}

	private static boolean anyBoIsDiagramContext(final List<DiagramElement> diagramElements, final AgeDiagram diagram,
			final ReferenceBuilderService refBuilder) {
		final CanonicalBusinessObjectReference diagramContextRef = diagram.getConfiguration().getContextBoReference();
		if (diagramContextRef == null) {
			return false;
		}

		return diagramElements.stream().map(de -> refBuilder.getCanonicalReference(de.getBusinessObject()))
				.filter(Predicates.notNull()).anyMatch(boRef -> boRef.equals(diagramContextRef));
	}

	private static boolean anyIsInAnnex(final List<DiagramElement> diagramElements) {
		return diagramElements.stream().map(DiagramElement::getBusinessObject).anyMatch(DeleteHandler::isInAnnex);
	}

	private static boolean anyRequiresRawDelete(final List<DiagramElement> diagramElements) {
		return diagramElements.stream()
				.anyMatch(de -> de.getBusinessObjectHandler() instanceof RawDeleter);
	}

	private static boolean isInAnnex(final Object bo) {
		for (Object tmp = getContainer(bo); tmp != null; tmp = getContainer(tmp)) {
			if (tmp instanceof AnnexLibrary || tmp instanceof AnnexSubclause) {
				return true;
			}
		}

		return false;
	}

	private static Object getContainer(final Object bo) {
		if (bo instanceof EmfContainerProvider) {
			return ((EmfContainerProvider) bo).getEmfContainer();
		} else if (bo instanceof EObject) {
			return ((EObject) bo).eContainer();
		} else {
			return null;
		}
	}
}