`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>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 }`