package org.jibeframework.core.app.method;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.collections.Predicate;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jibeframework.core.Context;
import org.jibeframework.core.JibeRuntimeException;
import org.jibeframework.core.annotation.ConversationAttribute;
import org.jibeframework.core.annotation.UIComponent;
import org.jibeframework.core.annotation.UIController;
import org.jibeframework.core.app.conversation.Conversation;
import org.jibeframework.core.util.Content;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.core.MethodParameter;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.core.convert.ConversionService;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestParam;
import sun.reflect.generics.reflectiveObjects.ParameterizedTypeImpl;
/**
* Responsibility of this service is dynamic parameters injection which happens
* on invocation of annotated methods
*
* @author dhalupa
*
*/
@Component
public class ArgumentsResolver implements InitializingBean, ApplicationContextAware {
private static final Log logger = LogFactory.getLog(ArgumentsResolver.class);
private List<ArgumentResolver> resolvers = new ArrayList<ArgumentResolver>();
private Map<String, RequestParamArgumentResolver> requestParamResolvers = new HashMap<String, RequestParamArgumentResolver>();
private static ThreadLocal<ArgumentCandidatesCache> argThreadLocal = new ThreadLocal<ArgumentCandidatesCache>();
private ParameterNameDiscoverer parameterNameDiscoverer = new LocalVariableTableParameterNameDiscoverer();
private ApplicationContext applicationContext;
@Autowired
@Qualifier("jibe.spring.ConversionService")
private ConversionService conversionService;
/**
* This method will return a list of object that will be used as parameters
* on method invocation
*
* @param method
* @param actualArguments
* @return
*/
@SuppressWarnings("unchecked")
public Object[] resolveArguments(Method method, Object[] actualArguments) {
Context context = Context.getCurrentContext();
Map<String, Object> data = (Map<String, Object>) context.getParams().get("data");
Class<?>[] parameterTypes = method.getParameterTypes();
MethodParameter[] parameters = new MethodParameter[parameterTypes.length];
for (int i = 0; i < parameterTypes.length; i++) {
parameters[i] = new MethodParameter(method, i);
parameters[i].initParameterNameDiscovery(parameterNameDiscoverer);
}
Object[] args = new Object[parameters.length];
for (int i = 0; i < parameters.length; i++) {
String name = parameters[i].getParameterName();
if (data != null && data.containsKey(name)) {
Object val = data.get(name);
if (conversionService.canConvert(val.getClass(), parameters[i].getParameterType())) {
args[i] = conversionService.convert(val, parameters[i].getParameterType());
} else {
args[i] = val;
}
}
if (args[i] == null) {
for (Annotation paramAnn : parameters[i].getParameterAnnotations()) {
if (data != null && RequestParam.class.isInstance(paramAnn)) {
RequestParam requestParam = (RequestParam) paramAnn;
String paramName = requestParam.value();
boolean required = requestParam.required();
String defaultValue = requestParam.defaultValue();
Object val = data.get(paramName);
if (val == null) {
if (defaultValue != null) {
val = defaultValue;
} else if (required) {
throw new JibeRuntimeException(paramName
+ " parameter has to be present in the request");
}
}
if (conversionService.canConvert(val.getClass(), parameters[i].getParameterType())) {
args[i] = conversionService.convert(val, parameters[i].getParameterType());
} else {
args[i] = val;
}
break;
} else if (ConversationAttribute.class.isInstance(paramAnn)) {
ConversationAttribute convAttrAnn = (ConversationAttribute) paramAnn;
if (StringUtils.isEmpty(convAttrAnn.value())) {
for (Object attr : context.getConversation().getAttributes().values()) {
if (attr != null && parameters[i].getParameterType().isAssignableFrom(attr.getClass())) {
args[i] = attr;
}
}
} else {
args[i] = context.getConversation().getAttribute(convAttrAnn.value());
}
}
}
}
Class<?> parameterType = parameters[i].getParameterType();
if (args[i] == null) {
for (int j = 0; j < actualArguments.length; j++) {
if (actualArguments[j] == null || actualArguments[j].getClass().isAssignableFrom(parameterType)) {
args[i] = actualArguments[j];
// remove, so that we do not inject twice
actualArguments[j] = null;
break;
}
}
}
if (args[i] == null) {
ArgumentCandidatesCache cache = argThreadLocal.get();
if (cache == null) {
cache = new ArgumentCandidatesCache();
}
Object arg = cache.fetch(parameters[i]);
if (arg.equals(ArgumentResolver.UNRESOLVED)) {
for (ArgumentResolver resolver : resolvers) {
arg = resolver.resolve(parameters[i], context);
if (!arg.equals(ArgumentResolver.UNRESOLVED)) {
args[i] = arg;
cache.add(parameters[i], arg);
}
}
} else {
args[i] = arg;
}
}
if (args[i] == null) {
UIComponent uiCmpAnn = parameters[i].getParameterAnnotation(UIComponent.class);
if (uiCmpAnn != null) {
args[i] = applicationContext.getBean(uiCmpAnn.value());
} else {
UIController cntAnn = parameters[i].getParameterAnnotation(UIController.class);
if (cntAnn != null) {
args[i] = applicationContext.getBean(cntAnn.value());
}
}
}
}
return args;
}
public static void addToArgsCache(Object object) {
argThreadLocal.get().add(object.getClass(), object);
}
public static void addToArgsCache(Class<?> genericType, Class<?>[] actualTypes, Object object) {
argThreadLocal.get().add(genericType, actualTypes, object);
}
public void initializeArgumentCandidates(Context context) {
ArgumentCandidatesCache cache = new ArgumentCandidatesCache();
argThreadLocal.set(cache);
for (String paramName : this.requestParamResolvers.keySet()) {
if (context.getParams().containsKey(paramName)) {
this.requestParamResolvers.get(paramName)
.addToArgumentsCache(cache, context.getParams().get(paramName));
}
}
}
/**
* Registers a resolver for a particular parameter type
*
* @param resolver
*/
public void registerResolver(ArgumentResolver resolver) {
this.resolvers.add(resolver);
}
/**
* Registers a resolver that will be invoked to add a parameter candidate
* into the candidates cache if http request contains parameter with
* registered name
*
* @param paramName
* parameter name to be registered
* @param resolver
*/
public void registerRequestParamResolver(String paramName, RequestParamArgumentResolver resolver) {
this.requestParamResolvers.put(paramName, resolver);
}
public void afterPropertiesSet() throws Exception {
registerResolver(new ArgumentResolver() {
public Object resolve(MethodParameter parameter, Context context) {
if (Context.class.isAssignableFrom(parameter.getParameterType())) {
return context;
}
return UNRESOLVED;
}
});
registerResolver(new ArgumentResolver() {
public Object resolve(MethodParameter parameter, Context context) {
if (HttpServletResponse.class.isAssignableFrom(parameter.getParameterType())) {
return context.getServletResponse();
}
return UNRESOLVED;
}
});
registerResolver(new ArgumentResolver() {
public Object resolve(MethodParameter parameter, Context context) {
if (Conversation.class.isAssignableFrom(parameter.getParameterType())) {
return context.getConversation();
}
return UNRESOLVED;
}
});
registerResolver(new ArgumentResolver() {
public Object resolve(MethodParameter parameter, Context context) {
if (HttpServletRequest.class.isAssignableFrom(parameter.getParameterType())) {
return context.getServletRequest();
}
return UNRESOLVED;
}
});
registerResolver(new ArgumentResolver() {
public Object resolve(MethodParameter parameter, Context context) {
if (Content.class.isAssignableFrom(parameter.getParameterType())) {
return context.getUploadedContent();
}
return UNRESOLVED;
}
});
registerResolver(new ArgumentResolver() {
public Object resolve(MethodParameter parameter, Context context) {
if (HttpSession.class.isAssignableFrom(parameter.getParameterType())) {
return context.getSession();
}
return UNRESOLVED;
}
});
registerResolver(new ArgumentResolver() {
public Object resolve(MethodParameter parameter, Context context) {
if (ArgumentCandidatesCache.class.isAssignableFrom(parameter.getParameterType())) {
return argThreadLocal.get();
}
return UNRESOLVED;
}
});
}
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
public static class ArgumentCandidatesCache {
private List<ArgumentHolder> cache = new ArrayList<ArgumentHolder>();
protected void add(MethodParameter parameter, Object object) {
final Type type = parameter.getGenericParameterType();
CollectionUtils.filter(this.cache, new Predicate() {
public boolean evaluate(Object object) {
ArgumentHolder holder = (ArgumentHolder) object;
return !holder.match(type);
}
});
cache.add(new ArgumentHolder(parameter, object));
}
public void add(final Class<?> type, Object object) {
CollectionUtils.filter(this.cache, new Predicate() {
public boolean evaluate(Object object) {
ArgumentHolder holder = (ArgumentHolder) object;
return !holder.match(type);
}
});
cache.add(new ArgumentHolder(type, object));
}
public void add(Class<?> genericType, Class<?>[] actualTypes, Object object) {
cache.add(new ArgumentHolder(genericType, actualTypes, object));
}
protected Object fetch(MethodParameter parameter) {
for (ArgumentHolder holder : cache) {
if (holder.match(parameter.getGenericParameterType())) {
return holder.getObject();
}
}
return ArgumentResolver.UNRESOLVED;
}
@Override
public String toString() {
return cache.toString();
}
}
public static class ArgumentHolder {
Class<?> type = null;
Class<?>[] actualArguments = null;
Object object;
public ArgumentHolder(Class<?> type, Object object) {
this.type = type;
this.object = object;
}
public ArgumentHolder(Class<?> genericType, Class<?>[] actualTypes, Object object) {
this.type = genericType;
this.actualArguments = actualTypes;
this.object = object;
}
protected ArgumentHolder(MethodParameter parameter, Object object) {
Type genericType = parameter.getGenericParameterType();
if (genericType instanceof Class<?>) {
this.type = (Class<?>) genericType;
} else {
ParameterizedTypeImpl parameterizedType = (ParameterizedTypeImpl) genericType;
this.type = parameterizedType.getRawType();
Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
this.actualArguments = new Class<?>[actualTypeArguments.length];
for (int i = 0; i < actualTypeArguments.length; i++) {
if (actualTypeArguments[i] instanceof Class<?>) {
this.actualArguments[i] = (Class<?>) actualTypeArguments[i];
} else {
throw new JibeRuntimeException("Nested generics are not supported");
}
}
}
this.object = object;
}
protected Object getObject() {
return object;
}
protected boolean match(Type parameterType) {
if (parameterType instanceof Class<?>) {
return !parameterType.equals(Object.class) && (((Class<?>) parameterType).isAssignableFrom(this.type));
} else {
ParameterizedTypeImpl parameterized = (ParameterizedTypeImpl) parameterType;
if (parameterized.getRawType().isAssignableFrom(this.type)) {
Type[] actualTypeArgumentsToTest = parameterized.getActualTypeArguments();
for (int i = 0; i < actualTypeArgumentsToTest.length; i++) {
if (actualTypeArgumentsToTest[i] instanceof Class<?>) {
if (!actualArguments[i].isAssignableFrom((Class<?>) actualTypeArgumentsToTest[i])) {
return false;
}
}
}
return true;
}
}
return false;
}
@Override
public String toString() {
return "ArgumentHolder [" + type.getName() + "]";
}
}
}