/*
* Copyright 2005 Joe Walker
*
* 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 org.directwebremoting.extend;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
* Call is a POJO to encapsulate the information required to make a single java
* call, including the result of the call (either returned data or exception).
* Either the Method and Parameters should be filled in to allow a call to be
* made or, the exception should be filled in indicating that things have gone
* wrong already.
* @author Joe Walker [joe at getahead dot ltd dot uk]
*/
public class Call
{
public Call(String callId, String scriptName, String methodName)
{
this.callId = callId;
this.scriptName = scriptName;
this.methodName = methodName;
}
public void setMarshallFailure(Throwable exception)
{
this.exception = exception;
this.method = null;
this.parameters = null;
}
/**
* @return the exception
*/
public Throwable getException()
{
return exception;
}
/**
* @return the method
*/
public Method getMethod()
{
return method;
}
/**
* @param method the method to set
*/
public void setMethod(Method method)
{
this.method = method;
}
/**
* @return the parameters
*/
public Object[] getParameters()
{
return parameters;
}
/**
* @param parameters the parameters to set
*/
public void setParameters(Object[] parameters)
{
this.parameters = parameters;
}
/**
* @return Returns the callId.
*/
public String getCallId()
{
return callId;
}
/**
* @return Returns the scriptName.
*/
public String getScriptName()
{
return scriptName;
}
/**
* @return Returns the methodName.
*/
public String getMethodName()
{
return methodName;
}
/**
* Find the method the best matches the method name and parameters.
* <p>
* This method used to be significantly more detailed in its matching
* sequences, this simpler version is less defined in the order in which it
* matches methods, but able to find more matches. If we discover that this
* version creates problems, the old version was around up to revision 2317.
*/
public void findMethod(CreatorManager creatorManager, ConverterManager converterManager, InboundContext inctx, int callNum)
{
if (scriptName == null)
{
throw new IllegalArgumentException("Missing class parameter");
}
if (methodName == null)
{
throw new IllegalArgumentException("Missing method parameter");
}
int inputArgCount = inctx.getParameterCount(callNum);
// Get a mutable list of all methods on the type specified by the creator
Creator creator = creatorManager.getCreator(scriptName, true);
List<Method> allMethods = new ArrayList<Method>();
allMethods.addAll(Arrays.asList(creator.getType().getMethods()));
// Remove all methods that don't have a matching name
for (Iterator<Method> it = allMethods.iterator(); it.hasNext();)
{
if (!it.next().getName().equals(methodName))
{
it.remove();
}
}
if (allMethods.isEmpty())
{
// Not even a name match
log.warn("No method called '" + methodName + "' found in " + creator.getType());
throw new IllegalArgumentException("Method name not found. See logs for details");
}
// Remove all the methods where we can't convert the parameters
allMethodsLoop:
for (Iterator<Method> it = allMethods.iterator(); it.hasNext();)
{
Method m = it.next();
Class<?>[] methodParamTypes = m.getParameterTypes();
// Remove non-varargs methods which declare less params than were passed
if (!m.isVarArgs() && methodParamTypes.length < inputArgCount)
{
it.remove();
continue allMethodsLoop;
}
// Remove methods where we can't convert the input
for (int i = 0; i < methodParamTypes.length; i++)
{
Class<?> methodParamType = methodParamTypes[i];
InboundVariable param = inctx.getParameter(callNum, i);
Class<?> inputType = converterManager.getClientDeclaredType(param);
// If we can't convert this parameter type, ignore the method
if (inputType == null && !converterManager.isConvertable(methodParamType))
{
it.remove();
continue allMethodsLoop;
}
// Remove methods which declare more non-nullable parameters than were passed
if (inputArgCount <= i && methodParamType.isPrimitive())
{
it.remove();
continue allMethodsLoop;
}
// Remove methods where the client passed a type and we can't use it.
if (inputType != null && !methodParamType.isAssignableFrom(inputType))
{
it.remove();
continue allMethodsLoop;
}
}
}
if (allMethods.isEmpty())
{
// Not even a name match
log.warn("No methods called " + creator.getType() + "." + methodName + "' are applicable for the passed parameters.");
throw new IllegalArgumentException("Method not found. See logs for details");
}
else if (allMethods.size() == 1)
{
method = allMethods.get(0);
checkProxiedMethod(creatorManager);
return;
}
// If we have methods that exactly match the param count we use a
// different matching algorithm, to when we don't
List<Method> exactParamCountMatches = new ArrayList<Method>();
for (Method m : allMethods)
{
if (!m.isVarArgs() && m.getParameterTypes().length == inputArgCount)
{
exactParamCountMatches.add(m);
}
}
if (exactParamCountMatches.size() == 1)
{
// One method with the right number of params - use that
method = exactParamCountMatches.get(0);
checkProxiedMethod(creatorManager);
return;
}
// Lots of methods with the right name, but none with the right
// parameter count. If we have exactly one varargs method, then we
// try that, otherwise we bail.
List<Method> varargsMathods = new ArrayList<Method>();
for (Method m : allMethods)
{
if (m.isVarArgs())
{
varargsMathods.add(m);
}
}
if (varargsMathods.size() == 1)
{
method = varargsMathods.get(0);
checkProxiedMethod(creatorManager);
return;
}
log.warn("Can't find single method to match " + creator.getType() + "." + methodName);
log.warn("- DWR does not continue where there is ambiguity about which method to execute.");
log.warn("- Input parameters: " + inputArgCount + ".Matching methods with param count match: " + exactParamCountMatches.size() + ". Number of matching varargs methods: " + varargsMathods.size());
log.warn("- Potential matches include:");
for (Method m : allMethods)
{
log.warn(" - " + m.toGenericString());
}
throw new IllegalArgumentException("Method not found. See logs for details");
}
/**
* The main issue here happens with JDK proxies, that is, those based on
* interfaces and is easily noticeable with Spring because it's designed to
* generate proxies on the fly for many different purposes (security, tx,..)
* For some unknown reasons but probably related to erasure, when a proxy is
* created and it contains a method with at least one generic parameter,
* that generic type information is lost. Those that rely on reflection to
* detect that info at runtime (our case when detecting the matching method
* for an incoming call) face a dead end. The solution involves detecting
* the proxy interface and obtain the original class (which holds the
* required information).
* <p>Here comes the problematic area. In the case of Spring all proxies
* implement the Advised interface which includes a method that returns the
* target class (and so fulfills our need). Of course, this means that:
* a) A Spring dependency appears and
* b) The solution only applies to Spring contexts.
* The first concern is solvable using Class.forName. The current fix does
* not solve the second. Probably a better solution should be implemented
* (for example, something that works under the AOP alliance umbrella).
*/
private void checkProxiedMethod(CreatorManager creatorManager)
{
/*
if (method != null)
{
Creator c = creatorManager.getCreator(scriptName, true);
if (Proxy.isProxyClass(c.getType()))
{
try
{
advisedClass = Class.forName("org.springframework.aop.framework.Advised");
if (advisedClass.isAssignableFrom(method.getDeclaringClass()))
{
Object target = c.getInstance(); // Should be a singleton
Method targetClassMethod = target.getClass().getMethod("getTargetClass");
Class<?> targetClass = (Class<?>) targetClassMethod.invoke(target);
method = targetClass.getDeclaredMethod(method.getName(), method.getParameterTypes());
}
}
catch (Exception ex)
{
// Probably not in Spring context so no Advised proxies at all
}
}
}
*/
/*
try
{
if (method != null && advisedClass != null && advisedClass.isAssignableFrom(method.getDeclaringClass()))
{
Creator creator = creatorManager.getCreator(scriptName, true);
if (Proxy.isProxyClass(creator.getType()))
{
Object target = creator.getInstance(); // Should be a singleton
Method targetClassMethod = target.getClass().getMethod("getTargetClass");
Class<?> targetClass = (Class<?>) targetClassMethod.invoke(target);
method = targetClass.getDeclaredMethod(method.getName(), method.getParameterTypes());
}
}
}
catch (Exception ex)
{
// Probably not in Spring context so no Advised proxies at all
}
*/
}
/* (non-Javadoc)
* @see java.lang.Object#toString()
*/
@Override
public String toString()
{
try
{
return scriptName + "." + methodName + "(...)";
}
catch (Exception ex)
{
return "Call(undefined)";
}
}
private final String callId;
private final String scriptName;
private final String methodName;
private Method method = null;
private Object[] parameters = null;
private Throwable exception = null;
/**
* The log stream
*/
private static final Log log = LogFactory.getLog(Call.class);
/**
* Spring/AOP hack
* @see #checkProxiedMethod(CreatorManager)
*/
private static Class<?> advisedClass;
/**
* Spring/AOP hack
* @see #checkProxiedMethod(CreatorManager)
*/
static
{
try
{
advisedClass = Class.forName("org.springframework.aop.framework.Advised");
log.debug("Found org.springframework.aop.framework.Advised enabling AOP checks");
}
catch (ClassNotFoundException ex)
{
log.debug("ClassNotFoundException on org.springframework.aop.framework.Advised skipping AOP checks");
}
}
}