package org.webpieces.router.impl.loader;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicReference;
import javax.inject.Inject;
import javax.inject.Singleton;
import org.webpieces.router.api.BodyContentBinder;
import org.webpieces.router.api.RouterConfig;
import org.webpieces.router.api.actions.Action;
import org.webpieces.router.api.actions.Redirect;
import org.webpieces.router.api.dto.MethodMeta;
import org.webpieces.router.api.dto.RouteType;
import org.webpieces.router.api.routing.Param;
import org.webpieces.router.api.routing.RouteFilter;
import org.webpieces.router.impl.ChainFilters;
import org.webpieces.router.impl.RouteMeta;
import org.webpieces.router.impl.body.BodyParsers;
import org.webpieces.router.impl.params.ParamToObjectTranslatorImpl;
import org.webpieces.util.filters.Service;
@Singleton
public class MetaLoader {
private ParamToObjectTranslatorImpl translator;
private Set<BodyContentBinder> bodyBinderPlugins;
private List<String> allBinderAnnotations = new ArrayList<>();
private BodyParsers requestBodyParsers;
private RouterConfig config;
@Inject
public MetaLoader(ParamToObjectTranslatorImpl translator, BodyParsers requestBodyParsers, RouterConfig config) {
this.translator = translator;
this.requestBodyParsers = requestBodyParsers;
this.config = config;
}
public void loadInstIntoMeta(RouteMeta meta, Object controllerInst, String methodStr) {
Method[] methods = controllerInst.getClass().getMethods();
List<Method> matches = new ArrayList<>();
for(Method m : methods) {
if(m.getName().equals(methodStr))
matches.add(m);
}
String controllerStr = controllerInst.getClass().getSimpleName();
if(matches.size() == 0)
throw new IllegalArgumentException("Invalid Route. Cannot find 'public' method='"+methodStr+"' on class="+controllerStr);
else if(matches.size() > 1)
throw new UnsupportedOperationException("You have more than one 'public' method named="+methodStr+" on class="+controllerStr+" This is not yet supported until we support method parameters(let us know you hit this and we will immediately implement)");
Method controllerMethod = matches.get(0);
Parameter[] parameters = controllerMethod.getParameters();
List<String> paramNames = new ArrayList<>();
for(Parameter p : parameters) {
String value;
String name = p.getName();
if(matchesBadName(name)) {
Param annotation = p.getAnnotation(Param.class);
if(annotation == null)
throw new IllegalArgumentException("Method='"+controllerMethod+"' has to have every argument annotated with @Param(paramName) since\n"
+ "you are not compiling with -parameters to enable the param names to be built into the *.class files. Most likely, you "
+ "changed the build.gradle we generated or switched to a different build system and did not enable this compiler option");
value = annotation.value();
} else {
//use the param name in the method...
value = name;
}
paramNames.add(value);
}
if(meta.getRoute().getRouteType() == RouteType.HTML) {
preconditionCheck(meta, controllerMethod);
} else if (meta.getRoute().getRouteType() == RouteType.CONTENT) {
BodyContentBinder binder = contentPreconditionCheck(meta, controllerMethod, parameters);
meta.setContentBinder(binder);
}
meta.setMethodParamNames(paramNames);
meta.setControllerInstance(controllerInst);
meta.setMethod(controllerMethod);
}
private BodyContentBinder contentPreconditionCheck(RouteMeta meta, Method controllerMethod, Parameter[] parameters) {
List<String> binderMatches = new ArrayList<>();
AtomicReference<BodyContentBinder> lastMatch = new AtomicReference<BodyContentBinder>(null);
for(BodyContentBinder binder : bodyBinderPlugins) {
for(Parameter p : parameters) {
Annotation[] annotations = p.getAnnotations();
Class<?> entityClass = p.getType();
recordParameterMatches(lastMatch, binderMatches, binder, annotations, entityClass);
}
Annotation[] annotations = controllerMethod.getAnnotations();
recordParameterMatches(lastMatch, binderMatches, binder, annotations, null);
}
Class<?> returnType = controllerMethod.getReturnType();
if(Action.class.isAssignableFrom(returnType))
throw new IllegalArgumentException("The method for content route="+meta.getRoute().getFullPath()+" is returning Action and this is not allowed. method="+controllerMethod);
if(binderMatches.size() == 0)
throw new IllegalArgumentException("there was not a single method parameter annotated with a Plugin"
+ " annotation on method="+controllerMethod+". looking at your\n"
+ "plugins, these are the annotations available="+allBinderAnnotations+"\n"
+ "You either need one parameter with one of the annotations OR\n"
+ "you need to annotata the method(if it is read only and no request is supplied)");
else if(binderMatches.size() > 1)
throw new IllegalArgumentException("there is more than one parameter with a Plugin"
+ " annotation on method="+controllerMethod+". These\n"
+ "are the ones we found(please delete one)="+binderMatches+"\n"
+ "Also make sure one parameter OR the method has the annotation, not both");
return lastMatch.get();
}
private void recordParameterMatches(AtomicReference<BodyContentBinder> lastMatch, List<String> binderMatches, BodyContentBinder binder, Annotation[] annotations, Class<?> entityClass) {
for(Annotation a : annotations) {
if(binder.isManaged(entityClass, a.annotationType())) {
binderMatches.add("@"+binder.getAnnotation().getSimpleName());
lastMatch.set(binder);
}
}
}
private void preconditionCheck(RouteMeta meta, Method controllerMethod) {
if(meta.getRoute().isPostOnly()) {
Class<?> clazz = controllerMethod.getReturnType();
if(CompletableFuture.class.isAssignableFrom(clazz)) {
Type genericReturnType = controllerMethod.getGenericReturnType();
ParameterizedType t = (ParameterizedType) genericReturnType;
Type type2 = t.getActualTypeArguments()[0];
if(!(type2 instanceof Class))
throw new IllegalArgumentException("Since this route="+meta+" is for POST, the method MUST return a type 'Redirect' or 'CompletableFuture<Redirect>' for this method="+controllerMethod);
@SuppressWarnings("rawtypes")
Class<?> type = (Class) type2;
if(!Redirect.class.isAssignableFrom(type))
throw new IllegalArgumentException("Since this route="+meta+" is for POST, the method MUST return a type 'Redirect' or 'CompletableFuture<Redirect>' not 'CompletableFuture<"+type.getSimpleName()+">'for this method="+controllerMethod);
} else if(!Redirect.class.isAssignableFrom(clazz))
throw new IllegalArgumentException("Since this route="+meta+" is for POST, the method MUST return a type 'Redirect' or 'CompletableFuture<Redirect>' not '"+clazz.getSimpleName()+"' for this method="+controllerMethod);
} else {
Class<?> clazz = controllerMethod.getReturnType();
if(CompletableFuture.class.isAssignableFrom(clazz)) {
Type genericReturnType = controllerMethod.getGenericReturnType();
ParameterizedType t = (ParameterizedType) genericReturnType;
Type type2 = t.getActualTypeArguments()[0];
if(!(type2 instanceof Class))
throw new IllegalArgumentException("This route="+meta+" has a method that MUST return a type 'Action' or 'CompletableFuture<Action>' for this method(and did not)="+controllerMethod);
@SuppressWarnings("rawtypes")
Class<?> type = (Class) type2;
if(!Action.class.isAssignableFrom(type))
throw new IllegalArgumentException("This route="+meta+" has a method that MUST return a type 'Action' or 'CompletableFuture<Action>' not 'CompletableFuture<"+type.getSimpleName()+">'for this method="+controllerMethod);
} else if(!Action.class.isAssignableFrom(clazz))
throw new IllegalArgumentException("This route="+meta+" has a method that MUST return a type 'Action' or 'CompletableFuture<Action>' not '"+clazz.getSimpleName()+"' for this method="+controllerMethod);
}
}
/**
* Specifially checks for param names named 'arg{number}' which is a compiler generated name when you don't use the -parameter compiler option
* @param name
* @return
*/
private boolean matchesBadName(String name) {
if(!name.startsWith("arg"))
return false;
String substring = name.substring(3);
//shoudl be a number
try {
Integer.parseInt(substring);
} catch(NumberFormatException e) {
return false; //who named their param argxxxxx...maybe some words are named like that
}
return true;
}
public Service<MethodMeta, Action> loadFilters(List<RouteFilter<?>> filters) {
Service<MethodMeta, Action> svc = new ServiceProxy(translator, requestBodyParsers, config);
for(RouteFilter<?> f : filters) {
svc = ChainFilters.addOnTop(svc, f);
}
return svc;
}
public void install(Set<BodyContentBinder> bodyBinders) {
this.bodyBinderPlugins = bodyBinders;
for(BodyContentBinder binder : bodyBinders) {
allBinderAnnotations.add("@"+binder.getAnnotation().getSimpleName());
}
}
}