| /******************************************************************************* |
| * Copyright (c) 2008-2011 Chair for Applied Software Engineering, |
| * Technische Universitaet Muenchen. |
| * All rights reserved. This program and the accompanying materials |
| * are made available under the terms of the Eclipse Public License v1.0 |
| * which accompanies this distribution, and is available at |
| * http://www.eclipse.org/legal/epl-v10.html |
| * |
| * Contributors: |
| * emueller |
| * koegel |
| ******************************************************************************/ |
| package org.eclipse.emf.emfstore.internal.common.model.util; |
| |
| import java.util.ArrayList; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Stack; |
| |
| 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.EReference; |
| import org.eclipse.emf.ecore.util.EContentAdapter; |
| import org.eclipse.emf.emfstore.internal.common.model.IdEObjectCollection; |
| import org.eclipse.emf.emfstore.internal.common.model.impl.NotifiableIdEObjectCollectionImpl; |
| |
| /** |
| * Notifies a about changes in its containment hierarchy. |
| * |
| * @author koegel |
| * @author emueller |
| */ |
| public class EObjectChangeNotifier extends EContentAdapter { |
| |
| private final NotifiableIdEObjectCollectionImpl collection; |
| private boolean isInitializing; |
| private final Stack<Notification> currentNotifications; |
| private final Stack<List<EObject>> removedModelElements; |
| private int reentrantCallToAddAdapterCounter; |
| private boolean notificationDisabled; |
| |
| /** |
| * Constructor. Attaches an Adapter to the given {@link Notifier} and forwards notifications to the given |
| * NotifiableIdEObjectCollection, that reacts appropriately. |
| * |
| * @param notifiableCollection |
| * a NotifiableIdEObjectCollection |
| * @param notifier |
| * the {@link Notifier} to listen to |
| */ |
| public EObjectChangeNotifier(NotifiableIdEObjectCollectionImpl notifiableCollection, Notifier notifier) { |
| collection = notifiableCollection; |
| isInitializing = true; |
| currentNotifications = new Stack<Notification>(); |
| removedModelElements = new Stack<List<EObject>>(); |
| notifier.eAdapters().add(this); |
| isInitializing = false; |
| reentrantCallToAddAdapterCounter = 0; |
| notificationDisabled = false; |
| } |
| |
| /** |
| * {@inheritDoc} |
| * |
| * @see org.eclipse.emf.ecore.util.EContentAdapter#addAdapter(org.eclipse.emf.common.notify.Notifier) |
| */ |
| @Override |
| protected void addAdapter(Notifier notifier) { |
| |
| try { |
| reentrantCallToAddAdapterCounter += 1; |
| if (!notifier.eAdapters().contains(this)) { |
| super.addAdapter(notifier); |
| } |
| } finally { |
| reentrantCallToAddAdapterCounter -= 1; |
| } |
| if (reentrantCallToAddAdapterCounter > 0 || currentNotifications.isEmpty()) { |
| // any other than the first call in re-entrant calls to addAdapter |
| // are going to call the project |
| return; |
| } |
| |
| final Notification currentNotification = currentNotifications.peek(); |
| |
| if (currentNotification != null && !currentNotification.isTouch() && !isInitializing |
| && notifier instanceof EObject) { |
| final EObject modelElement = (EObject) notifier; |
| if (!collection.contains(modelElement) && isInCollectionHierarchy(modelElement)) { |
| collection.modelElementAdded(collection, modelElement); |
| } |
| } |
| } |
| |
| /** |
| * {@inheritDoc} |
| * |
| * @see org.eclipse.emf.ecore.util.EContentAdapter#removeAdapter(org.eclipse.emf.common.notify.Notifier) |
| */ |
| @Override |
| protected void removeAdapter(Notifier notifier) { |
| |
| if (isInitializing || currentNotifications.isEmpty()) { |
| return; |
| } |
| |
| final Notification currentNotification = currentNotifications.peek(); |
| |
| if (currentNotification != null && currentNotification.isTouch()) { |
| return; |
| } |
| |
| if (currentNotification != null && currentNotification.getFeature() instanceof EReference) { |
| // do not remove adapter because of opposite |
| final EReference eReference = (EReference) currentNotification.getFeature(); |
| if (eReference.isContainment() && eReference.getEOpposite() != null |
| && !eReference.getEOpposite().isTransient()) { |
| return; |
| } |
| } |
| |
| if (notifier instanceof EObject) { |
| final EObject modelElement = (EObject) notifier; |
| if (!isInCollectionHierarchy(modelElement) |
| && (collection.contains(modelElement) || collection.getDeletedModelElementId(modelElement) != null)) { |
| removedModelElements.peek().add(modelElement); |
| } |
| |
| } |
| } |
| |
| /** |
| * Checks whether the given {@link EObject} is within the collection. |
| * |
| * @param modelElement |
| * the {@link EObject} whose containment should be checked |
| * @return true, if the {@link EObject} is contained in the collection, |
| * false otherwise |
| */ |
| private boolean isInCollectionHierarchy(EObject modelElement) { |
| final EObject parent = modelElement.eContainer(); |
| if (parent == null) { |
| return false; |
| } |
| |
| if (parent == collection) { |
| return true; |
| } |
| |
| if (collection.contains(parent)) { |
| return true; |
| } |
| |
| return isInCollectionHierarchy(parent); |
| } |
| |
| /** |
| * {@inheritDoc} |
| * |
| * @see org.eclipse.emf.ecore.util.EContentAdapter#notifyChanged(org.eclipse.emf.common.notify.Notification) |
| */ |
| @Override |
| public void notifyChanged(Notification notification) { |
| if (notificationDisabled) { |
| return; |
| } |
| |
| removedModelElements.push(new ArrayList<EObject>()); |
| currentNotifications.push(notification); |
| final Object feature = notification.getFeature(); |
| final Object notifier = notification.getNotifier(); |
| final Object newValue = notification.getNewValue(); |
| |
| if (feature instanceof EReference) { |
| final EReference eReference = (EReference) feature; |
| |
| // Do not create notifications for transient features |
| if (eReference.isTransient()) { |
| currentNotifications.pop(); |
| return; |
| } |
| |
| if (eReference.isContainer()) { |
| handleContainer(notification, eReference); |
| } |
| } |
| |
| super.notifyChanged(notification); |
| |
| // detect if the notification is about a reference to an object outside of the collection => notify |
| // project |
| if (feature instanceof EReference && newValue != null && !notification.isTouch()) { |
| final EReference reference = (EReference) feature; |
| |
| if (!reference.isContainment() && !reference.isContainer()) { |
| if (newValue instanceof EObject) { |
| handleSingleReference((EObject) newValue); |
| } else if (newValue instanceof List<?>) { |
| handleMultiReference((List<?>) newValue); |
| } |
| } else if (reference.isContainment() |
| && reference.getEType().getInstanceClass() != null |
| && Map.Entry.class.isAssignableFrom(reference.getEType().getInstanceClass())) { |
| handleMapEntry((Map.Entry<?, ?>) newValue, reference); |
| } |
| } |
| |
| currentNotifications.pop(); |
| notifyCollection(notification, notifier); |
| notifyCollectionAboutRemovedElements(); |
| } |
| |
| private void notifyCollectionAboutRemovedElements() { |
| for (final EObject removedElement : removedModelElements.pop()) { |
| collection.modelElementRemoved(collection, removedElement); |
| } |
| } |
| |
| private void notifyCollection(Notification notification, final Object notifier) { |
| if (!notification.isTouch() && notifier instanceof EObject && hasId(notifier)) { |
| collection.notify(notification, collection, (EObject) notifier); |
| } |
| } |
| |
| private boolean hasId(Object notifier) { |
| return collection.getModelElementId((EObject) notifier) != null |
| || collection.getDeletedModelElementId((EObject) notifier) != null; |
| } |
| |
| private void handleMapEntry(Map.Entry<?, ?> entry, EReference reference) { |
| for (final EReference ref : reference.getEReferenceType().getEReferences()) { |
| if (ref.getName().equals("key") && entry.getKey() instanceof EObject) { //$NON-NLS-1$ |
| handleSingleReference((EObject) entry.getKey()); |
| } else if (ref.getName().equals("value") && entry.getValue() instanceof EObject) { //$NON-NLS-1$ |
| handleSingleReference((EObject) entry.getValue()); |
| } |
| } |
| } |
| |
| private void handleMultiReference(List<?> list) { |
| for (final Object obj : list) { |
| |
| if (!(obj instanceof EObject)) { |
| continue; |
| } |
| |
| final EObject eObject = (EObject) obj; |
| |
| if (!collection.contains(eObject)) { |
| collection.addCutElement(getCutElement(eObject)); |
| } |
| } |
| } |
| |
| private void handleSingleReference(EObject newEObject) { |
| if (!collection.contains(newEObject)) { |
| if (ModelUtil.isSingleton(newEObject)) { |
| return; |
| } |
| collection.addCutElement(getCutElement(newEObject)); |
| } |
| } |
| |
| private EObject getCutElement(EObject eObject) { |
| final EObject referenceElement = eObject; |
| /* find topmost parent */ |
| while (eObject.eContainer() != null) { |
| /* |
| * Bug 501971 |
| * if we are contained in a different IdEObjectCollection (e.g. a second EMFStore project) simply add the |
| * used element from the reference as a cut element (default before 501971) |
| */ |
| if (IdEObjectCollection.class.isInstance(eObject)) { |
| return referenceElement; |
| } |
| eObject = eObject.eContainer(); |
| } |
| |
| /* |
| * Bug 501971 |
| * otherwise move the full containment tree |
| */ |
| return eObject; |
| } |
| |
| /** |
| * @param notification |
| */ |
| private void handleContainer(Notification notification, EReference eReference) { |
| if (notification.getEventType() == Notification.SET) { |
| final Object newValue = notification.getNewValue(); |
| final Object oldValue = notification.getOldValue(); |
| if (newValue == null && oldValue != null) { |
| removeAdapter((Notifier) notification.getNotifier()); |
| } |
| } |
| } |
| |
| /** |
| * @param notificationDisabled |
| * the notificationDisabled to set |
| */ |
| public void disableNotifications(boolean notificationDisabled) { |
| this.notificationDisabled = notificationDisabled; |
| } |
| |
| } |