Source code: com/virtuosotechnologies/lib/util/EventBroadcastHelper.java
1 /*
2 ================================================================================
3
4 FILE: EventBroadcastHelper.java
5
6 PROJECT:
7
8 Virtuoso Utilities
9
10 CONTENTS:
11
12 Helper class for broadcasting events to listeners
13
14 PROGRAMMERS:
15
16 Daniel Azuma (DA) <dazuma@kagi.com>
17
18 COPYRIGHT:
19
20 Copyright (C) 2003 Daniel Azuma (dazuma@kagi.com)
21
22 This program is free software; you can redistribute it and/or
23 modify it under the terms of the GNU General Public License as
24 published by the Free Software Foundation; either version 2
25 of the License, or (at your option) any later version.
26
27 This program is distributed in the hope that it will be useful,
28 but WITHOUT ANY WARRANTY; without even the implied warranty of
29 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
30 GNU General Public License for more details.
31
32 You should have received a copy of the GNU General Public
33 License along with this program; if not, write to
34 Free Software Foundation, Inc.
35 59 Temple Place, Suite 330
36 Boston, MA 02111-1307 USA
37
38 ================================================================================
39 */
40
41
42 package com.virtuosotechnologies.lib.util;
43
44 import java.util.EventObject;
45 import java.util.EventListener;
46 import java.util.List;
47 import java.util.ArrayList;
48 import java.util.Set;
49 import java.util.HashSet;
50 import java.util.Iterator;
51 import java.lang.ref.WeakReference;
52 import java.lang.reflect.Method;
53 import java.lang.reflect.InvocationTargetException;
54
55
56 /**
57 * A helper class for broadcasting events.
58 * <p>
59 * This class is meant to be used internally by any class that needs
60 * to broadcast events. It provides the following facilities:
61 * <ul><li>
62 * It manages a set of listeners, providing helper methods for adding
63 * and removing listeners.
64 * </li><li>
65 * It optionally references listeners via weak references, so they can
66 * be garbage collected and do not need to be removed explicitly.
67 * </li><li>
68 * It provides methods for broadcasting an event to all listeners.
69 * </li></ul>
70 * Typical usage will involve keeping a data member of type
71 * EventBroadcastHelper in your client class. Your add*Listener() and
72 * remove*Listener() methods can delegate to this helper, and
73 * you may use the helper's fireEvent() method to fire events.
74 */
75 public final class EventBroadcastHelper
76 {
77 // Class of listeners
78 private Class listenerClass_;
79
80 // List of listeners
81 private List listeners_;
82
83 // Debugging
84 private boolean verbose_;
85 private String name_;
86
87
88 /**
89 * A helper for listener interfaces that want to provide static
90 * Method members for fast access to their methods. This allows a
91 * listener interface to initialize a static member in a single line
92 * so a static initializer inner class is not needed.
93 *
94 * @param listenerClass class object for the listener
95 * @param name name of the method
96 * @param eventClass class of the event parameter.
97 * @return a Method object for the method
98 * @exception ClassCastException the listener wasn't an EventListener,
99 * or the event wasn't an EventObject.
100 * @exception IllegalArgumentException no such method.
101 */
102 public static Method getListenerMethod(
103 Class listenerClass,
104 String name,
105 Class eventClass)
106 {
107 if (!EventListener.class.isAssignableFrom(listenerClass))
108 {
109 throw new ClassCastException(listenerClass.getName());
110 }
111 if (!EventObject.class.isAssignableFrom(eventClass))
112 {
113 throw new ClassCastException(eventClass.getName());
114 }
115 try
116 {
117 return listenerClass.getMethod(name, new Class[]{eventClass});
118 }
119 catch (NoSuchMethodException e)
120 {
121 throw new IllegalArgumentException("No such method: " +
122 listenerClass.getName() + "." + name + "(" +
123 eventClass.getName() + ")");
124 }
125 }
126
127
128 /**
129 * A helper for listener interfaces that want to provide static
130 * Method members for fast access to their methods. This version
131 * assumes that the listener class declares exactly one method,
132 * and it returns that method.
133 *
134 * @param listenerClass class object for the listener
135 * @return a Method object for the method
136 * @exception ClassCastException the listener wasn't an EventListener,
137 * or the parameter taken by its unique method wasn't an EventObject.
138 * @exception IllegalArgumentException no methods, or multiple methods,
139 * or the unique method took no parameters of multiple parameters.
140 */
141 public static Method getUniqueListenerMethod(
142 Class listenerClass)
143 {
144 if (!EventListener.class.isAssignableFrom(listenerClass))
145 {
146 throw new ClassCastException(listenerClass.getName());
147 }
148 Method[] methods = listenerClass.getDeclaredMethods();
149 if (methods.length != 1)
150 {
151 throw new IllegalArgumentException("Expected exactly one method in " +
152 listenerClass.getName());
153 }
154 Class[] paramTypes = methods[0].getParameterTypes();
155 if (paramTypes.length != 1)
156 {
157 throw new IllegalArgumentException("Expected exactly one parameter for " +
158 listenerClass.getName() + "." + methods[0].getName() + "()");
159 }
160 if (!EventObject.class.isAssignableFrom(paramTypes[0]))
161 {
162 throw new ClassCastException(paramTypes[0].getName());
163 }
164 return methods[0];
165 }
166
167
168 /**
169 * Constructor.
170 *
171 * @param listenerClass the type of listener that will listen to
172 * this broadcaster. Normally, you should pass the class of the
173 * listener interface.
174 */
175 public EventBroadcastHelper(
176 Class listenerClass)
177 {
178 if (!EventListener.class.isAssignableFrom(listenerClass))
179 {
180 throw new ClassCastException(listenerClass.getName());
181 }
182 listenerClass_ = listenerClass;
183 listeners_ = new ArrayList();
184 verbose_ = false;
185 name_ = null;
186 }
187
188
189 /**
190 * Get verbose mode
191 */
192 public boolean isVerbose()
193 {
194 return verbose_;
195 }
196
197
198 /**
199 * Set verbose mode
200 */
201 public void setVerbose(
202 boolean verbose)
203 {
204 verbose_ = verbose;
205 }
206
207
208 /**
209 * Set identifying name that will be returned from toString
210 */
211 public void setName(
212 String name)
213 {
214 name_ = name;
215 }
216
217
218 /**
219 * toString override
220 */
221 public String toString()
222 {
223 if (name_ != null)
224 {
225 return name_;
226 }
227 else
228 {
229 return super.toString();
230 }
231 }
232
233
234 /**
235 * Returns the listener class associated with this broadcaster.
236 *
237 * @return the listener class
238 */
239 public Class getListenerClass()
240 {
241 return listenerClass_;
242 }
243
244
245 /**
246 * Helper for addListener methods. Checks to make sure the listener
247 * is valid, and isn't already added. Returns true if it is okay to
248 * add the listener.
249 */
250 private boolean addListenerHelper(
251 EventListener listener)
252 {
253 if (!listenerClass_.isAssignableFrom(listener.getClass()))
254 {
255 throw new ClassCastException(listener.getClass().getName());
256 }
257 for (Iterator iter = listeners_.iterator(); iter.hasNext(); )
258 {
259 Object lis = iter.next();
260 if (lis instanceof WeakReference)
261 {
262 lis = ((WeakReference)lis).get();
263 }
264 if (lis == null)
265 {
266 iter.remove();
267 }
268 else if (lis == listener)
269 {
270 return false;
271 }
272 }
273 return true;
274 }
275
276
277 /**
278 * Add a listener to the set of listeners. Has no effect if the
279 * listener is already in the set. References the new listener
280 * through a weak reference, which means the reference will go away
281 * when the listener gets collected. This has two implications:
282 * <ul><li>
283 * If the listener is going to get collected otherwise, you don't
284 * need to explicitly remove it from the listener list.
285 * </li>
286 * <li>
287 * You shouldn't add a listener weakly without storing a reference
288 * to it, or it might get collected prematurely. For example,
289 * the following is a no-no:
290 * <pre>addListenerWeak(new MyListener());</pre>
291 * </li></ul>
292 *
293 * @param listener a reference to the listener to add. It must be
294 * castable to the listener class associated with this broadcaster.
295 * @exception ClassCastException the listener isn't castable to the
296 * associated listener class
297 */
298 public synchronized void addListenerWeak(
299 EventListener listener)
300 {
301 if (addListenerHelper(listener))
302 {
303 listeners_.add(new WeakReference(listener));
304 }
305 }
306
307
308 /**
309 * Add a listener to the set of listeners. Has no effect if the
310 * listener is already in the set. References the new listener
311 * through a strong reference, which means the reference will not
312 * be collected until the listener is explicitly removed.
313 *
314 * @param listener a reference to the listener to add. It must be
315 * castable to the listener class associated with this broadcaster.
316 * @exception ClassCastException the listener isn't castable to the
317 * associated listener class
318 */
319 public synchronized void addListenerStrong(
320 EventListener listener)
321 {
322 if (addListenerHelper(listener))
323 {
324 listeners_.add(listener);
325 }
326 }
327
328
329 /**
330 * Removes a listener from the set of listeners. Has no effect if the
331 * listener is not in the list.
332 *
333 * @param listener a reference to the listener to remove.
334 */
335 public synchronized void removeListener(
336 EventListener listener)
337 {
338 for (Iterator iter = listeners_.iterator(); iter.hasNext(); )
339 {
340 Object lis = iter.next();
341 if (lis instanceof WeakReference)
342 {
343 lis = ((WeakReference)lis).get();
344 }
345 if (lis == null || lis == listener)
346 {
347 iter.remove();
348 }
349 }
350 }
351
352
353 /**
354 * Removes all listeners from the set of listeners.
355 */
356 public synchronized void removeAllListeners()
357 {
358 listeners_.clear();
359 }
360
361
362 /**
363 * Returns a copy of the set of listeners as a set. The copy is
364 * not backed by the original, so modifications made to the returned
365 * set do not affect the actual set of listeners.
366 *
367 * @return set of listeners.
368 */
369 public synchronized Set getListenersAsSet()
370 {
371 HashSet ret = new HashSet();
372 for (Iterator iter = listeners_.iterator(); iter.hasNext(); )
373 {
374 Object lis = iter.next();
375 if (lis instanceof WeakReference)
376 {
377 lis = ((WeakReference)lis).get();
378 }
379 if (lis == null)
380 {
381 iter.remove();
382 }
383 else
384 {
385 ret.add(lis);
386 }
387 }
388 return ret;
389 }
390
391
392 /**
393 * Returns a copy of the set of listeners as a list. The copy is
394 * not backed by the original, so modifications made to the returned
395 * list do not affect the actual set of listeners.
396 *
397 * @return list of listeners.
398 */
399 public synchronized List getListenersAsList()
400 {
401 ArrayList ret = new ArrayList();
402 for (Iterator iter = listeners_.iterator(); iter.hasNext(); )
403 {
404 Object lis = iter.next();
405 if (lis instanceof WeakReference)
406 {
407 lis = ((WeakReference)lis).get();
408 }
409 if (lis == null)
410 {
411 iter.remove();
412 }
413 else
414 {
415 ret.add(lis);
416 }
417 }
418 return ret;
419 }
420
421
422 /**
423 * Fires an event. This version takes the name of the method to invoke
424 * and an event object to send.
425 * <p>
426 * This version of fireEvent may be a little slow because it
427 * looks up the method object from the name and parameters via
428 * reflection. If an event is to be fired rapidly, you may want to
429 * create and cache the method object yourself, and call the other
430 * version of fireEvent() that takes a method object.
431 *
432 * @param methodName name of method to invoke
433 * @param event event object to send
434 * @exception IllegalArgumentException the given method was not
435 * found in the listener class, or it is not accessible
436 * because it is private or protected.
437 * @exception IllegalStateException the method threw a checked
438 * exception.
439 */
440 public void fireEvent(
441 String methodName,
442 EventObject event)
443 {
444 try
445 {
446 fireAbortableEvent(methodName, event);
447 }
448 catch (EventAbortedException e)
449 {
450 throw new IllegalStateException(
451 "Unexpected EventAbortedException thrown: " + e);
452 }
453 }
454
455
456 /**
457 * Fires an event. This version takes a method object to invoke
458 * and an event object to send.
459 *
460 * @param method method to invoke
461 * @param event event object to send
462 * @exception IllegalArgumentException the given method is not
463 * accessible because it is private or protected.
464 * @exception IllegalStateException the method threw a checked
465 * exception.
466 */
467 public void fireEvent(
468 Method method,
469 EventObject event)
470 {
471 try
472 {
473 fireAbortableEvent(method, event);
474 }
475 catch (EventAbortedException e)
476 {
477 throw new IllegalStateException(
478 "Unexpected EventAbortedException thrown: " + e);
479 }
480 }
481
482
483 /**
484 * Fires an event. This version takes the name of the method to invoke
485 * and an event object to send.
486 * <p>
487 * This version of fireEvent may be a little slow because it
488 * looks up the method object from the name and parameters via
489 * reflection. If an event is to be fired rapidly, you may want to
490 * create and cache the method object yourself, and call the other
491 * version of fireEvent() that takes a method object.
492 *
493 * @param methodName name of method to invoke
494 * @param event event object to send
495 * @exception EventAbortedException event aborted by a listener
496 * @exception IllegalArgumentException the given method was not
497 * found in the listener class, or it is not accessible
498 * because it is private or protected.
499 * @exception IllegalStateException the method threw a checked
500 * exception.
501 */
502 public void fireAbortableEvent(
503 String methodName,
504 EventObject event)
505 throws
506 EventAbortedException
507 {
508 for (Class eventClass = event.getClass();
509 EventObject.class.isAssignableFrom(eventClass);
510 eventClass = eventClass.getSuperclass())
511 {
512 try
513 {
514 Method method = listenerClass_.getMethod(methodName,
515 new Class[]{eventClass});
516 fireAbortableEvent(method, event);
517 return;
518 }
519 catch (NoSuchMethodException e)
520 {
521 // Not this event class. Try its superclass...
522 }
523 }
524 throw new IllegalArgumentException("No such method: " +
525 listenerClass_.getName() + "." + methodName + "(" +
526 event.getClass().getName() + ")");
527 }
528
529
530 /**
531 * Fires an event. This version takes a method object to invoke
532 * and an event object to send.
533 *
534 * @param method method to invoke
535 * @param event event object to send
536 * @exception EventAbortedException event aborted by a listener
537 * @exception IllegalArgumentException the given method is not
538 * accessible because it is private or protected.
539 * @exception IllegalStateException the method threw a checked
540 * exception.
541 */
542 public void fireAbortableEvent(
543 Method method,
544 EventObject event)
545 throws
546 EventAbortedException
547 {
548 Object[] args = new Object[]{event};
549 // Note: we copy the listener list and iterate over the copy rather than the
550 // original in order to obtain thread safety. We can't synchronize this whole
551 // method because then listeners will be called while this object's monitor is
552 // obtained. This can cause lock order violations (leading to thread deadlock),
553 // since this object should be a lower-level object than any client. Yes,
554 // making a copy is slower, but it's necessary for thread safety.
555 for (Iterator iter = getListenersAsList().iterator(); iter.hasNext(); )
556 {
557 Object lis = iter.next();
558 try
559 {
560 method.invoke(lis, args);
561 }
562 catch (IllegalAccessException e)
563 {
564 throw new IllegalArgumentException(
565 "Can't access method: " + method);
566 }
567 catch (InvocationTargetException e)
568 {
569 Throwable contained = e.getTargetException();
570 if (contained instanceof EventAbortedException)
571 {
572 throw (EventAbortedException)contained;
573 }
574 if (contained instanceof RuntimeException)
575 {
576 throw (RuntimeException)contained;
577 }
578 if (contained instanceof Error)
579 {
580 throw (Error)contained;
581 }
582 throw new IllegalStateException(
583 "Unexpected checked exception thrown: " + contained);
584 }
585 }
586 }
587 }