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);

+        }

+    }

+}