ModelEventPumpMDRImpl.java

/* $Id$
 *****************************************************************************
 * Copyright (c) 2005-2012 Contributors - see below
 * 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:
 *    Tom Morris
 *****************************************************************************
 *
 * Some portions of this file was previously release using the BSD License:
 */

// Copyright (c) 2005-2008 The Regents of the University of California. All
// Rights Reserved. Permission to use, copy, modify, and distribute this
// software and its documentation without fee, and without a written
// agreement is hereby granted, provided that the above copyright notice
// and this paragraph appear in all copies. This software program and
// documentation are copyrighted by The Regents of the University of
// California. The software program and documentation are supplied "AS
// IS", without any accompanying services from The Regents. The Regents
// does not warrant that the operation of the program will be
// uninterrupted or error-free. The end-user understands that the program
// was developed for research purposes and is advised not to rely
// exclusively on the program for any reason. IN NO EVENT SHALL THE
// UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT,
// SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING LOST PROFITS,
// ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF
// THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE POSSIBILITY OF
// SUCH DAMAGE. THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY
// WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE
// PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND THE UNIVERSITY OF
// CALIFORNIA HAS NO OBLIGATIONS TO PROVIDE MAINTENANCE, SUPPORT,
// UPDATES, ENHANCEMENTS, OR MODIFICATIONS.

package org.argouml.model.mdr;

import java.beans.PropertyChangeListener;
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 java.util.logging.Level;
import java.util.logging.Logger;

import javax.jmi.model.Association;
import javax.jmi.model.AssociationEnd;
import javax.jmi.model.Attribute;
import javax.jmi.model.GeneralizableElement;
import javax.jmi.model.ModelElement;
import javax.jmi.model.ModelPackage;
import javax.jmi.model.MofClass;
import javax.jmi.model.NameNotFoundException;
import javax.jmi.model.Reference;
import javax.jmi.reflect.InvalidObjectException;
import javax.jmi.reflect.RefAssociation;
import javax.jmi.reflect.RefBaseObject;
import javax.jmi.reflect.RefObject;

import org.argouml.model.AbstractModelEventPump;
import org.argouml.model.AddAssociationEvent;
import org.argouml.model.AttributeChangeEvent;
import org.argouml.model.DeleteInstanceEvent;
import org.argouml.model.InvalidElementException;
import org.argouml.model.Model;
import org.argouml.model.NotImplementedException;
import org.argouml.model.RemoveAssociationEvent;
import org.argouml.model.UmlChangeEvent;
import org.argouml.model.UmlChangeListener;
import org.netbeans.api.mdr.MDRManager;
import org.netbeans.api.mdr.MDRepository;
import org.netbeans.api.mdr.events.AssociationEvent;
import org.netbeans.api.mdr.events.AttributeEvent;
import org.netbeans.api.mdr.events.InstanceEvent;
import org.netbeans.api.mdr.events.MDRChangeEvent;
import org.netbeans.api.mdr.events.MDRPreChangeListener;
import org.netbeans.api.mdr.events.TransactionEvent;
import org.netbeans.api.mdr.events.VetoChangeException;

/**
 * The ModelEventPump for the MDR implementation.<p>
 *
 * This implements three different event dispatching interfaces
 * which support a variety of different types of listener registration.
 * We keep a single event listener registered with the repository
 * for all events and then re-dispatch events to those listeners
 * who have requested them.<p>
 *
 * @since ARGO0.19.5
 * @author Ludovic Ma&icirc;tre
 * @author Tom Morris
 */
