/* * JBoss, Home of Professional Open Source. * * See the LEGAL.txt file distributed with this work for information regarding copyright ownership and licensing. * * See the AUTHORS.txt file distributed with this work for a full listing of individual contributors. */ package org.teiid.designer.core.transaction; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import org.eclipse.emf.common.notify.Notification; import org.eclipse.emf.ecore.EClass; import org.eclipse.emf.ecore.EObject; import org.eclipse.emf.ecore.EReference; import org.eclipse.emf.ecore.EStructuralFeature; import org.eclipse.emf.ecore.InternalEObject; import org.eclipse.emf.ecore.impl.ENotificationImpl; import org.eclipse.emf.ecore.resource.Resource; import org.eclipse.emf.ecore.resource.ResourceSet; import org.eclipse.emf.ecore.util.EcoreUtil; import org.teiid.core.designer.util.CoreArgCheck; import org.teiid.designer.metamodels.core.AnnotationContainer; import org.teiid.designer.metamodels.core.ModelAnnotation; import org.teiid.designer.metamodels.diagram.DiagramContainer; import org.teiid.designer.metamodels.transformation.SqlTransformation; import org.teiid.designer.metamodels.transformation.TransformationContainer; /** * @since 8.0 */ public class TxnNotificationFilter { private final List eventSets; private final ResourceSet resourceSet; // ================================================================================== // C O N S T R U C T O R S // ================================================================================== /** * @since 5.0 */ public TxnNotificationFilter( final ResourceSet theResourceSet ) { CoreArgCheck.isNotNull(theResourceSet); this.eventSets = new ArrayList(); this.resourceSet = theResourceSet; } // ================================================================================== // P U B L I C M E T H O D S // ================================================================================== /** * Return the list of SourceNotification instances representing the finalized set of notifications for a transaction */ public List getSourcedNotifications( final Object source ) { // Remove any empty NotifierEventSet instances removeEmptyEventSets(); // Get a SourcedNotification for each NotifierEventSet List result = new ArrayList(this.eventSets.size()); for (Iterator i = this.eventSets.iterator(); i.hasNext();) { NotifierEventSet es = (NotifierEventSet)i.next(); if (!es.isEmpty()) { SourcedNotification sn = es.getSourcedNotification(source); if (sn != null) { result.add(sn); } } } return result; } /** * Clean any state for this filter */ public void clear() { for (Iterator i = this.eventSets.iterator(); i.hasNext();) { NotifierEventSet es = (NotifierEventSet)i.next(); es.clear(); } this.eventSets.clear(); } /** * Process the new notification * * @param notification * @return * @since 5.0 */ public boolean addNotification( final Notification notification ) { // If the notification can be ignored then return if (isIgnorable(notification)) { return false; } // Remove any empty NotifierEventSet instances removeEmptyEventSets(); // If there is an existing NotifierEventSet instance for this notifier then // simple add the new notification to that instance final Object notifier = notification.getNotifier(); NotifierEventSet eventSet = getExistingEventSet(notifier); if (eventSet != null) { eventSet.addNotification(notification); return true; } // If we can determine immediately that the notification represents a new root // object then create a new NotifierEventSet instance and return final Object feature = notification.getFeature(); if (feature == null && (notifier instanceof Resource || notifier instanceof ResourceSet)) { eventSet = new NotifierEventSet(notifier); eventSet.addNotification(notification); this.eventSets.add(eventSet); return true; } // If the new notifier is the ancestor of the notifier for an existing NotifierEventSet // instance then we should remove the existing instance and create a new NotifierEventSet // for the parent notifier for (Iterator i = this.eventSets.iterator(); i.hasNext();) { NotifierEventSet es = (NotifierEventSet)i.next(); // If the new notifier is the ancestor of the notifier for an existing NotifierEventSet if (!es.isEmpty() && isAncestor(notifier, es.getNotifier())) { // One exception to this is when the notifier for a remove notification is not // a child of any NotifierEventSet added objects. We don't want to remove // notifications in which an object is removed from one eContainer and // added to a newly created eContainer (e.g. remove column from an existing // table and add it to a new created table) if (isRemove(notification) && !isAncestor(es.getAddObjects(), notifier)) { // do nothing } else { es.clear(); i.remove(); } } } eventSet = new NotifierEventSet(notifier); eventSet.addNotification(notification); this.eventSets.add(eventSet); return true; } // ================================================================================== // P R O T E C T E D M E T H O D S // ================================================================================== /** * Return true if it is determined that the specified notification can be ignored by this filter. The following rules are used * to determine when a notification can be ignored: <li>notification with a null notifier <li>touch notification <li> * notification with an EObject notifier and null feature <li>add/remove notification associated with creating a new * ModelAnnotation, DiagramContainer, TransformationContainer, AnnotationContainer instance <li>notification for a resource * not related to adding or removing EObjects <li>set notification in which the old and new value are the same <li> * notification in which the resource for the notifier is not within the ResourceSet associated with this filter <li> * notification in which the notifier is a child/descendant of an existing NotifierEventSet's notifier * * @param notification * @return */ protected boolean isIgnorable( final Notification notification ) { if (notification == null || notification.getNotifier() == null) { return true; } // If the notification is just a touch, don't add it. if (notification.isTouch()) { return true; } // If the notification does not change any feature of an EObject then don't process it. The // feature could be null if the notifier was a Resource or ResourceSet, so check that it is // an EObject notifier final Object notifier = notification.getNotifier(); if (notification.getFeature() == null && notifier instanceof EObject) { return true; } // Check if this notification cannot be ignored due to special circumstances ... if (isSpecial(notification)) { return false; } // Check for other specific ignorable event types final Object oldVal = notification.getOldValue(); final Object newVal = notification.getNewValue(); switch (notification.getEventType()) { case Notification.REMOVE: { if (newVal == null && (oldVal instanceof ModelAnnotation || oldVal instanceof DiagramContainer || oldVal instanceof TransformationContainer || oldVal instanceof AnnotationContainer)) { return true; } break; } case Notification.ADD: { if (oldVal == null && (newVal instanceof ModelAnnotation || newVal instanceof DiagramContainer || newVal instanceof TransformationContainer || newVal instanceof AnnotationContainer)) { return true; } break; } case Notification.SET: { // If the feature is null then igore the notification if (notification.getFeature() == null) { return true; } // If the old and new values are equal, then ignore the notification if (oldVal == newVal || (oldVal != null && oldVal.equals(newVal))) { return true; } break; } case Notification.ADD_MANY: { break; } case Notification.REMOVE_MANY: { break; } case Notification.UNSET: { break; } case Notification.MOVE: { break; } default: { // do nothing } } // Don't process notifications for objects that are not contained in resources // associated with this notification filter Resource r = null; if (notifier instanceof EObject) { r = ((EObject)notifier).eResource(); if (r == null || !this.resourceSet.getResources().contains(r)) { return true; } } else if (notifier instanceof Resource) { r = (Resource)notifier; if (!this.resourceSet.getResources().contains(r)) { return true; } } else if (notifier instanceof EClass) { r = ((EClass)notifier).eResource(); if (r == null || !this.resourceSet.getResources().contains(r)) { return true; } } // Check if the notifier for this notification is a descendant of one of the // notifiers in an existing NotifierEventSet. If we have captured notifications // for the parent object, then we can ignore the notifications for the child. for (Iterator i = this.eventSets.iterator(); i.hasNext();) { NotifierEventSet es = (NotifierEventSet)i.next(); // If the NotifierEventSet notifier is a parent of the notification's notifier // then we can ignore the child notification if (!es.isEmpty() && isAncestor(es.getNotifier(), notifier)) { // One exception to this is when the notifier for a remove notification is not // a child of any NotifierEventSet added objects. We don't want to ignore // notifications in which an object is removed from one eContainer and // added to a newly created eContainer (e.g. remove column from an existing // table and add it to a new created table) if (isRemove(notification) && !isAncestor(es.getAddObjects(), notifier)) { return false; } return true; } } return false; } /** * Return true if this notification must not be ignored due to special circumstances The current list of special circumstances * are as follows: <li>If the notifier is a SqlTranformation instance and there is an existing NotifierEventSet for its parent * SqlTransformationMappingRoot * * @param notification * @return */ protected boolean isSpecial( final Notification notification ) { final Object notifier = notification.getNotifier(); switch (notification.getEventType()) { case Notification.REMOVE: case Notification.REMOVE_MANY: case Notification.ADD: case Notification.ADD_MANY: { // If the notifier is a SqlTranformation instance and we have an NotifierEventSet // for its parent SqlTransformationMappingRoot instance then do not ignore this // notification. The notification is needed to update SQL text due to a SqlAlias // being added or removed (see defect 21257) if (notifier instanceof SqlTransformation && getExistingEventSet(((EObject)notifier).eContainer()) != null) { return true; } break; } default: { // do nothing } } return false; } /** * Return the target of this notification. If the notification is an add or remove the target is the new or old value, * respectively. If the notification is a set then the target is the notifier. * * @param notification * @return */ protected Collection getNotificationTarget( final Notification notification ) { final Object oldVal = notification.getOldValue(); final Object newVal = notification.getNewValue(); Collection target = Collections.singletonList(notification.getNotifier()); switch (notification.getEventType()) { case Notification.REMOVE: target = Collections.singletonList(oldVal); break; case Notification.REMOVE_MANY: target = ((List)oldVal).isEmpty() ? Collections.EMPTY_LIST : ((List)oldVal); break; case Notification.ADD: target = Collections.singletonList(newVal); break; case Notification.ADD_MANY: target = ((List)newVal).isEmpty() ? Collections.EMPTY_LIST : ((List)newVal); break; default: { // do nothing } } return target; } /** * Return true if the event type of this notification is REMOVE or REMOVE_MANY, otherwise return false; * * @param notification * @return */ protected boolean isRemove( final Notification notification ) { switch (notification.getEventType()) { case Notification.REMOVE: case Notification.REMOVE_MANY: return true; default: { // do nothing } } return false; } /** * Return true if the event type of this notification is ADD or ADD_MANY, otherwise return false; * * @param notification * @return */ protected boolean isAdd( final Notification notification ) { switch (notification.getEventType()) { case Notification.ADD: case Notification.ADD_MANY: return true; default: { // do nothing } } return false; } /** * Return any existing NotifierEventSet with the specified notifier * * @param notifier * @return */ protected NotifierEventSet getExistingEventSet( final Object notifier ) { for (Iterator i = this.eventSets.iterator(); i.hasNext();) { NotifierEventSet es = (NotifierEventSet)i.next(); if (es.getNotifier() == notifier) { return es; } } return null; } /** * Check all existing NotifierEventSet instances and remove those that current are empty */ protected void removeEmptyEventSets() { for (Iterator i = this.eventSets.iterator(); i.hasNext();) { NotifierEventSet eventSet = (NotifierEventSet)i.next(); if (eventSet.isEmpty()) { eventSet.clear(); i.remove(); } } } /** * Return true if this NotifierEventSet has an add or remove type notification in which its target is an ancestor of the * specified value * * @param es * @return */ protected boolean isAncestor( final Collection ancestors, final Object obj ) { if (ancestors != null && !ancestors.isEmpty()) { for (Iterator i = ancestors.iterator(); i.hasNext();) { if (isAncestor(i.next(), obj)) { return true; } } } return false; } /** * Returns whether the second object is directly or indirectly contained by the first object, i.e., whether the second object * is in the content tree of the first. * * @param ancestor the ancestor object in question. * @param obj the object to test. * @return whether the first object is an ancestor of the second object. */ protected boolean isAncestor( final Object ancestor, final Object obj ) { if (ancestor == null || obj == null || ancestor == obj) { return false; } if (obj instanceof EObject) { if (ancestor instanceof EObject) { return EcoreUtil.isAncestor((EObject)ancestor, (EObject)obj); } else if (ancestor instanceof Resource) { return EcoreUtil.isAncestor((Resource)ancestor, (EObject)obj); } else if (ancestor instanceof ResourceSet) { return EcoreUtil.isAncestor((ResourceSet)ancestor, (EObject)obj); } } else if (obj instanceof Resource) { if (ancestor instanceof ResourceSet) { Object parent = ((Resource)obj).getResourceSet(); if (ancestor == parent) { return true; } } } return false; } // ================================================================================== // I N N E R C L A S S // ================================================================================== /** * A NotifierEventSet instance is a collection of all notifications with the same notifier */ private class NotifierEventSet { private final Object notifier; private final List notifications; private final Map addEvents; private final Map removeEvents; private final Set addObjects; private final Set removeObjects; // ================================================================================== // C O N S T R U C T O R S // ================================================================================== /** * @since 4.3 */ public NotifierEventSet( final Object theNotifier ) { this.notifier = theNotifier; this.notifications = new ArrayList(); this.addEvents = new HashMap(); this.removeEvents = new HashMap(); this.addObjects = new HashSet(); this.removeObjects = new HashSet(); } // ================================================================================== // P U B L I C M E T H O D S // ================================================================================== public boolean addNotification( final Notification notification ) { // If the notification can be ignored then return if (isIgnorable(notification)) { return false; } // Check if the same object was added and removed within the same set of notifications final Object oldVal = notification.getOldValue(); final Object newVal = notification.getNewValue(); switch (notification.getEventType()) { case Notification.REMOVE: { Notification event = (Notification)this.addEvents.get(oldVal); if (event != null && event.getEventType() == Notification.ADD) { this.addEvents.remove(oldVal); this.addObjects.remove(oldVal); this.notifications.remove(event); return false; } if (oldVal != null) { event = (Notification)this.removeEvents.get(oldVal); if (event != null) { // Do not overwrite a containment notification Object feature = event.getFeature(); if (feature instanceof EReference && ((EReference)feature).isContainment()) { break; } } // Assertion.isNull(this.removeEvents.get(oldVal)); this.removeEvents.put(oldVal, notification); this.removeObjects.add(oldVal); } break; } case Notification.REMOVE_MANY: { if (oldVal instanceof List && !((List)oldVal).isEmpty()) { Object firstOldVal = ((List)oldVal).get(0); Notification event = (Notification)this.addEvents.get(firstOldVal); if (event != null && event.getEventType() == Notification.ADD_MANY) { Object eventVal = event.getNewValue(); if (eventVal instanceof List && oldVal.equals(eventVal)) { this.addEvents.remove(firstOldVal); this.addObjects.removeAll((List)oldVal); this.notifications.remove(event); return false; } } if (firstOldVal != null) { event = (Notification)this.removeEvents.get(firstOldVal); if (event != null) { // Do not overwrite a containment notification Object feature = event.getFeature(); if (feature instanceof EReference && ((EReference)feature).isContainment()) { break; } } // Assertion.isNull(this.removeEvents.get(firstOldVal)); this.removeEvents.put(firstOldVal, notification); this.removeObjects.addAll((List)oldVal); } } break; } case Notification.ADD: { Notification event = (Notification)this.removeEvents.get(newVal); if (event != null && event.getEventType() == Notification.REMOVE) { this.removeEvents.remove(newVal); this.removeObjects.remove(newVal); this.notifications.remove(event); // If a remove event is followed by an add event on the same notifier and // feature, check if this indicates a MOVE if (isMoveNotification(event, notification)) { Notification moveEvent = createMoveNotification(event, notification); this.notifications.add(moveEvent); return true; } return false; } if (newVal != null) { event = (Notification)this.addEvents.get(newVal); if (event != null) { // Do not overwrite a containment notification Object feature = event.getFeature(); if (feature instanceof EReference && ((EReference)feature).isContainment()) { break; } } // Assertion.isNull(this.addEvents.get(newVal)); this.addEvents.put(newVal, notification); this.addObjects.add(newVal); } break; } case Notification.ADD_MANY: { if (newVal instanceof List && !((List)newVal).isEmpty()) { Object firstNewVal = ((List)newVal).get(0); Notification event = (Notification)this.removeEvents.get(firstNewVal); if (event != null && event.getEventType() == Notification.REMOVE_MANY) { Object eventVal = event.getOldValue(); if (eventVal instanceof List && newVal.equals(eventVal)) { this.removeEvents.remove(firstNewVal); this.removeObjects.removeAll((List)newVal); this.notifications.remove(event); return false; } } if (firstNewVal != null) { event = (Notification)this.addEvents.get(firstNewVal); if (event != null) { // Do not overwrite a containment notification Object feature = event.getFeature(); if (feature instanceof EReference && ((EReference)feature).isContainment()) { break; } } // Assertion.isNull(this.addEvents.get(firstNewVal)); this.addEvents.put(firstNewVal, notification); this.addObjects.addAll((List)newVal); } } break; } case Notification.SET: { // Setting a new value on a single valued feature is equivalent to an add if (newVal != null && oldVal == null) { Notification event = (Notification)this.removeEvents.get(newVal); if (event != null && event.getEventType() == Notification.SET) { this.removeEvents.remove(newVal); this.notifications.remove(event); return false; } event = (Notification)this.addEvents.get(newVal); if (event != null) { // Do not overwrite a containment notification Object feature = event.getFeature(); if (feature instanceof EReference && ((EReference)feature).isContainment()) { break; } } // Assertion.isNull(this.addEvents.get(newVal)); this.addEvents.put(newVal, notification); this.addObjects.add(newVal); // Nulling an old value on a single valued feature is equivalent to an remove } else if (newVal == null && oldVal != null) { Notification event = (Notification)this.addEvents.get(oldVal); if (event != null && event.getEventType() == Notification.SET) { this.addEvents.remove(oldVal); this.addObjects.remove(oldVal); this.notifications.remove(event); return false; } event = (Notification)this.removeEvents.get(oldVal); if (event != null) { // Do not overwrite a containment notification Object feature = event.getFeature(); if (feature instanceof EReference && ((EReference)feature).isContainment()) { break; } } // Assertion.isNull(this.removeEvents.get(oldVal)); this.removeEvents.put(oldVal, notification); this.removeObjects.add(oldVal); } break; } case Notification.UNSET: { break; } case Notification.MOVE: { break; } default: { // do nothing } } // Add the notification to the event set this.notifications.add(notification); return true; } public Notification getPrimaryNotification() { if (this.notifications.isEmpty()) { return null; } for (Iterator i = this.notifications.iterator(); i.hasNext();) { final Notification n = (Notification)i.next(); // Return any ADD or REMOVE notifications as the primary notification if one exists final Object feature = n.getFeature(); if (feature instanceof EReference && ((EReference)feature).isContainment()) { switch (n.getEventType()) { case Notification.REMOVE: { return n; } case Notification.REMOVE_MANY: { return n; } case Notification.ADD: { return n; } case Notification.ADD_MANY: { return n; } case Notification.SET: { break; } case Notification.UNSET: { break; } case Notification.MOVE: { break; } default: { // do nothing } } } } return (Notification)this.notifications.get(0); } public Object getNotifier() { return this.notifier; } public SourcedNotification getSourcedNotification( final Object source ) { SourcedNotification sn = null; if (!isEmpty()) { sn = new SourcedNotificationImpl(source, getPrimaryNotification()); for (Iterator i = this.notifications.iterator(); i.hasNext();) { sn.add((Notification)i.next()); } } return sn; } public Collection getAddObjects() { return this.addObjects; } public boolean isEmpty() { return this.notifications.isEmpty(); } public void clear() { this.notifications.clear(); this.addEvents.clear(); this.removeEvents.clear(); } // ================================================================================== // P R O T E C T E D M E T H O D S // ================================================================================== protected boolean isIgnorable( final Notification notification ) { if (notification == null || notification.getNotifier() == null) { return true; } // If the notification is just a touch, don't add it. if (notification.isTouch()) { return true; } // If this notification does not apply to this NotifierEventSet ... if (notification.getNotifier() != this.notifier) { return true; } // If the notification does not change any feature of an EObject then don't process it. The // feature could be null if the notifier was a Resource or ResourceSet, so check that it is // an EObject notifier if (notification.getFeature() == null && notification.getNotifier() instanceof EObject) { return true; } return false; } protected boolean isMoveNotification( final Notification removeEvent, final Notification addEvent ) { if (addEvent.getNotifier() instanceof EObject && addEvent.getFeature() == removeEvent.getFeature() && addEvent.getPosition() != removeEvent.getPosition()) { return true; } return false; } protected Notification createMoveNotification( final Notification removeEvent, final Notification addEvent ) { Notification moveEvent = new ENotificationImpl((InternalEObject)addEvent.getNotifier(), // notifier Notification.MOVE, // eventType (EStructuralFeature)addEvent.getFeature(), // feature new Integer(removeEvent.getPosition()), // oldValue addEvent.getNewValue(), // newValue addEvent.getPosition()); // position return moveEvent; } } }