| /** |
| ******************************************************************************** |
| * Copyright (c) 2018-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.HashMap; |
| import java.util.HashSet; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Objects; |
| import java.util.Set; |
| import java.util.regex.Pattern; |
| import java.util.stream.Collectors; |
| |
| import org.eclipse.emf.common.notify.Notification; |
| import org.eclipse.emf.common.notify.Notifier; |
| import org.eclipse.emf.ecore.EObject; |
| import org.eclipse.emf.ecore.EStructuralFeature; |
| import org.eclipse.emf.ecore.resource.Resource; |
| import org.eclipse.emf.ecore.util.EContentAdapter; |
| import org.eclipse.emf.ecore.util.ECrossReferenceAdapter; |
| import org.eclipse.jdt.annotation.NonNull; |
| |
| public class AmaltheaCrossReferenceAdapter extends ECrossReferenceAdapter { |
| 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_STREAM_MESSAGE = "Stream argument is null, expected: PrintStream"; |
| |
| private final Map<String, Set<INamed>> nameIndex = new HashMap<>(); |
| |
| public @NonNull Set<@NonNull INamed> getElements(final @NonNull String name) { |
| checkArgument(name != null, ARG_NAME_MESSAGE); |
| |
| return getElements(name, INamed.class); |
| } |
| |
| @SuppressWarnings("null") |
| public <T extends INamed> @NonNull Set<@NonNull T> getElements(final @NonNull String name, final @NonNull Class<T> targetClass) { |
| checkArgument(name != null, ARG_NAME_MESSAGE); |
| checkArgument(targetClass != null, ARG_CLASS_MESSAGE); |
| |
| final Set<T> result = new HashSet<>(); |
| final Set<INamed> values = nameIndex.get(name); |
| if (values != null) { |
| for (final INamed namedObject : values) { |
| if (targetClass.isInstance(namedObject)) { |
| result.add(targetClass.cast(namedObject)); |
| } |
| } |
| } |
| return result; |
| } |
| |
| public @NonNull Set<@NonNull INamed> getElements(final @NonNull Pattern namePattern) { |
| checkArgument(namePattern != null, ARG_PATTERN_MESSAGE); |
| |
| return getElements(namePattern, INamed.class); |
| } |
| |
| @SuppressWarnings("null") |
| public <T extends INamed> @NonNull Set<@NonNull T> getElements(final @NonNull Pattern namePattern, final @NonNull Class<T> targetClass) { |
| checkArgument(namePattern != null, ARG_PATTERN_MESSAGE); |
| checkArgument(targetClass != null, ARG_CLASS_MESSAGE); |
| |
| final Set<T> result = new HashSet<>(); |
| for (final Map.Entry<String, Set<INamed>> entry : nameIndex.entrySet()) { |
| if (namePattern.matcher(entry.getKey()).matches()) { |
| for (final INamed namedObject : entry.getValue()) { |
| if (targetClass.isInstance(namedObject)) { |
| result.add(targetClass.cast(namedObject)); |
| } |
| } |
| } |
| } |
| return result; |
| } |
| |
| @SuppressWarnings("null") |
| public <T extends INamed> @NonNull Set<@NonNull T> getElements(final @NonNull Class<T> targetClass) { |
| checkArgument(targetClass != null, ARG_CLASS_MESSAGE); |
| |
| final Set<T> result = new HashSet<>(); |
| for (final Map.Entry<String, Set<INamed>> entry : nameIndex.entrySet()) { |
| for (final INamed namedObject : entry.getValue()) { |
| if (targetClass.isInstance(namedObject)) { |
| result.add(targetClass.cast(namedObject)); |
| } |
| } |
| } |
| return result; |
| } |
| |
| /** |
| * Returns sets of objects with the same type and same unique name |
| * |
| * @return Sets of objects |
| */ |
| @SuppressWarnings("null") |
| public @NonNull List<@NonNull Set<@NonNull IReferable>> getObjectsWithConflictingNames() { |
| final @NonNull List<@NonNull Set<@NonNull IReferable>> result = new ArrayList<>(); |
| |
| for (Set<INamed> objects : nameIndex.values()) { |
| if (objects.size() < 2) continue; |
| |
| Map<?, List<IReferable>> map1 = objects.stream() |
| .filter(IReferable.class::isInstance) |
| .map(IReferable.class::cast) |
| .collect(Collectors.groupingBy(IReferable::getClass)); |
| |
| for (List<IReferable> objectsWithSameType : map1.values()) { |
| if (objectsWithSameType.size() < 2) continue; |
| |
| Map<String, List<IReferable>> map2 = objectsWithSameType.stream() |
| .collect(Collectors.groupingBy(IReferable::getUniqueName)); |
| |
| for (List<IReferable> objectsWithSameUniqueName : map2.values()) { |
| if (objectsWithSameUniqueName.size() < 2) continue; |
| |
| result.add(new HashSet<>(objectsWithSameUniqueName)); |
| } |
| } |
| } |
| |
| return result; |
| } |
| |
| @Override |
| protected void selfAdapt(final Notification notification) { |
| final Object notifier = notification.getNotifier(); |
| if (notifier instanceof INamed && notification.getFeature() == AmaltheaPackage.eINSTANCE.getINamed_Name()) { |
| // Update the name index if an INamed's name is changed. |
| final INamed namedObj = (INamed) notifier; |
| |
| int eventType = notification.getEventType(); |
| if (eventType == Notification.UNSET) { |
| final String oldValue = notification.getOldStringValue(); |
| removeFromNameIndex(namedObj, oldValue); |
| } else if (eventType == Notification.SET) { |
| final String oldValue = notification.getOldStringValue(); |
| removeFromNameIndex(namedObj, oldValue); |
| final String newValue = notification.getNewStringValue(); |
| addToNameIndex(namedObj, newValue); |
| } |
| } else { |
| super.selfAdapt(notification); |
| } |
| } |
| |
| @Override |
| protected void setTarget(final EObject target) { |
| super.setTarget(target); |
| |
| if (target instanceof INamed) { |
| // Add the INamed's name to the name index. |
| final INamed namedObj = (INamed) target; |
| addToNameIndex(namedObj, namedObj.getName()); |
| } |
| } |
| |
| @Override |
| protected void unsetTarget(final EObject target) { |
| super.unsetTarget(target); |
| |
| if (target instanceof INamed) { |
| // Remove the INamed's name to the name index. |
| final INamed namedObj = (INamed) target; |
| removeFromNameIndex(namedObj, namedObj.getName()); |
| } |
| } |
| |
| private void addToNameIndex(final INamed eObject, final String name) { |
| if (name != null) { |
| Set<INamed> objSet = nameIndex.computeIfAbsent(name, k -> new HashSet<>()); |
| objSet.add(eObject); |
| } |
| } |
| |
| private void removeFromNameIndex(final INamed eObject, final String name) { |
| if (name != null) { |
| final Set<INamed> objSet = nameIndex.get(name); |
| if (objSet != null) { |
| objSet.remove(eObject); |
| if (objSet.isEmpty()) { |
| nameIndex.remove(name); |
| } |
| } |
| } |
| } |
| |
| /** |
| * Handles a containment change by adding and removing the adapter just like |
| * {@link EContentAdapter#handleContainment(Notification)}. |
| */ |
| @Override |
| protected void handleContainment(final Notification notification) { |
| switch (notification.getEventType()) { |
| case Notification.RESOLVE: |
| handleResolve(notification); |
| break; |
| case Notification.UNSET: |
| handleUnset(notification); |
| break; |
| case Notification.SET: |
| handleSet(notification); |
| break; |
| case Notification.ADD: |
| handleAdd(notification); |
| break; |
| case Notification.ADD_MANY: |
| handleAddMany(notification); |
| break; |
| case Notification.REMOVE: |
| handleRemove(notification); |
| break; |
| case Notification.REMOVE_MANY: |
| handleRemoveMany(notification); |
| break; |
| default: |
| // no action required |
| } |
| } |
| |
| private void handleResolve(final Notification notification) { |
| // We need to be careful that the proxy may be resolved while we are attaching this adapter. |
| // We need to avoid attaching the adapter during the resolve |
| // and also attaching it again as we walk the eContents() later. |
| // Checking here avoids having to check during addAdapter. |
| // |
| final Notifier oldValue = (Notifier) notification.getOldValue(); |
| if (oldValue.eAdapters().contains(this)) { |
| removeAdapter(oldValue); |
| final Notifier newValue = (Notifier) notification.getNewValue(); |
| addAdapter(newValue); |
| } |
| } |
| |
| private void handleUnset(final Notification notification) { |
| final Object oldValue = notification.getOldValue(); |
| if (oldValue != Boolean.TRUE && oldValue != Boolean.FALSE) { |
| if (oldValue != null) { |
| removeAdapter((Notifier) oldValue); |
| } |
| handleAdd(notification); |
| } |
| } |
| |
| private void handleSet(final Notification notification) { |
| handleRemove(notification); |
| handleAdd(notification); |
| } |
| |
| private void handleAdd(final Notification notification) { |
| final Notifier newValue = (Notifier) notification.getNewValue(); |
| if (newValue != null) { |
| addAdapter(newValue); |
| } |
| } |
| |
| private void handleAddMany(final Notification notification) { |
| @SuppressWarnings("unchecked") |
| final Collection<Notifier> newValues = (Collection<Notifier>) notification.getNewValue(); |
| for (final Notifier newValue : newValues) { |
| addAdapter(newValue); |
| } |
| } |
| |
| private void handleRemove(final Notification notification) { |
| final Notifier oldValue = (Notifier) notification.getOldValue(); |
| if (oldValue != null) { |
| removeAdapter(oldValue); |
| } |
| } |
| |
| private void handleRemoveMany(final Notification notification) { |
| @SuppressWarnings("unchecked") |
| final Collection<Notifier> oldValues = (Collection<Notifier>) notification.getOldValue(); |
| for (final Notifier oldContentValue : oldValues) { |
| removeAdapter(oldContentValue); |
| } |
| } |
| |
| /** |
| * Dumps adapter info to a print stream |
| * |
| * @param stream output stream (use System.out to print to console) |
| */ |
| public void dumpInfo(final @NonNull PrintStream stream) { |
| checkArgument(stream != null, ARG_STREAM_MESSAGE); |
| |
| stream.println("Amalthea Cross Reference Adapter {"); |
| |
| stream.println(" Id: " + this.getClass().getName() + '@' + Integer.toHexString(hashCode())); |
| |
| stream.print(" Resources: "); |
| Set<Resource> resources = inverseCrossReferencer.keySet().stream() |
| .map(EObject::eResource).filter(Objects::nonNull).collect(Collectors.toSet()); |
| if (resources.isEmpty()) { |
| stream.println("[]"); |
| } else { |
| stream.println("["); |
| for (Resource resource : resources) { |
| stream.println(" " + resource.getURI()); |
| } |
| stream.println(" ]"); |
| } |
| |
| stream.println(" Cross Reference Map size: " + inverseCrossReferencer.size()); |
| stream.println(" Name Index size: " + nameIndex.size()); |
| stream.println('}'); |
| } |
| |
| /** |
| * Dumps cross reference map to a print stream |
| * |
| * @param stream output stream (use System.out to print to console) |
| */ |
| public void dumpCrossReferenceMap(final @NonNull PrintStream stream) { |
| checkArgument(stream != null, ARG_STREAM_MESSAGE); |
| |
| stream.println("Cross Reference Map (size: " + inverseCrossReferencer.size() + ") {"); |
| |
| for (Map.Entry<EObject, Collection<EStructuralFeature.Setting>> entry : inverseCrossReferencer.entrySet()) { |
| EObject eObject = entry.getKey(); |
| stream.print(" "); |
| stream.print(description(eObject)); |
| stream.print(": "); |
| |
| Collection<EStructuralFeature.Setting> collection = entry.getValue(); |
| if (collection.isEmpty()) { |
| stream.println("[]"); |
| } else { |
| stream.println("["); |
| for (Iterator<EStructuralFeature.Setting> j = collection.iterator(); j.hasNext();) { |
| EStructuralFeature.Setting setting = j.next(); |
| |
| EObject object = setting.getEObject(); |
| EStructuralFeature feature = setting.getEStructuralFeature(); |
| stream.print(" "); |
| stream.print(feature.getName()); |
| stream.print(" <- "); |
| stream.print(description(object)); |
| if (j.hasNext()) { |
| stream.println(","); |
| } |
| } |
| stream.println(']'); |
| } |
| } |
| |
| stream.println('}'); |
| } |
| |
| /** |
| * Dumps name index to a print stream |
| * |
| * @param stream output stream (use System.out to print to console) |
| */ |
| public void dumpNameIndex(final @NonNull PrintStream stream) { |
| checkArgument(stream != null, ARG_STREAM_MESSAGE); |
| |
| stream.println("Name Index (size: " + nameIndex.size() + ") {"); |
| |
| nameIndex.entrySet() |
| .stream() |
| .sorted(Map.Entry.<String, Set<INamed>>comparingByKey()) |
| .forEachOrdered(e -> { |
| stream.print(" \"" + e.getKey() + "\": "); |
| stream.println(e.getValue() |
| .stream() |
| .map(obj -> obj.eClass().getName()) |
| .sorted() |
| .collect(Collectors.toList()) |
| ); |
| }); |
| |
| stream.println('}'); |
| } |
| |
| private String description(EObject eObject) { |
| StringBuilder result = new StringBuilder(); |
| result.append(eObject.eClass().getName()); |
| if (eObject instanceof INamed) { |
| result.append(" \"" + ((INamed) eObject).getName() + "\""); |
| } |
| if (eObject.eContainer() instanceof INamed) { |
| INamed cont = (INamed) eObject.eContainer(); |
| result.append(" (container: " + cont.eClass().getName() + " \"" + cont.getName() + "\")"); |
| } |
| return result.toString(); |
| } |
| |
| } |