blob: 331dbfd837b3b65e5f0bfd18dd9519e4ba66056a [file] [log] [blame]
/**
********************************************************************************
* 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();
}
}