class ModelEventPumpMDRImpl extends AbstractModelEventPump implements
        MDRPreChangeListener {

    /**
     * Logger.
     */
    private static final Logger LOG =
        Logger.getLogger(ModelEventPumpMDRImpl.class.getName());

    private static final boolean VETO_READONLY_CHANGES = true;

    private MDRModelImplementation modelImpl;

    private Object registrationMutex = new Byte[0];

    private MDRepository repository;

    private Boolean eventCountMutex = new Boolean(false);
    private int pendingEvents = 0;

    private Thread eventThread;

    /**
     * Map of Element/attribute tuples and the listeners they have registered.
     */
    private Registry<PropertyChangeListener> elements =
        new Registry<PropertyChangeListener>();

    /**
     * Map of Class/attribute tuples and the listeners they have registered.
     */
    private Registry<PropertyChangeListener> listenedClasses =
        new Registry<PropertyChangeListener>();

    /**
     * Map of subtypes for all types in our metamodel.
     */
    private Map<String, Collection<String>> subtypeMap;

    /**
     * Map of all valid property names (association end names & attribute names)
     * for each class.
     */
    private Map<String, Collection<String>> propertyNameMap;

    /**
     * Constructor.
     *
     * @param implementation The implementation.
     */
    public ModelEventPumpMDRImpl(MDRModelImplementation implementation) {
        this(implementation, MDRManager.getDefault().getDefaultRepository());
    }

    /**
     * Constructor.
     *
     * @param implementation The implementation.
     * @param repo The repository.
     */
    public ModelEventPumpMDRImpl(MDRModelImplementation implementation,
            MDRepository repo) {
        super();
        modelImpl = implementation;
        repository = repo;
        subtypeMap = buildTypeMap(modelImpl.getModelPackage());
        propertyNameMap = buildPropertyNameMap(modelImpl.getModelPackage());
    }

    /*
     * @see org.argouml.model.AbstractModelEventPump#addModelEventListener(java.beans.PropertyChangeListener,
     *      java.lang.Object, java.lang.String[])
     */
    public void addModelEventListener(PropertyChangeListener listener,
            Object modelElement, String[] propertyNames) {
        if (listener == null) {
            throw new IllegalArgumentException("A listener must be supplied");
        }

        if (modelElement == null) {
            throw new IllegalArgumentException(
                    "A model element must be supplied");
        }

        registerModelEvent(listener, modelElement, propertyNames);
    }

    /*
     * @see org.argouml.model.AbstractModelEventPump#addModelEventListener(java.beans.PropertyChangeListener,
     *      java.lang.Object, java.lang.String[])
     */
    public void addModelEventListener(UmlChangeListener listener,
            Object modelElement, String[] propertyNames) {
        throw new NotImplementedException();
    }

    /*
     * @see org.argouml.model.AbstractModelEventPump#addModelEventListener(java.beans.PropertyChangeListener,
     *      java.lang.Object)
     */
    public void addModelEventListener(PropertyChangeListener listener,
            Object modelElement) {
        if (listener == null) {
            throw new IllegalArgumentException("A listener must be supplied");
        }

        if (modelElement == null) {
            throw new IllegalArgumentException(
                    "A model element must be supplied");
        }

        registerModelEvent(listener, modelElement, null);
    }

    /*
     * @see org.argouml.model.AbstractModelEventPump#removeModelEventListener(java.beans.PropertyChangeListener,
     *      java.lang.Object, java.lang.String[])
     */
    public void removeModelEventListener(PropertyChangeListener listener,
            Object modelelement, String[] propertyNames) {
        unregisterModelEvent(listener, modelelement, propertyNames);
    }

    /*
     * @see org.argouml.model.AbstractModelEventPump#removeModelEventListener(java.beans.PropertyChangeListener,
     *      java.lang.Object, java.lang.String[])
     */
    public void removeModelEventListener(UmlChangeListener listener,
            Object modelelement, String[] propertyNames) {
        throw new NotImplementedException();
    }

    /*
     * @see org.argouml.model.AbstractModelEventPump#removeModelEventListener(java.beans.PropertyChangeListener,
     *      java.lang.Object)
     */
    public void removeModelEventListener(PropertyChangeListener listener,
            Object modelelement) {
        unregisterModelEvent(listener, modelelement, null);
    }

    /*
     * @see org.argouml.model.AbstractModelEventPump#addClassModelEventListener(java.beans.PropertyChangeListener,
     *      java.lang.Object, java.lang.String[])
     */
    public void addClassModelEventListener(PropertyChangeListener listener,
            Object modelClass, String[] propertyNames) {
        registerClassEvent(listener, modelClass, propertyNames);
    }

    /*
     * @see org.argouml.model.AbstractModelEventPump#removeClassModelEventListener(java.beans.PropertyChangeListener,
     *      java.lang.Object, java.lang.String[])
     */
    public void removeClassModelEventListener(PropertyChangeListener listener,
            Object modelClass, String[] propertyNames) {
        unregisterClassEvent(listener, modelClass, propertyNames);
    }

    /**
     * Detect a change event in MDR and convert this to a change event from the
     * model interface.  We also keep track of the number of pending changes so
     * that we can implement a simple flush interface.<p>
     *
     * The conversions are according to this table.
     * <pre>
     * MDR Event         MDR Event Type            Propogated Event
     *
     * InstanceEvent     EVENT_INSTANCE_DELETE     DeleteInstanceEvent
     * AttributeEvent    EVENT_ATTRIBUTE_SET       AttributeChangeEvent
     * AssociationEvent  EVENT_ASSOCIATION_ADD     AddAssociationEvent
     * AssociationEvent  EVENT_ASSOCIATION_REMOVE  RemoveAssociationEvent
     * </pre>
     * Any other events are ignored and not propogated beyond the model
     * subsystem.
     *
     * @param mdrEvent Change event from MDR
     * @see org.netbeans.api.mdr.events.MDRChangeListener#change
     */
    public void change(MDRChangeEvent mdrEvent) {

        if (eventThread == null) {
            eventThread = Thread.currentThread();
        }

        // TODO: This should be done after all events are delivered, but leave
        // it here for now to avoid last minute synchronization problems
        decrementEvents();

        // Quick exit if it's a transaction event
        // (we get a lot of them and they are all ignored)
        if (mdrEvent instanceof TransactionEvent) {
            return;
        }

        List<UmlChangeEvent> events = new ArrayList<UmlChangeEvent>();

        if (mdrEvent instanceof AttributeEvent) {
            AttributeEvent ae = (AttributeEvent) mdrEvent;
            events.add(new AttributeChangeEvent(ae.getSource(),
                    ae.getAttributeName(), ae.getOldElement(),
                    ae.getNewElement(), mdrEvent));
        } else if (mdrEvent instanceof InstanceEvent
                && mdrEvent.isOfType(InstanceEvent.EVENT_INSTANCE_DELETE)) {
            InstanceEvent ie = (InstanceEvent) mdrEvent;
            events.add(new DeleteInstanceEvent(ie.getSource(),
                    "remove", null, null, mdrEvent));
            // Clean up index entries
            String mofid = ((InstanceEvent)mdrEvent).getInstance().refMofId();
            modelImpl.removeElement(mofid);
        } else if (mdrEvent instanceof AssociationEvent) {
            AssociationEvent ae = (AssociationEvent) mdrEvent;
            if (ae.isOfType(AssociationEvent.EVENT_ASSOCIATION_ADD)) {
                events.add(new AddAssociationEvent(
                        ae.getNewElement(),
                        mapPropertyName(ae.getEndName()),
                        ae.getOldElement(), // will always be null
                        ae.getFixedElement(),
                        ae.getFixedElement(),
                        mdrEvent));
                // Create a change event for the corresponding property
                events.add(new AttributeChangeEvent(
                        ae.getNewElement(),
                        mapPropertyName(ae.getEndName()),
                        ae.getOldElement(), // will always be null
                        ae.getFixedElement(),
                        mdrEvent));
                // Create an event for the other end of the association
                events.add(new AddAssociationEvent(
                        ae.getFixedElement(),
                        otherAssocEnd(ae),
                        ae.getOldElement(), // will always be null
                        ae.getNewElement(),
                        ae.getNewElement(),
                        mdrEvent));
                // and a change event for that end
                events.add(new AttributeChangeEvent(
                        ae.getFixedElement(),
                        otherAssocEnd(ae),
                        ae.getOldElement(), // will always be null
                        ae.getNewElement(),
                        mdrEvent));
            } else if (ae.isOfType(AssociationEvent.EVENT_ASSOCIATION_REMOVE)) {
                events.add(new RemoveAssociationEvent(
                        ae.getOldElement(),
                        mapPropertyName(ae.getEndName()),
                        ae.getFixedElement(),
                        ae.getNewElement(), // will always be null
                        ae.getFixedElement(),
                        mdrEvent));
                // Create a change event for the associated property
                events.add(new AttributeChangeEvent(
                        ae.getOldElement(),
                        mapPropertyName(ae.getEndName()),
                        ae.getFixedElement(),
                        ae.getNewElement(), // will always be null
                        mdrEvent));
                // Create an event for the other end of the association
                events.add(new RemoveAssociationEvent(
                        ae.getFixedElement(),
                        otherAssocEnd(ae),
                        ae.getOldElement(),
                        ae.getNewElement(), // will always be null
                        ae.getOldElement(),
                        mdrEvent));
                // Create a change event for the associated property
                events.add(new AttributeChangeEvent(
                        ae.getFixedElement(),
                        otherAssocEnd(ae),
                        ae.getOldElement(),
                        ae.getNewElement(), // will always be null
                        mdrEvent));
            } else if (ae.isOfType(AssociationEvent.EVENT_ASSOCIATION_SET)) {
                LOG.log(Level.SEVERE, "Unexpected EVENT_ASSOCIATION_SET received");
            } else {
                LOG.log(Level.SEVERE, "Unknown association event type " + ae.getType());
            }
        } else {
            String name = mdrEvent.getClass().getName();
            // Cut down on debugging noise
            if (!name.endsWith("CreateInstanceEvent")) {
                LOG.log(Level.FINE, "Ignoring MDR event " + mdrEvent);
            }
        }

        for (UmlChangeEvent event : events) {
            fire(event);
            // Unregister deleted instances after all events have been delivered
            if (event instanceof DeleteInstanceEvent) {
                elements.unregister(null, ((RefBaseObject) event.getSource())
                        .refMofId(), null);
            }
        }
    }

    private boolean isReadOnly(RefBaseObject object) {
        return modelImpl.isReadOnly(object.refOutermostPackage());
    }

    /**
     * @param e Event from MDR indicating a planned change.
     * @see org.netbeans.api.mdr.events.MDRPreChangeListener#plannedChange
     */
    public void plannedChange(MDRChangeEvent e) {

        if (VETO_READONLY_CHANGES) {
            if (e instanceof InstanceEvent) {
                if (e.isOfType(InstanceEvent.EVENT_INSTANCE_CREATE)) {
                    RefBaseObject element = (RefBaseObject) ((InstanceEvent) e)
                            .getSource();
                    if (isReadOnly(element)) {
                        throw new VetoChangeException(e.getSource(), null);
                    }
                } else {
                    RefObject element = ((InstanceEvent) e).getInstance();
                    if (isReadOnly(element)) {
                        throw new VetoChangeException(e.getSource(), element);
                    }
                }
            } else if (e instanceof AssociationEvent) {
                // TODO: association changes are too hard to check easily
                // we don't know which end of the association is significant
                // without checking the metamodel for navigability of the ends
//                RefObject element = ((AssociationEvent) e).getFixedElement();
//                if (isReadOnly(element)) {
//                    throw new VetoChangeException(element, element);
//                }
            } else if (e instanceof AttributeEvent) {
                RefObject element = (RefObject) ((AttributeEvent) e)
                        .getSource();
                if (isReadOnly(element)) {
                    throw new VetoChangeException(element, element);
                }
            }
        }

        synchronized (eventCountMutex) {
            pendingEvents++;
        }

        // Prototypical logging code that can be enabled and modified to
        // discover who's creating certain types of events
//        if (/* LOG.isDebugEnabled() && */ e instanceof AssociationEvent) {
//            AssociationEvent ae = (AssociationEvent) e;
//            if (ae.isOfType(AssociationEvent.EVENT_ASSOCIATION_REMOVE)
//                    && "namespace".equals(ae.getEndName())
//                    /* && ae.getFixedElement() instanceof UmlPackage */) {
//                LOG.log(Level.FINE, "Removing element " + ae.getOldElement()
//                        + " from package " + ae.getFixedElement());
//            }
//        }
    }

    /**
     * @param e
     *            MDR event which was announced to plannedChange then
     *            subsequently cancelled.
     * @see org.netbeans.api.mdr.events.MDRPreChangeListener#changeCancelled
     */
    public void changeCancelled(MDRChangeEvent e) {
        decrementEvents();
    }

    /**
     * Decrement count of outstanding events and wake
     * any waiters when it becomes zero.
     */
    private void decrementEvents() {

        synchronized (eventCountMutex) {
            pendingEvents--;
            if (pendingEvents <= 0) {
                eventCountMutex.notifyAll();
            }
        }
    }

    /**
     * Fire an event to any registered listeners.
     */
    private void fire(UmlChangeEvent event) {
        String mofId = ((RefBaseObject) event.getSource()).refMofId();
        String className  = getClassName(event.getSource());

        // Any given listener is only called once even if it is
        // registered for multiple relevant matches
        Set<PropertyChangeListener> listeners =
                new HashSet<PropertyChangeListener>();
        synchronized (registrationMutex) {
            listeners.addAll(elements.getMatches(mofId, event
                    .getPropertyName()));

            // This will include all subtypes registered
            listeners.addAll(listenedClasses.getMatches(className, event
                    .getPropertyName()));
        }

        if (LOG.isLoggable(Level.FINE)) {
            LOG.log(Level.FINE, "Firing "
                    + modelImpl.getMetaTypes().getName(event)
                    + " source "
                    + modelImpl.getMetaTypes().getName(
                            event.getSource())
                    + " [" + ((RefBaseObject) event.getSource()).refMofId()
                    + "]."  + event.getPropertyName()
                    + "," + formatElement(event.getOldValue())
                    + "->" + formatElement(event.getNewValue()));
        }

        if (!listeners.isEmpty()) {
            for (PropertyChangeListener pcl : listeners) {
                if (false /*(LOG.isDebugEnabled()*/) {
                    LOG.log(Level.FINE, "Firing event on " + pcl.getClass().getName()
                            + "[" + pcl + "]");
                }
                pcl.propertyChange(event);
            }
        } else {
            // For debugging you probably want either this
            // OR the logging for every event which is fired - not both
            if (false/*LOG.isDebugEnabled()*/) {
                LOG.log(Level.FINE, "No listener for "
                        + modelImpl.getMetaTypes().getName(event)
                        + " source "
                        + modelImpl.getMetaTypes().getName(
                                event.getSource())
                        + " ["
                        + ((RefBaseObject) event.getSource()).refMofId() + "]."
                        + event.getPropertyName() + "," + event.getOldValue()
                        + "->" + event.getNewValue());
            }
        }
    }


    /**
     * Register a listener for a Model Event.  The ModelElement's
     * MofID is used as the string to match against.
     */
    private void registerModelEvent(PropertyChangeListener listener,
            Object modelElement, String[] propertyNames) {
        if (listener == null || modelElement == null) {
            throw new IllegalArgumentException("Neither listener (" + listener
                    + ") or modelElement (" + modelElement
                    + ") can be null! [Property names: " + propertyNames + "]");
        }

        // Fetch the key before going in synchronized mode
        String mofId = ((RefBaseObject) modelElement).refMofId();
        try {
            verifyAttributeNames(((RefBaseObject) modelElement).refMetaObject(),
                    propertyNames);
        } catch (InvalidObjectException e) {
            throw new InvalidElementException(e);
        }
        if (LOG.isLoggable(Level.FINE)) {
            LOG.log(Level.FINE, "Register ["
                    + " element:" + formatElement(modelElement)
                    + ", properties:" + formatArray(propertyNames)
                    + ", listener:" + listener
                    + "]");
        }
        synchronized (registrationMutex) {
            elements.register(listener, mofId, propertyNames);
        }
    }

    /**
     * Unregister a listener for a Model Event.
     */
    private void unregisterModelEvent(PropertyChangeListener listener,
            Object modelElement, String[] propertyNames) {
        if (listener == null || modelElement == null) {
            LOG.log(Level.SEVERE, "Attempt to unregister null listener(" + listener
                    + ") or modelElement (" + modelElement
                    + ")! [Property names: " + propertyNames + "]");
            return;
        }
        if (!(modelElement instanceof RefBaseObject)) {
            LOG.log(Level.SEVERE, "Ignoring non-RefBaseObject received by "
                    + "unregisterModelEvent - " + modelElement);
            return;
        }
        String mofId = ((RefBaseObject) modelElement).refMofId();
        if (LOG.isLoggable(Level.FINE)) {
            LOG.log(Level.FINE, "Unregister ["
                    + " element:" + formatElement(modelElement)
                    + ", properties:" + formatArray(propertyNames)
                    + ", listener:" + listener
                    + "]");
        }
        synchronized (registrationMutex) {
            elements.unregister(listener, mofId, propertyNames);
        }
    }

    /**
     * Register a listener for metamodel Class (and all its
     * subclasses), optionally qualified by a list of
     * property names.
     *
     * TODO: verify that property/event names are legal for
     * this class in the metamodel
     */
    private void registerClassEvent(PropertyChangeListener listener,
            Object modelClass, String[] propertyNames) {

        if (modelClass instanceof Class) {
            String className = getClassName(modelClass);
            if (LOG.isLoggable(Level.FINE)) {
                LOG.log(Level.FINE, "Register class ["
                        + modelImpl.getMetaTypes().getName(modelClass)
                        + "properties:" + formatArray(propertyNames)
                        + ", listener:" + listener + "]");
            }
            Collection<String> subtypes = subtypeMap.get(className);
            verifyAttributeNames(className, propertyNames);
            synchronized (registrationMutex) {
                listenedClasses.register(listener, className, propertyNames);
                for (String subtype : subtypes) {
                    listenedClasses.register(listener, subtype, propertyNames);
                }
            }
            return;
        }
        throw new IllegalArgumentException(
                "Don't know how to register class event for object "
                        + modelClass);
    }


    /**
     * Unregister a listener for a class and its subclasses.
     */
    private void unregisterClassEvent(PropertyChangeListener listener,
            Object modelClass, String[] propertyNames) {
        if (modelClass instanceof Class) {
            String className = getClassName(modelClass);
            if (LOG.isLoggable(Level.FINE)) {
                LOG.log(Level.FINE,
                        "Unregister class [" + className
                        + ", properties:" + formatArray(propertyNames)
                        + ", listener:" + listener + "]");
            }
            Collection<String> subtypes = subtypeMap.get(className);
            synchronized (registrationMutex) {
                listenedClasses.unregister(listener, className, propertyNames);
                for (String subtype : subtypes) {
                    listenedClasses.unregister(listener, subtype,
                            propertyNames);
                }
            }
            return;
        }
        throw new IllegalArgumentException(
                "Don't know how to unregister class event for object "
                        + modelClass);
    }

    private String getClassName(Object elementOrClass) {
        return modelImpl.getMetaTypes().getName(elementOrClass);
    }

    /*
     * @see org.argouml.model.ModelEventPump#startPumpingEvents()
     */
    public void startPumpingEvents() {
        LOG.log(Level.FINE, "Start pumping events");
        repository.addListener(this);
    }

    /*
     * @see org.argouml.model.ModelEventPump#stopPumpingEvents()
     */
    public void stopPumpingEvents() {
        LOG.log(Level.FINE, "Stop pumping events");
        repository.removeListener(this);
    }

    /*
     * @see org.argouml.model.ModelEventPump#flushModelEvents()
     */
    public void flushModelEvents() {
        while (true) {
            synchronized (eventCountMutex) {
                if (pendingEvents <= 0
                        // Don't wait on ourselves, we'll deadlock!
                        // TODO: We might want to throw an exception here
                        || Thread.currentThread().equals(eventThread)) {
                    return;
                }
                try {
                    eventCountMutex.wait();
                } catch (InterruptedException e) {
                    LOG.log(Level.SEVERE, "Interrupted while waiting in flushModelEvents");
                }
            }
        }

    }

    /**
     * Get name of opposite end of association using
     * reflection on metamodel.
     */
    private String otherAssocEnd(AssociationEvent ae) {
        RefAssociation ra = (RefAssociation) ae.getSource();
        Association a = (Association) ra.refMetaObject();
        AssociationEnd aend = null;
        try {
            aend = (AssociationEnd) a.lookupElementExtended(ae.getEndName());
        } catch (NameNotFoundException e) {
            LOG.log(Level.SEVERE, "Failed to find other end of association : "
                    + ae.getSource() + " -> " + ae.getEndName());
            return null;
        }
        return aend.otherEnd().getName();
    }


    /**
     * Map from UML 1.4 names to UML 1.3 names
     * expected by ArgoUML.<p>
     *
     * Note: It would have less performance impact to do the
     * mapping during listener registration, but ArgoUML
     * depends on the value in the event.
     */
    private static String mapPropertyName(String name) {

        // TODO: We don't want to do this once we have dropped UML1.3
        // Map UML 1.4 names to UML 1.3 equivalents
        if ("typedParameter".equals(name)) {
            return "parameter";
        }
        if ("typedFeature".equals(name)) {
            return "feature";
        }
        return name;
    }

    /**
     * Formatters for debug output.
     */
    private String formatArray(String[] array) {
        if (array == null) {
            return null;
        }
        String result = "[";
        for (int i = 0; i < array.length; i++) {
            result = result + array[i] + ", ";
        }
        return result.substring(0, result.length() - 2) + "]";
    }

    private String formatElement(Object element) {
        try {
            if (element instanceof RefBaseObject) {
                return modelImpl.getMetaTypes().getName(element)
                        + "<" + ((RefBaseObject) element).refMofId() + ">";
            } else if (element != null) {
                return element.toString();
            }
        } catch (InvalidObjectException e) {
            return modelImpl.getMetaTypes().getName(element)
                    + "<deleted>";
        }
        return null;
    }


    /**
     * Traverse metamodel and build list of subtypes for every metatype.
     */
    private Map<String, Collection<String>> buildTypeMap(ModelPackage extent) {
        Map<String, Collection<String>> names =
            new HashMap<String, Collection<String>>();
        for (Object metaclass : extent.getMofClass().refAllOfClass()) {
            ModelElement element = (ModelElement) metaclass;
            String name = element.getName();
            if (names.containsKey(name)) {
                LOG.log(Level.SEVERE,
                        "Found duplicate class '" + name + "' in metamodel");
            } else {
                names.put(name, getSubtypes(extent, element));
                // LOG.log(Level.FINE, " Class " + name + " has subtypes : "
                // + names.get(name));
            }
        }
        return names;
    }

    /**
     * Recursive method to get all subtypes.
     *
     * TODO: Does this have a scalability problem?
     */
    private Collection<String> getSubtypes(ModelPackage extent,
            ModelElement me) {
        Collection<String> allSubtypes = new HashSet<String>();
        if (me instanceof GeneralizableElement) {
            GeneralizableElement ge = (GeneralizableElement) me;
            Collection<ModelElement> subtypes = extent.getGeneralizes()
                    .getSubtype(ge);
            for (ModelElement st : subtypes) {
                allSubtypes.add(st.getName());
                allSubtypes.addAll(getSubtypes(extent, st));
            }
        }
        return allSubtypes;
    }

    /**
     * Traverse metamodel and build list of names for all attributes and
     * reference ends.
     */
    private Map<String, Collection<String>> buildPropertyNameMap(
            ModelPackage extent) {
        Map<String, Collection<String>> names =
                new HashMap<String, Collection<String>>();
        for (Reference reference : (Collection<Reference>) extent
                .getReference().refAllOfClass()) {
            mapAssociationEnd(names, reference.getExposedEnd());
            mapAssociationEnd(names, reference.getReferencedEnd());
        }
        for (Attribute attribute : (Collection<Attribute>) extent
                .getAttribute().refAllOfClass()) {
            mapPropertyName(names, attribute.getContainer(),
                    attribute.getName());
        }
        return names;
    }

    private void mapAssociationEnd(Map<String, Collection<String>> names,
            AssociationEnd end) {
        ModelElement type = end.otherEnd().getType();
        mapPropertyName(names, type, end.getName());
    }

    private boolean mapPropertyName(Map<String, Collection<String>> names,
            ModelElement type, String propertyName) {
        String typeName = type.getName();
        boolean added = mapPropertyName(names, typeName, propertyName);

        Collection<String> subtypes = subtypeMap.get(typeName);
        if (subtypes != null) {
            for (String subtype : subtypes) {
                added &= mapPropertyName(names, subtype, propertyName);
            }
        }

        return added;
    }

    private boolean mapPropertyName(Map<String, Collection<String>> names,
            String typeName, String propertyName) {
        if (!names.containsKey(typeName)) {
            names.put(typeName, new HashSet<String>());
        }
        boolean added = names.get(typeName).add(propertyName);
        if (!added) {
            // Because we map both ends of an association we'll see many
            // names twice
//                LOG.log(Level.FINE, "Duplicate property name found - {0}:{1}", new Object[]{typeName, propertyName});
        } else {
            LOG.log(Level.FINE, "Added property name - {0}:{1}", new Object[]{typeName, propertyName});
        }
        return added;
    }

    /**
     * Check whether given attribute names exist for this
     * metatype in the metamodel.  Throw exception if not found.
     */
    private void verifyAttributeNames(String className, String[] attributes) {
        // convert classname to RefObject
        RefObject ro = null;
        verifyAttributeNames(ro, attributes);
    }

    /**
     * Check whether given attribute names exist for this
     * metatype in the metamodel.  Throw exception if not found.
     */
    private void verifyAttributeNames(RefObject metaobject,
            String[] attributes) {
        // Only do verification if debug level logging is on
        // TODO: Should we leave this on always? - tfm
        if (LOG.isLoggable(Level.FINE)) {
            if (metaobject == null || attributes == null) {
                return;
            }
            // If we don't have a MofClass, see if we can get one from the
            // instance
            if (!(metaobject instanceof MofClass)) {
                metaobject = metaobject.refMetaObject();
            }

            // If we still don't have a MofClass, something's wrong
            if (!(metaobject instanceof MofClass)) {
                throw new IllegalArgumentException(
                        "Argument must be MofClass or instance of MofClass");
            }

            MofClass metaclass = (MofClass) metaobject;
            Collection<String> names = propertyNameMap.get(metaclass.getName());
            if (names == null) {
                names = Collections.emptySet();
            }

            for (String attribute : attributes) {
                if (!names.contains(attribute)
                        && !"remove".equals(attribute)) {

                    // TODO: We also have code registering for the names of
                    // a tagged value like "derived"
                    LOG.log(Level.SEVERE,
                            "Property '" + attribute
                             + "' for class '"
                             + metaclass.getName()
                             + "' doesn't exist in metamodel");
//                    throw new IllegalArgumentException("Property '"
//                            + attribute + "' doesn't exist in metamodel");
                }
            }
        }
    }


    @SuppressWarnings("unchecked")
    public List getDebugInfo() {
        List info = new ArrayList();
        info.add("Event Listeners");
        for (Iterator it = elements.registry.entrySet().iterator();
                it.hasNext(); ) {
            Map.Entry entry = (Map.Entry) it.next();
            String item = entry.getKey().toString();
            List modelElementNode = newDebugNode(getDebugDescription(item));
            info.add(modelElementNode);
            Map propertyMap = (Map) entry.getValue();
            for (Iterator propertyIterator = propertyMap.entrySet().iterator();
                    propertyIterator.hasNext();) {
                Map.Entry propertyEntry = (Map.Entry) propertyIterator.next();
                List propertyNode =
                    newDebugNode(propertyEntry.getKey().toString());
                modelElementNode.add(propertyNode);

                List listenerList = (List) propertyEntry.getValue();
                for (Iterator listIt = listenerList.iterator();
                        listIt.hasNext(); ) {
                    Object listener = listIt.next();
                    List listenerNode =
                        newDebugNode(
                                listener.getClass().getName());
                    propertyNode.add(listenerNode);
                }
            }
        }

        return info;
    }

    private List<String> newDebugNode(String name) {
        List<String> list = new ArrayList<String>();
        list.add(name);
        return list;
    }

    private String getDebugDescription(String mofId) {
        Object modelElement = repository.getByMofId(mofId);
        String name = Model.getFacade().getName(modelElement);
        if (name != null && name.trim().length() != 0) {
            return "\"" + name + "\" - " + modelElement.toString();
        } else {
            return modelElement.toString();
        }
    }

}


