From ee51d976f097f7b725a7030d156c1a458a121e84 Mon Sep 17 00:00:00 2001 From: Laurent Fasani Date: Tue, 13 Sep 2022 14:19:59 +0200 Subject: [PATCH] [1301] Add a cross reference adapter to clean the inverse references This adapter specializes ECrossReferenceAdapter in case of deletion of an EObject or a Resource in order to: * clean the proxies of the inverse references * clean the ECrossReferenceAdapter itself Bug: https://github.com/eclipse-sirius/sirius-components/issues/1301 Signed-off-by: Laurent Fasani --- .../EditingContextCrossReferenceAdapter.java | 118 ++++++++++++++++++ ...itingContextCrossReferenceAdapterTest.java | 114 +++++++++++++++++ 2 files changed, 232 insertions(+) create mode 100644 packages/emf/backend/sirius-components-emf/src/main/java/org/eclipse/sirius/components/emf/services/EditingContextCrossReferenceAdapter.java create mode 100644 packages/emf/backend/sirius-components-emf/src/test/java/org/eclipse/sirius/components/emf/services/EditingContextCrossReferenceAdapterTest.java diff --git a/packages/emf/backend/sirius-components-emf/src/main/java/org/eclipse/sirius/components/emf/services/EditingContextCrossReferenceAdapter.java b/packages/emf/backend/sirius-components-emf/src/main/java/org/eclipse/sirius/components/emf/services/EditingContextCrossReferenceAdapter.java new file mode 100644 index 0000000000..ce4be6c137 --- /dev/null +++ b/packages/emf/backend/sirius-components-emf/src/main/java/org/eclipse/sirius/components/emf/services/EditingContextCrossReferenceAdapter.java @@ -0,0 +1,118 @@ +/******************************************************************************* + * Copyright (c) 2022 CEA. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ +package org.eclipse.sirius.components.emf.services; + +import java.util.ArrayList; +import java.util.Collection; + +import org.eclipse.emf.common.notify.Notification; +import org.eclipse.emf.common.util.TreeIterator; +import org.eclipse.emf.ecore.EObject; +import org.eclipse.emf.ecore.EReference; +import org.eclipse.emf.ecore.EStructuralFeature; +import org.eclipse.emf.ecore.EStructuralFeature.Setting; +import org.eclipse.emf.ecore.resource.Resource; +import org.eclipse.emf.ecore.util.ECrossReferenceAdapter; +import org.eclipse.emf.ecore.util.EcoreUtil; + +/** + * A {@link ECrossReferenceAdapter}@ that is able to clean dangling references when an object is deleted or when a + * resource is removed from the ResourceSet. + * + * @author lfasani + */ +public class EditingContextCrossReferenceAdapter extends ECrossReferenceAdapter { + + @Override + protected void handleContainment(Notification notification) { + Object oldValue = notification.getOldValue(); + int eventType = notification.getEventType(); + + if (eventType == Notification.REMOVE) { + this.handleRemoveObject(oldValue); + + } else if (eventType == Notification.REMOVE_MANY) { + for (Object object : (Collection) oldValue) { + this.handleRemoveObject(object); + } + } else { + super.handleContainment(notification); + } + } + + private void handleRemoveObject(Object object) { + if (object instanceof Resource) { + this.handleRemoveResource((Resource) object); + } else if (object instanceof EObject) { + this.handleRemoveEObject((EObject) object); + } + } + + private void handleRemoveEObject(EObject eObject) { + // Remove all inverse references + this.clearReferencesTo(eObject); + TreeIterator allProperContents = EcoreUtil.getAllProperContents(eObject, false); + while (allProperContents.hasNext()) { + Object object = allProperContents.next(); + if (object instanceof EObject) { + this.clearReferencesTo((EObject) object); + } + } + + // clean the crossRefererenceAdapter + this.unsetTarget(eObject); + } + + private void handleRemoveResource(Resource resource) { + // Remove all inverse references + TreeIterator allProperContents = EcoreUtil.getAllProperContents(resource, false); + while (allProperContents.hasNext()) { + Object object = allProperContents.next(); + if (object instanceof EObject) { + this.clearReferencesTo((EObject) object); + } + } + + // clean the crossRefererenceAdapter + this.unsetTarget(resource); + } + + private void clearReferencesTo(EObject referencedObject) { + Collection settings = this.getInverseReferences(referencedObject); + + if (settings != null) { + for (Setting setting : this.getNonContainmentReferences(settings)) { + try { + EcoreUtil.remove(setting, referencedObject); + } catch (NullPointerException e) { + // thrown when trying to remove from an object that is present in the being removed Resource. + // But we do not care about this resource. + } + } + } + } + + private Collection getNonContainmentReferences(Collection inverseReferences) { + Collection containmentReferences = new ArrayList<>(); + for (EStructuralFeature.Setting setting : inverseReferences) { + EStructuralFeature eStructuralFeature = setting.getEStructuralFeature(); + if (eStructuralFeature instanceof EReference) { + EReference eReference = (EReference) eStructuralFeature; + if (!eReference.isContainment()) { + containmentReferences.add(setting); + } + } + } + return containmentReferences; + } +} diff --git a/packages/emf/backend/sirius-components-emf/src/test/java/org/eclipse/sirius/components/emf/services/EditingContextCrossReferenceAdapterTest.java b/packages/emf/backend/sirius-components-emf/src/test/java/org/eclipse/sirius/components/emf/services/EditingContextCrossReferenceAdapterTest.java new file mode 100644 index 0000000000..d2151b5e26 --- /dev/null +++ b/packages/emf/backend/sirius-components-emf/src/test/java/org/eclipse/sirius/components/emf/services/EditingContextCrossReferenceAdapterTest.java @@ -0,0 +1,114 @@ +/******************************************************************************* + * Copyright (c) 2022 CEA. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Obeo - initial API and implementation + *******************************************************************************/ +package org.eclipse.sirius.components.emf.services; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.UUID; + +import org.eclipse.emf.ecore.EClass; +import org.eclipse.emf.ecore.EPackage; +import org.eclipse.emf.ecore.EReference; +import org.eclipse.emf.ecore.EcoreFactory; +import org.eclipse.emf.ecore.resource.Resource; +import org.eclipse.emf.ecore.resource.ResourceSet; +import org.eclipse.emf.ecore.resource.impl.ResourceSetImpl; +import org.junit.jupiter.api.Test; + +/** + * Tests the EditingContextCrossReferenceAdapter class. + * + * @author lfasani + */ +public class EditingContextCrossReferenceAdapterTest { + private EReference eReference; + + private EClass referencedClass; + + private Resource referencedResource; + + @Test + public void testRemoveResource() { + ResourceSet resourceSet = new ResourceSetImpl(); + this.createModel(resourceSet); + + EditingContextCrossReferenceAdapter editingContextCrossReferenceAdapter = new EditingContextCrossReferenceAdapter(); + resourceSet.eAdapters().add(editingContextCrossReferenceAdapter); + + assertThat(this.eReference.getEType()).isNotNull().isEqualTo(this.referencedClass); + assertThat(editingContextCrossReferenceAdapter.getInverseReferences(this.referencedClass)).isNotEmpty().filteredOn(setting -> { + Object object = setting.getEObject().eGet(setting.getEStructuralFeature()); + return object != null && object.equals(this.referencedClass); + }).isNotEmpty(); + + resourceSet.getResources().remove(this.referencedResource); + + this.checkClean(editingContextCrossReferenceAdapter); + + } + + @Test + public void testRemoveReferencedEObject() { + ResourceSet resourceSet = new ResourceSetImpl(); + this.createModel(resourceSet); + + EditingContextCrossReferenceAdapter editingContextCrossReferenceAdapter = new EditingContextCrossReferenceAdapter(); + resourceSet.eAdapters().add(editingContextCrossReferenceAdapter); + + assertThat(this.eReference.getEType()).isNotNull().isEqualTo(this.referencedClass); + assertThat(editingContextCrossReferenceAdapter.getInverseReferences(this.referencedClass)).isNotEmpty().filteredOn(setting -> { + Object object = setting.getEObject().eGet(setting.getEStructuralFeature()); + return object != null && object.equals(this.referencedClass); + }).isNotEmpty(); + + this.referencedClass.getEPackage().getEClassifiers().remove(this.referencedClass); + + this.checkClean(editingContextCrossReferenceAdapter); + + } + + /** + * Check that the CrossReferenceAdapter and the proxies are correctly cleaned. + */ + private void checkClean(EditingContextCrossReferenceAdapter editingContextCrossReferenceAdapter) { + assertThat(this.eReference.getEType()).isNull(); + assertThat(editingContextCrossReferenceAdapter.getInverseReferences(this.eReference)).filteredOn(setting -> { + Object object = setting.getEObject().eGet(setting.getEStructuralFeature()); + return object != null && object.equals(this.referencedClass); + }).isEmpty(); + } + + private void createModel(ResourceSet resourceSet) { + + this.referencedResource = new JSONResourceFactory().createResourceFromPath(UUID.randomUUID().toString()); + resourceSet.getResources().add(this.referencedResource); + + EPackage ePackage = EcoreFactory.eINSTANCE.createEPackage(); + this.referencedResource.getContents().add(ePackage); + this.referencedClass = EcoreFactory.eINSTANCE.createEClass(); + this.referencedClass.setName("referencedClass"); //$NON-NLS-1$ + ePackage.getEClassifiers().add(this.referencedClass); + + Resource referencingResource = new JSONResourceFactory().createResourceFromPath(UUID.randomUUID().toString()); + resourceSet.getResources().add(referencingResource); + + ePackage = EcoreFactory.eINSTANCE.createEPackage(); + referencingResource.getContents().add(ePackage); + EClass referencingClass = EcoreFactory.eINSTANCE.createEClass(); + ePackage.getEClassifiers().add(referencingClass); + this.eReference = EcoreFactory.eINSTANCE.createEReference(); + referencingClass.getEReferences().add(this.eReference); + this.eReference.setEType(this.referencedClass); + } + +}