/*
* Copyright 2000-2016 Vaadin Ltd.
*
* Licensed 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 com.vaadin.event;
import java.io.IOException;
import java.io.NotSerializableException;
import java.io.Serializable;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.EventListener;
import java.util.EventObject;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* <p>
* One registered event listener. This class contains the listener object
* reference, listened event type, the trigger method to call when the event
* fires, and the optional argument list to pass to the method and the index of
* the argument to replace with the event object.
* </p>
*
* <p>
* This Class provides several constructors that allow omission of the optional
* arguments, and giving the listener method directly, or having the constructor
* to reflect it using merely the name of the method.
* </p>
*
* <p>
* It should be pointed out that the method
* {@link #receiveEvent(EventObject event)} is the one that filters out the
* events that do not match with the given event type and thus do not result in
* calling of the trigger method.
* </p>
*
* @author Vaadin Ltd.
* @since 3.0
*/
@SuppressWarnings("serial")
public class ListenerMethod implements EventListener, Serializable {
/**
* Type of the event that should trigger this listener. Also the subclasses
* of this class are accepted to trigger the listener.
*/
private final Class<?> eventType;
/**
* The object containing the trigger method.
*/
private final Object target;
/**
* The trigger method to call when an event passing the given criteria
* fires.
*/
private transient Method method;
/**
* Optional argument set to pass to the trigger method.
*/
private Object[] arguments;
/**
* Optional index to <code>arguments</code> that point out which one should
* be replaced with the triggering event object and thus be passed to the
* trigger method.
*/
private int eventArgumentIndex;
/* Special serialization to handle method references */
private void writeObject(java.io.ObjectOutputStream out)
throws IOException {
try {
out.defaultWriteObject();
String name = method.getName();
Class<?>[] paramTypes = method.getParameterTypes();
out.writeObject(name);
out.writeObject(paramTypes);
} catch (NotSerializableException e) {
getLogger().log(Level.WARNING,
"Error in serialization of the application: Class {0} must implement serialization.",
target.getClass().getName());
throw e;
}
}
/* Special serialization to handle method references */
private void readObject(java.io.ObjectInputStream in)
throws IOException, ClassNotFoundException {
in.defaultReadObject();
try {
String name = (String) in.readObject();
Class<?>[] paramTypes = (Class<?>[]) in.readObject();
// We can not use getMethod directly as we want to support anonymous
// inner classes
method = findHighestMethod(target.getClass(), name, paramTypes);
} catch (SecurityException e) {
getLogger().log(Level.SEVERE, "Internal deserialization error", e);
}
}
private static Method findHighestMethod(Class<?> cls, String method,
Class<?>[] paramTypes) {
Class<?>[] ifaces = cls.getInterfaces();
for (int i = 0; i < ifaces.length; i++) {
Method ifaceMethod = findHighestMethod(ifaces[i], method,
paramTypes);
if (ifaceMethod != null) {
return ifaceMethod;
}
}
if (cls.getSuperclass() != null) {
Method parentMethod = findHighestMethod(cls.getSuperclass(), method,
paramTypes);
if (parentMethod != null) {
return parentMethod;
}
}
Method[] methods = cls.getMethods();
for (int i = 0; i < methods.length; i++) {
// we ignore parameter types for now - you need to add this
if (methods[i].getName().equals(method)) {
return methods[i];
}
}
return null;
}
/**
* <p>
* Constructs a new event listener from a trigger method, it's arguments and
* the argument index specifying which one is replaced with the event object
* when the trigger method is called.
* </p>
*
* <p>
* This constructor gets the trigger method as a parameter so it does not
* need to reflect to find it out.
* </p>
*
* @param eventType
* the event type that is listener listens to. All events of this
* kind (or its subclasses) result in calling the trigger method.
* @param target
* the object instance that contains the trigger method
* @param method
* the trigger method
* @param arguments
* the arguments to be passed to the trigger method
* @param eventArgumentIndex
* An index to the argument list. This index points out the
* argument that is replaced with the event object before the
* argument set is passed to the trigger method. If the
* eventArgumentIndex is negative, the triggering event object
* will not be passed to the trigger method, though it is still
* called.
* @throws java.lang.IllegalArgumentException
* if <code>method</code> is not a member of <code>target</code>
* .
*/
public ListenerMethod(Class<?> eventType, Object target, Method method,
Object[] arguments, int eventArgumentIndex)
throws java.lang.IllegalArgumentException {
// Checks that the object is of correct type
if (!method.getDeclaringClass().isAssignableFrom(target.getClass())) {
throw new java.lang.IllegalArgumentException(
"The method " + method.getName()
+ " cannot be used for the given target: "
+ target.getClass().getName());
}
// Checks that the event argument is null
if (eventArgumentIndex >= 0 && arguments[eventArgumentIndex] != null) {
throw new java.lang.IllegalArgumentException(
"argument[" + eventArgumentIndex + "] must be null");
}
// Checks the event type is supported by the method
if (eventArgumentIndex >= 0
&& !method.getParameterTypes()[eventArgumentIndex]
.isAssignableFrom(eventType)) {
throw new java.lang.IllegalArgumentException(
"The method " + method.getName()
+ " does not accept the given eventType: "
+ eventType.getName());
}
this.eventType = eventType;
this.target = target;
this.method = method;
this.arguments = arguments;
this.eventArgumentIndex = eventArgumentIndex;
}
/**
* <p>
* Constructs a new event listener from a trigger method name, it's
* arguments and the argument index specifying which one is replaced with
* the event object. The actual trigger method is reflected from
* <code>object</code>, and <code>java.lang.IllegalArgumentException</code>
* is thrown unless exactly one match is found.
* </p>
*
* @param eventType
* the event type that is listener listens to. All events of this
* kind (or its subclasses) result in calling the trigger method.
* @param target
* the object instance that contains the trigger method.
* @param methodName
* the name of the trigger method. If the object does not contain
* the method or it contains more than one matching methods
* <code>java.lang.IllegalArgumentException</code> is thrown.
* @param arguments
* the arguments to be passed to the trigger method.
* @param eventArgumentIndex
* An index to the argument list. This index points out the
* argument that is replaced with the event object before the
* argument set is passed to the trigger method. If the
* eventArgumentIndex is negative, the triggering event object
* will not be passed to the trigger method, though it is still
* called.
* @throws java.lang.IllegalArgumentException
* unless exactly one match <code>methodName</code> is found in
* <code>target</code>.
*/
public ListenerMethod(Class<?> eventType, Object target, String methodName,
Object[] arguments, int eventArgumentIndex)
throws java.lang.IllegalArgumentException {
// Finds the correct method
final Method[] methods = target.getClass().getMethods();
Method method = null;
for (int i = 0; i < methods.length; i++) {
if (methods[i].getName().equals(methodName)) {
method = methods[i];
}
}
if (method == null) {
throw new IllegalArgumentException("Method " + methodName
+ " not found in class " + target.getClass().getName());
}
// Checks that the event argument is null
if (eventArgumentIndex >= 0 && arguments[eventArgumentIndex] != null) {
throw new java.lang.IllegalArgumentException(
"argument[" + eventArgumentIndex + "] must be null");
}
// Checks the event type is supported by the method
if (eventArgumentIndex >= 0
&& !method.getParameterTypes()[eventArgumentIndex]
.isAssignableFrom(eventType)) {
throw new java.lang.IllegalArgumentException(
"The method " + method.getName()
+ " does not accept the given eventType: "
+ eventType.getName());
}
this.eventType = eventType;
this.target = target;
this.method = method;
this.arguments = arguments;
this.eventArgumentIndex = eventArgumentIndex;
}
/**
* <p>
* Constructs a new event listener from the trigger method and it's
* arguments. Since the the index to the replaced parameter is not specified
* the event triggering this listener will not be passed to the trigger
* method.
* </p>
*
* <p>
* This constructor gets the trigger method as a parameter so it does not
* need to reflect to find it out.
* </p>
*
* @param eventType
* the event type that is listener listens to. All events of this
* kind (or its subclasses) result in calling the trigger method.
* @param target
* the object instance that contains the trigger method.
* @param method
* the trigger method.
* @param arguments
* the arguments to be passed to the trigger method.
* @throws java.lang.IllegalArgumentException
* if <code>method</code> is not a member of <code>target</code>
* .
*/
public ListenerMethod(Class<?> eventType, Object target, Method method,
Object[] arguments) throws java.lang.IllegalArgumentException {
// Check that the object is of correct type
if (!method.getDeclaringClass().isAssignableFrom(target.getClass())) {
throw new java.lang.IllegalArgumentException(
"The method " + method.getName()
+ " cannot be used for the given target: "
+ target.getClass().getName());
}
this.eventType = eventType;
this.target = target;
this.method = method;
this.arguments = arguments;
eventArgumentIndex = -1;
}
/**
* <p>
* Constructs a new event listener from a trigger method name and it's
* arguments. Since the the index to the replaced parameter is not specified
* the event triggering this listener will not be passed to the trigger
* method.
* </p>
*
* <p>
* The actual trigger method is reflected from <code>target</code>, and
* <code>java.lang.IllegalArgumentException</code> is thrown unless exactly
* one match is found.
* </p>
*
* @param eventType
* the event type that is listener listens to. All events of this
* kind (or its subclasses) result in calling the trigger method.
* @param target
* the object instance that contains the trigger method.
* @param methodName
* the name of the trigger method. If the object does not contain
* the method or it contains more than one matching methods
* <code>java.lang.IllegalArgumentException</code> is thrown.
* @param arguments
* the arguments to be passed to the trigger method.
* @throws java.lang.IllegalArgumentException
* unless exactly one match <code>methodName</code> is found in
* <code>object</code>.
*/
public ListenerMethod(Class<?> eventType, Object target, String methodName,
Object[] arguments) throws java.lang.IllegalArgumentException {
// Find the correct method
final Method[] methods = target.getClass().getMethods();
Method method = null;
for (int i = 0; i < methods.length; i++) {
if (methods[i].getName().equals(methodName)) {
method = methods[i];
}
}
if (method == null) {
throw new IllegalArgumentException("Method " + methodName
+ " not found in class " + target.getClass().getName());
}
this.eventType = eventType;
this.target = target;
this.method = method;
this.arguments = arguments;
eventArgumentIndex = -1;
}
/**
* <p>
* Constructs a new event listener from a trigger method. Since the argument
* list is unspecified no parameters are passed to the trigger method when
* the listener is triggered.
* </p>
*
* <p>
* This constructor gets the trigger method as a parameter so it does not
* need to reflect to find it out.
* </p>
*
* @param eventType
* the event type that is listener listens to. All events of this
* kind (or its subclasses) result in calling the trigger method.
* @param target
* the object instance that contains the trigger method.
* @param method
* the trigger method.
* @throws java.lang.IllegalArgumentException
* if <code>method</code> is not a member of <code>object</code>
* .
*/
public ListenerMethod(Class<?> eventType, Object target, Method method)
throws java.lang.IllegalArgumentException {
// Checks that the object is of correct type
if (!method.getDeclaringClass().isAssignableFrom(target.getClass())) {
throw new java.lang.IllegalArgumentException(
"The method " + method.getName()
+ " cannot be used for the given target: "
+ target.getClass().getName());
}
this.eventType = eventType;
this.target = target;
this.method = method;
eventArgumentIndex = -1;
final Class<?>[] params = method.getParameterTypes();
if (params.length == 0) {
arguments = new Object[0];
} else if (params.length == 1
&& params[0].isAssignableFrom(eventType)) {
arguments = new Object[] { null };
eventArgumentIndex = 0;
} else {
throw new IllegalArgumentException(
"Method requires unknown parameters");
}
}
/**
* <p>
* Constructs a new event listener from a trigger method name. Since the
* argument list is unspecified no parameters are passed to the trigger
* method when the listener is triggered.
* </p>
*
* <p>
* The actual trigger method is reflected from <code>object</code>, and
* <code>java.lang.IllegalArgumentException</code> is thrown unless exactly
* one match is found.
* </p>
*
* @param eventType
* the event type that is listener listens to. All events of this
* kind (or its subclasses) result in calling the trigger method.
* @param target
* the object instance that contains the trigger method.
* @param methodName
* the name of the trigger method. If the object does not contain
* the method or it contains more than one matching methods
* <code>java.lang.IllegalArgumentException</code> is thrown.
* @throws java.lang.IllegalArgumentException
* unless exactly one match <code>methodName</code> is found in
* <code>target</code>.
*/
public ListenerMethod(Class<?> eventType, Object target, String methodName)
throws java.lang.IllegalArgumentException {
// Finds the correct method
final Method[] methods = target.getClass().getMethods();
Method method = null;
for (int i = 0; i < methods.length; i++) {
if (methods[i].getName().equals(methodName)) {
method = methods[i];
}
}
if (method == null) {
throw new IllegalArgumentException("Method " + methodName
+ " not found in class " + target.getClass().getName());
}
this.eventType = eventType;
this.target = target;
this.method = method;
eventArgumentIndex = -1;
final Class<?>[] params = method.getParameterTypes();
if (params.length == 0) {
arguments = new Object[0];
} else if (params.length == 1
&& params[0].isAssignableFrom(eventType)) {
arguments = new Object[] { null };
eventArgumentIndex = 0;
} else {
throw new IllegalArgumentException(
"Method requires unknown parameters");
}
}
/**
* Receives one event from the <code>EventRouter</code> and calls the
* trigger method if it matches with the criteria defined for the listener.
* Only the events of the same or subclass of the specified event class
* result in the trigger method to be called.
*
* @param event
* the fired event. Unless the trigger method's argument list and
* the index to the to be replaced argument is specified, this
* event will not be passed to the trigger method.
*/
public void receiveEvent(EventObject event) {
// Only send events supported by the method
if (eventType.isAssignableFrom(event.getClass())) {
try {
if (eventArgumentIndex >= 0) {
if (eventArgumentIndex == 0 && arguments.length == 1) {
method.invoke(target, event);
} else {
final Object[] arg = new Object[arguments.length];
System.arraycopy(arguments, 0, arg, 0, arg.length);
arg[eventArgumentIndex] = event;
method.invoke(target, arg);
}
} else {
method.invoke(target, arguments);
}
} catch (final java.lang.IllegalAccessException e) {
// This should never happen
throw new java.lang.RuntimeException(
"Internal error - please report", e);
} catch (final java.lang.reflect.InvocationTargetException e) {
// An exception was thrown by the invocation target. Throw it
// forwards.
throw new MethodException(
"Invocation of method " + method.getName() + " in "
+ target.getClass().getName() + " failed.",
e.getTargetException());
}
}
}
/**
* Checks if the given object and event match with the ones stored in this
* listener.
*
* @param target
* the object to be matched against the object stored by this
* listener.
* @param eventType
* the type to be tested for equality against the type stored by
* this listener.
* @return <code>true</code> if <code>target</code> is the same object as
* the one stored in this object and <code>eventType</code> equals
* the event type stored in this object. *
*/
public boolean matches(Class<?> eventType, Object target) {
return (this.target == target) && (eventType.equals(this.eventType));
}
/**
* Checks if the given object, event and method match with the ones stored
* in this listener.
*
* @param target
* the object to be matched against the object stored by this
* listener.
* @param eventType
* the type to be tested for equality against the type stored by
* this listener.
* @param method
* the method to be tested for equality against the method stored
* by this listener.
* @return <code>true</code> if <code>target</code> is the same object as
* the one stored in this object, <code>eventType</code> equals with
* the event type stored in this object and <code>method</code>
* equals with the method stored in this object
*/
public boolean matches(Class<?> eventType, Object target, Method method) {
return (this.target == target) && (eventType.equals(this.eventType)
&& method.equals(this.method));
}
@Override
public int hashCode() {
int hash = 7;
hash = 31 * hash + eventArgumentIndex;
hash = 31 * hash + (eventType == null ? 0 : eventType.hashCode());
hash = 31 * hash + (target == null ? 0 : target.hashCode());
hash = 31 * hash + (method == null ? 0 : method.hashCode());
return hash;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
// return false if obj is a subclass (do not use instanceof check)
if ((obj == null) || (obj.getClass() != getClass())) {
return false;
}
// obj is of same class, test it further
ListenerMethod t = (ListenerMethod) obj;
return eventArgumentIndex == t.eventArgumentIndex
&& (eventType == t.eventType
|| (eventType != null && eventType.equals(t.eventType)))
&& (target == t.target
|| (target != null && target.equals(t.target)))
&& (method == t.method
|| (method != null && method.equals(t.method)))
&& (arguments == t.arguments
|| (Arrays.equals(arguments, t.arguments)));
}
/**
* Exception that wraps an exception thrown by an invoked method. When
* <code>ListenerMethod</code> invokes the target method, it may throw
* arbitrary exception. The original exception is wrapped into
* MethodException instance and rethrown by the <code>ListenerMethod</code>.
*
* @author Vaadin Ltd.
* @since 3.0
*/
public class MethodException extends RuntimeException
implements Serializable {
private MethodException(String message, Throwable cause) {
super(message, cause);
}
}
/**
* Compares the type of this ListenerMethod to the given type
*
* @param eventType
* The type to compare with
* @return true if this type of this ListenerMethod matches the given type,
* false otherwise
*/
public boolean isType(Class<?> eventType) {
return this.eventType == eventType;
}
/**
* Compares the type of this ListenerMethod to the given type
*
* @param eventType
* The type to compare with
* @return true if this event type can be assigned to the given type, false
* otherwise
*/
public boolean isOrExtendsType(Class<?> eventType) {
return eventType.isAssignableFrom(this.eventType);
}
/**
* Returns the target object which contains the trigger method.
*
* @return The target object
*/
public Object getTarget() {
return target;
}
private static final Logger getLogger() {
return Logger.getLogger(ListenerMethod.class.getName());
}
}