diff options
Diffstat (limited to 'src/main/java/org/apache/commons/lang3/event/EventListenerSupport.java')
-rw-r--r-- | src/main/java/org/apache/commons/lang3/event/EventListenerSupport.java | 334 |
1 files changed, 334 insertions, 0 deletions
diff --git a/src/main/java/org/apache/commons/lang3/event/EventListenerSupport.java b/src/main/java/org/apache/commons/lang3/event/EventListenerSupport.java new file mode 100644 index 000000000..9a934ef91 --- /dev/null +++ b/src/main/java/org/apache/commons/lang3/event/EventListenerSupport.java @@ -0,0 +1,334 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.commons.lang3.event; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.Serializable; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.CopyOnWriteArrayList; + +import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.Validate; + +/** + * An EventListenerSupport object can be used to manage a list of event + * listeners of a particular type. The class provides + * {@link #addListener(Object)} and {@link #removeListener(Object)} methods + * for registering listeners, as well as a {@link #fire()} method for firing + * events to the listeners. + * + * <p> + * To use this class, suppose you want to support ActionEvents. You would do: + * </p> + * <pre><code> + * public class MyActionEventSource + * { + * private EventListenerSupport<ActionListener> actionListeners = + * EventListenerSupport.create(ActionListener.class); + * + * public void someMethodThatFiresAction() + * { + * ActionEvent e = new ActionEvent(this, ActionEvent.ACTION_PERFORMED, "somethingCool"); + * actionListeners.fire().actionPerformed(e); + * } + * } + * </code></pre> + * + * <p> + * Serializing an {@link EventListenerSupport} instance will result in any + * non-{@link Serializable} listeners being silently dropped. + * </p> + * + * @param <L> the type of event listener that is supported by this proxy. + * + * @since 3.0 + */ +public class EventListenerSupport<L> implements Serializable { + + /** Serialization version */ + private static final long serialVersionUID = 3593265990380473632L; + + /** + * The list used to hold the registered listeners. This list is + * intentionally a thread-safe copy-on-write-array so that traversals over + * the list of listeners will be atomic. + */ + private List<L> listeners = new CopyOnWriteArrayList<>(); + + /** + * The proxy representing the collection of listeners. Calls to this proxy + * object will be sent to all registered listeners. + */ + private transient L proxy; + + /** + * Empty typed array for #getListeners(). + */ + private transient L[] prototypeArray; + + /** + * Creates an EventListenerSupport object which supports the specified + * listener type. + * + * @param <T> the type of the listener interface + * @param listenerInterface the type of listener interface that will receive + * events posted using this class. + * + * @return an EventListenerSupport object which supports the specified + * listener type. + * + * @throws NullPointerException if {@code listenerInterface} is + * {@code null}. + * @throws IllegalArgumentException if {@code listenerInterface} is + * not an interface. + */ + public static <T> EventListenerSupport<T> create(final Class<T> listenerInterface) { + return new EventListenerSupport<>(listenerInterface); + } + + /** + * Creates an EventListenerSupport object which supports the provided + * listener interface. + * + * @param listenerInterface the type of listener interface that will receive + * events posted using this class. + * + * @throws NullPointerException if {@code listenerInterface} is + * {@code null}. + * @throws IllegalArgumentException if {@code listenerInterface} is + * not an interface. + */ + public EventListenerSupport(final Class<L> listenerInterface) { + this(listenerInterface, Thread.currentThread().getContextClassLoader()); + } + + /** + * Creates an EventListenerSupport object which supports the provided + * listener interface using the specified class loader to create the JDK + * dynamic proxy. + * + * @param listenerInterface the listener interface. + * @param classLoader the class loader. + * + * @throws NullPointerException if {@code listenerInterface} or + * {@code classLoader} is {@code null}. + * @throws IllegalArgumentException if {@code listenerInterface} is + * not an interface. + */ + public EventListenerSupport(final Class<L> listenerInterface, final ClassLoader classLoader) { + this(); + Objects.requireNonNull(listenerInterface, "listenerInterface"); + Objects.requireNonNull(classLoader, "classLoader"); + Validate.isTrue(listenerInterface.isInterface(), "Class %s is not an interface", + listenerInterface.getName()); + initializeTransientFields(listenerInterface, classLoader); + } + + /** + * Create a new EventListenerSupport instance. + * Serialization-friendly constructor. + */ + private EventListenerSupport() { + } + + /** + * Returns a proxy object which can be used to call listener methods on all + * of the registered event listeners. All calls made to this proxy will be + * forwarded to all registered listeners. + * + * @return a proxy object which can be used to call listener methods on all + * of the registered event listeners + */ + public L fire() { + return proxy; + } + +//********************************************************************************************************************** +// Other Methods +//********************************************************************************************************************** + + /** + * Registers an event listener. + * + * @param listener the event listener (may not be {@code null}). + * + * @throws NullPointerException if {@code listener} is + * {@code null}. + */ + public void addListener(final L listener) { + addListener(listener, true); + } + + /** + * Registers an event listener. Will not add a pre-existing listener + * object to the list if {@code allowDuplicate} is false. + * + * @param listener the event listener (may not be {@code null}). + * @param allowDuplicate the flag for determining if duplicate listener + * objects are allowed to be registered. + * + * @throws NullPointerException if {@code listener} is {@code null}. + * @since 3.5 + */ + public void addListener(final L listener, final boolean allowDuplicate) { + Objects.requireNonNull(listener, "listener"); + if (allowDuplicate || !listeners.contains(listener)) { + listeners.add(listener); + } + } + + /** + * Returns the number of registered listeners. + * + * @return the number of registered listeners. + */ + int getListenerCount() { + return listeners.size(); + } + + /** + * Unregisters an event listener. + * + * @param listener the event listener (may not be {@code null}). + * + * @throws NullPointerException if {@code listener} is + * {@code null}. + */ + public void removeListener(final L listener) { + Objects.requireNonNull(listener, "listener"); + listeners.remove(listener); + } + + /** + * Gets an array containing the currently registered listeners. + * Modification to this array's elements will have no effect on the + * {@link EventListenerSupport} instance. + * @return L[] + */ + public L[] getListeners() { + return listeners.toArray(prototypeArray); + } + + /** + * Serialize. + * @param objectOutputStream the output stream + * @throws IOException if an IO error occurs + */ + private void writeObject(final ObjectOutputStream objectOutputStream) throws IOException { + final ArrayList<L> serializableListeners = new ArrayList<>(); + + // don't just rely on instanceof Serializable: + ObjectOutputStream testObjectOutputStream = new ObjectOutputStream(new ByteArrayOutputStream()); + for (final L listener : listeners) { + try { + testObjectOutputStream.writeObject(listener); + serializableListeners.add(listener); + } catch (final IOException exception) { + //recreate test stream in case of indeterminate state + testObjectOutputStream = new ObjectOutputStream(new ByteArrayOutputStream()); + } + } + /* + * we can reconstitute everything we need from an array of our listeners, + * which has the additional advantage of typically requiring less storage than a list: + */ + objectOutputStream.writeObject(serializableListeners.toArray(prototypeArray)); + } + + /** + * Deserialize. + * @param objectInputStream the input stream + * @throws IOException if an IO error occurs + * @throws ClassNotFoundException if the class cannot be resolved + */ + private void readObject(final ObjectInputStream objectInputStream) throws IOException, ClassNotFoundException { + @SuppressWarnings("unchecked") // Will throw CCE here if not correct + final L[] srcListeners = (L[]) objectInputStream.readObject(); + + this.listeners = new CopyOnWriteArrayList<>(srcListeners); + + final Class<L> listenerInterface = ArrayUtils.getComponentType(srcListeners); + + initializeTransientFields(listenerInterface, Thread.currentThread().getContextClassLoader()); + } + + /** + * Initialize transient fields. + * @param listenerInterface the class of the listener interface + * @param classLoader the class loader to be used + */ + private void initializeTransientFields(final Class<L> listenerInterface, final ClassLoader classLoader) { + // Will throw CCE here if not correct + this.prototypeArray = ArrayUtils.newInstance(listenerInterface, 0); + createProxy(listenerInterface, classLoader); + } + + /** + * Create the proxy object. + * @param listenerInterface the class of the listener interface + * @param classLoader the class loader to be used + */ + private void createProxy(final Class<L> listenerInterface, final ClassLoader classLoader) { + proxy = listenerInterface.cast(Proxy.newProxyInstance(classLoader, + new Class[] { listenerInterface }, createInvocationHandler())); + } + + /** + * Create the {@link InvocationHandler} responsible for broadcasting calls + * to the managed listeners. Subclasses can override to provide custom behavior. + * @return ProxyInvocationHandler + */ + protected InvocationHandler createInvocationHandler() { + return new ProxyInvocationHandler(); + } + + /** + * An invocation handler used to dispatch the event(s) to all the listeners. + */ + protected class ProxyInvocationHandler implements InvocationHandler { + + /** + * Propagates the method call to all registered listeners in place of the proxy listener object. + * + * @param unusedProxy the proxy object representing a listener on which the invocation was called; not used + * @param method the listener method that will be called on all of the listeners. + * @param args event arguments to propagate to the listeners. + * @return the result of the method call + * @throws InvocationTargetException if an error occurs + * @throws IllegalArgumentException if an error occurs + * @throws IllegalAccessException if an error occurs + */ + @Override + public Object invoke(final Object unusedProxy, final Method method, final Object[] args) + throws IllegalAccessException, IllegalArgumentException, InvocationTargetException { + for (final L listener : listeners) { + method.invoke(listener, args); + } + return null; + } + } +} |