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