View Javadoc
1   /*
2     File: PropertyChangeMulticaster.java
3   
4     Originally written by Doug Lea and released into the public domain.
5     This may be used for any purposes whatsoever without acknowledgment.
6     Thanks for the assistance and support of Sun Microsystems Labs,
7     and everyone contributing, testing, and using this code.
8   
9     This class is based on Sun JDK java.beans.VetoableChangeSupport,
10    which is copyrighted by Sun. (It shares practically no code, but for 
11    consistency, the documentation was lifted and adapted here.)
12  
13    History:
14    Date       Who                What
15    14Mar1999   dl                 first release
16  */
17  
18  package org.dbunit.util.concurrent;
19  
20  import org.slf4j.Logger;
21  import org.slf4j.LoggerFactory;
22  
23  import java.beans.PropertyChangeEvent;
24  import java.beans.PropertyChangeListener;
25  import java.io.IOException;
26  import java.io.ObjectInputStream;
27  import java.io.ObjectOutputStream;
28  import java.io.Serializable;
29  import java.util.HashMap;
30  
31  /**
32   * This class is interoperable with java.beans.PropertyChangeSupport,
33   * but relies on a streamlined copy-on-write scheme similar to
34   * that used in CopyOnWriteArrayList. This leads to much better
35   * performance in most event-intensive programs. It also adheres to clarified
36   * semantics of add and remove  operations.
37   * <p>
38   * <b>Sample usage.</b>
39   * 
40   * <pre>
41   * class Thing {
42   *   protected Color myColor = Color.red; // an example property
43   *
44   *   protected PropertyChangeMulticaster listeners =
45   *     new PropertyChangeMulticaster(this);
46   *
47   *   // registration methods, including:
48   *   void addListener(PropertyChangeListener l) {
49   *     // Use the `ifAbsent' version to avoid duplicate notifications
50   *     listeners.addPropertyChangeListenerIfAbsent(l);
51   *   }
52   *  
53   *  public synchronized Color getColor() { // accessor
54   *    return myColor;
55   *  }
56   *
57   *   // internal synchronized state change method; returns old value
58   *   protected synchronized Color assignColor(Color newColor) { 
59   *     Color oldColor = myColor;
60   *     myColor = newColor; 
61   *     return oldColor;
62   *   }
63   *
64   *   public void setColor(Color newColor) {
65   *     // atomically change state
66   *     Color oldColor = assignColor(newColor);
67   *     // broadcast change notification without holding synch lock
68   *     listeners.firePropertyChange("color", oldColor, newColor);
69   *   }
70   * }
71   * </pre>   
72   * <p>[<a href="http://gee.cs.oswego.edu/dl/classes/EDU/oswego/cs/dl/util/concurrent/intro.html"> Introduction to this package. </a>]
73   * 
74   * @author Doug Lea
75   * @author Last changed by: $Author$
76   * @version $Revision$ $Date$
77   * @since ? (pre 2.1)
78   */
79  public class PropertyChangeMulticaster implements Serializable {
80  
81      /**
82       * Logger for this class
83       */
84      private static final Logger logger = LoggerFactory.getLogger(PropertyChangeMulticaster.class);
85  
86    // In order to allow this class to be lifted out without using
87    // the whole package, the basic mechanics of CopyOnWriteArrayList
88    // are used here, but not the class itself. 
89    // This also makes it barely faster.
90  
91    /**
92     * The array of listeners. Copied on each update
93     **/
94  
95    protected transient PropertyChangeListener[] listeners = new PropertyChangeListener[0];
96  
97  
98    /** 
99     * The object to be provided as the "source" for any generated events.
100    * @serial
101    */
102   protected final Object source;
103 
104   /** 
105    * HashMap for managing listeners for specific properties.
106    * Maps property names to PropertyChangeMulticaster objects.
107    * @serial
108    */
109   protected HashMap children;
110 
111   /**
112    * Return the child associated with property, or null if no such
113    **/
114 
115   protected synchronized PropertyChangeMulticaster getChild(String propertyName) {
116     return (children == null)? null : 
117       ((PropertyChangeMulticaster)children.get(propertyName));
118   }
119 
120 
121   /**
122    * Constructs a <code>PropertyChangeMulticaster</code> object.
123    *
124    * @param sourceBean  The bean to be given as the source for any events.
125    * @exception NullPointerException if sourceBean is null
126    */
127   
128   public PropertyChangeMulticaster(Object sourceBean) {
129     if (sourceBean == null) {
130       throw new NullPointerException();
131     }
132 
133     source = sourceBean;
134   }
135 
136   /**
137    * Add a VetoableChangeListener to the listener list.
138    * The listener is registered for all properties.
139    * If the listener is added multiple times, it will
140    * receive multiple change notifications upon any firePropertyChange
141    *
142    * @param listener  The PropertyChangeListener to be added
143    * @exception NullPointerException If listener is null
144    */
145   
146   public synchronized void addPropertyChangeListener(PropertyChangeListener listener) {
147         logger.debug("addPropertyChangeListener(listener={}) - start", listener);
148 
149     if (listener == null) throw new NullPointerException();
150 
151     int len = listeners.length;
152     PropertyChangeListener[] newArray = new PropertyChangeListener[len + 1];
153     if (len > 0)
154       System.arraycopy(listeners, 0, newArray, 0, len);
155     newArray[len] = listener;
156     listeners = newArray;
157   }
158 
159 
160   /**
161    * Add a PropertyChangeListener to the listener list if it is 
162    * not already present.
163    * The listener is registered for all properties.
164    * The operation maintains Set semantics: If the listener is already 
165    * registered, the operation has no effect.
166    *
167    * @param listener  The PropertyChangeListener to be added
168    * @exception NullPointerException If listener is null
169    */
170   
171   public synchronized void addPropertyChangeListenerIfAbsent(PropertyChangeListener listener) {
172         logger.debug("addPropertyChangeListenerIfAbsent(listener={}) - start", listener);
173 
174     if (listener == null) throw new NullPointerException();
175 
176     // Copy while checking if already present.
177     int len = listeners.length; 
178     PropertyChangeListener[] newArray = new PropertyChangeListener[len + 1];
179     for (int i = 0; i < len; ++i) {
180       newArray[i] = listeners[i];
181       if (listener.equals(listeners[i]))
182 	return; // already present -- throw away copy
183     }
184     newArray[len] = listener;
185     listeners = newArray;
186   }
187 
188 
189   /**
190    * Remove a PropertyChangeListener from the listener list.
191    * It removes at most one occurrence of the given listener.
192    * If the listener was added multiple times it must be removed
193    * mulitple times.
194    * This removes a PropertyChangeListener that was registered
195    * for all properties, and has no effect if registered for only
196    * one or more specified properties.
197    *
198    * @param listener  The PropertyChangeListener to be removed
199    */
200   
201   public synchronized void removePropertyChangeListener(PropertyChangeListener listener) {
202         logger.debug("removePropertyChangeListener(listener={}) - start", listener);
203 
204     int newlen = listeners.length-1;
205     if (newlen < 0 || listener == null) return;
206 
207     // Copy while searching for element to remove
208 
209     PropertyChangeListener[] newArray = new PropertyChangeListener[newlen];
210 
211     for (int i = 0; i < newlen; ++i) { 
212       if (listener.equals(listeners[i])) {
213         //  copy remaining and exit
214         for (int k = i + 1; k <= newlen; ++k) newArray[k-1] = listeners[k];
215         listeners = newArray;
216         return;
217       }
218       else
219         newArray[i] = listeners[i];
220     }
221 
222     // special-case last cell
223     if (listener.equals(listeners[newlen]))
224       listeners = newArray;
225   }
226 
227   /**
228    * Add a PropertyChangeListener for a specific property.  The listener
229    * will be invoked only when a call on firePropertyChange names that
230    * specific property. However, if a listener is registered both for all
231    * properties and a specific property, it will receive multiple 
232    * notifications upon changes to that property.
233    *
234    * @param propertyName  The name of the property to listen on.
235    * @param listener  The PropertyChangeListener to be added
236    * @exception NullPointerException If listener is null
237    */
238   
239   public void addPropertyChangeListener(String propertyName,
240                                         PropertyChangeListener listener) {
241         logger.debug("addPropertyChangeListener(propertyName={}, listener={}) - start", propertyName, listener);
242 
243     if (listener == null) throw new NullPointerException();
244 
245     PropertyChangeMulticaster child = null;
246 
247     synchronized(this) {
248       if (children == null) 
249         children = new HashMap();
250       else 
251         child = (PropertyChangeMulticaster)children.get(propertyName);
252       
253       if (child == null) {
254         child = new PropertyChangeMulticaster(source);
255         children.put(propertyName, child);
256       }
257     }
258 
259     child.addPropertyChangeListener(listener);
260   }
261 
262   /**
263    * Add a PropertyChangeListener for a specific property, if it is not
264    * already registered.  The listener
265    * will be invoked only when a call on firePropertyChange names that
266    * specific property. 
267    *
268    * @param propertyName  The name of the property to listen on.
269    * @param listener  The PropertyChangeListener to be added
270    * @exception NullPointerException If listener is null
271    */
272   
273   public void addPropertyChangeListenerIfAbsent(String propertyName,
274                                         PropertyChangeListener listener) {
275         logger.debug("addPropertyChangeListenerIfAbsent(propertyName={}, listener={}) - start", propertyName, listener);
276 
277     if (listener == null) throw new NullPointerException();
278 
279     PropertyChangeMulticaster child = null;
280 
281     synchronized(this) {
282       if (children == null) 
283         children = new HashMap();
284       else 
285         child = (PropertyChangeMulticaster)children.get(propertyName);
286       
287       if (child == null) {
288         child = new PropertyChangeMulticaster(source);
289         children.put(propertyName, child);
290       }
291     }
292 
293     child.addPropertyChangeListenerIfAbsent(listener);
294   }
295 
296   /**
297    * Remove a PropertyChangeListener for a specific property.
298    * Affects only the given property. 
299    * If the listener is also registered for all properties,
300    * then it will continue to be registered for them.
301    *
302    * @param propertyName  The name of the property that was listened on.
303    * @param listener  The PropertyChangeListener to be removed
304    */
305   
306   public void removePropertyChangeListener(String propertyName,
307                                            PropertyChangeListener listener) {
308         logger.debug("removePropertyChangeListener(propertyName={}, listener={}) - start", propertyName, listener);
309 
310     PropertyChangeMulticaster child = getChild(propertyName);
311     if (child != null) 
312       child.removePropertyChangeListener(listener);
313   }
314 
315 
316   /**
317    * Helper method to relay evt to all listeners. 
318    * Called by all public firePropertyChange methods.
319    **/
320 
321   protected void multicast(PropertyChangeEvent evt) {
322         logger.debug("multicast(evt={}) - start", evt);
323 
324     PropertyChangeListener[] array;  // bind in synch block below
325     PropertyChangeMulticaster child = null;
326 
327     synchronized (this) {
328       array = listeners;
329 
330       if (children != null && evt.getPropertyName() != null)
331         child = (PropertyChangeMulticaster)children.get(evt.getPropertyName());
332     }
333     
334     for (int i = 0; i < array.length; ++i) 
335       array[i].propertyChange(evt);
336     
337     if (child != null) 
338       child.multicast(evt);
339 
340   }
341 
342   
343   /**
344    * Report a bound property update to any registered listeners.
345    * No event is fired if old and new are equal and non-null.
346    *
347    * @param propertyName  The programmatic name of the property
348    *		that was changed.
349    * @param oldValue  The old value of the property.
350    * @param newValue  The new value of the property.
351    */
352   public void firePropertyChange(String propertyName, 
353                                  Object oldValue, Object newValue) {
354 	  if (logger.isDebugEnabled())
355 	  {
356 		  logger.debug("firePropertyChange(propertyName={}, oldValue={}, newValue={}) - start",
357 				  new Object[]{ propertyName, oldValue, newValue });
358 	  }
359    
360     if (oldValue == null || newValue == null || !oldValue.equals(newValue)) {
361       multicast(new PropertyChangeEvent(source,
362                                         propertyName, 
363                                         oldValue, 
364                                         newValue));
365     }
366     
367   }
368 
369   /**
370    * Report an int bound property update to any registered listeners.
371    * No event is fired if old and new are equal and non-null.
372    * <p>
373    * This is merely a convenience wrapper around the more general
374    * firePropertyChange method that takes Object values.
375    *
376    * @param propertyName  The programmatic name of the property
377    *		that was changed.
378    * @param oldValue  The old value of the property.
379    * @param newValue  The new value of the property.
380    */
381   public void firePropertyChange(String propertyName, 
382                                  int oldValue, int newValue) {
383 	  if (logger.isDebugEnabled())
384 	  {
385 		  logger.debug("firePropertyChange(propertyName={}, oldValue={}, newValue={}) - start",
386 				  new Object[]{ propertyName, String.valueOf(oldValue), String.valueOf(newValue) });
387 	  }
388 
389     if (oldValue != newValue) {
390       multicast(new PropertyChangeEvent(source,
391                                         propertyName, 
392                                         new Integer(oldValue), 
393                                         new Integer(newValue)));
394     }
395   }
396 
397 
398   /**
399    * Report a boolean bound property update to any registered listeners.
400    * No event is fired if old and new are equal and non-null.
401    * <p>
402    * This is merely a convenience wrapper around the more general
403    * firePropertyChange method that takes Object values.
404    *
405    * @param propertyName  The programmatic name of the property
406    *		that was changed.
407    * @param oldValue  The old value of the property.
408    * @param newValue  The new value of the property.
409    */
410   public void firePropertyChange(String propertyName, 
411                                  boolean oldValue, boolean newValue) {
412 	  if (logger.isDebugEnabled())
413 	  {
414 		  logger.debug("firePropertyChange(propertyName={}, oldValue={}, newValue={}) - start",
415 				  new Object[]{ propertyName, String.valueOf(oldValue), String.valueOf(newValue) });
416 	  }
417 
418     if (oldValue != newValue) {
419       multicast(new PropertyChangeEvent(source,
420                                         propertyName, 
421                                         new Boolean(oldValue), 
422                                         new Boolean(newValue)));
423     }
424   }
425 
426   /**
427    * Fire an existing PropertyChangeEvent to any registered listeners.
428    * No event is fired if the given event's old and new values are
429    * equal and non-null.
430    * @param evt  The PropertyChangeEvent object.
431    */
432   public void firePropertyChange(PropertyChangeEvent evt) {
433         logger.debug("firePropertyChange(evt={}) - start", evt);
434 
435     Object oldValue = evt.getOldValue();
436     Object newValue = evt.getNewValue();
437     if (oldValue == null || newValue == null || !oldValue.equals(newValue)) 
438       multicast(evt);
439   }
440 
441   /**
442    * Check if there are any listeners for a specific property.
443    * If propertyName is null, return whether there are any listeners at all.
444    *
445    * @param propertyName  the property name.
446    * @return true if there are one or more listeners for the given property
447    * 
448    */
449   public boolean hasListeners(String propertyName) {
450         logger.debug("hasListeners(propertyName={}) - start", propertyName);
451 
452     PropertyChangeMulticaster child;
453 
454     synchronized (this) {
455       if (listeners.length > 0)
456         return true;
457       else if (propertyName == null || children == null)
458         return false;
459       else {
460         child = (PropertyChangeMulticaster)children.get(propertyName);
461         if (child == null)
462           return false;
463       }
464     }
465     
466     return child.hasListeners(null);
467   }
468 
469 
470   /**
471    * @serialData Null terminated list of <code>PropertyChangeListeners</code>.
472    * <p>
473    * At serialization time we skip non-serializable listeners and
474    * only serialize the serializable listeners.
475    *
476    */
477   private synchronized void writeObject(ObjectOutputStream s) throws IOException {
478         logger.debug("writeObject(s={}) - start", s);
479 
480     s.defaultWriteObject();
481     
482     for (int i = 0; i < listeners.length; i++) {      
483       if (listeners[i] instanceof Serializable) {
484         s.writeObject(listeners[i]);
485       }
486     }
487     s.writeObject(null);
488   }
489   
490   
491   private void readObject(ObjectInputStream s) throws ClassNotFoundException, IOException {
492         logger.debug("readObject(s={}) - start", s);
493 
494     listeners = new PropertyChangeListener[0];     // paranoically reset
495     s.defaultReadObject();
496     
497     Object listenerOrNull;
498     while (null != (listenerOrNull = s.readObject())) {
499       addPropertyChangeListener((PropertyChangeListener)listenerOrNull);
500     }
501   }
502 
503 }