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 isfinal
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) ; } } } }