/**
 * A simple typed registry which supports two levels of string keys.
 *
 * @param <T> type of object to be registered
 * @author Tom Morris
 */
class Registry<T> {

    private static final Logger LOG = Logger.getLogger(Registry.class.getName());

    Map<String, Map<String, List<T>>> registry;

    /**
     * Construct a new registry for the given type of object.
     */
    Registry() {
        registry = Collections
                .synchronizedMap(new HashMap<String, Map<String, List<T>>>());
    }

    /**
     * Register an object with given keys(s) in the registry. The object is
     * registered in multiple locations for quick lookup. During matching an
     * object registered without subkeys will match any subkey. Multiple calls
     * with the same item and key pair will only result in a single registration
     * being made.
     *
     * @param item object to be registered
     * @param key primary key for registration
     * @param subkeys array of subkeys. If null, register under primary key
     *                only. The special value of the empty string ("") must not
     *                be used as a subkey by the caller.
     */
    void register(T item, String key,
            String[] subkeys) {

        // Lookup primary key, creating new entry if needed
        Map<String, List<T>> entry = registry.get(key);
        if (entry == null) {
            entry = new HashMap<String, List<T>>();
            registry.put(key, entry);
        }

        // If there are no subkeys, register using our special value
        // to indicate that this is a primary key only registration
        if (subkeys == null || subkeys.length < 1) {
            subkeys =
                new String[] {
                    "",
                };
        }

        for (int i = 0; i < subkeys.length; i++) {
            List<T> list = entry.get(subkeys[i]);
            if (list == null) {
                list = new ArrayList<T>();
                entry.put(subkeys[i], list);
            }
            if (!list.contains(item)) {
                list.add(item);
            } else {
                LOG.log(Level.FINE, "Duplicate registration attempt for {0}: {1} Listener: {2}",
                        new Object[]{key,subkeys,item});
            }
        }
    }

