EventRouter.java
001 /*
002  * Copyright 2009-2012 the original author or authors.
003  *
004  * Licensed under the Apache License, Version 2.0 (the "License");
005  * you may not use this file except in compliance with the License.
006  * You may obtain a copy of the License at
007  *
008  *      http://www.apache.org/licenses/LICENSE-2.0
009  *
010  * Unless required by applicable law or agreed to in writing, software
011  * distributed under the License is distributed on an "AS IS" BASIS,
012  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013  * See the License for the specific language governing permissions and
014  * limitations under the License.
015  */
016 package org.codehaus.griffon.runtime.core;
017 
018 import griffon.core.GriffonArtifact;
019 import griffon.core.UIThreadManager;
020 import griffon.util.RunnableWithArgs;
021 import groovy.lang.*;
022 import org.codehaus.groovy.runtime.InvokerHelper;
023 import org.slf4j.Logger;
024 import org.slf4j.LoggerFactory;
025 
026 import java.util.*;
027 import java.util.concurrent.BlockingQueue;
028 import java.util.concurrent.LinkedBlockingQueue;
029 
030 import static griffon.util.GriffonNameUtils.capitalize;
031 import static griffon.util.GriffonNameUtils.isBlank;
032 import static java.util.Collections.EMPTY_LIST;
033 import static java.util.Collections.synchronizedList;
034 import static org.codehaus.groovy.runtime.MetaClassHelper.convertToTypeArray;
035 
036 /**
037  * An event handling helper.<p>
038  * Listeners may be of type<ul>
039  <li><tt>Script</tt></li>
040  <li><tt>Map</tt></li>
041  <li><tt>Closure</tt></li>
042  <li><tt>RunnableWithArgs</tt></li>
043  <li><tt>Object</tt> (a Java bean)</li>
044  </ul>
045  <p/>
046  * With the exception of Maps and Closures, the naming convention for an eventHandler is
047  * "on" + eventName, Maps and Closures require handlers to be named as eventName only.<p>
048  * Some examples of eventHandler names are: onStartupStart, onMyCoolEvent.
049  * Event names must follow the camelCase naming convention.<p>
050  *
051  @author Andres Almiray
052  */
053 public class EventRouter {
054     private boolean enabled = true;
055     private final List listeners = synchronizedList(new ArrayList());
056     private final Map<Script, Binding> scriptBindings = new LinkedHashMap<Script, Binding>();
057     private final Map<String, List> closureListeners = Collections.synchronizedMap(new LinkedHashMap<String, List>());
058     private final BlockingQueue<Runnable> deferredEvents = new LinkedBlockingQueue<Runnable>();
059 
060     private static final Logger LOG = LoggerFactory.getLogger(EventRouter.class);
061     private static final Object LOCK = new Object();
062     private static int count = 1;
063 
064     private static int identifier() {
065         synchronized (LOCK) {
066             return count++;
067         }
068     }
069 
070     public EventRouter() {
071         new Thread(new Runnable() {
072             public void run() {
073                 while (true) {
074                     try {
075                         deferredEvents.take().run();
076                     catch (InterruptedException e) {
077                         // ignore ?
078                     }
079                 }
080             }
081         }"EventRouter-" + identifier()).start();
082     }
083 
084     /**
085      * Returns the current enabled state.
086      *
087      @return true if the router is enabled; false otherwise.
088      */
089     public boolean isEnabled() {
090         synchronized (LOCK) {
091             return this.enabled;
092         }
093     }
094 
095     /**
096      * Sets the enabled state of this router.</p>
097      * A disabled router will simply discard all events that are sent to it, in other
098      * words listeners will never be notified. Discarded events cannot be recovered, even
099      * if the router is enabled at a later point in time.
100      *
101      @param enabled the value for the enabled state
102      */
103     public void setEnabled(boolean enabled) {
104         synchronized (LOCK) {
105             this.enabled = enabled;
106         }
107     }
108 
109     /**
110      * Publishes an event with optional arguments.</p>
111      * Event listeners will be notified in the same thread
112      * that originated the event.
113      *
114      @param eventName the name of the event
115      */
116     public void publish(String eventName) {
117         publish(eventName, EMPTY_LIST);
118     }
119 
120     /**
121      * Publishes an event with optional arguments.</p>
122      * Event listeners will be notified in the same thread
123      * that originated the event.
124      *
125      @param eventName the name of the event
126      @param params    the event's arguments
127      */
128     public void publish(String eventName, List params) {
129         if (!isEnabled()) return;
130         if (isBlank(eventName)) return;
131         if (params == nullparams = EMPTY_LIST;
132         buildPublisher(eventName, params, "synchronously").run();
133     }
134 
135     /**
136      * Publishes an event with optional arguments.</p>
137      * Event listeners are guaranteed to be notified
138      * outside of the UI thread always.
139      *
140      @param eventName the name of the event
141      @deprecated use #eventOutsideUI() instead
142      */
143     @Deprecated
144     public void publishOutside(String eventName) {
145         publishOutside(eventName, EMPTY_LIST);
146     }
147 
148     /**
149      * Publishes an event with optional arguments.</p>
150      * Event listeners are guaranteed to be notified
151      * outside of the UI thread always.
152      *
153      @param eventName the name of the event
154      @param params    the event's arguments
155      @deprecated use #eventOutsideUI() instead
156      */
157     @Deprecated
158     public void publishOutside(String eventName, List params) {
159         publishOutsideUI(eventName, params);
160     }
161 
162     /**
163      * Publishes an event with optional arguments.</p>
164      * Event listeners are guaranteed to be notified
165      * outside of the UI thread always.
166      *
167      @param eventName the name of the event
168      */
169     public void publishOutsideUI(String eventName) {
170         publishOutsideUI(eventName, EMPTY_LIST);
171     }
172 
173     /**
174      * Publishes an event with optional arguments.</p>
175      * Event listeners are guaranteed to be notified
176      * outside of the UI thread always.
177      *
178      @param eventName the name of the event
179      @param params    the event's arguments
180      */
181     public void publishOutsideUI(String eventName, List params) {
182         if (!isEnabled()) return;
183         if (isBlank(eventName)) return;
184         if (params == nullparams = EMPTY_LIST;
185         UIThreadManager.getInstance().executeOutside(buildPublisher(eventName, params, "outside UI"));
186     }
187 
188     /**
189      * Publishes an event with optional arguments.</p>
190      * Event listeners are guaranteed to be notified
191      * in a different thread than the publisher's, always.
192      *
193      @param eventName the name of the event
194      */
195     public void publishAsync(String eventName) {
196         publishAsync(eventName, EMPTY_LIST);
197     }
198 
199     /**
200      * Publishes an event with optional arguments.</p>
201      * Event listeners are guaranteed to be notified
202      * in a different thread than the publisher's, always.
203      *
204      @param eventName the name of the event
205      @param params    the event's arguments
206      */
207     public void publishAsync(String eventName, List params) {
208         if (!isEnabled()) return;
209         if (isBlank(eventName)) return;
210         if (params == nullparams = EMPTY_LIST;
211         deferredEvents.offer(buildPublisher(eventName, params, "asynchronously"));
212     }
213 
214     private Runnable buildPublisher(final String event, final List params, final String mode) {
215         return new Runnable() {
216             public void run() {
217                 String eventName = capitalize(event);
218                 if (LOG.isTraceEnabled()) {
219                     LOG.trace("Triggering event '" + eventName + "' " + mode);
220                 }
221                 String eventHandler = "on" + eventName;
222                 // defensive copying to avoid CME during event dispatching
223                 // GRIFFON-224
224                 List listenersCopy = new ArrayList();
225                 synchronized (listeners) {
226                     listenersCopy.addAll(listeners);
227                 }
228                 synchronized (closureListeners) {
229                     List list = closureListeners.get(eventName);
230                     if (list != null) {
231                         for (Object listener : list) {
232                             listenersCopy.add(listener);
233                         }
234                     }
235                 }
236 
237                 for (Object listener : listenersCopy) {
238                     if (listener instanceof Script) {
239                         fireEvent((Scriptlistener, eventHandler, params);
240                     else if (listener instanceof Closure) {
241                         fireEvent((Closurelistener, eventHandler, params);
242                     else if (listener instanceof RunnableWithArgs) {
243                         fireEvent((RunnableWithArgslistener, eventHandler, params);
244                     else {
245                         fireEvent(listener, eventHandler, params);
246                     }
247                 }
248             }
249         };
250     }
251 
252     private Object[] asArray(List list) {
253         return list.toArray(new Object[list.size()]);
254     }
255 
256     private void invokeHandler(Object handler, List params) {
257         if (handler instanceof Closure) {
258             ((Closurehandler).call(asArray(params));
259         else if (handler instanceof RunnableWithArgs) {
260             ((RunnableWithArgshandler).run(asArray(params));
261         }
262     }
263 
264     private void fireEvent(Script script, String eventHandler, List params) {
265         Binding binding = scriptBindings.get(script);
266         if (binding == null) {
267             binding = new Binding();
268             script.setBinding(binding);
269             script.run();
270             scriptBindings.put(script, binding);
271         }
272 
273         invokeHandler(binding.getVariable(eventHandler), params);
274     }
275 
276     private void fireEvent(Map map, String eventHandler, List params) {
277         eventHandler = eventHandler.substring(2);
278         invokeHandler(map.get(eventHandler), params);
279     }
280 
281     private void fireEvent(Closure closure, String eventHandler, List params) {
282         closure.call(asArray(params));
283     }
284 
285     private void fireEvent(RunnableWithArgs runnable, String eventHandler, List params) {
286         runnable.run(asArray(params));
287     }
288 
289     private void fireEvent(Object instance, String eventHandler, List params) {
290         MetaClass mc = metaClassOf(instance);
291         MetaProperty mp = mc.getMetaProperty(eventHandler);
292         if (mp != null && mp.getProperty(instance!= null) {
293             invokeHandler(mp.getProperty(instance), params);
294             return;
295         }
296 
297         Class[] argTypes = convertToTypeArray(asArray(params));
298         MetaMethod mm = mc.pickMethod(eventHandler, argTypes);
299         if (mm != null) {
300             mm.invoke(instance, asArray(params));
301         }
302     }
303 
304     private MetaClass metaClassOf(Object obj) {
305         if (obj instanceof GriffonArtifact) {
306             return ((GriffonArtifactobj).getGriffonClass().getMetaClass();
307         else if (obj instanceof GroovyObject) {
308             return ((GroovyObjectobj).getMetaClass();
309         }
310         return GroovySystem.getMetaClassRegistry().getMetaClass(obj.getClass());
311     }
312 
313     /**
314      * Adds an event listener.<p>
315      <p/>
316      * A listener may be a<ul>
317      <li><tt>Script</tt></li>
318      <li><tt>Map</tt></li>
319      <li><tt>Object</tt> (a Java bean)</li>
320      </ul>
321      <p/>
322      * With the exception of Maps, the naming convention for an eventHandler is
323      * "on" + eventName, Maps require handlers to be named as eventName only.<p>
324      * Some examples of eventHandler names are: onStartupStart, onMyCoolEvent.
325      * Event names must follow the camelCase naming convention.<p>
326      *
327      @param listener an event listener of type Script, Map or Object
328      */
329     public void addEventListener(Object listener) {
330         if (listener == null || listener instanceof Closure || listener instanceof RunnableWithArgsreturn;
331         if (listener instanceof Map) {
332             addEventListener((Maplistener);
333             return;
334         }
335         synchronized (listeners) {
336             if (listeners.contains(listener)) return;
337             try {
338                 LOG.debug("Adding listener " + listener);
339             catch (UnsupportedOperationException uoe) {
340                 LOG.debug("Adding listener " + listener.getClass().getName());
341             }
342             listeners.add(listener);
343         }
344     }
345 
346     /**
347      * Adds a Map containing event listeners.<p>
348      <p/>
349      * An event listener may be a<ul>
350      <li><tt>Closure</tt></li>
351      <li><tt>RunnableWithArgs</tt></li>
352      </ul>
353      <p/>
354      * Maps require handlers to be named as eventName only.<p>
355      * Some examples of eventHandler names are: StartupStart, MyCoolEvent.
356      * Event names must follow the camelCase naming convention.<p>
357      *
358      @param listener an event listener of type Script, Map or Object
359      */
360     public void addEventListener(Map<String, Object> listener) {
361         if (listener == nullreturn;
362         for (Map.Entry<String, Object> entry : listener.entrySet()) {
363             Object value = entry.getValue();
364             if (value instanceof Closure) {
365                 addEventListener(entry.getKey()(Closurevalue);
366             else if (value instanceof RunnableWithArgs) {
367                 addEventListener(entry.getKey()(RunnableWithArgsvalue);
368             }
369         }
370     }
371 
372     /**
373      * Removes an event listener.<p>
374      <p/>
375      * A listener may be a<ul>
376      <li><tt>Script</tt></li>
377      <li><tt>Map</tt></li>
378      <li><tt>Object</tt> (a Java bean)</li>
379      </ul>
380      <p/>
381      * With the exception of Maps, the naming convention for an eventHandler is
382      * "on" + eventName, Maps require handlers to be named as eventName only.<p>
383      * Some examples of eventHandler names are: onStartupStart, onMyCoolEvent.
384      * Event names must follow the camelCase naming convention.<p>
385      *
386      @param listener an event listener of type Script, Map or Object
387      */
388     public void removeEventListener(Object listener) {
389         if (listener == null || listener instanceof Closure || listener instanceof RunnableWithArgsreturn;
390         if (listener instanceof Map) {
391             removeEventListener((Maplistener);
392             return;
393         }
394         synchronized (listeners) {
395             if (LOG.isDebugEnabled()) {
396                 try {
397                     LOG.debug("Removing listener " + listener);
398                 catch (UnsupportedOperationException uoe) {
399                     LOG.debug("Removing listener " + listener.getClass().getName());
400                 }
401             }
402             listeners.remove(listener);
403             removeNestedListeners(listener);
404         }
405     }
406 
407     /**
408      * Removes a Map containing event listeners.<p>
409      <p/>
410      * An event listener may be a<ul>
411      <li><tt>Closure</tt></li>
412      <li><tt>RunnableWithArgs</tt></li>
413      </ul>
414      <p/>
415      * Maps require handlers to be named as eventName only.<p>
416      * Some examples of eventHandler names are: StartupStart, MyCoolEvent.
417      * Event names must follow the camelCase naming convention.<p>
418      *
419      @param listener an event listener of type Script, Map or Object
420      */
421     public void removeEventListener(Map<String, Object> listener) {
422         if (listener == nullreturn;
423         for (Map.Entry<String, Object> entry : listener.entrySet()) {
424             Object value = entry.getValue();
425             if (value instanceof Closure) {
426                 removeEventListener(entry.getKey()(Closurevalue);
427             else if (value instanceof RunnableWithArgs) {
428                 removeEventListener(entry.getKey()(RunnableWithArgsvalue);
429             }
430         }
431     }
432 
433     /**
434      * Adds a Closure as an event listener.<p>
435      * Event names must follow the camelCase naming convention.
436      *
437      @param eventName the name of the event
438      @param listener  the event listener
439      */
440     public void addEventListener(String eventName, Closure listener) {
441         if (isBlank(eventName|| listener == nullreturn;
442         synchronized (closureListeners) {
443             List list = closureListeners.get(capitalize(eventName));
444             if (list == null) {
445                 list = new ArrayList();
446                 closureListeners.put(capitalize(eventName), list);
447             }
448             if (list.contains(listener)) return;
449             if (LOG.isDebugEnabled()) {
450                 LOG.debug("Adding listener " + listener.getClass().getName() " on " + capitalize(eventName));
451             }
452             list.add(listener);
453         }
454     }
455 
456     /**
457      * Adds a Runnable as an event listener.<p>
458      * Event names must follow the camelCase naming convention.
459      *
460      @param eventName the name of the event
461      @param listener  the event listener
462      */
463     public void addEventListener(String eventName, RunnableWithArgs listener) {
464         if (isBlank(eventName|| listener == nullreturn;
465         synchronized (closureListeners) {
466             List list = closureListeners.get(capitalize(eventName));
467             if (list == null) {
468                 list = new ArrayList();
469                 closureListeners.put(capitalize(eventName), list);
470             }
471             if (list.contains(listener)) return;
472             if (LOG.isDebugEnabled()) {
473                 LOG.debug("Adding listener " + listener.getClass().getName() " on " + capitalize(eventName));
474             }
475             list.add(listener);
476         }
477     }
478 
479     /**
480      * Removes a Closure as an event listener.<p>
481      * Event names must follow the camelCase naming convention.
482      *
483      @param eventName the name of the event
484      @param listener  the event listener
485      */
486     public void removeEventListener(String eventName, Closure listener) {
487         if (isBlank(eventName|| listener == nullreturn;
488         synchronized (closureListeners) {
489             List list = closureListeners.get(capitalize(eventName));
490             if (list != null) {
491                 if (LOG.isDebugEnabled()) {
492                     LOG.debug("Removing listener " + listener.getClass().getName() " on " + capitalize(eventName));
493                 }
494                 list.remove(listener);
495             }
496         }
497     }
498 
499     /**
500      * Removes a Runnable as an event listener.<p>
501      * Event names must follow the camelCase naming convention.
502      *
503      @param eventName the name of the event
504      @param listener  the event listener
505      */
506     public void removeEventListener(String eventName, RunnableWithArgs listener) {
507         if (isBlank(eventName|| listener == nullreturn;
508         synchronized (closureListeners) {
509             List list = closureListeners.get(capitalize(eventName));
510             if (list != null) {
511                 if (LOG.isDebugEnabled()) {
512                     LOG.debug("Removing listener " + listener.getClass().getName() " on " + capitalize(eventName));
513                 }
514                 list.remove(listener);
515             }
516         }
517     }
518 
519     private void removeNestedListeners(Object subject) {
520         synchronized (closureListeners) {
521             for (Map.Entry<String, List> event : closureListeners.entrySet()) {
522                 String eventName = event.getKey();
523                 List listenerList = event.getValue();
524                 List toRemove = new ArrayList();
525                 for (Object listener : listenerList) {
526                     if (isNestedListener(listener, subject)) {
527                         toRemove.add(listener);
528                     }
529                 }
530                 for (Object listener : toRemove) {
531                     if (LOG.isDebugEnabled()) {
532                         LOG.debug("Removing listener " + listener.getClass().getName() " on " + capitalize(eventName));
533                     }
534                     listenerList.remove(listener);
535                 }
536             }
537         }
538     }
539 
540     private boolean isNestedListener(Object listener, Object subject) {
541         if (listener instanceof Closure) {
542             return ((Closurelistener).getOwner().equals(subject);
543         else if (listener instanceof RunnableWithArgs) {
544             Class listenerClass = listener.getClass();
545             if (listenerClass.isMemberClass() && listenerClass.getEnclosingClass().equals(subject.getClass())) {
546                 return subject.equals(InvokerHelper.getProperty(listener, "this$0"));
547             }
548         }
549         return false;
550     }
551 }