package org.apache.commons.events.observable ; import java.beans.PropertyChangeSupport ; import java.beans.PropertyChangeListener ; import java.beans.PropertyChangeEvent ; import java.util.Map ; import java.util.Set ; import java.util.Collection ; import org.apache.commons.collections.map.AbstractMapDecorator ; /** *

* Decorates a Map implementation with a bound * property named "collection". *

*

* Each modifying method call made on this Map is * handled as a change to the "collection" property. This * facility serves to notify subscribers of a change to the collection * but does not allow users the option of vetoing the change. To * gain the ability to veto the change, use a {@link ConstrainedMap} * decorater. *

*

* Due to the fact that a Map offers several "views" of the * same data, some confusion may arise as to what action caused a * particular event. For instance, entries in the map may be * deleted by Map.values().remove(object). There is no * known method, short of digging into the implementation of a Map, * to determine which key-value pair was deleted by this operation, * particularly if there is more than one occurence of a specific * value. (Conversely, there is also no means of controlling which * key-value pair is deleted by this method; therefore it's not a * terribly smart thing to do.) *

*

* This implementation of a bound map makes no attempt to interpret * events generated by these alternate views of the data. It merely * ensures that registered listeners receive all events generated * by any view of the Map's data. It is left to the client * to interpret the events generated by their map's usage. *

* * @since Commons Events 1.0 * @author Bryce Nordgren / USDA Forest Service */ public class BoundMap extends AbstractMapDecorator { // Fields //----------------------------------------------------------------------- /** * Child-accessible factory used to construct * {@link CollectionChangeEvent}s. * This field is final and is set by the constructor, so * while the children may use it to instantiate events, they * may not change it. */ protected final CollectionChangeEventFactory eventFactory ; /** Utility to support listener registry and event dispatch. */ private final PropertyChangeSupport changeSupport ; // Constructors //----------------------------------------------------------------------- /** * Constructor that wraps (not copies) and takes a * {@link CollectionChangeEventFactory}. *

* This should be used if the client wants to provide a user-specific * CollectionChangeEventFactory implementation. Note that the * same instance of the factory may not be used with multiple * collection decorators. * * @param map the map to decorate, must not be null * @param eventFactory the factory which instantiates * {@link CollectionChangeEvent}s. * @throws IllegalArgumentException if the map or event factory * is null. * @throws UnsupportedOperationException if the eventFactory * has already been used with another decorator. */ protected BoundMap( final Map map, CollectionChangeEventFactory eventFactory) { // initialize parent super(map) ; // Make a default event factory if necessary if (eventFactory == null) { eventFactory = new DefaultCollectionChangeEventFactory() ; } // install the event factory eventFactory.setCollection(this) ; this.eventFactory = eventFactory ; // initialize property change support. changeSupport = new PropertyChangeSupport(this) ; } /** * Constructor that wraps (not copies) and uses the * {@link DefaultCollectionChangeEventFactory}. *

* This should be used if the default change events are considered * adequate to the task of monitoring changes to the collection. * * @param map the map to decorate, must not be null * @throws IllegalArgumentException if the map is null */ protected BoundMap(final Map map) { this(map, null) ; } // Factory Methods //----------------------------------------------------------------------- /** * Factory method to decorate an existing Map using a user-defined * {@link CollectionChangeEventFactory}. *

* This should be used if the client wants to provide a user-specific * CollectionChangeEventFactory implementation. Note that the * same instance of the factory may not be used with multiple * collection decorators. * * @param map the map to decorate, must not be null * @param eventFactory the factory which instantiates * {@link CollectionChangeEvent}s. * @throws IllegalArgumentException if the map or event factory * is null. * @throws UnsupportedOperationException if the eventFactory * has already been used with another decorator. */ public static BoundMap decorate( final Map map, final CollectionChangeEventFactory eventFactory) { return new BoundMap(map, eventFactory) ; } /** * Factory method to decorate an existing Map using the provided * {@link DefaultCollectionChangeEventFactory}. *

* This should be used if the default change events are considered * adequate to the task of monitoring changes to the collection. * * @param map the map to decorate, must not be null * @throws IllegalArgumentException if the map is null */ public static BoundMap decorate( final Map map ) { return new BoundMap(map) ; } // Listener Management //----------------------------------------------------------------------- /** * Registers a listener with this decorator. The Listener must * implement the PropertyChangeListener interface. * Adding a listener more than once will result in more than * one notification for each change event. * * @param l The listener to register with this decorator. */ public void addPropertyChangeListener(PropertyChangeListener l) { changeSupport.addPropertyChangeListener(l) ; } /** * Unregisters a listener from this decorator. The Listener must * implement the PropertyChangeListener interface. * If the listener was registered more than once, calling this method * cancels out a single registration. If the listener is not * registered with this object, no action is taken. * * @param l The listener to unregister with this decorator. */ public void removePropertyChangeListener(PropertyChangeListener l) { changeSupport.removePropertyChangeListener(l) ; } /** * This is a utility method to allow subclasses to fire property change * events. * @param evt The pre-constructed event. */ protected void firePropertyChange(PropertyChangeEvent evt) { changeSupport.firePropertyChange(evt) ; } /** * Package private method to create an EventRepeater from within the * context of a particular BoundCollection object. This event repeater * will relay events to all property change listeners subscribed to this * bound collection. * @return the event repeater object. */ EventRepeater createEventRepeater() { return new EventRepeater(this) ; } // Decoration of Map Methods //----------------------------------------------------------------------- public Object put(Object key, Object value) { Object formerValue = map.put(key, value) ; CollectionChangeEvent evt = eventFactory.createPut( key, value, formerValue, true) ; firePropertyChange(evt) ; return formerValue ; } public void putAll(Map toPut) { map.putAll(toPut) ; CollectionChangeEvent evt = eventFactory.createPutAll(toPut, true) ; firePropertyChange(evt) ; } public void clear() { map.clear() ; CollectionChangeEvent evt = eventFactory.createClear(true) ; firePropertyChange(evt) ; } public Object remove(Object key) { Object formerValue = map.remove(key) ; CollectionChangeEvent evt = eventFactory.createRemove( formerValue, true) ; firePropertyChange(evt) ; return formerValue ; } /** *

* Returns a Set view of the Map entries. This set is backed by the * map, so changes to either view is reflected in the other. * Accordingly, our CollectionChangeEvents are sent to the * Set's subscribers, and the Set's CollectionChangeEvents are sent * to our subscribers. *

*

* The interface contract for this method specifies that most * methods of removal are supported, but no methods of addition. *

* @return An entry set view decorated with a BoundSet. */ public Set entrySet() { Set entries = map.entrySet() ; // make a copy of the event factory CollectionChangeEventFactory factoryCopy = (CollectionChangeEventFactory)(eventFactory.clone()) ; // bind the entry set. BoundSet boundEntries = BoundSet.decorate(entries, factoryCopy) ; // relay "boundEntries" changes to our listeners boundEntries.addPropertyChangeListener(createEventRepeater()) ; // relay our changes to "boundEntries" listeners addPropertyChangeListener(boundEntries.createEventRepeater()) ; return boundEntries ; } /** *

* Returns a Set view of the Map's keys. This set is backed by the * map, so changes to either view is reflected in the other. * Accordingly, our CollectionChangeEvents are sent to the * Set's subscribers, and the Set's CollectionChangeEvents are sent * to our subscribers. *

*

* The interface contract for this method specifies that most * methods of removal are supported, but no methods of addition. *

* @return A set view of the keys decorated with a BoundSet. */ public Set keySet() { Set keys = map.keySet() ; // make a copy of the event factory CollectionChangeEventFactory factoryCopy = (CollectionChangeEventFactory)(eventFactory.clone()) ; // bind the set of keys. BoundSet boundKeys = BoundSet.decorate(keys, factoryCopy) ; // relay "boundKeys" changes to our listeners boundKeys.addPropertyChangeListener(createEventRepeater()) ; // relay our changes to "boundKeys" listeners addPropertyChangeListener(boundKeys.createEventRepeater()) ; return boundKeys ; } /** *

* Returns a Collection view of the Map's values. This collection * is backed by the * map, so changes to either view is reflected in the other. * Accordingly, our CollectionChangeEvents are sent to the * Collection's subscribers, and the Collection's CollectionChangeEvents * are sent to our subscribers. *

*

* The interface contract for this method specifies that most * methods of removal are supported, but no methods of addition. *

* @return A collection view of the values decorated with a BoundCollection. */ public Collection values() { Collection values = map.values() ; // make a copy of the event factory CollectionChangeEventFactory factoryCopy = (CollectionChangeEventFactory)(eventFactory.clone()) ; // bind the collection of values BoundCollection boundValues = BoundCollection.decorate( values, factoryCopy) ; // relay "boundValues" changes to our listeners boundValues.addPropertyChangeListener(createEventRepeater()) ; // relay our changes to "boundValues" listeners addPropertyChangeListener(boundValues.createEventRepeater()) ; return boundValues ; } // Utility classes //----------------------------------------------------------------------- /** * This class subscribes to events from another collection and fires them * to all the subscribers of this collection. */ private class EventRepeater implements PropertyChangeListener { private Object myself ; public EventRepeater(Object me) { myself = me ; } /** * Relay events which did not originate with my list. This * prevents an infinite loop. */ public void propertyChange(PropertyChangeEvent evt) { if ((evt != null) && (evt.getSource() != myself)) { firePropertyChange(evt) ; } } } }