blob: 1db25c8761e50cd5a53d45c373afc0cac6610501 [file] [log] [blame]
/*
* 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 flex.messaging.util;
import flex.messaging.io.TypeMarshaller;
import flex.messaging.io.TypeMarshallingContext;
import flex.messaging.MessageException;
import java.util.Map;
import java.util.HashMap;
import java.util.List;
import java.lang.reflect.Method;
/**
* A utility class used to find a suitable method based on matching
* signatures to the types of set of arguments. Since the arguments
* may be from more loosely typed environments such as ActionScript,
* a translator can be employed to handle type conversion. Note that
* there isn't a great guarantee for which method will be selected
* when several overloaded methods match very closely through the use
* of various combinations of generic types.
*/
public class MethodMatcher {
private final Map<MethodKey, Method> methodCache = new HashMap<MethodKey, Method>();
private static final int ARGUMENT_CONVERSION_ERROR = 10006;
private static final int CANNOT_INVOKE_METHOD = 10007;
/**
* Default constructor.
*/
public MethodMatcher() {
}
/**
* Utility method that searches a class for a given method, taking
* into account the supplied parameters when evaluating overloaded
* method signatures.
*
* @param c The class.
* @param methodName Desired method to search for
* @param parameters Required to distinguish between overloaded methods of the same name
* @return The best-match <tt>Method</tt>.
*/
public Method getMethod(Class c, String methodName, List parameters) {
// Keep track of the best method match found
Match bestMatch = new Match(methodName);
// Determine supplied parameter types.
Class[] suppliedParamTypes = paramTypes(parameters);
// Create a key to search our method cache
MethodKey methodKey = new MethodKey(c, methodName, suppliedParamTypes);
Method method = null;
if (methodCache.containsKey(methodKey)) {
method = methodCache.get(methodKey);
String thisMethodName = method.getName();
bestMatch.matchedMethodName = thisMethodName;
} else {
try // First, try an exact match.
{
method = c.getMethod(methodName, suppliedParamTypes);
synchronized (methodCache) {
Method method2 = methodCache.get(methodKey);
if (method2 == null)
methodCache.put(methodKey, method);
else
method = method2;
}
} catch (SecurityException e) {
// NOWARN
} catch (NoSuchMethodException e) {
// NOWARN
}
if (method == null) // Otherwise, search the long way.
{
Method[] methods = c.getMethods();
for (Method thisMethod : c.getMethods()) {
String thisMethodName = thisMethod.getName();
// FIXME: Do we want to do this case-insensitively in Flex 2.0?
// First, search by name; for backwards compatibility
// we continue to check case-insensitively
if (!thisMethodName.equalsIgnoreCase(methodName))
continue;
// Next, search on params
Match currentMatch = new Match(methodName);
currentMatch.matchedMethodName = thisMethodName;
// If we've not yet had a match, this is our best match so far.
if (bestMatch.matchedMethodName == null)
bestMatch = currentMatch;
// Number of parameters must match.
Class[] desiredParamTypes = thisMethod.getParameterTypes();
currentMatch.methodParamTypes = desiredParamTypes;
if (desiredParamTypes.length != suppliedParamTypes.length)
continue;
currentMatch.matchedByNumberOfParams = true;
// If we've not yet matched any params, this is our best match so far.
if (!bestMatch.matchedByNumberOfParams && bestMatch.matchedParamCount == 0)
bestMatch = currentMatch;
// Parameter types must also be compatible. Don't actually convert
// the parameter just yet, only count the matches and exact matches.
convertParams(parameters, desiredParamTypes, currentMatch, false);
// If we've not yet had this many params match, this is our best match so far.
if (currentMatch.matchedParamCount >= bestMatch.matchedParamCount
&& currentMatch.exactMatchedParamCount >= bestMatch.exactMatchedParamCount)
bestMatch = currentMatch;
// If all types were compatible, we have a match.
if (currentMatch.matchedParamCount == desiredParamTypes.length
&& bestMatch == currentMatch) {
method = thisMethod;
synchronized (methodCache) {
Method method2 = methodCache.get(methodKey);
if (method2 == null || method2 != method)
methodCache.put(methodKey, method);
else
method = method2;
}
// Don't break as there might be other methods with the
// same number of arguments but with better match count.
// break;
}
}
}
}
if (method == null) {
methodNotFound(methodName, suppliedParamTypes, bestMatch);
} else if (bestMatch.paramTypeConversionFailure != null) {
//Error occurred while attempting to convert an input argument's type.
MessageException me = new MessageException();
me.setMessage(ARGUMENT_CONVERSION_ERROR);
me.setCode("Server.Processing");
me.setRootCause(bestMatch.paramTypeConversionFailure);
throw me;
}
// Call convertParams one last time before returning method. This ensures
// that parameters List is converted using bestMatch.
Class<?>[] desiredParamTypes = method.getParameterTypes();
bestMatch.methodParamTypes = desiredParamTypes;
convertParams(parameters, desiredParamTypes, bestMatch, true);
return method;
}
/**
* Utility method to convert a collection of parameters to desired types. We keep track
* of the progress of the conversion to allow callers to gauge the success of the conversion.
* This is important for ranking overloaded-methods and debugging purposes.
*
* @param parameters actual parameters for an invocation
* @param desiredParamTypes classes in the signature of a potential match for the invocation
* @param currentMatch the currently best known match
* @param convert determines whether the actual conversion should take place.
*/
public static void convertParams(List parameters, Class[] desiredParamTypes, Match currentMatch, boolean convert) {
int matchCount = 0;
int exactMatchCount = 0;
currentMatch.matchedParamCount = 0;
currentMatch.convertedSuppliedTypes = new Class[desiredParamTypes.length];
TypeMarshaller marshaller = TypeMarshallingContext.getTypeMarshaller();
for (int i = 0; i < desiredParamTypes.length; i++) {
Object param = parameters.get(i);
// Consider null param to match
if (param != null) {
Object obj = null;
Class objClass = null;
if (marshaller != null) {
try {
obj = marshaller.convert(param, desiredParamTypes[i]);
} catch (MessageException ex) {
currentMatch.paramTypeConversionFailure = ex;
break;
} catch (ClassCastException ex) {
currentMatch.paramTypeConversionFailure = ex;
break;
}
// We need to catch the exception here as the conversion of parameter types could fail
catch (Exception e) {
currentMatch.paramTypeConversionFailure = e;
break;
}
} else {
obj = param;
}
currentMatch.convertedSuppliedTypes[i] = (obj != null ? (objClass = obj.getClass()) : null);
// Things match if we now have an object which is assignable from the
// method param class type or if we have an Object which corresponds to
// a primitive
if (objClass != null && isAssignableFrom(desiredParamTypes[i], objClass)) {
// See if there's an exact match before parameter is converted.
if (isAssignableFrom(desiredParamTypes[i], param.getClass()))
exactMatchCount++;
if (convert) // Convert the parameter.
parameters.set(i, obj);
matchCount++;
} else {
break;
}
} else {
matchCount++;
}
}
currentMatch.matchedParamCount = matchCount;
currentMatch.exactMatchedParamCount = exactMatchCount;
}
private static boolean isAssignableFrom(Class first, Class second) {
return (first.isAssignableFrom(second)) ||
(first == Integer.TYPE && Integer.class.isAssignableFrom(second)) ||
(first == Double.TYPE && Double.class.isAssignableFrom(second)) ||
(first == Long.TYPE && Long.class.isAssignableFrom(second)) ||
(first == Boolean.TYPE && Boolean.class.isAssignableFrom(second)) ||
(first == Character.TYPE && Character.class.isAssignableFrom(second)) ||
(first == Float.TYPE && Float.class.isAssignableFrom(second)) ||
(first == Short.TYPE && Short.class.isAssignableFrom(second)) ||
(first == Byte.TYPE && Byte.class.isAssignableFrom(second));
}
/**
* Utility method that iterates over a collection of input
* parameters to determine their types while logging
* the class names to create a unique identifier for a
* method signature.
*
* @param parameters - A list of supplied parameters.
* @return An array of <tt>Class</tt> instances indicating the class of each corresponding parameter.
*/
public static Class[] paramTypes(List parameters) {
Class[] paramTypes = new Class[parameters.size()];
for (int i = 0; i < paramTypes.length; i++) {
Object p = parameters.get(i);
paramTypes[i] = p == null ? Object.class : p.getClass();
}
return paramTypes;
}
/**
* Utility method to provide more detailed information in the event that a search
* for a specific method failed for the service class.
*
* @param methodName the name of the missing method
* @param suppliedParamTypes the types of parameters supplied for the search
* @param bestMatch the best match found during the search
*/
public static void methodNotFound(String methodName, Class[] suppliedParamTypes, Match bestMatch) {
// Set default error message...
// Cannot invoke method '{methodName}'.
int errorCode = CANNOT_INVOKE_METHOD;
Object[] errorParams = new Object[]{methodName};
String errorDetailVariant = "0";
// Method '{methodName}' not found.
Object[] errorDetailParams = new Object[]{methodName};
if (bestMatch.matchedMethodName != null) {
// Cannot invoke method '{bestMatch.matchedMethodName}'.
errorCode = CANNOT_INVOKE_METHOD;
errorParams = new Object[]{bestMatch.matchedMethodName};
int suppliedParamCount = suppliedParamTypes.length;
int expectedParamCount = bestMatch.methodParamTypes != null ? bestMatch.methodParamTypes.length : 0;
if (suppliedParamCount != expectedParamCount) {
// {suppliedParamCount} arguments were sent but {expectedParamCount} were expected.
errorDetailVariant = "1";
errorDetailParams = new Object[]{new Integer(suppliedParamCount), new Integer(expectedParamCount)};
} else {
String suppliedTypes = bestMatch.listTypes(suppliedParamTypes);
String convertedTypes = bestMatch.listConvertedTypes();
String expectedTypes = bestMatch.listExpectedTypes();
if (expectedTypes != null) {
if (suppliedTypes != null) {
if (convertedTypes != null) {
// The expected argument types are ({expectedTypes})
// but the supplied types were ({suppliedTypes})
// and converted to ({convertedTypes}).
errorDetailVariant = "2";
errorDetailParams = new Object[]{expectedTypes, suppliedTypes, convertedTypes};
} else {
// The expected argument types are ({expectedTypes})
// but the supplied types were ({suppliedTypes})
// with none successfully converted.
errorDetailVariant = "3";
errorDetailParams = new Object[]{expectedTypes, suppliedTypes};
}
} else {
// The expected argument types are ({expectedTypes})
// but no arguments were provided.
errorDetailVariant = "4";
errorDetailParams = new Object[]{expectedTypes};
}
} else {
// No arguments were expected but the following types were supplied (suppliedTypes)
errorDetailVariant = "5";
errorDetailParams = new Object[]{suppliedTypes};
}
}
}
MessageException ex = new MessageException();
ex.setMessage(errorCode, errorParams);
ex.setCode(MessageException.CODE_SERVER_RESOURCE_UNAVAILABLE);
if (errorDetailVariant != null)
ex.setDetails(errorCode, errorDetailVariant, errorDetailParams);
if (bestMatch.paramTypeConversionFailure != null)
ex.setRootCause(bestMatch.paramTypeConversionFailure);
throw ex;
}
/**
* A utility class to help rank methods in the search
* for a best match, given a name and collection of
* input parameters.
*/
public static class Match {
/**
* Constructor.
*
* @param name the name of the method to match
*/
public Match(String name) {
this.methodName = name;
}
/**
* Returns true if desired and found method names match.
*
* @return true if desired and found method names match
*/
public boolean matchedExactlyByName() {
return matchedMethodName != null ? matchedMethodName.equals(methodName) : false;
}
/**
* Returns true if desired and found method names match only when case is ignored.
*
* @return true if desired and found method names match only when case is ignored
*/
public boolean matchedLooselyByName() {
return matchedMethodName != null ?
(!matchedExactlyByName() && matchedMethodName.equalsIgnoreCase(methodName)) : false;
}
/**
* Lists the classes in the signature of the method matched.
*
* @return the classes in the signature of the method matched
*/
public String listExpectedTypes() {
return listTypes(methodParamTypes);
}
/**
* Lists the classes corresponding to actual invocation parameters once they have been
* converted as best they could to match the classes in the invoked method's signature.
*
* @return the classes corresponding to actual invocation parameters once they have been
* converted as best they could to match the classes in the invoked method's signature
*/
public String listConvertedTypes() {
return listTypes(convertedSuppliedTypes);
}
/**
* Creates a string representation of the class names in the array of types passed into
* this method.
*
* @param types an array of types whose names are to be listed
* @return a string representation of the class names in the array of types
*/
public String listTypes(Class[] types) {
if (types == null || types.length == 0)
return null;
StringBuffer sb = new StringBuffer();
for (int i = 0; i < types.length; i++) {
if (i > 0)
sb.append(", ");
Class c = types[i];
if (c != null) {
if (c.isArray()) {
c = c.getComponentType();
sb.append(c.getName()).append("[]");
} else {
sb.append(c.getName());
}
} else {
sb.append("null");
}
}
return sb.toString();
}
final String methodName;
String matchedMethodName;
boolean matchedByNumberOfParams;
int matchedParamCount;
int exactMatchedParamCount;
Class[] methodParamTypes;
Class[] convertedSuppliedTypes;
Exception paramTypeConversionFailure;
}
}