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>a <tt>Script</tt></li>
040 * <li>a <tt>Map</tt></li>
041 * <li>a <tt>Closure</tt></li>
042 * <li>a <tt>RunnableWithArgs</tt></li>
043 * <li>a <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 == null) params = 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 == null) params = 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 == null) params = 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((Script) listener, eventHandler, params);
240 } else if (listener instanceof Closure) {
241 fireEvent((Closure) listener, eventHandler, params);
242 } else if (listener instanceof RunnableWithArgs) {
243 fireEvent((RunnableWithArgs) listener, 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 ((Closure) handler).call(asArray(params));
259 } else if (handler instanceof RunnableWithArgs) {
260 ((RunnableWithArgs) handler).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 ((GriffonArtifact) obj).getGriffonClass().getMetaClass();
307 } else if (obj instanceof GroovyObject) {
308 return ((GroovyObject) obj).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>a <tt>Script</tt></li>
318 * <li>a <tt>Map</tt></li>
319 * <li>a <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 RunnableWithArgs) return;
331 if (listener instanceof Map) {
332 addEventListener((Map) listener);
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>a <tt>Closure</tt></li>
351 * <li>a <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 == null) return;
362 for (Map.Entry<String, Object> entry : listener.entrySet()) {
363 Object value = entry.getValue();
364 if (value instanceof Closure) {
365 addEventListener(entry.getKey(), (Closure) value);
366 } else if (value instanceof RunnableWithArgs) {
367 addEventListener(entry.getKey(), (RunnableWithArgs) value);
368 }
369 }
370 }
371
372 /**
373 * Removes an event listener.<p>
374 * <p/>
375 * A listener may be a<ul>
376 * <li>a <tt>Script</tt></li>
377 * <li>a <tt>Map</tt></li>
378 * <li>a <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 RunnableWithArgs) return;
390 if (listener instanceof Map) {
391 removeEventListener((Map) listener);
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>a <tt>Closure</tt></li>
412 * <li>a <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 == null) return;
423 for (Map.Entry<String, Object> entry : listener.entrySet()) {
424 Object value = entry.getValue();
425 if (value instanceof Closure) {
426 removeEventListener(entry.getKey(), (Closure) value);
427 } else if (value instanceof RunnableWithArgs) {
428 removeEventListener(entry.getKey(), (RunnableWithArgs) value);
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 == null) return;
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 == null) return;
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 == null) return;
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 == null) return;
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 ((Closure) listener).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 }
|