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 }