Misc. event utils.
git-svn-id: https://svn.apache.org/repos/asf/commons/proper/lang/trunk@966589 13f79535-47bb-0310-9956-ffa450edef68
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 0000000..3dc566c
--- /dev/null
+++ b/src/main/java/org/apache/commons/lang3/event/EventListenerSupport.java
@@ -0,0 +1,143 @@
+/*
+ * 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.lang.reflect.InvocationHandler;
+import java.lang.reflect.Method;
+import java.lang.reflect.Proxy;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+/**
+ * An EventListenerSupport object can be used to manage a list of event listeners of a particular type.
+ * <p/>
+ * To use this class, suppose you want to support ActionEvents. You would do:
+ * <pre>
+ * public class MyActionEventSource
+ * {
+ * private EventListenerSupport<ActionListener> actionListeners = EventListenerSupport.create(ActionListener.class);
+ * <p/>
+ * public void someMethodThatFiresAction()
+ * {
+ * ActionEvent e = new ActionEvent(this, ActionEvent.ACTION_PERFORMED, "somethingCool");
+ * actionListeners.getProxy().actionPerformed(e);
+ * }
+ * }
+ * </pre>
+ *
+ * @param <L> The event listener type
+ */
+public class EventListenerSupport<L>
+{
+ private final List<L> listeners;
+ private final L proxy;
+
+ /**
+ * Creates an EventListenerSupport object which supports the specified listener type.
+ *
+ * @param listenerType the listener type
+ * @return an EventListenerSupport object which supports the specified listener type
+ */
+ public static <T> EventListenerSupport<T> create(Class<T> listenerType)
+ {
+ return new EventListenerSupport<T>(listenerType);
+ }
+
+ /**
+ * Creates an EventListenerSupport object which supports the provided listener interface.
+ *
+ * @param listenerInterface the listener interface
+ */
+ public EventListenerSupport(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
+ */
+ public EventListenerSupport(Class<L> listenerInterface, ClassLoader classLoader)
+ {
+ listeners = new CopyOnWriteArrayList<L>();
+ proxy = listenerInterface.cast(Proxy.newProxyInstance(classLoader, new Class[]{listenerInterface},
+ new ProxyInvocationHandler()));
+ }
+
+ /**
+ * Returns a proxy object which can be used to call listener methods on all of the registered event 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
+ */
+ public void addListener(L listener)
+ {
+ listeners.add(0, listener);
+ }
+
+ /**
+ * Returns the number of registered listeners.
+ *
+ * @return the number of registered listeners
+ */
+ public int getListenerCount()
+ {
+ return listeners.size();
+ }
+
+ /**
+ * Unregisters an event listener.
+ *
+ * @param listener the event listener
+ */
+ public void removeListener(L listener)
+ {
+ listeners.remove(listener);
+ }
+
+ /**
+ * An invocation handler used to dispatch the event(s) to all the listeners.
+ */
+ private class ProxyInvocationHandler implements InvocationHandler
+ {
+ public Object invoke(Object proxy, Method method, Object[] args) throws Throwable
+ {
+ for (int i = listeners.size() - 1; i >= 0; --i)
+ {
+ method.invoke(listeners.get(i), args);
+ }
+ return null;
+ }
+ }
+}
diff --git a/src/main/java/org/apache/commons/lang3/event/EventUtils.java b/src/main/java/org/apache/commons/lang3/event/EventUtils.java
new file mode 100644
index 0000000..56e676c
--- /dev/null
+++ b/src/main/java/org/apache/commons/lang3/event/EventUtils.java
@@ -0,0 +1,102 @@
+/*
+ * 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 org.apache.commons.lang3.reflect.MethodUtils;
+
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Proxy;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+public class EventUtils
+{
+ public static <L> void addEventListener(Object eventSource, Class<L> listenerType, L listener)
+ {
+ try
+ {
+ MethodUtils.invokeMethod(eventSource, "add" + listenerType.getSimpleName(), listener);
+ }
+ catch (NoSuchMethodException e)
+ {
+ throw new IllegalArgumentException("Class " + eventSource.getClass() + " does not have an accesible add" + listenerType.getSimpleName() + " method which takes a parameter of type " + listenerType.getClass().getName() + ".");
+ }
+ catch (IllegalAccessException e)
+ {
+ throw new IllegalArgumentException("Class " + eventSource.getClass() + " does not have an accesible add" + listenerType.getSimpleName () + " method which takes a parameter of type " + listenerType.getClass().getName() + ".");
+ }
+ catch (InvocationTargetException e)
+ {
+ throw new RuntimeException("Unable to add listener.", e.getCause());
+ }
+ }
+
+ /**
+ * Binds an event listener to a specific method on a specific object.
+ *
+ * @param target the target object
+ * @param methodName the name of the method to be called
+ * @param eventSource the object which is generating events (JButton, JList, etc.)
+ * @param listenerType the listener interface (ActionListener.class, SelectionListener.class, etc.)
+ * @param eventTypes the event types (method names) from the listener interface (if none specified, all will be
+ * supported)
+ */
+ public static void bindEventsToMethod(Object target, String methodName, Object eventSource, Class listenerType, String... eventTypes)
+ {
+ final Object listener = Proxy.newProxyInstance(target.getClass().getClassLoader(), new Class[] { listenerType }, new EventBindingInvocationHandler(target, methodName, eventTypes));
+ addEventListener(eventSource, listenerType, listener);
+ }
+
+ private static class EventBindingInvocationHandler implements InvocationHandler
+ {
+ private final Object target;
+ private final String methodName;
+ private final Set<String> eventTypes;
+
+ public EventBindingInvocationHandler(final Object target, final String methodName, String[] eventTypes)
+ {
+ this.target = target;
+ this.methodName = methodName;
+ this.eventTypes = new HashSet<String>(Arrays.asList(eventTypes));
+ }
+
+ public Object invoke(final Object proxy, final Method method, final Object[] parameters) throws Throwable
+ {
+ if ( eventTypes.isEmpty() || eventTypes.contains(method.getName()))
+ {
+ if (hasMatchingParametersMethod(method))
+ {
+ return MethodUtils.invokeMethod(target, methodName, parameters);
+ }
+ else
+ {
+ return MethodUtils.invokeMethod(target, methodName, new Object[]{});
+ }
+ }
+ return null;
+ }
+
+ private boolean hasMatchingParametersMethod(final Method method)
+ {
+ return MethodUtils.getAccessibleMethod(target.getClass(), methodName, method.getParameterTypes()) != null;
+ }
+ }
+}
diff --git a/src/test/java/org/apache/commons/lang3/event/EventListenerSupportTest.java b/src/test/java/org/apache/commons/lang3/event/EventListenerSupportTest.java
new file mode 100644
index 0000000..a1e3da1
--- /dev/null
+++ b/src/test/java/org/apache/commons/lang3/event/EventListenerSupportTest.java
@@ -0,0 +1,77 @@
+/*
+ * 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 junit.framework.TestCase;
+
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.util.ArrayList;
+import java.util.List;
+
+public class EventListenerSupportTest extends TestCase
+{
+ public void testEventDispatchOrder()
+ {
+ EventListenerSupport<ActionListener> listenerSupport = EventListenerSupport.create(ActionListener.class);
+ final List<ActionListener> calledListeners = new ArrayList<ActionListener>();
+
+ final ActionListener listener1 = createListener(calledListeners);
+ final ActionListener listener2 = createListener(calledListeners);
+ listenerSupport.addListener(listener1);
+ listenerSupport.addListener(listener2);
+ listenerSupport.fire().actionPerformed(new ActionEvent("Hello", 0, "Hello"));
+ assertEquals(calledListeners.size(), 2);
+ assertSame(calledListeners.get(0), listener1);
+ assertSame(calledListeners.get(1), listener2);
+ }
+
+ public void testRemoveListenerDuringEvent()
+ {
+ final EventListenerSupport<ActionListener> listenerSupport = EventListenerSupport.create(ActionListener.class);
+ for (int i = 0; i < 10; ++i)
+ {
+ addDeregisterListener(listenerSupport);
+ }
+ assertEquals(listenerSupport.getListenerCount(), 10);
+ listenerSupport.fire().actionPerformed(new ActionEvent("Hello", 0, "Hello"));
+ assertEquals(listenerSupport.getListenerCount(), 0);
+ }
+
+ private void addDeregisterListener(final EventListenerSupport<ActionListener> listenerSupport)
+ {
+ listenerSupport.addListener(new ActionListener()
+ {
+ public void actionPerformed(ActionEvent e)
+ {
+ listenerSupport.removeListener(this);
+ }
+ });
+ }
+
+ private ActionListener createListener(final List<ActionListener> calledListeners)
+ {
+ return new ActionListener()
+ {
+ public void actionPerformed(ActionEvent e)
+ {
+ calledListeners.add(this);
+ }
+ };
+ }
+}
diff --git a/src/test/java/org/apache/commons/lang3/event/EventUtilsTest.java b/src/test/java/org/apache/commons/lang3/event/EventUtilsTest.java
new file mode 100644
index 0000000..3c4dd8d
--- /dev/null
+++ b/src/test/java/org/apache/commons/lang3/event/EventUtilsTest.java
@@ -0,0 +1,123 @@
+/*
+ * 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 junit.framework.TestCase;
+
+import java.beans.PropertyChangeEvent;
+import java.beans.PropertyChangeListener;
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.Method;
+import java.lang.reflect.Proxy;
+import java.util.Map;
+import java.util.TreeMap;
+
+public class EventUtilsTest extends TestCase
+{
+ public void testAddEventListener()
+ {
+ final PropertyChangeSource src = new PropertyChangeSource();
+ EventCountingInvociationHandler handler = new EventCountingInvociationHandler();
+ PropertyChangeListener listener = handler.createListener(PropertyChangeListener.class);
+ assertEquals(0, handler.getEventCount("propertyChange"));
+ EventUtils.addEventListener(src, PropertyChangeListener.class, listener);
+ assertEquals(0, handler.getEventCount("propertyChange"));
+ src.setProperty("newValue");
+ assertEquals(1, handler.getEventCount("propertyChange"));
+ }
+
+ public void testBindEventsToMethod()
+ {
+ final PropertyChangeSource src = new PropertyChangeSource();
+ final EventCounter counter = new EventCounter();
+ EventUtils.bindEventsToMethod(counter, "eventOccurred", src, PropertyChangeListener.class);
+ assertEquals(0, counter.getCount());
+ src.setProperty("newValue");
+ assertEquals(1, counter.getCount());
+ }
+
+ public static class EventCounter
+ {
+ private int count;
+
+ public void eventOccurred()
+ {
+ count++;
+ }
+
+ public int getCount()
+ {
+ return count;
+ }
+ }
+
+ private static class EventCountingInvociationHandler implements InvocationHandler
+ {
+ private Map<String, Integer> eventCounts = new TreeMap<String, Integer>();
+
+ public <L> L createListener(Class<L> listenerType)
+ {
+ return listenerType.cast(Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),
+ new Class[]{listenerType},
+ this));
+ }
+
+ public int getEventCount(String eventName)
+ {
+ Integer count = eventCounts.get(eventName);
+ return count == null ? 0 : count;
+ }
+
+ public Object invoke(Object proxy, Method method, Object[] args) throws Throwable
+ {
+ Integer count = eventCounts.get(method.getName());
+ if (count == null)
+ {
+ eventCounts.put(method.getName(), 1);
+ }
+ else
+ {
+ eventCounts.put(method.getName(), count + 1);
+ }
+ return null;
+ }
+ }
+
+ public static class PropertyChangeSource
+ {
+ private EventListenerSupport<PropertyChangeListener> listeners = EventListenerSupport.create(PropertyChangeListener.class);
+
+ private String property;
+
+ public void setProperty(String property)
+ {
+ String oldValue = this.property;
+ this.property = property;
+ listeners.fire().propertyChange(new PropertyChangeEvent(this, "property", "oldValue", property));
+ }
+
+ public void addPropertyChangeListener(PropertyChangeListener listener)
+ {
+ listeners.addListener(listener);
+ }
+
+ public void removePropertyChangeListener(PropertyChangeListener listener)
+ {
+ listeners.removeListener(listener);
+ }
+ }
+}