/** * Copyright (C) 2015 Valkyrie RCP * * 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.valkyriercp.util; import org.springframework.binding.collection.AbstractCachingMapDecorator; import org.springframework.core.NestedRuntimeException; import org.springframework.core.style.ToStringCreator; import org.springframework.util.Assert; import java.io.Serializable; import java.lang.reflect.Array; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.Iterator; import java.util.Map; import java.util.NoSuchElementException; /** * Helper implementation of an event listener list. * <p> * Provides methods for maintaining a list of listeners and firing events on * that list. This class is thread safe and serializable. * <p> * Usage Example: * * <pre> * private EventListenerListHelper fooListeners = new EventListenerListHelper(FooListener.class); * * public void addFooListener(FooListener listener) { * fooListeners.add(listener); * } * * public void removeFooListener(FooListener listener) { * fooListeners.remove(listener); * } * * protected void fireFooXXX() { * fooListeners.fire("fooXXX", new Event()); * } * * protected void fireFooYYY() { * fooListeners.fire("fooYYY"); * } * </pre> * * @author Oliver Hutchison * @author Keith Donald */ public class EventListenerListHelper implements Serializable { private static final long serialVersionUID = 1L; private static final Object[] EMPTY_OBJECT_ARRAY = new Object[0]; private static final Iterator EMPTY_ITERATOR = new Iterator() { /** * {@inheritDoc} */ public boolean hasNext() { return false; } /** * Unsupported operation. * * @throws UnsupportedOperationException always. */ public void remove() { throw new UnsupportedOperationException(); } /** * {@inheritDoc} */ public Object next() { throw new UnsupportedOperationException(); } }; private static final Map methodCache = new AbstractCachingMapDecorator() { /** * Creates a value to cache under the given key {@code o}, which must be a * {@link MethodCacheKey}. The value to be created will be a {@link java.lang.reflect.Method} object that is * specified by the given key. * * @param o The key that the newly created object will be stored under. This is expected to * be an instance of {@link MethodCacheKey} that contains the class, method name and number * of parameters of the {@link java.lang.reflect.Method} to be created. * * @throws ClassCastException if {@code o} can not be assigned to {@link MethodCacheKey}. * @throws IllegalArgumentException if the listener class specified by {@code o}, does not * have an implementation of the method specified in the given key. */ protected Object create(Object o) { MethodCacheKey key = (MethodCacheKey) o; Method fireMethod = null; Method[] methods = key.listenerClass.getMethods(); for (int i = 0; i < methods.length; i++) { Method method = methods[i]; if (method.getName().equals(key.methodName) && method.getParameterTypes().length == key.numParams) { if (fireMethod != null) { throw new UnsupportedOperationException("Listener class [" + key.listenerClass + "] has more than 1 implementation of method [" + key.methodName + "] with [" + key.numParams + "] parameters."); } fireMethod = method; } } if (fireMethod == null) { throw new IllegalArgumentException("Listener class [" + key.listenerClass + "] does not implement method [" + key.methodName + "] with [" + key.numParams + "] parameters."); } return fireMethod; } }; private final Class listenerClass; private volatile Object[] listeners = EMPTY_OBJECT_ARRAY; /** * Create new <code>EventListenerListHelper</code> instance that will maintain * a list of event listeners of the given class. * @param listenerClass The class of the listeners that will be maintained by this list helper. * * @throws IllegalArgumentException if {@code listenerClass} is null. * */ public EventListenerListHelper(Class listenerClass) { Assert.notNull(listenerClass, "The listenerClass argument is required"); this.listenerClass = listenerClass; } /** * Returns whether or not any listeners are registered with this list. * * @return true if there are registered listeners. */ public boolean hasListeners() { return listeners.length > 0; } /** * Returns true if there are no listeners registered with this list. * * @return true if there are no registered listeners. */ public boolean isEmpty() { return !hasListeners(); } /** * Returns the total number of listeners registered with this list. * * @return the total number of regisetered listeners. */ public int getListenerCount() { return listeners.length; } /** * Returns an array of all the listeners registered with this list. This * method is intended for use in subclasses that require the fastest * possible access to the listener list. It is recommended that unless * performance is absolutely critical access to the listener list should be * through the <code>iterator</code>,<code>forEach</code> and * <code>fire</code> methods only. * <p> * NOTE: The array returned by this method is used internally by this class * and must NOT be modified. */ protected Object[] getListeners() { return listeners; } /** * Returns an iterator over the list of listeners registered with this list. The returned * iterator does not allow removal of listeners. To remove a listener, use the * {@link #remove(Object)} method. * * @return An iterator for the registered listeners, never null. */ public Iterator iterator() { if (listeners == EMPTY_OBJECT_ARRAY) return EMPTY_ITERATOR; return new ObjectArrayIterator(listeners); } /** * Invokes the method with the given name and no parameters on each of the listeners registered * with this list. * * @param methodName the name of the method to invoke. * * @throws IllegalArgumentException if no method with the given name and an empty parameter * list exists on the listener class maintained by this list helper. */ public void fire(String methodName) { if (listeners != EMPTY_OBJECT_ARRAY) { fireEventByReflection(methodName, EMPTY_OBJECT_ARRAY); } } /** * Invokes the method with the given name and a single parameter on each of the listeners * registered with this list. * * @param methodName the name of the method to invoke. * @param arg the single argument to pass to each invocation. * * @throws IllegalArgumentException if no method with the given name and a single formal * parameter exists on the listener class managed by this list helper. */ public void fire(String methodName, Object arg) { if (listeners != EMPTY_OBJECT_ARRAY) { fireEventByReflection(methodName, new Object[] { arg }); } } /** * Invokes the method with the given name and two parameters on each of the listeners * registered with this list. * * @param methodName the name of the method to invoke. * @param arg1 the first argument to pass to each invocation. * @param arg2 the second argument to pass to each invocation. * * @throws IllegalArgumentException if no method with the given name and 2 formal parameters * exists on the listener class managed by this list helper. */ public void fire(String methodName, Object arg1, Object arg2) { if (listeners != EMPTY_OBJECT_ARRAY) { fireEventByReflection(methodName, new Object[] { arg1, arg2 }); } } /** * Invokes the method with the given name and number of formal parameters on each of the * listeners registered with this list. * * @param methodName the name of the method to invoke. * @param args an array of arguments to pass to each invocation. * * @throws IllegalArgumentException if no method with the given name and number of formal * parameters exists on the listener class managed by this list helper. */ public void fire(String methodName, Object[] args) { if (listeners != EMPTY_OBJECT_ARRAY) { fireEventByReflection(methodName, args); } } /** * Adds <code>listener</code> to the list of registered listeners. If * listener is already registered this method will do nothing. * * @param listener The event listener to be registered. * * @return true if the listener was registered, false if {@code listener} was null or it is * already registered with this list helper. * * @throws IllegalArgumentException if {@code listener} is not assignable to the class of * listener that this instance manages. */ public boolean add(Object listener) { if (listener == null) { return false; } checkListenerType(listener); synchronized (this) { if (listeners == EMPTY_OBJECT_ARRAY) { listeners = new Object[] { listener }; } else { int listenersLength = listeners.length; for (int i = 0; i < listenersLength; i++) { if (listeners[i] == listener) { return false; } } Object[] tmp = new Object[listenersLength + 1]; tmp[listenersLength] = listener; System.arraycopy(listeners, 0, tmp, 0, listenersLength); listeners = tmp; } } return true; } /** * Adds all the given listeners to the list of registered listeners. If any of the elements in * the array are null or are listeners that are already registered, they will not be registered * again. * * @param listenersToAdd The collection of listeners to be added. May be null. * * @return true if the list of registered listeners changed as a result of attempting to * register the given collection of listeners. * * @throws IllegalArgumentException if any of the listeners in the given collection are of a * type that is not assignable to the class of listener that this instance manages. */ public boolean addAll(Object[] listenersToAdd) { if (listenersToAdd == null) { return false; } boolean changed = false; for (int i = 0; i < listenersToAdd.length; i++) { if (add(listenersToAdd[i])) { changed = true; } } return changed; } /** * Removes <code>listener</code> from the list of registered listeners. * * @param listener The listener to be removed. * * @throws IllegalArgumentException if {@code listener} is null or not assignable to the class * of listener that is maintained by this instance. */ public void remove(Object listener) { checkListenerType(listener); synchronized (this) { if (listeners == EMPTY_OBJECT_ARRAY) return; int listenersLength = listeners.length; int index = 0; for (; index < listenersLength; index++) { if (listeners[index] == listener) { break; } } if (index < listenersLength) { if (listenersLength == 1) { listeners = EMPTY_OBJECT_ARRAY; } else { Object[] tmp = new Object[listenersLength - 1]; System.arraycopy(listeners, 0, tmp, 0, index); if (index < tmp.length) { System.arraycopy(listeners, index + 1, tmp, index, tmp.length - index); } listeners = tmp; } } } } /** * Removes all registered listeners. */ public void clear() { synchronized (this) { if (this.listeners == EMPTY_OBJECT_ARRAY) return; this.listeners = EMPTY_OBJECT_ARRAY; } } /** * Invokes the method with the given name on each of the listeners registered with this list * helper. The given arguments are passed to each method invocation. * * @param methodName The name of the method to be invoked on the listeners. * @param eventArgs The arguments that will be passed to each method invocation. The number * of arguments is also used to determine the method to be invoked. * * @throws EventBroadcastException if an error occurs invoking the event method on any of the * listeners. */ private void fireEventByReflection(String methodName, Object[] eventArgs) { Method eventMethod = (Method)methodCache.get(new MethodCacheKey(listenerClass, methodName, eventArgs.length)); Object[] listenersCopy = listeners; for (int i = 0; i < listenersCopy.length; i++) { try { eventMethod.invoke(listenersCopy[i], eventArgs); } catch (InvocationTargetException e) { throw new EventBroadcastException("Exception thrown by listener", e.getCause()); } catch (IllegalAccessException e) { throw new EventBroadcastException("Unable to invoke listener", e); } } } /** * Indicates that an error has occurred attempting to broadcast an event to listeners. */ public static class EventBroadcastException extends NestedRuntimeException { /** * Creates a new {@code EventBroadcastException} with the given detail message and nested * exception. * * @param msg The detail message. * @param ex The nested exception. */ public EventBroadcastException(String msg, Throwable ex) { super(msg, ex); } } private void checkListenerType(Object listener) { if (!listenerClass.isInstance(listener)) { throw new IllegalArgumentException("Listener [" + listener + "] is not an instance of [" + listenerClass + "]."); } } private static class ObjectArrayIterator implements Iterator { private final Object[] array; private int index; public ObjectArrayIterator(Object[] array) { this.array = array; } public boolean hasNext() { return index < array.length; } public Object next() { if (index > array.length - 1) { throw new NoSuchElementException(); } return array[index++]; } public void remove() { throw new UnsupportedOperationException(); } } private static class MethodCacheKey { public final Class listenerClass; public final String methodName; public final int numParams; public MethodCacheKey(Class listenerClass, String methodName, int numParams) { Assert.notNull(listenerClass); Assert.notNull(methodName); this.listenerClass = listenerClass; this.methodName = methodName; this.numParams = numParams; } public boolean equals(Object o2) { // includes check for null if (!(o2 instanceof MethodCacheKey)) { return false; } MethodCacheKey k2 = (MethodCacheKey) o2; return listenerClass.equals(k2.listenerClass) && methodName.equals(k2.methodName) && numParams == k2.numParams; } /** * {@inheritDoc} */ public int hashCode() { return listenerClass.hashCode() ^ methodName.hashCode() ^ numParams; } } /** * Returns an object which is a copy of the collection of listeners registered with this instance. * * @return A copy of the registered listeners array, never null. */ public Object toArray() { if (listeners == EMPTY_OBJECT_ARRAY) return Array.newInstance(listenerClass, 0); Object[] listenersCopy = listeners; Object copy = Array.newInstance(listenerClass, listenersCopy.length); System.arraycopy(listenersCopy, 0, copy, 0, listenersCopy.length); return copy; } /** * {@inheritDoc} */ public String toString() { return new ToStringCreator(this).append("listenerClass", listenerClass).append("listeners", listeners) .toString(); } }