| /** |
| ******************************************************************************** |
| * Copyright (c) 2015-2021 Robert Bosch GmbH and others. |
| * |
| * 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 |
| * |
| * Contributors: |
| * Robert Bosch GmbH - initial API and implementation |
| ******************************************************************************** |
| */ |
| |
| package org.eclipse.app4mc.amalthea.model; |
| |
| import static com.google.common.base.Preconditions.checkArgument; |
| |
| import java.io.PrintStream; |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Objects; |
| import java.util.Set; |
| import java.util.regex.Pattern; |
| import java.util.stream.Collectors; |
| |
| import org.eclipse.emf.common.notify.Adapter; |
| import org.eclipse.emf.common.notify.Notifier; |
| import org.eclipse.emf.common.util.EList; |
| import org.eclipse.emf.common.util.TreeIterator; |
| import org.eclipse.emf.common.util.URI; |
| 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.InternalEObject; |
| import org.eclipse.emf.ecore.resource.Resource; |
| import org.eclipse.emf.ecore.resource.ResourceSet; |
| import org.eclipse.emf.ecore.util.EcoreEList; |
| import org.eclipse.emf.ecore.util.EcoreUtil; |
| import org.eclipse.jdt.annotation.NonNull; |
| import org.eclipse.jdt.annotation.Nullable; |
| |
| /** |
| * This class provides static methods for efficient model navigation, search and delete. |
| * <p> |
| * An {@link AmaltheaCrossReferenceAdapter} is created and attached to the root context of the model. |
| * The adapter maintains an index that allows fast access to inverse references and an index based on object name. |
| * <p> |
| * The method {@link AmaltheaIndex#getInverseReferences getInverseReferences} is called from several |
| * generated model objects to compute inverse transient references based on the incoming references. |
| */ |
| public final class AmaltheaIndex { |
| |
| // Suppress default constructor |
| private AmaltheaIndex() { |
| throw new IllegalStateException("Utility class"); |
| } |
| |
| private static final String ARG_OBJECT_MESSAGE = "Object argument is null, expected: EObject"; |
| private static final String ARG_COLLECTION_MESSAGE = "Collection argument is null, expected: Collection<? extends EObject>"; |
| private static final String ARG_NOTIFIER_MESSAGE = "Context argument is null, expected: Notifier"; |
| private static final String ARG_STREAM_MESSAGE = "Stream argument is null, expected: PrintStream"; |
| private static final String ARG_NAME_MESSAGE = "Name argument is null, expected: String"; |
| private static final String ARG_PATTERN_MESSAGE = "Pattern argument is null, expected: Pattern"; |
| private static final String ARG_CLASS_MESSAGE = "Class argument is null, expected: Class<T extends INamed>"; |
| private static final String ARG_RESULT_REFERENCE_MESSAGE = "Result reference argument is null, expected: EReference"; |
| private static final String ARG_TARGET_REFERENCES_MESSAGE = "Target references argument is null, expected: Set<EReference>"; |
| private static final String ARG_TARGET_REFERENCE_MESSAGE = "Target reference argument is null, expected: EReference"; |
| |
| /** |
| * Computes a list of objects that refer to an EObject via dedicated references. |
| * <p> |
| * This method is called from several generated model objects to compute |
| * an inverse transient reference based on the incoming references. |
| * |
| * @param eObject object that implements a derived transient opposite reference |
| * @param resultEReference opposite reference definition (resulting objects are computed) |
| * @param targetEReferences list of incoming references |
| * @return |
| * EcoreEList.UnmodifiableEList <T> |
| */ |
| public static <T> EList<T> getInverseReferences(final @NonNull EObject eObject, final @NonNull EReference resultEReference, |
| final @NonNull Set<@NonNull EReference> targetEReferences) { |
| checkArgument(eObject != null, ARG_OBJECT_MESSAGE); |
| checkArgument(resultEReference != null, ARG_RESULT_REFERENCE_MESSAGE); |
| checkArgument(targetEReferences != null, ARG_TARGET_REFERENCES_MESSAGE); |
| |
| final AmaltheaCrossReferenceAdapter amaltheaAdapter = getOrCreateAmaltheaAdapter(eObject); |
| |
| // Get references from adapter |
| |
| final List<EObject> result = new ArrayList<>(); |
| final Collection<Setting> nonNavigableInverseReferences = amaltheaAdapter |
| .getNonNavigableInverseReferences(eObject); |
| for (final Setting setting : nonNavigableInverseReferences) { |
| if (targetEReferences.contains(setting.getEStructuralFeature())) { |
| final EObject targetEObject = setting.getEObject(); |
| result.add(targetEObject); |
| } |
| } |
| |
| // Return immutable list |
| |
| final int size = result.size(); |
| final Object[] values = result.toArray(); |
| return new EcoreEList.UnmodifiableEList<>((InternalEObject) eObject, resultEReference, size, values); |
| } |
| |
| /** |
| * Returns a set of objects that refer to the given <code>eObject</code>. |
| * |
| * @param eObject |
| * @return Set of objects (EObject) |
| */ |
| public static Set<EObject> getReferringObjects(final @NonNull EObject eObject) { |
| checkArgument(eObject != null, ARG_OBJECT_MESSAGE); |
| |
| return getOrCreateAmaltheaAdapter(eObject).getNonNavigableInverseReferences(eObject) |
| .stream() |
| .map(Setting::getEObject) |
| .collect(Collectors.toSet()); |
| } |
| |
| /** |
| * Returns a type filtered set of objects that refer to the given <code>eObject</code>. |
| * |
| * @param eObject |
| * @param targetClass |
| * @return Set of objects of given type <T extends EObject> |
| */ |
| public static <T extends EObject> Set<T> getReferringObjects(final @NonNull EObject eObject, final @NonNull Class<T> targetClass) { |
| checkArgument(eObject != null, ARG_OBJECT_MESSAGE); |
| checkArgument(targetClass != null, ARG_CLASS_MESSAGE); |
| |
| return getOrCreateAmaltheaAdapter(eObject).getNonNavigableInverseReferences(eObject) |
| .stream() |
| .map(Setting::getEObject) |
| .filter(targetClass::isInstance) |
| .map(targetClass::cast) |
| .collect(Collectors.toSet()); |
| } |
| |
| /** |
| * Returns a filtered set of objects that refer to the given <code>eObject</code> |
| * <p> |
| * Filters: |
| * <ul> |
| * <li>type (<code>targetClass</code>)</li> |
| * <li>reference (<code>targetEReference</code>)</li> |
| * </ul> |
| * |
| * @param eObject |
| * @param targetClass |
| * @param targetEReference |
| * @return Set of objects of given type <T extends EObject> |
| */ |
| public static <T extends EObject> Set<T> getReferringObjects(final @NonNull EObject eObject, final @NonNull Class<T> targetClass, final @NonNull EReference targetEReference) { |
| checkArgument(eObject != null, ARG_OBJECT_MESSAGE); |
| checkArgument(targetEReference != null, ARG_TARGET_REFERENCE_MESSAGE); |
| checkArgument(targetClass != null, ARG_CLASS_MESSAGE); |
| |
| return getOrCreateAmaltheaAdapter(eObject).getNonNavigableInverseReferences(eObject) |
| .stream() |
| .filter(setting -> setting.getEStructuralFeature() == targetEReference) |
| .map(Setting::getEObject) |
| .filter(targetClass::isInstance) |
| .map(targetClass::cast) |
| .collect(Collectors.toSet()); |
| } |
| |
| public static IReferable getEObjectByID(final @NonNull Notifier context, final @Nullable String id) { |
| checkArgument(context != null, ARG_NOTIFIER_MESSAGE); |
| |
| if (id == null || id.isEmpty()) return null; |
| |
| int beginIndex = id.contains("/") ? id.lastIndexOf('/') + 1 : 0; |
| int endIndex = id.indexOf("?type="); |
| if (endIndex > beginIndex) { |
| String name = id.substring(beginIndex, endIndex); |
| if (name.equals("no-name")) name = ""; |
| |
| for (IReferable elem : getElements(context, name, IReferable.class)) { |
| // first check the type because computation of unique name is more expensive |
| if (id.endsWith(elem.eClass().getName()) && id.equals(elem.getUniqueName())) { |
| return elem; |
| } |
| } |
| } |
| |
| return null; |
| } |
| |
| public static List<Set<IReferable>> getObjectsWithConflictingNames(final @NonNull Notifier context) { |
| checkArgument(context != null, ARG_NOTIFIER_MESSAGE); |
| |
| // call index |
| @NonNull List<@NonNull Set<@NonNull IReferable>> list = getOrCreateAmaltheaAdapter(context).getObjectsWithConflictingNames(); |
| |
| // filter scope |
| List<Set<IReferable>> result = new ArrayList<>(); |
| for (Set<IReferable> initialSet : list) { |
| Set<IReferable> filteredSet = filterElements(context, initialSet); |
| if (filteredSet.size() > 1) { |
| result.add(filteredSet); |
| } |
| } |
| |
| return result; |
| } |
| |
| /** |
| * Creates the index explicitly. If the index is already available the method will have no effect. |
| * <p> |
| * This method is optional. Normally the index will be built on demand. |
| * |
| * @param eObject |
| */ |
| public static void buildIndex(final @NonNull Notifier context) { |
| checkArgument(context != null, ARG_NOTIFIER_MESSAGE); |
| |
| getOrCreateAmaltheaAdapter(context); |
| } |
| |
| /** |
| * Deletes the object from its {@link EObject#eResource containing} resource |
| * and/or its {@link EObject#eContainer containing} object as well as from any |
| * other feature that references it within the enclosing root context |
| * (resource set, resource or root object). Contained children of the object |
| * are similarly removed from any features that reference them. |
| * |
| * @param eObject object to delete |
| */ |
| public static void delete(final @NonNull EObject eObject) { |
| checkArgument(eObject != null, ARG_OBJECT_MESSAGE); |
| |
| ArrayList<EObject> list = new ArrayList<>(); |
| list.add(eObject); |
| deleteAll(list, true); |
| } |
| |
| |
| /** |
| * Deletes the objects from their {@link EObject#eResource containing} resource |
| * and/or their {@link EObject#eContainer containing} object as well as from any |
| * other feature that references it within the enclosing root context |
| * (resource set, resource or root object). Contained children of the object |
| * are similarly removed from any features that reference them. |
| * |
| * @param eObjects objects to delete |
| */ |
| public static void deleteAll(final @NonNull Collection<? extends EObject> eObjects) { |
| checkArgument(eObjects != null, ARG_COLLECTION_MESSAGE); |
| |
| deleteAll(eObjects, true); |
| } |
| |
| |
| /** |
| * Deletes the objects from their {@link EObject#eResource containing} resource |
| * and/or their {@link EObject#eContainer containing} object as well as from any |
| * other feature that references it within the enclosing root context |
| * (resource set, resource or root object). |
| * If recursive is true, contained children of the object are similarly removed |
| * from any features that reference them. |
| * |
| * @param eObjects objects to delete |
| * @param recursive true: contained children should also be deleted |
| */ |
| public static void deleteAll(final @NonNull Collection<? extends EObject> eObjects, boolean recursive) { |
| checkArgument(eObjects != null, ARG_COLLECTION_MESSAGE); |
| |
| @SuppressWarnings("null") |
| Set<@NonNull ? extends EObject> objectsToDelete = eObjects.stream() |
| .filter(Objects::nonNull).collect(Collectors.toSet()); |
| |
| // extended argument check: ensure a common context |
| |
| Notifier commonContext = ensureCommonContext(objectsToDelete); |
| if (commonContext == null) |
| return; |
| |
| // collect objects to delete |
| |
| Set<EObject> eAllObjects = new HashSet<>(); |
| if (recursive) { |
| // add contained objects |
| for (EObject eObject : objectsToDelete) { |
| for (TreeIterator<EObject> j = eObject.eAllContents(); j.hasNext();) { |
| InternalEObject childEObject = (InternalEObject) j.next(); |
| eAllObjects.add(childEObject); |
| } |
| } |
| } |
| // add original objects |
| eAllObjects.addAll(objectsToDelete); |
| |
| // delete the objects |
| |
| final AmaltheaCrossReferenceAdapter amaltheaAdapter = getOrCreateAmaltheaAdapter(commonContext); |
| for (final EObject eObject : eAllObjects) { |
| final Collection<Setting> inverseReferences = amaltheaAdapter.getInverseReferences(eObject); |
| for (final EStructuralFeature.Setting settingObject : inverseReferences) { |
| if (settingObject.getEStructuralFeature().isChangeable()) { |
| // remove the reference relation |
| EcoreUtil.remove(settingObject, eObject); |
| } |
| } |
| |
| // remove the element |
| EcoreUtil.remove(eObject); |
| } |
| } |
| |
| private static Notifier ensureCommonContext(Set<@NonNull ? extends EObject> objectsToDelete) { |
| Notifier commonContext = null; |
| for (final EObject eObject : objectsToDelete) { |
| final Notifier context = getRootContext(eObject); |
| if (commonContext == null) { |
| commonContext = context; |
| } else if (commonContext != context) { |
| throw new IllegalArgumentException("Objects don't share a common context"); |
| } |
| } |
| return commonContext; |
| } |
| |
| /** |
| * Finds elements by class |
| * |
| * @param context EObject, Resource or ResourceSet |
| * @param targetClass for example: <code>Label.class</code> |
| * @return Set of named objects (INamed) |
| */ |
| public static <T extends INamed> Set<T> getElements(final @NonNull Notifier context, final @NonNull Class<T> targetClass) { |
| checkArgument(context != null, ARG_NOTIFIER_MESSAGE); |
| checkArgument(targetClass != null, ARG_CLASS_MESSAGE); |
| |
| @NonNull Set<@NonNull T> elements = getOrCreateAmaltheaAdapter(context).getElements(targetClass); |
| |
| return filterElements(context, elements); |
| } |
| |
| /** |
| * Finds elements by name and class |
| * |
| * @param context EObject, Resource or ResourceSet |
| * @param name String |
| * @param targetClass for example: <code>Label.class</code> |
| * @return Set of named objects (INamed) |
| */ |
| public static <T extends INamed> Set<T> getElements(final @NonNull Notifier context, final @NonNull String name, |
| final @NonNull Class<T> targetClass) { |
| checkArgument(context != null, ARG_NOTIFIER_MESSAGE); |
| checkArgument(name != null, ARG_NAME_MESSAGE); |
| checkArgument(targetClass != null, ARG_CLASS_MESSAGE); |
| |
| @NonNull Set<@NonNull T> elements = getOrCreateAmaltheaAdapter(context).getElements(name, targetClass); |
| |
| return filterElements(context, elements); |
| } |
| |
| /** |
| * Finds elements by name pattern and class |
| * |
| * @param context EObject, Resource or ResourceSet |
| * @param namePattern for example: <code>Pattern.compile("Prefix_.*")</code> |
| * @param targetClass for example: <code>Label.class</code> |
| * @return Set of named objects (INamed) |
| */ |
| public static <T extends INamed> Set<T> getElements(final @NonNull Notifier context, final @NonNull Pattern namePattern, |
| final @NonNull Class<T> targetClass) { |
| checkArgument(context != null, ARG_NOTIFIER_MESSAGE); |
| checkArgument(namePattern != null, ARG_PATTERN_MESSAGE); |
| checkArgument(targetClass != null, ARG_CLASS_MESSAGE); |
| |
| @NonNull Set<@NonNull T> elements = getOrCreateAmaltheaAdapter(context).getElements(namePattern, targetClass); |
| |
| return filterElements(context, elements); |
| } |
| |
| /** |
| * Dumps adapter info to a print stream |
| * |
| * @param context EObject, Resource or ResourceSet |
| * @param info content selector <ul> |
| * <li> 1 - basic adapter info (resources, size of maps) |
| * <li> 2 - cross reference map |
| * <li> 3 - name index </ul> |
| * @param stream output stream (use System.out to print to console) |
| */ |
| public static void dumpAdapterInfo(final @NonNull Notifier context, int info, final @NonNull PrintStream stream) { |
| checkArgument(context != null, ARG_NOTIFIER_MESSAGE); |
| checkArgument(stream != null, ARG_STREAM_MESSAGE); |
| |
| AmaltheaCrossReferenceAdapter adapter = getOrCreateAmaltheaAdapter(context); |
| |
| switch (info) { |
| case 1: adapter.dumpInfo(stream); break; |
| case 2: adapter.dumpCrossReferenceMap(stream); break; |
| case 3: adapter.dumpNameIndex(stream); break; |
| default: stream.append("undefined info selector: " + info); |
| } |
| } |
| |
| private static synchronized @NonNull AmaltheaCrossReferenceAdapter getOrCreateAmaltheaAdapter(final @NonNull Notifier target) { |
| // Try to get Amalthea adapter |
| final EList<Adapter> adapters = getRootContext(target).eAdapters(); |
| for (final Adapter adapter : adapters) { |
| if (adapter instanceof AmaltheaCrossReferenceAdapter) { |
| return (AmaltheaCrossReferenceAdapter) adapter; |
| } |
| } |
| |
| // Create Amalthea adapter |
| final AmaltheaCrossReferenceAdapter amaltheaAdapter = new AmaltheaCrossReferenceAdapter(); |
| adapters.add(amaltheaAdapter); |
| return amaltheaAdapter; |
| } |
| |
| /** |
| * Gets the root context of an EObject |
| */ |
| private static @NonNull Notifier getRootContext(final @NonNull EObject eObject) { |
| final EObject rootContainer = EcoreUtil.getRootContainer(eObject); |
| final Resource resource = rootContainer.eResource(); |
| if (resource != null) { |
| return getRootContext(resource); |
| } |
| |
| return rootContainer; |
| } |
| |
| /** |
| * Gets the root context of a Resource |
| */ |
| private static @NonNull Notifier getRootContext(final @NonNull Resource resource) { |
| final ResourceSet resourceSet = resource.getResourceSet(); |
| if (resourceSet != null) { |
| return resourceSet; |
| } |
| |
| return resource; |
| } |
| |
| /** |
| * Gets the root context of a Notifier (EObject, Resource or Resource Set) |
| */ |
| private static @NonNull Notifier getRootContext(final @NonNull Notifier notifier) { |
| if (notifier instanceof EObject) { |
| return getRootContext((EObject) notifier); |
| } |
| else if (notifier instanceof Resource) { |
| return getRootContext((Resource) notifier); |
| } |
| |
| return notifier; |
| } |
| |
| /** |
| * Gets the resource of a Notifier (EObject, Resource or Resource Set) |
| */ |
| private static Resource getResource(final Notifier notifier) { |
| if (notifier instanceof EObject) { |
| return((EObject) notifier).eResource(); |
| } |
| else if (notifier instanceof Resource) { |
| return (Resource) notifier; |
| } |
| |
| return null; |
| } |
| |
| /** |
| * Removes elements out of scope (to handle larger editing domains / resource sets) |
| */ |
| private static <T extends INamed> Set<T> filterElements(final @NonNull Notifier context, Set<@NonNull T> elements) { |
| Notifier rootContext = getRootContext(context); |
| Resource resource = getResource(context); |
| if (rootContext instanceof ResourceSet && resource != null) { |
| URI folderUri = resource.getURI().trimSegments(1); |
| return elements.stream() |
| .filter(e -> e.eResource().getURI().trimSegments(1).equals(folderUri)) |
| .collect(Collectors.toSet()); |
| } |
| |
| return elements; |
| } |
| |
| } |