AbstractEventRouter.java
001 /*
002  * Copyright 2008-2015 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.event;
017 
018 import griffon.core.CallableWithArgs;
019 import griffon.core.RunnableWithArgs;
020 import griffon.core.event.Event;
021 import griffon.core.event.EventRouter;
022 import griffon.util.GriffonClassUtils;
023 import griffon.util.MethodDescriptor;
024 import griffon.util.MethodUtils;
025 import org.slf4j.Logger;
026 import org.slf4j.LoggerFactory;
027 
028 import javax.annotation.Nonnull;
029 import javax.annotation.Nullable;
030 import java.lang.reflect.Method;
031 import java.util.ArrayList;
032 import java.util.LinkedHashMap;
033 import java.util.List;
034 import java.util.Map;
035 import java.util.concurrent.ConcurrentHashMap;
036 
037 import static griffon.util.GriffonClassUtils.convertToTypeArray;
038 import static griffon.util.GriffonNameUtils.capitalize;
039 import static griffon.util.GriffonNameUtils.requireNonBlank;
040 import static java.util.Arrays.asList;
041 import static java.util.Collections.EMPTY_LIST;
042 import static java.util.Collections.synchronizedList;
043 import static java.util.Objects.requireNonNull;
044 
045 /**
046  @author Andres Almiray
047  */
048 public abstract class AbstractEventRouter implements EventRouter {
049     protected static final Object[] LOCK = new Object[0];
050     private static final String ERROR_EVENT_NAME_BLANK = "Argument 'eventName' must not be blank";
051     private static final String ERROR_EVENT_HANDLER_BLANK = "Argument 'eventHandler' must not be blank";
052     private static final String ERROR_MODE_BLANK = "Argument 'mode' must not be blank";
053     private static final String ERROR_LISTENER_NULL = "Argument 'listener' must not be null";
054     private static final String ERROR_EVENT_CLASS_NULL = "Argument 'eventClass' must not be null";
055     private static final String ERROR_EVENT_NULL = "Argument 'event' must not be null";
056     private static final String ERROR_CALLABLE_NULL = "Argument 'callable' must not be null";
057     private static final String ERROR_RUNNABLE_NULL = "Argument 'runnable' must not be null";
058     private static final String ERROR_PARAMS_NULL = "Argument 'params' must not be null";
059     private static final String ERROR_INSTANCE_NULL = "Argument 'instance' must not be null";
060     private static final String ERROR_OWNER_NULL = "Argument 'owner' must not be null";
061     private static final Logger LOG = LoggerFactory.getLogger(AbstractEventRouter.class);
062     protected final List<Object> instanceListeners = synchronizedList(new ArrayList<>());
063     protected final Map<String, List<Object>> functionalListeners = new ConcurrentHashMap<>();
064     private final MethodCache methodCache = new MethodCache();
065     private boolean enabled = true;
066 
067     @Override
068     public boolean isEventPublishingEnabled() {
069         synchronized (LOCK) {
070             return this.enabled;
071         }
072     }
073 
074     @Override
075     public void setEventPublishingEnabled(boolean enabled) {
076         synchronized (LOCK) {
077             this.enabled = enabled;
078         }
079     }
080 
081     @Override
082     public void publishEvent(@Nonnull String eventName) {
083         publishEvent(eventName, EMPTY_LIST);
084     }
085 
086     @Override
087     public void publishEvent(@Nonnull String eventName, @Nullable List<?> params) {
088         if (!isEventPublishingEnabled()) return;
089         requireNonBlank(eventName, ERROR_EVENT_NAME_BLANK);
090         if (params == nullparams = EMPTY_LIST;
091         buildPublisher(eventName, params, "synchronously").run();
092     }
093 
094     @Override
095     public void publishEventOutsideUI(@Nonnull String eventName) {
096         publishEventOutsideUI(eventName, EMPTY_LIST);
097     }
098 
099     @Override
100     public void publishEventOutsideUI(@Nonnull String eventName, @Nullable List<?> params) {
101         if (!isEventPublishingEnabled()) return;
102         requireNonBlank(eventName, ERROR_EVENT_NAME_BLANK);
103         if (params == nullparams = EMPTY_LIST;
104         final Runnable publisher = buildPublisher(eventName, params, "outside UI");
105         doPublishOutsideUI(publisher);
106     }
107 
108     protected abstract void doPublishOutsideUI(@Nonnull Runnable publisher);
109 
110     @Override
111     public void publishEventAsync(@Nonnull String eventName) {
112         publishEventAsync(eventName, EMPTY_LIST);
113     }
114 
115     @Override
116     public void publishEventAsync(@Nonnull String eventName, @Nullable List<?> params) {
117         if (!isEventPublishingEnabled()) return;
118         requireNonBlank(eventName, ERROR_EVENT_NAME_BLANK);
119         if (params == nullparams = EMPTY_LIST;
120         final Runnable publisher = buildPublisher(eventName, params, "asynchronously");
121         doPublishAsync(publisher);
122     }
123 
124     protected abstract void doPublishAsync(@Nonnull Runnable publisher);
125 
126     @Override
127     public void publishEvent(@Nonnull Event event) {
128         requireNonNull(event, ERROR_EVENT_NULL);
129         publishEvent(event.getClass().getSimpleName(), asList(event));
130     }
131 
132     @Override
133     public void publishEventOutsideUI(@Nonnull Event event) {
134         requireNonNull(event, ERROR_EVENT_NULL);
135         publishEventOutsideUI(event.getClass().getSimpleName(), asList(event));
136     }
137 
138     @Override
139     public void publishEventAsync(@Nonnull Event event) {
140         requireNonNull(event, ERROR_EVENT_NULL);
141         publishEventAsync(event.getClass().getSimpleName(), asList(event));
142     }
143 
144     @Override
145     public <E extends Event> void removeEventListener(@Nonnull Class<E> eventClass, @Nonnull CallableWithArgs<?> listener) {
146         requireNonNull(eventClass, ERROR_EVENT_CLASS_NULL);
147         removeEventListener(eventClass.getSimpleName(), listener);
148     }
149 
150     @Override
151     public <E extends Event> void removeEventListener(@Nonnull Class<E> eventClass, @Nonnull RunnableWithArgs listener) {
152         requireNonNull(eventClass, ERROR_EVENT_CLASS_NULL);
153         removeEventListener(eventClass.getSimpleName(), listener);
154     }
155 
156     protected void fireEvent(@Nonnull RunnableWithArgs runnable, @Nonnull List<?> params) {
157         requireNonNull(runnable, ERROR_RUNNABLE_NULL);
158         requireNonNull(params, ERROR_PARAMS_NULL);
159         runnable.run(asArray(params));
160     }
161 
162     protected void fireEvent(@Nonnull CallableWithArgs<?> callable, @Nonnull List<?> params) {
163         requireNonNull(callable, ERROR_CALLABLE_NULL);
164         requireNonNull(params, ERROR_PARAMS_NULL);
165         callable.call(asArray(params));
166     }
167 
168     protected void fireEvent(@Nonnull Object instance, @Nonnull String eventHandler, @Nonnull List<?> params) {
169         requireNonNull(instance, ERROR_INSTANCE_NULL);
170         requireNonBlank(eventHandler, ERROR_EVENT_HANDLER_BLANK);
171         requireNonNull(params, ERROR_PARAMS_NULL);
172 
173         Class[] argTypes = convertToTypeArray(asArray(params));
174         MethodDescriptor target = new MethodDescriptor(eventHandler, argTypes);
175         Method method = methodCache.findMatchingMethodFor(instance.getClass(), target);
176 
177         if (method != null) {
178             MethodUtils.invokeSafe(method, instance, asArray(params));
179         }
180     }
181 
182     @Override
183     public <E extends Event> void addEventListener(@Nonnull Class<E> eventClass, @Nonnull CallableWithArgs<?> listener) {
184         requireNonNull(eventClass, ERROR_EVENT_CLASS_NULL);
185         addEventListener(eventClass.getSimpleName(), listener);
186     }
187 
188     @Override
189     public <E extends Event> void addEventListener(@Nonnull Class<E> eventClass, @Nonnull RunnableWithArgs listener) {
190         requireNonNull(eventClass, ERROR_EVENT_CLASS_NULL);
191         addEventListener(eventClass.getSimpleName(), listener);
192     }
193 
194     @Override
195     @SuppressWarnings("unchecked")
196     public void addEventListener(@Nonnull Object listener) {
197         requireNonNull(listener, ERROR_LISTENER_NULL);
198         if (listener instanceof RunnableWithArgs) {
199             throw new IllegalArgumentException("Cannot add an event listener of type " + RunnableWithArgs.class.getName() +
200                 " because the target event name is missing. " + listener);
201         }
202         if (listener instanceof CallableWithArgs) {
203             throw new IllegalArgumentException("Cannot add an event listener of type " + CallableWithArgs.class.getName() +
204                 " because the target event name is missing. " + listener);
205         }
206 
207         if (listener instanceof Map) {
208             addEventListener((Maplistener);
209             return;
210         }
211 
212         if (!methodCache.isEventListener(listener.getClass())) {
213             return;
214         }
215 
216         synchronized (instanceListeners) {
217             if (instanceListeners.contains(listener)) return;
218             try {
219                 LOG.debug("Adding listener {}", listener);
220             catch (UnsupportedOperationException uoe) {
221                 LOG.debug("Adding listener {}", listener.getClass().getName());
222             }
223             instanceListeners.add(listener);
224         }
225     }
226 
227     @Override
228     public void addEventListener(@Nonnull Map<String, Object> listener) {
229         requireNonNull(listener, ERROR_LISTENER_NULL);
230         for (Map.Entry<String, Object> entry : listener.entrySet()) {
231             Object eventHandler = entry.getValue();
232             if (eventHandler instanceof RunnableWithArgs) {
233                 addEventListener(entry.getKey()(RunnableWithArgseventHandler);
234             else if (eventHandler instanceof CallableWithArgs) {
235                 addEventListener(entry.getKey()(CallableWithArgseventHandler);
236             else {
237                 throw new IllegalArgumentException("Unsupported functional event listener " + eventHandler);
238             }
239         }
240     }
241 
242     @Override
243     @SuppressWarnings("unchecked")
244     public void removeEventListener(@Nonnull Object listener) {
245         requireNonNull(listener, ERROR_LISTENER_NULL);
246         if (listener instanceof RunnableWithArgs) {
247             throw new IllegalArgumentException("Cannot remove an event listener of type " + RunnableWithArgs.class.getName() +
248                 " because the target event name is missing. " + listener);
249         }
250         if (listener instanceof CallableWithArgs) {
251             throw new IllegalArgumentException("Cannot remove an event listener of type " + CallableWithArgs.class.getName() +
252                 " because the target event name is missing. " + listener);
253         }
254 
255         if (listener instanceof Map) {
256             removeEventListener((Maplistener);
257             return;
258         }
259         synchronized (instanceListeners) {
260             try {
261                 LOG.debug("Removing listener {}", listener);
262             catch (UnsupportedOperationException uoe) {
263                 LOG.debug("Removing listener {}", listener.getClass().getName());
264             }
265             instanceListeners.remove(listener);
266             removeNestedListeners(listener);
267         }
268     }
269 
270     @Override
271     public void removeEventListener(@Nonnull Map<String, Object> listener) {
272         requireNonNull(listener, ERROR_LISTENER_NULL);
273         for (Map.Entry<String, Object> entry : listener.entrySet()) {
274             Object eventHandler = entry.getValue();
275             if (eventHandler instanceof RunnableWithArgs) {
276                 removeEventListener(entry.getKey()(RunnableWithArgseventHandler);
277             else if (eventHandler instanceof CallableWithArgs) {
278                 removeEventListener(entry.getKey()(CallableWithArgseventHandler);
279             else {
280                 throw new IllegalArgumentException("Unsupported functional event listener " + eventHandler);
281             }
282         }
283     }
284 
285     @Override
286     public void addEventListener(@Nonnull String eventName, @Nonnull CallableWithArgs<?> listener) {
287         requireNonBlank(eventName, ERROR_EVENT_NAME_BLANK);
288         requireNonNull(listener, ERROR_LISTENER_NULL);
289         synchronized (functionalListeners) {
290             List<Object> list = functionalListeners.get(capitalize(eventName));
291             if (list == null) {
292                 list = new ArrayList<>();
293                 functionalListeners.put(capitalize(eventName), list);
294             }
295             if (list.contains(listener)) return;
296             LOG.debug("Adding listener {} on {}", listener.getClass().getName(), capitalize(eventName));
297             list.add(listener);
298         }
299     }
300 
301     @Override
302     public void addEventListener(@Nonnull String eventName, @Nonnull RunnableWithArgs listener) {
303         requireNonBlank(eventName, ERROR_EVENT_NAME_BLANK);
304         requireNonNull(listener, ERROR_LISTENER_NULL);
305         synchronized (functionalListeners) {
306             List<Object> list = functionalListeners.get(capitalize(eventName));
307             if (list == null) {
308                 list = new ArrayList<>();
309                 functionalListeners.put(capitalize(eventName), list);
310             }
311             if (list.contains(listener)) return;
312             LOG.debug("Adding listener {} on {}", listener.getClass().getName(), capitalize(eventName));
313             list.add(listener);
314         }
315     }
316 
317     @Override
318     public void removeEventListener(@Nonnull String eventName, @Nonnull CallableWithArgs<?> listener) {
319         requireNonBlank(eventName, ERROR_EVENT_NAME_BLANK);
320         requireNonNull(listener, ERROR_LISTENER_NULL);
321         synchronized (functionalListeners) {
322             List<Object> list = functionalListeners.get(capitalize(eventName));
323             if (list != null) {
324                 LOG.debug("Removing listener {} on {}", listener.getClass().getName(), capitalize(eventName));
325                 list.remove(listener);
326             }
327         }
328     }
329 
330     @Override
331     public void removeEventListener(@Nonnull String eventName, @Nonnull RunnableWithArgs listener) {
332         requireNonBlank(eventName, ERROR_EVENT_NAME_BLANK);
333         requireNonNull(listener, ERROR_LISTENER_NULL);
334         synchronized (functionalListeners) {
335             List<Object> list = functionalListeners.get(capitalize(eventName));
336             if (list != null) {
337                 LOG.debug("Removing listener {} on {}", listener.getClass().getName(), capitalize(eventName));
338                 list.remove(listener);
339             }
340         }
341     }
342 
343     protected Runnable buildPublisher(@Nonnull final String event, @Nonnull final List<?> params, @Nonnull final String mode) {
344         requireNonNull(event, ERROR_EVENT_NULL);
345         requireNonNull(params, ERROR_PARAMS_NULL);
346         requireNonBlank(mode, ERROR_MODE_BLANK);
347         return new Runnable() {
348             public void run() {
349                 String eventName = capitalize(event);
350                 LOG.debug("Triggering event '{}' {}", eventName, mode);
351                 String eventHandler = "on" + eventName;
352                 // defensive copying to avoid CME during event dispatching
353                 // GRIFFON-224
354                 List<Object> listenersCopy = new ArrayList<>();
355                 synchronized (instanceListeners) {
356                     listenersCopy.addAll(instanceListeners);
357                 }
358                 synchronized (functionalListeners) {
359                     List list = functionalListeners.get(eventName);
360                     if (list != null) {
361                         for (Object listener : list) {
362                             listenersCopy.add(listener);
363                         }
364                     }
365                 }
366 
367                 for (Object listener : listenersCopy) {
368                     if (listener instanceof RunnableWithArgs) {
369                         fireEvent((RunnableWithArgslistener, params);
370                     else if (listener instanceof CallableWithArgs) {
371                         fireEvent((CallableWithArgs<?>listener, params);
372                     else {
373                         fireEvent(listener, eventHandler, params);
374                     }
375                 }
376             }
377         };
378     }
379 
380     protected void removeNestedListeners(@Nonnull Object owner) {
381         requireNonNull(owner, ERROR_OWNER_NULL);
382         synchronized (functionalListeners) {
383             for (Map.Entry<String, List<Object>> event : functionalListeners.entrySet()) {
384                 String eventName = event.getKey();
385                 List<Object> listenerList = event.getValue();
386                 List<Object> toRemove = new ArrayList<>();
387                 for (Object listener : listenerList) {
388                     if (isNestedListener(listener, owner)) {
389                         toRemove.add(listener);
390                     }
391                 }
392                 for (Object listener : toRemove) {
393                     LOG.debug("Removing listener {} on {}", listener.getClass().getName(), capitalize(eventName));
394                     listenerList.remove(listener);
395                 }
396             }
397         }
398     }
399 
400     protected boolean isNestedListener(@Nonnull Object listener, @Nonnull Object owner) {
401         requireNonNull(listener, ERROR_LISTENER_NULL);
402         requireNonNull(owner, ERROR_OWNER_NULL);
403         Class<?> listenerClass = listener.getClass();
404         return listenerClass.isMemberClass() &&
405             listenerClass.getEnclosingClass().equals(owner.getClass()) &&
406             owner.equals(GriffonClassUtils.getFieldValue(listener, "this$0"));
407     }
408 
409     protected Object[] asArray(@Nonnull List<?> list) {
410         return list.toArray(new Object[list.size()]);
411     }
412 
413     protected static class MethodCache {
414         private final Map<Class<?>, Map<String, List<MethodInfo>>> methodMap = new ConcurrentHashMap<>();
415 
416         public boolean isEventListener(@Nonnull Class<?> klass) {
417             Map<String, List<MethodInfo>> methodMetadata = methodMap.get(klass);
418             if (methodMetadata == null) {
419                 methodMetadata = fetchMethodMetadata(klass);
420                 if (!methodMetadata.isEmpty()) {
421                     methodMap.put(klass, methodMetadata);
422                 else {
423                     methodMetadata = null;
424                 }
425             }
426             return methodMetadata != null;
427         }
428 
429         @Nullable
430         public Method findMatchingMethodFor(@Nonnull Class<?> klass, @Nonnull MethodDescriptor target) {
431             Map<String, List<MethodInfo>> methodMetadata = methodMap.get(klass);
432 
433             List<MethodInfo> descriptors = methodMetadata.get(target.getName());
434             if (descriptors != null) {
435                 for (MethodInfo info : descriptors) {
436                     if (info.descriptor.matches(target)) {
437                         return info.method;
438                     }
439                 }
440             }
441 
442             return null;
443         }
444 
445         private Map<String, List<MethodInfo>> fetchMethodMetadata(Class<?> klass) {
446             Map<String, List<MethodInfo>> methodMetadata = new LinkedHashMap<>();
447 
448             for (Method method : klass.getMethods()) {
449                 MethodDescriptor descriptor = MethodDescriptor.forMethod(method);
450                 if (GriffonClassUtils.isEventHandler(descriptor)) {
451                     String methodName = method.getName();
452                     List<MethodInfo> descriptors = methodMetadata.get(methodName);
453                     if (descriptors == null) {
454                         descriptors = new ArrayList<>();
455                         methodMetadata.put(methodName, descriptors);
456                     }
457                     descriptors.add(new MethodInfo(descriptor, method));
458                 }
459             }
460 
461             return methodMetadata;
462         }
463     }
464 
465     protected static class MethodInfo {
466         private final MethodDescriptor descriptor;
467         private final Method method;
468 
469         public MethodInfo(MethodDescriptor descriptor, Method method) {
470             this.descriptor = descriptor;
471             this.method = method;
472         }
473 
474         public MethodDescriptor getDescriptor() {
475             return descriptor;
476         }
477 
478         public Method getMethod() {
479             return method;
480         }
481     }
482 }