001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      https://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017
018package org.apache.commons.lang3.event;
019
020import java.lang.reflect.InvocationHandler;
021import java.lang.reflect.Method;
022import java.lang.reflect.Proxy;
023import java.util.Arrays;
024import java.util.HashSet;
025import java.util.Set;
026
027import org.apache.commons.lang3.ObjectUtils;
028import org.apache.commons.lang3.reflect.MethodUtils;
029
030/**
031 * Provides some useful event-based utility methods.
032 *
033 * @since 3.0
034 */
035public class EventUtils {
036
037    private static final class EventBindingInvocationHandler implements InvocationHandler {
038        private final Object target;
039        private final String methodName;
040        private final Set<String> eventTypes;
041
042        /**
043         * Creates a new instance of {@link EventBindingInvocationHandler}.
044         *
045         * @param target The target object for method invocations.
046         * @param methodName The name of the method to be invoked.
047         * @param eventTypes The names of the supported event types.
048         */
049        EventBindingInvocationHandler(final Object target, final String methodName, final String[] eventTypes) {
050            this.target = target;
051            this.methodName = methodName;
052            this.eventTypes = new HashSet<>(Arrays.asList(eventTypes));
053        }
054
055        /**
056         * Tests whether a method for the passed in parameters can be found.
057         *
058         * @param method The listener method invoked.
059         * @return A flag whether the parameters could be matched.
060         */
061        private boolean hasMatchingParametersMethod(final Method method) {
062            return MethodUtils.getAccessibleMethod(target.getClass(), methodName, method.getParameterTypes()) != null;
063        }
064
065        /**
066         * Handles a method invocation on the proxy object.
067         *
068         * @param proxy The proxy instance.
069         * @param method The method to be invoked.
070         * @param parameters The parameters for the method invocation.
071         * @return The result of the method call.
072         * @throws SecurityException Thrown if an underlying accessible object's method denies the request.
073         * @see SecurityManager#checkPermission
074         * @throws Throwable Thrown if an error occurs.
075         */
076        @Override
077        public Object invoke(final Object proxy, final Method method, final Object[] parameters) throws Throwable {
078            if (method.getDeclaringClass() == Object.class) {
079                // Handle Object methods locally instead of dispatching them to the bound target,
080                // mirroring java.beans.EventHandler: routine host actions (hash-based collections,
081                // logging, equality checks during listener de-registration) must not invoke the
082                // target method and must not return null into an unboxing context.
083                switch (method.getName()) {
084                case "hashCode":
085                    return Integer.valueOf(System.identityHashCode(proxy));
086                case "equals":
087                    return Boolean.valueOf(proxy == parameters[0]);
088                default: // toString
089                    return ObjectUtils.identityToString(proxy);
090                }
091            }
092            if (eventTypes.isEmpty() || eventTypes.contains(method.getName())) {
093                if (hasMatchingParametersMethod(method)) {
094                    return MethodUtils.invokeMethod(target, methodName, parameters);
095                }
096                return MethodUtils.invokeMethod(target, methodName);
097            }
098            return null;
099        }
100    }
101
102    /**
103     * Adds an event listener to the specified source.  This looks for an "add" method corresponding to the event
104     * type (addActionListener, for example).
105     *
106     * @param eventSource   The event source.
107     * @param listenerType  The event listener type.
108     * @param listener      The listener.
109     * @param <L>           the event listener type.
110     * @throws IllegalArgumentException Thrown if the object doesn't support the listener type.
111     */
112    public static <L> void addEventListener(final Object eventSource, final Class<L> listenerType, final L listener) {
113        try {
114            MethodUtils.invokeMethod(eventSource, "add" + listenerType.getSimpleName(), listener);
115        } catch (final ReflectiveOperationException e) {
116            throw new IllegalArgumentException("Unable to add listener for class " + eventSource.getClass().getName()
117                    + " and public add" + listenerType.getSimpleName()
118                    + " method which takes a parameter of type " + listenerType.getName() + ".");
119        }
120    }
121
122    /**
123     * Binds an event listener to a specific method on a specific object.
124     *
125     * @param <L>          the event listener type.
126     * @param target       The target object.
127     * @param methodName   The name of the method to be called.
128     * @param eventSource  The object which is generating events (JButton, JList, etc.).
129     * @param listenerType The listener interface (ActionListener.class, SelectionListener.class, etc.).
130     * @param eventTypes   The event types (method names) from the listener interface (if none specified, all will be
131     *                     supported).
132     */
133    public static <L> void bindEventsToMethod(final Object target, final String methodName, final Object eventSource,
134            final Class<L> listenerType, final String... eventTypes) {
135        final L listener = listenerType.cast(Proxy.newProxyInstance(target.getClass().getClassLoader(),
136                new Class[] { listenerType }, new EventBindingInvocationHandler(target, methodName, eventTypes)));
137        addEventListener(eventSource, listenerType, listener);
138    }
139
140    /**
141     * Make private in 4.0.
142     *
143     * @deprecated TODO Make private in 4.0.
144     */
145    @Deprecated
146    public EventUtils() {
147        // empty
148    }
149}