    /**
     * Unregister an item or all items which match key set.
     *
     * @param item object to be unregistered.  If null, unregister all
     * matching objects.
     * @param key primary key for registration
     * @param subkeys array of subkeys.  If null, unregister under primary
     * key only.
     */
    void unregister(T item, String key, String[] subkeys) {
        Map<String, List<T>> entry = registry.get(key);
        if (entry == null) {
            return;
        }

        if (subkeys != null && subkeys.length > 0) {
            for (int i = 0; i < subkeys.length; i++) {
                lookupRemoveItem(entry, subkeys[i], item);
            }
        } else {
            if (item == null) {
                registry.remove(key);
            } else {
                lookupRemoveItem(entry, "", item);
            }
        }
    }

    private void lookupRemoveItem(Map<String, List<T>> map, String key,
            T item) {
        List<T> list = map.get(key);
        if (list == null) {
            return;
        }
        if (item == null) {
            map.remove(key);
            return;
        }
        if (LOG.isLoggable(Level.FINE)) {
            if (!list.contains(item)) {
                LOG.log(Level.FINE,
                        "Attempt to unregister non-existant registration {0} Listener: {1}",
                        new Object[]{key, item});
            }
        }
        while (list.contains(item)) {
            list.remove(item);
        }
        if (list.isEmpty()) {
            map.remove(key);
        }
    }

    /**
     * Return a list of items which have been registered for given key(s).
     * Returns items registered both for the key/subkey pair as well as
     * those registered just for the primary key.
     * @param key
     * @param subkey
     * @return collection of items previously registered.
     */
    Collection<T> getMatches(String key, String subkey) {
        List<T> results = new ArrayList<T>();
        Map<String, List<T>> entry = registry.get(key);
        if (entry != null) {
            if (entry.containsKey(subkey)) {
                results.addAll(entry.get(subkey));
            }
            if (entry.containsKey("")) {
                results.addAll(entry.get(""));
            }
        }
        return results;
    }

}