// $Id: ModelEventPumpEUMLImpl.java 18220 2010-04-08 20:37:15Z tfmorris $ /******************************************************************************* * Copyright (c) 2007,2010 Bogdan Pistol and other contributors * 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: * Bogdan Pistol - initial implementation *******************************************************************************/ package org.argouml.model.euml; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.util.ArrayList; import java.util.EventObject; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import org.apache.log4j.Logger; import org.argouml.model.AbstractModelEventPump; import org.argouml.model.AddAssociationEvent; import org.argouml.model.AttributeChangeEvent; import org.argouml.model.DeleteInstanceEvent; import org.argouml.model.RemoveAssociationEvent; import org.eclipse.emf.common.command.CommandStackListener; import org.eclipse.emf.common.notify.Notification; import org.eclipse.emf.common.notify.Notifier; import org.eclipse.emf.common.notify.impl.NotificationImpl; import org.eclipse.emf.ecore.ENamedElement; import org.eclipse.emf.ecore.EObject; import org.eclipse.emf.ecore.EReference; import org.eclipse.uml2.uml.Property; /** * The implementation of the ModelEventPump for eUML. */ class ModelEventPumpEUMLImpl extends AbstractModelEventPump { /** * A list of model elements that when removed should not create delete * events. See issue */ final private List<Property> deleteEventIgnoreList = new ArrayList<Property>(); /** * A listener attached to a UML element */ private class Listener { private PropertyChangeListener listener; private Set<String> props; Listener(PropertyChangeListener listener, String[] properties) { this.listener = listener; if (properties != null) { setProperties(properties); } } void setProperties(String[] properties) { if (properties == null) { props = null; } else { if (props == null) { props = new HashSet<String>(); } for (String s : properties) { props.add(s); } } } void removeProperties(String[] properties) { if (props == null) { return; } for (String s : properties) { props.remove(s); } } PropertyChangeListener getListener() { return listener; } Set<String> getProperties() { return props; } } /** * The model implementation. */ private EUMLModelImplementation modelImpl; private RootContainerAdapter rootContainerAdapter = new RootContainerAdapter(this); // Access should be fast private Map<Object, List<Listener>> registerForElements = new HashMap<Object, List<Listener>>(); // Iteration should be fast private Map<Object, List<Listener>> registerForClasses = new LinkedHashMap<Object, List<Listener>>(); private Object mutex; private static final Logger LOG = Logger.getLogger(ModelEventPumpEUMLImpl.class); public static final int COMMAND_STACK_UPDATE = Notification.EVENT_TYPE_COUNT + 1; /** * Constructor. * * @param implementation * The ModelImplementation. */ public ModelEventPumpEUMLImpl(EUMLModelImplementation implementation) { modelImpl = implementation; mutex = this; implementation.getEditingDomain().getCommandStack() .addCommandStackListener(new CommandStackListener() { public void commandStackChanged(EventObject event) { notifyChanged(new NotificationImpl( COMMAND_STACK_UPDATE, false, false)); } }); } /** * Setter for the root container * * @param container */ public void setRootContainer(Notifier container) { rootContainerAdapter.setRootContainer(container); } public RootContainerAdapter getRootContainer() { return rootContainerAdapter; } public void addClassModelEventListener(PropertyChangeListener listener, Object modelClass, String[] propertyNames) { if (!(modelClass instanceof Class && EObject.class.isAssignableFrom((Class) modelClass))) { throw new IllegalArgumentException( "The model class must be instance of " //$NON-NLS-1$ + "java.lang.Class<EObject>"); //$NON-NLS-1$ } registerListener( modelClass, listener, propertyNames, registerForClasses); } public void addModelEventListener(PropertyChangeListener listener, Object modelElement, String[] propertyNames) { if (LOG.isDebugEnabled()) { LOG.debug("Adding a listener to " //$NON-NLS-1$ + modelElement + " for " //$NON-NLS-1$ + propertyNames); } if (!(modelElement instanceof EObject)) { throw new IllegalArgumentException( "The modelelement must be instance " //$NON-NLS-1$ + "of EObject."); //$NON-NLS-1$ } registerListener( modelElement, listener, propertyNames, registerForElements); } public void addModelEventListener(PropertyChangeListener listener, Object modelelement) { addModelEventListener(listener, modelelement, (String[]) null); } private void registerListener(Object notifier, PropertyChangeListener listener, String[] propertyNames, Map<Object, List<Listener>> register) { if (notifier == null || listener == null) { throw new NullPointerException( "The model element/class and the " //$NON-NLS-1$ + "listener must be non-null."); //$NON-NLS-1$ } synchronized (mutex) { List<Listener> list = register.get(notifier); boolean found = false; if (list == null) { list = new ArrayList<Listener>(); } else { for (Listener l : list) { if (l.getListener() == listener) { l.setProperties(propertyNames); found = true; break; } } } if (!found) { list.add(new Listener(listener, propertyNames)); register.put(notifier, list); } } } public void flushModelEvents() { // TODO: Auto-generated method stub } public void removeClassModelEventListener(PropertyChangeListener listener, Object modelClass, String[] propertyNames) { if (!(modelClass instanceof Class && EObject.class .isAssignableFrom((Class) modelClass))) { throw new IllegalArgumentException(); } unregisterListener( modelClass, listener, propertyNames, registerForClasses); } public void removeModelEventListener(PropertyChangeListener listener, Object modelelement, String[] propertyNames) { if (!(modelelement instanceof EObject)) { throw new IllegalArgumentException(); } unregisterListener( modelelement, listener, propertyNames, registerForElements); } public void removeModelEventListener(PropertyChangeListener listener, Object modelelement) { removeModelEventListener(listener, modelelement, (String[]) null); } private void unregisterListener(Object notifier, PropertyChangeListener listener, String[] propertyNames, Map<Object, List<Listener>> register) { if (notifier == null || listener == null) { throw new NullPointerException( "The model element/class and the " //$NON-NLS-1$ + "listener must be non-null."); //$NON-NLS-1$ } synchronized (mutex) { List<Listener> list = register.get(notifier); if (list == null) { return; } Iterator<Listener> iter = list.iterator(); while (iter.hasNext()) { Listener l = iter.next(); if (l.getListener() == listener) { if (propertyNames != null) { l.removeProperties(propertyNames); } else { iter.remove(); } break; } } } } /** * @see org.eclipse.emf.common.notify.Adapter#notifyChanged(Notification) * @param notification * The notification event */ public void notifyChanged(Notification notification) { if (notification.getEventType() == Notification.REMOVING_ADAPTER) { return; } ENamedElement feature = (ENamedElement) notification.getFeature(); String featureName = feature == null ? "" : feature.getName(); //$NON-NLS-1$ Object oldValue = notification.getOldValue(); Object newValue = notification.getNewValue(); LOG.debug("event - Property: " //$NON-NLS-1$ + featureName + " Old: " + oldValue //$NON-NLS-1$ + " New: " + newValue //$NON-NLS-1$ + " From: " + notification.getNotifier()); //$NON-NLS-1$ class EventAndListeners { public EventAndListeners(PropertyChangeEvent e, List<PropertyChangeListener> l) { event = e; listeners = l; } private PropertyChangeEvent event; private List<PropertyChangeListener> listeners; } List<EventAndListeners> events = new ArrayList<EventAndListeners>(); if (notification.getEventType() == Notification.SET) { if (notification.getFeature() instanceof ENamedElement) { String propName = mapPropertyName(((ENamedElement) notification .getFeature()).getName()); events.add(new EventAndListeners(new AttributeChangeEvent( notification.getNotifier(), propName, notification.getOldValue(), notification.getNewValue(), null), getListeners( notification.getNotifier(), propName))); } } else if (notification.getEventType() == Notification.ADD || notification.getEventType() == Notification.REMOVE) { if (notification.getFeature() instanceof EReference) { EReference ref = (EReference) notification.getFeature(); String propName = mapPropertyName(ref.getName()); if (notification.getEventType() == Notification.ADD) { events.add(new EventAndListeners(new AddAssociationEvent( notification.getNotifier(), propName, null, notification.getNewValue(), notification.getNewValue(), null), getListeners( notification.getNotifier(), propName))); events.add(new EventAndListeners(new AttributeChangeEvent( notification.getNotifier(), propName, null, notification.getNewValue(), null), getListeners( notification.getNotifier(), propName))); } else { if (isDeleteEventRequired(oldValue)) { // Changing of a property can result in the property // being removed and added again (eclipse behaviour) // we don't want to mistake this for deletion of the // property. See issue 5853 events.add(new EventAndListeners( new DeleteInstanceEvent( notification.getOldValue(), "remove", //$NON-NLS-1$ null, null, null), getListeners( notification.getOldValue()))); } events.add(new EventAndListeners( new RemoveAssociationEvent( notification.getNotifier(), propName, notification.getOldValue(), null, notification.getOldValue(), null), getListeners( notification.getNotifier(), propName))); events.add(new EventAndListeners( new AttributeChangeEvent( notification.getNotifier(), propName, notification.getOldValue(), null, null), getListeners( notification.getNotifier(), propName))); } EReference oppositeRef = ref.getEOpposite(); if (oppositeRef != null) { propName = mapPropertyName(oppositeRef.getName()); if (notification.getEventType() == Notification.ADD) { events.add(new EventAndListeners( new AddAssociationEvent( notification.getNewValue(), propName, null, notification.getNotifier(), notification.getNotifier(), null), getListeners( notification.getNewValue(), propName))); events.add(new EventAndListeners( new AttributeChangeEvent( notification.getNewValue(), propName, null, notification.getNotifier(), null), getListeners( notification.getNewValue(), propName))); } else { events.add(new EventAndListeners( new AddAssociationEvent( notification.getOldValue(), propName, notification.getNotifier(), null, notification.getNotifier(), null), getListeners( notification.getOldValue(), propName))); events.add(new EventAndListeners( new AttributeChangeEvent( notification.getOldValue(), propName, notification.getNotifier(), null, null), getListeners( notification.getOldValue(), propName))); } } } } for (EventAndListeners e : events) { if (e.listeners != null) { for (PropertyChangeListener l : e.listeners) { l.propertyChange(e.event); } } } } /** * Determine if we should create a delete event for the given property * when EMF tells us it has been removed. This is currently used to * work around the problem discussed in issue 5853. * * @param element * @return true if */ private boolean isDeleteEventRequired( final Object element) { if (element instanceof Property) { synchronized (deleteEventIgnoreList) { if (deleteEventIgnoreList.contains(element)) { deleteEventIgnoreList.remove(element); return false; } } } return true; } /** * Add Element to list which will cause the next delete event to * be ignored. * * @param property */ void addElementForDeleteEventIgnore(Property property) { synchronized (deleteEventIgnoreList) { deleteEventIgnoreList.add(property); } } private List<PropertyChangeListener> getListeners(Object element) { return getListeners(element, null); } @SuppressWarnings("unchecked") private List<PropertyChangeListener> getListeners(Object element, String propName) { List<PropertyChangeListener> returnedList = new ArrayList<PropertyChangeListener>(); synchronized (mutex) { addListeners(returnedList, element, propName, registerForElements); for (Object o : registerForClasses.keySet()) { if (o instanceof Class) { Class type = (Class) o; if (type.isAssignableFrom(element.getClass())) { addListeners( returnedList, o, propName, registerForClasses); } } } } return returnedList.isEmpty() ? null : returnedList; } private void addListeners(List<PropertyChangeListener> listeners, Object element, String propName, Map<Object, List<Listener>> register) { List<Listener> list = register.get(element); if (list != null) { for (Listener l : list) { if (propName == null || l.getProperties() == null || l.getProperties().contains(propName)) { listeners.add(l.getListener()); } } } } public void startPumpingEvents() { rootContainerAdapter.setDeliverEvents(true); } public void stopPumpingEvents() { rootContainerAdapter.setDeliverEvents(false); } private String mapPropertyName(String name) { // TODO: map UML2 names to UML1.x names if (name.equals("ownedAttribute")) { //$NON-NLS-1$ return "feature"; //$NON-NLS-1$ } return name; } @SuppressWarnings("unchecked") public List getDebugInfo() { List info = new ArrayList(); info.add("Event Listeners"); //$NON-NLS-1$ for (Iterator it = registerForElements.entrySet().iterator(); it.hasNext(); ) { Map.Entry entry = (Map.Entry) it.next(); String item = entry.getKey().toString(); List modelElementNode = newDebugNode(item); info.add(modelElementNode); List listenerList = (List) entry.getValue(); Map<String, List<String>> map = new HashMap<String, List<String>>(); for (Iterator listIt = listenerList.iterator(); listIt.hasNext();) { Listener listener = (Listener) listIt.next(); if (listener.getProperties() != null) { for (String eventName : listener.getProperties()) { if (!map.containsKey(eventName)) { map.put(eventName, new LinkedList<String>()); } map.get(eventName).add( listener.getListener().getClass().getName()); } } else { if (!map.containsKey("")) { map.put("", new LinkedList<String>()); } map.get("") .add(listener.getListener().getClass().getName()); } } for (Map.Entry o : map.entrySet()) { modelElementNode.add((String) o.getKey()); modelElementNode.add((List<String>) o.getValue()); } } return info; } private List<String> newDebugNode(String name) { List<String> list = new ArrayList<String>(); list.add(name); return list; } }