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 || !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 				  propertyName, oldValue, newValue);
387 	  }
388 
389     if (oldValue != newValue) {
390       multicast(new PropertyChangeEvent(source,
391                                         propertyName, oldValue, newValue));
392     }
393   }
394 
395 
396   /**
397    * Report a boolean bound property update to any registered listeners.
398    * No event is fired if old and new are equal and non-null.
399    * <p>
400    * This is merely a convenience wrapper around the more general
401    * firePropertyChange method that takes Object values.
402    *
403    * @param propertyName  The programmatic name of the property
404    *		that was changed.
405    * @param oldValue  The old value of the property.
406    * @param newValue  The new value of the property.
407    */
408   public void firePropertyChange(String propertyName, 
409                                  boolean oldValue, boolean newValue) {
410 	  if (logger.isDebugEnabled())
411 	  {
412 		  logger.debug("firePropertyChange(propertyName={}, oldValue={}, newValue={}) - start",
413 				  new Object[]{ propertyName, String.valueOf(oldValue), String.valueOf(newValue) });
414 	  }
415 
416     if (oldValue != newValue) {
417       multicast(new PropertyChangeEvent(source,
418                                         propertyName, 
419                                         new Boolean(oldValue), 
420                                         new Boolean(newValue)));
421     }
422   }
423 
424   /**
425    * Fire an existing PropertyChangeEvent to any registered listeners.
426    * No event is fired if the given event's old and new values are
427    * equal and non-null.
428    * @param evt  The PropertyChangeEvent object.
429    */
430   public void firePropertyChange(PropertyChangeEvent evt) {
431         logger.debug("firePropertyChange(evt={}) - start", evt);
432 
433     Object oldValue = evt.getOldValue();
434     Object newValue = evt.getNewValue();
435     if (oldValue == null || !oldValue.equals(newValue)) 
436       multicast(evt);
437   }
438 
439   /**
440    * Check if there are any listeners for a specific property.
441    * If propertyName is null, return whether there are any listeners at all.
442    *
443    * @param propertyName  the property name.
444    * @return true if there are one or more listeners for the given property
445    * 
446    */
447   public boolean hasListeners(String propertyName) {
448         logger.debug("hasListeners(propertyName={}) - start", propertyName);
449 
450     PropertyChangeMulticaster child;
451 
452     synchronized (this) {
453       if (listeners.length > 0)
454         return true;
455       else if (propertyName == null || children == null)
456         return false;
457       else {
458         child = (PropertyChangeMulticaster)children.get(propertyName);
459         if (child == null)
460           return false;
461       }
462     }
463     
464     return child.hasListeners(null);
465   }
466 
467 
468   /**
469    * @serialData Null terminated list of <code>PropertyChangeListeners</code>.
470    * <p>
471    * At serialization time we skip non-serializable listeners and
472    * only serialize the serializable listeners.
473    *
474    */
475   private synchronized void writeObject(ObjectOutputStream s) throws IOException {
476         logger.debug("writeObject(s={}) - start", s);
477 
478     s.defaultWriteObject();
479     
480     for (int i = 0; i < listeners.length; i++) {      
481       if (listeners[i] instanceof Serializable) {
482         s.writeObject(listeners[i]);
483       }
484     }
485     s.writeObject(null);
486   }
487   
488   
489   private void readObject(ObjectInputStream s) throws ClassNotFoundException, IOException {
490         logger.debug("readObject(s={}) - start", s);
491 
492     listeners = new PropertyChangeListener[0];     // paranoically reset
493     s.defaultReadObject();
494     
495     Object listenerOrNull;
496     while (null != (listenerOrNull = s.readObject())) {
497       addPropertyChangeListener((PropertyChangeListener)listenerOrNull);
498     }
499   }
500 
501 }