EventListenerSupport.java
/*
* 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;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.commons.lang3.function.FailableConsumer;
/**
* 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);
* }
* }
* }</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 {
/**
* An invocation handler used to dispatch the event(s) to all the listeners.
*/
protected class ProxyInvocationHandler implements InvocationHandler {
private final FailableConsumer<Throwable, IllegalAccessException> handler;
/**
* Constructs a new instance.
*/
public ProxyInvocationHandler() {
this(ExceptionUtils::rethrow);
}
/**
* Constructs a new instance.
*
* @param handler Handles Throwables.
* @since 3.15.0
*/
public ProxyInvocationHandler(final FailableConsumer<Throwable, IllegalAccessException> handler) {
this.handler = Objects.requireNonNull(handler);
}
/**
* Handles an exception thrown by a listener. By default rethrows the given Throwable.
*
* @param t The Throwable
* @throws IllegalAccessException thrown by the listener.
* @throws IllegalArgumentException thrown by the listener.
* @throws InvocationTargetException thrown by the listener.
* @since 3.15.0
*/
protected void handle(final Throwable t) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException {
handler.accept(t);
}
/**
* 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) {
try {
method.invoke(listener, args);
} catch (final Throwable t) {
handle(t);
}
}
return null;
}
}
/** Serialization version */
private static final long serialVersionUID = 3593265990380473632L;
/**
* 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);
}
/**
* 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;
/**
* Constructs a new EventListenerSupport instance.
* Serialization-friendly constructor.
*/
private EventListenerSupport() {
}
/**
* 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);
}
/**
* 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);
}
}
/**
* Creates 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();
}
/**
* Creates 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()));
}
/**
* 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;
}
/**
* Gets the number of registered listeners.
*
* @return the number of registered listeners.
*/
int getListenerCount() {
return listeners.size();
}
/**
* 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);
}
/**
* Initializes 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);
}
/**
* Deserializes.
*
* @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());
}
/**
* 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);
}
/**
* Serializes.
*
* @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));
}
}