Docjar: A Java Source and Docuemnt Enginecom.*    java.*    javax.*    org.*    all    new    plug-in

Quick Search    Search Deep

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 }