From 6ef1437ef4449459bd6167b427119c7c827d3af7 Mon Sep 17 00:00:00 2001 From: "James W. Carman" Date: Thu, 22 Jul 2010 11:35:07 +0000 Subject: [PATCH] Misc. event utils. git-svn-id: https://svn.apache.org/repos/asf/commons/proper/lang/trunk@966589 13f79535-47bb-0310-9956-ffa450edef68 --- .../lang3/event/EventListenerSupport.java | 143 ++++++++++++++++++ .../commons/lang3/event/EventUtils.java | 102 +++++++++++++ .../lang3/event/EventListenerSupportTest.java | 77 ++++++++++ .../commons/lang3/event/EventUtilsTest.java | 123 +++++++++++++++ 4 files changed, 445 insertions(+) create mode 100644 src/main/java/org/apache/commons/lang3/event/EventListenerSupport.java create mode 100644 src/main/java/org/apache/commons/lang3/event/EventUtils.java create mode 100644 src/test/java/org/apache/commons/lang3/event/EventListenerSupportTest.java create mode 100644 src/test/java/org/apache/commons/lang3/event/EventUtilsTest.java 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..3dc566ca0 --- /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. + *

+ * To use this class, suppose you want to support ActionEvents. You would do: + *

+ * public class MyActionEventSource
+ * {
+ *   private EventListenerSupport actionListeners = EventListenerSupport.create(ActionListener.class);
+ * 

+ * public void someMethodThatFiresAction() + * { + * ActionEvent e = new ActionEvent(this, ActionEvent.ACTION_PERFORMED, "somethingCool"); + * actionListeners.getProxy().actionPerformed(e); + * } + * } + *

+ * + * @param The event listener type + */ +public class EventListenerSupport +{ + private final List 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 EventListenerSupport create(Class listenerType) + { + return new EventListenerSupport(listenerType); + } + + /** + * Creates an EventListenerSupport object which supports the provided listener interface. + * + * @param listenerInterface the listener interface + */ + public EventListenerSupport(Class 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 listenerInterface, ClassLoader classLoader) + { + listeners = new CopyOnWriteArrayList(); + 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 000000000..56e676c61 --- /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 void addEventListener(Object eventSource, Class 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 eventTypes; + + public EventBindingInvocationHandler(final Object target, final String methodName, String[] eventTypes) + { + this.target = target; + this.methodName = methodName; + this.eventTypes = new HashSet(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 000000000..a1e3da162 --- /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 listenerSupport = EventListenerSupport.create(ActionListener.class); + final List calledListeners = new ArrayList(); + + 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 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 listenerSupport) + { + listenerSupport.addListener(new ActionListener() + { + public void actionPerformed(ActionEvent e) + { + listenerSupport.removeListener(this); + } + }); + } + + private ActionListener createListener(final List 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 000000000..3c4dd8d2d --- /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 eventCounts = new TreeMap(); + + public L createListener(Class 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 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); + } + } +}