/* * Copyright 2008-2017 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.codehaus.griffon.runtime.core.event; import griffon.core.CallableWithArgs; import griffon.core.ExceptionHandler; import griffon.core.ExecutorServiceManager; import griffon.core.RunnableWithArgs; import griffon.core.event.Event; import griffon.core.event.EventRouter; import griffon.util.GriffonClassUtils; import griffon.util.MethodDescriptor; import griffon.util.MethodUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.annotation.Nonnull; import javax.annotation.Nullable; import javax.inject.Inject; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ThreadFactory; import java.util.concurrent.atomic.AtomicInteger; import static griffon.util.GriffonClassUtils.convertToTypeArray; import static griffon.util.GriffonNameUtils.capitalize; import static griffon.util.GriffonNameUtils.requireNonBlank; import static java.util.Arrays.asList; import static java.util.Collections.EMPTY_LIST; import static java.util.Collections.unmodifiableCollection; import static java.util.Objects.requireNonNull; /** * @author Andres Almiray */ public abstract class AbstractEventRouter implements EventRouter { protected static final Object[] LOCK = new Object[0]; private static final String ERROR_EVENT_NAME_BLANK = "Argument 'eventName' must not be blank"; private static final String ERROR_EVENT_HANDLER_BLANK = "Argument 'eventHandler' must not be blank"; private static final String ERROR_MODE_BLANK = "Argument 'mode' must not be blank"; private static final String ERROR_LISTENER_NULL = "Argument 'listener' must not be null"; private static final String ERROR_EVENT_CLASS_NULL = "Argument 'eventClass' must not be null"; private static final String ERROR_EVENT_NULL = "Argument 'event' must not be null"; private static final String ERROR_CALLABLE_NULL = "Argument 'callable' must not be null"; private static final String ERROR_RUNNABLE_NULL = "Argument 'runnable' must not be null"; private static final String ERROR_PARAMS_NULL = "Argument 'params' must not be null"; private static final String ERROR_INSTANCE_NULL = "Argument 'instance' must not be null"; private static final String ERROR_OWNER_NULL = "Argument 'owner' must not be null"; private static final Logger LOG = LoggerFactory.getLogger(AbstractEventRouter.class); protected final Map<String, List<Object>> instanceListeners = new ConcurrentHashMap<>(); protected final Map<String, List<Object>> functionalListeners = new ConcurrentHashMap<>(); private final MethodCache methodCache = new MethodCache(); private boolean enabled = true; protected static final AtomicInteger EVENT_ROUTER_ID = new AtomicInteger(1); protected ExecutorServiceManager executorServiceManager; protected final ExecutorService executorService; protected final int eventRouterId; @Inject private ExceptionHandler exceptionHandler; public AbstractEventRouter() { eventRouterId = EVENT_ROUTER_ID.getAndIncrement(); executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors(), new DefaultThreadFactory(eventRouterId)); } @Inject public void setExecutorServiceManager(@Nonnull ExecutorServiceManager executorServiceManager) { requireNonNull(executorServiceManager, "Argument 'executorServiceManager' must not be null"); if (this.executorServiceManager != null) { this.executorServiceManager.remove(executorService); } this.executorServiceManager = executorServiceManager; this.executorServiceManager.add(executorService); } protected void runInsideExecutorService(@Nonnull final Runnable runnable) { requireNonNull(runnable, ERROR_RUNNABLE_NULL); executorService.submit(new Runnable() { public void run() { try { runnable.run(); } catch (Throwable throwable) { exceptionHandler.uncaughtException(Thread.currentThread(), throwable); } } }); } @Override public boolean isEventPublishingEnabled() { synchronized (LOCK) { return this.enabled; } } @Override public void setEventPublishingEnabled(boolean enabled) { synchronized (LOCK) { this.enabled = enabled; } } @Override public void publishEvent(@Nonnull String eventName) { publishEvent(eventName, EMPTY_LIST); } @Override public void publishEvent(@Nonnull String eventName, @Nullable List<?> params) { if (!isEventPublishingEnabled()) return; requireNonBlank(eventName, ERROR_EVENT_NAME_BLANK); if (params == null) params = EMPTY_LIST; buildPublisher(eventName, params, "synchronously").run(); } @Override public void publishEventOutsideUI(@Nonnull String eventName) { publishEventOutsideUI(eventName, EMPTY_LIST); } @Override public void publishEventOutsideUI(@Nonnull String eventName, @Nullable List<?> params) { if (!isEventPublishingEnabled()) return; requireNonBlank(eventName, ERROR_EVENT_NAME_BLANK); if (params == null) params = EMPTY_LIST; final Runnable publisher = buildPublisher(eventName, params, "outside UI"); doPublishOutsideUI(publisher); } protected abstract void doPublishOutsideUI(@Nonnull Runnable publisher); @Override public void publishEventAsync(@Nonnull String eventName) { publishEventAsync(eventName, EMPTY_LIST); } @Override public void publishEventAsync(@Nonnull String eventName, @Nullable List<?> params) { if (!isEventPublishingEnabled()) return; requireNonBlank(eventName, ERROR_EVENT_NAME_BLANK); if (params == null) params = EMPTY_LIST; final Runnable publisher = buildPublisher(eventName, params, "asynchronously"); doPublishAsync(publisher); } protected abstract void doPublishAsync(@Nonnull Runnable publisher); @Override public void publishEvent(@Nonnull Event event) { requireNonNull(event, ERROR_EVENT_NULL); publishEvent(event.getClass().getSimpleName(), asList(event)); } @Override public void publishEventOutsideUI(@Nonnull Event event) { requireNonNull(event, ERROR_EVENT_NULL); publishEventOutsideUI(event.getClass().getSimpleName(), asList(event)); } @Override public void publishEventAsync(@Nonnull Event event) { requireNonNull(event, ERROR_EVENT_NULL); publishEventAsync(event.getClass().getSimpleName(), asList(event)); } @Override public <E extends Event> void removeEventListener(@Nonnull Class<E> eventClass, @Nonnull CallableWithArgs<?> listener) { requireNonNull(eventClass, ERROR_EVENT_CLASS_NULL); removeEventListener(eventClass.getSimpleName(), listener); } @Override public <E extends Event> void removeEventListener(@Nonnull Class<E> eventClass, @Nonnull RunnableWithArgs listener) { requireNonNull(eventClass, ERROR_EVENT_CLASS_NULL); removeEventListener(eventClass.getSimpleName(), listener); } protected void fireEvent(@Nonnull RunnableWithArgs runnable, @Nonnull List<?> params) { requireNonNull(runnable, ERROR_RUNNABLE_NULL); requireNonNull(params, ERROR_PARAMS_NULL); runnable.run(asArray(params)); } protected void fireEvent(@Nonnull CallableWithArgs<?> callable, @Nonnull List<?> params) { requireNonNull(callable, ERROR_CALLABLE_NULL); requireNonNull(params, ERROR_PARAMS_NULL); callable.call(asArray(params)); } protected void fireEvent(@Nonnull Object instance, @Nonnull String eventHandler, @Nonnull List<?> params) { requireNonNull(instance, ERROR_INSTANCE_NULL); requireNonBlank(eventHandler, ERROR_EVENT_HANDLER_BLANK); requireNonNull(params, ERROR_PARAMS_NULL); Class[] argTypes = convertToTypeArray(asArray(params)); MethodDescriptor target = new MethodDescriptor(eventHandler, argTypes); Method method = methodCache.findMatchingMethodFor(instance.getClass(), target); if (method != null) { MethodUtils.invokeUnwrapping(method, instance, asArray(params)); } } @Override public <E extends Event> void addEventListener(@Nonnull Class<E> eventClass, @Nonnull CallableWithArgs<?> listener) { requireNonNull(eventClass, ERROR_EVENT_CLASS_NULL); addEventListener(eventClass.getSimpleName(), listener); } @Override public <E extends Event> void addEventListener(@Nonnull Class<E> eventClass, @Nonnull RunnableWithArgs listener) { requireNonNull(eventClass, ERROR_EVENT_CLASS_NULL); addEventListener(eventClass.getSimpleName(), listener); } @Override @SuppressWarnings("unchecked") public void addEventListener(@Nonnull Object listener) { requireNonNull(listener, ERROR_LISTENER_NULL); if (listener instanceof RunnableWithArgs) { throw new IllegalArgumentException("Cannot add an event listener of type " + RunnableWithArgs.class.getName() + " because the target event name is missing. " + listener); } if (listener instanceof CallableWithArgs) { throw new IllegalArgumentException("Cannot add an event listener of type " + CallableWithArgs.class.getName() + " because the target event name is missing. " + listener); } if (listener instanceof Map) { addEventListener((Map) listener); return; } if (!methodCache.isEventListener(listener.getClass())) { return; } boolean added = false; for (String eventName : methodCache.fetchMethodMetadata(listener.getClass()).keySet()) { eventName = eventName.substring(2); // cut off "on" from the name List<Object> instances = instanceListeners.get(eventName); if (instances == null) { instances = new ArrayList<>(); instanceListeners.put(eventName, instances); } synchronized (instances) { if (!instances.contains(listener)) { added = true; instances.add(listener); } } } if (added) { try { LOG.debug("Adding listener {}", listener); } catch (UnsupportedOperationException uoe) { LOG.debug("Adding listener {}", listener.getClass().getName()); } } } @Override public void addEventListener(@Nonnull Map<String, Object> listener) { requireNonNull(listener, ERROR_LISTENER_NULL); for (Map.Entry<String, Object> entry : listener.entrySet()) { Object eventHandler = entry.getValue(); if (eventHandler instanceof RunnableWithArgs) { addEventListener(entry.getKey(), (RunnableWithArgs) eventHandler); } else if (eventHandler instanceof CallableWithArgs) { addEventListener(entry.getKey(), (CallableWithArgs) eventHandler); } else { throw new IllegalArgumentException("Unsupported functional event listener " + eventHandler); } } } @Override @SuppressWarnings("unchecked") public void removeEventListener(@Nonnull Object listener) { requireNonNull(listener, ERROR_LISTENER_NULL); if (listener instanceof RunnableWithArgs) { throw new IllegalArgumentException("Cannot remove an event listener of type " + RunnableWithArgs.class.getName() + " because the target event name is missing. " + listener); } if (listener instanceof CallableWithArgs) { throw new IllegalArgumentException("Cannot remove an event listener of type " + CallableWithArgs.class.getName() + " because the target event name is missing. " + listener); } if (listener instanceof Map) { removeEventListener((Map) listener); return; } boolean removed = false; for (String eventName : methodCache.fetchMethodMetadata(listener.getClass()).keySet()) { eventName = eventName.substring(2); // cut off "on" from the name List<Object> instances = instanceListeners.get(eventName); if (instances != null && instances.contains(listener)) { instances.remove(listener); removed = true; if (instances.isEmpty()) { instanceListeners.remove(eventName); } } } boolean nestedRemoved = removeNestedListeners(listener); if (removed || nestedRemoved) { try { LOG.debug("Removing listener {}", listener); } catch (UnsupportedOperationException uoe) { LOG.debug("Removing listener {}", listener.getClass().getName()); } } } @Override public void removeEventListener(@Nonnull Map<String, Object> listener) { requireNonNull(listener, ERROR_LISTENER_NULL); for (Map.Entry<String, Object> entry : listener.entrySet()) { Object eventHandler = entry.getValue(); if (eventHandler instanceof RunnableWithArgs) { removeEventListener(entry.getKey(), (RunnableWithArgs) eventHandler); } else if (eventHandler instanceof CallableWithArgs) { removeEventListener(entry.getKey(), (CallableWithArgs) eventHandler); } else { throw new IllegalArgumentException("Unsupported functional event listener " + eventHandler); } } } @Override public void addEventListener(@Nonnull String eventName, @Nonnull CallableWithArgs<?> listener) { requireNonBlank(eventName, ERROR_EVENT_NAME_BLANK); requireNonNull(listener, ERROR_LISTENER_NULL); synchronized (functionalListeners) { List<Object> list = functionalListeners.get(capitalize(eventName)); if (list == null) { list = new ArrayList<>(); functionalListeners.put(capitalize(eventName), list); } if (list.contains(listener)) return; LOG.debug("Adding listener {} on {}", listener.getClass().getName(), capitalize(eventName)); list.add(listener); } } @Override public void addEventListener(@Nonnull String eventName, @Nonnull RunnableWithArgs listener) { requireNonBlank(eventName, ERROR_EVENT_NAME_BLANK); requireNonNull(listener, ERROR_LISTENER_NULL); synchronized (functionalListeners) { List<Object> list = functionalListeners.get(capitalize(eventName)); if (list == null) { list = new ArrayList<>(); functionalListeners.put(capitalize(eventName), list); } if (list.contains(listener)) return; LOG.debug("Adding listener {} on {}", listener.getClass().getName(), capitalize(eventName)); list.add(listener); } } @Override public void removeEventListener(@Nonnull String eventName, @Nonnull CallableWithArgs<?> listener) { requireNonBlank(eventName, ERROR_EVENT_NAME_BLANK); requireNonNull(listener, ERROR_LISTENER_NULL); synchronized (functionalListeners) { List<Object> list = functionalListeners.get(capitalize(eventName)); if (list != null) { LOG.debug("Removing listener {} on {}", listener.getClass().getName(), capitalize(eventName)); list.remove(listener); } } } @Override public void removeEventListener(@Nonnull String eventName, @Nonnull RunnableWithArgs listener) { requireNonBlank(eventName, ERROR_EVENT_NAME_BLANK); requireNonNull(listener, ERROR_LISTENER_NULL); synchronized (functionalListeners) { List<Object> list = functionalListeners.get(capitalize(eventName)); if (list != null) { LOG.debug("Removing listener {} on {}", listener.getClass().getName(), capitalize(eventName)); list.remove(listener); } } } @Nonnull @Override public Collection<Object> getEventListeners() { List<Object> listeners = new ArrayList<>(); synchronized (instanceListeners) { Set<Object> instances = new HashSet<>(); for (List<Object> objects : instanceListeners.values()) { instances.addAll(objects); } listeners.addAll(instances); } synchronized (functionalListeners) { for (List<Object> objects : functionalListeners.values()) { listeners.addAll(objects); } } return unmodifiableCollection(listeners); } @Nonnull @Override public Collection<Object> getEventListeners(@Nonnull String eventName) { requireNonBlank(eventName, ERROR_EVENT_NAME_BLANK); List<Object> listeners = new ArrayList<>(); List<Object> instances = instanceListeners.get(eventName); if (instances != null) listeners.addAll(instances); instances = functionalListeners.get(eventName); if (instances != null) listeners.addAll(instances); return unmodifiableCollection(listeners); } protected Runnable buildPublisher(@Nonnull final String event, @Nonnull final List<?> params, @Nonnull final String mode) { requireNonNull(event, ERROR_EVENT_NULL); requireNonNull(params, ERROR_PARAMS_NULL); requireNonBlank(mode, ERROR_MODE_BLANK); return new Runnable() { public void run() { String eventName = capitalize(event); LOG.debug("Triggering event '{}' {}", eventName, mode); String eventHandler = "on" + eventName; // defensive copying to avoid CME during event dispatching List<Object> listenersCopy = new ArrayList<>(); List<Object> instances = instanceListeners.get(eventName); if (instances != null) { listenersCopy.addAll(instances); } synchronized (functionalListeners) { List list = functionalListeners.get(eventName); if (list != null) { for (Object listener : list) { listenersCopy.add(listener); } } } for (Object listener : listenersCopy) { if (listener instanceof RunnableWithArgs) { fireEvent((RunnableWithArgs) listener, params); } else if (listener instanceof CallableWithArgs) { fireEvent((CallableWithArgs<?>) listener, params); } else { fireEvent(listener, eventHandler, params); } } } }; } protected boolean removeNestedListeners(@Nonnull Object owner) { requireNonNull(owner, ERROR_OWNER_NULL); boolean removed = false; synchronized (functionalListeners) { for (Map.Entry<String, List<Object>> event : functionalListeners.entrySet()) { String eventName = event.getKey(); List<Object> listenerList = event.getValue(); List<Object> toRemove = new ArrayList<>(); for (Object listener : listenerList) { if (isNestedListener(listener, owner)) { toRemove.add(listener); } } removed = toRemove.size() > 0; for (Object listener : toRemove) { LOG.debug("Removing listener {} on {}", listener.getClass().getName(), capitalize(eventName)); listenerList.remove(listener); } } } return removed; } protected boolean isNestedListener(@Nonnull Object listener, @Nonnull Object owner) { requireNonNull(listener, ERROR_LISTENER_NULL); requireNonNull(owner, ERROR_OWNER_NULL); Class<?> listenerClass = listener.getClass(); return (listenerClass.isMemberClass() || listenerClass.isAnonymousClass() || listenerClass.isLocalClass()) && owner.getClass().equals(listenerClass.getEnclosingClass()) && owner.equals(GriffonClassUtils.getFieldValue(listener, "this$0")); } protected Object[] asArray(@Nonnull List<?> list) { return list.toArray(new Object[list.size()]); } protected static class MethodCache { private final Map<Class<?>, Map<String, List<MethodInfo>>> methodMap = new ConcurrentHashMap<>(); public boolean isEventListener(@Nonnull Class<?> klass) { Map<String, List<MethodInfo>> methodMetadata = methodMap.get(klass); if (methodMetadata == null) { methodMetadata = fetchMethodMetadata(klass); if (!methodMetadata.isEmpty()) { methodMap.put(klass, methodMetadata); } else { methodMetadata = null; } } return methodMetadata != null; } @Nullable public Method findMatchingMethodFor(@Nonnull Class<?> klass, @Nonnull MethodDescriptor target) { Map<String, List<MethodInfo>> methodMetadata = methodMap.get(klass); List<MethodInfo> descriptors = methodMetadata.get(target.getName()); if (descriptors != null) { for (MethodInfo info : descriptors) { if (info.descriptor.matches(target)) { return info.method; } } } return null; } private Map<String, List<MethodInfo>> fetchMethodMetadata(Class<?> klass) { Map<String, List<MethodInfo>> methodMetadata = new LinkedHashMap<>(); for (Method method : klass.getMethods()) { MethodDescriptor descriptor = MethodDescriptor.forMethod(method); if (GriffonClassUtils.isEventHandler(descriptor)) { String methodName = method.getName(); List<MethodInfo> descriptors = methodMetadata.get(methodName); if (descriptors == null) { descriptors = new ArrayList<>(); methodMetadata.put(methodName, descriptors); } descriptors.add(new MethodInfo(descriptor, method)); } } return methodMetadata; } } protected static class MethodInfo { private final MethodDescriptor descriptor; private final Method method; public MethodInfo(MethodDescriptor descriptor, Method method) { this.descriptor = descriptor; this.method = method; } public MethodDescriptor getDescriptor() { return descriptor; } public Method getMethod() { return method; } } private static class DefaultThreadFactory implements ThreadFactory { private final ThreadGroup group; private final AtomicInteger threadNumber = new AtomicInteger(1); private final String namePrefix; private DefaultThreadFactory(int eventRouterId) { SecurityManager s = System.getSecurityManager(); group = (s != null) ? s.getThreadGroup() : Thread.currentThread().getThreadGroup(); namePrefix = "event-router-" + eventRouterId + "-thread-"; } public Thread newThread(Runnable r) { Thread t = new Thread(group, r, namePrefix + threadNumber.getAndIncrement(), 0); if (t.isDaemon()) t.setDaemon(false); if (t.getPriority() != Thread.NORM_PRIORITY) t.setPriority(Thread.NORM_PRIORITY); return t; } } }