/**
*
*/
package cn.bran.play.routing;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.HEAD;
import javax.ws.rs.OPTIONS;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import cn.bran.japid.util.StringUtils;
public class RouterMethod {
private boolean autoRouting;
private String withExtension = ""; // the artificial url extension such as .html
List<Annotation> httpMethodAnnotations = new ArrayList<Annotation>();
Method meth;
String pathSpec;
Pattern pathSpecPattern;
public Pattern valueExtractionPattern;
String produce;
String[] consumeTypes = new String[] {};
List<ParamSpec> paramSpecList = new ArrayList<ParamSpec>();
/**
*
* no method annotation means taking "any"
*
* @param m
*/
public RouterMethod(Method m, String pathPrefix) {
Annotation[] annotations = m.getAnnotations();
for (Annotation a : annotations) {
if (a instanceof GET || a instanceof POST || a instanceof PUT || a instanceof DELETE || a instanceof HEAD
|| a instanceof OPTIONS)
httpMethodAnnotations.add(a);
}
meth = m;
Consumes consumes = m.getAnnotation(Consumes.class);
if (consumes != null) {
consumeTypes = consumes.value();
}
OptionalExt artExt = m.getAnnotation(OptionalExt.class);
if (artExt != null)
this.withExtension = artExt.value();
Annotation[][] parameterAnnotations = m.getParameterAnnotations();
Class<?>[] paramTypes = m.getParameterTypes();
// now parse the path spec
Path p = m.getAnnotation(Path.class);
if (p != null && p.value().length() > 0) {
pathSpec = pathPrefix + JaxrsRouter.prefixSlash(p.value());
pathSpecPattern = Pattern.compile(pathSpec.replaceAll(JaxrsRouter.urlParamCapture, "\\\\{(.*)\\\\}"));
if (pathSpec.contains("{") && pathSpec.contains("}")) {
List<RegMatch> rootParamNameMatches = RegMatch.findAllMatchesIn(pathSpecPattern, pathSpec);
List<String> rootParamNames = new ArrayList<String>();
for (RegMatch rm : rootParamNameMatches) {
rootParamNames.addAll(rm.subgroups);
}
for (String s : rootParamNames) {
if (s != null)
paramSpecList.add(new ParamSpec(s));
}
if (parameterAnnotations.length < paramSpecList.size()) {
throw new RuntimeException(
"param number does not match that of the param captures in the path annotation pattern");
}
int pc = 0;
for (Annotation[] paramAnnos : parameterAnnotations) {
if (paramAnnos.length == 0 && !autoRouting) {
throw new RuntimeException(
"in none-auto-routing mode: no capturing annotations (@PathParam/@QueryParam) for the parameter at the method position: "
+ m.getName() + ":" + pc);
}
boolean hasCapture = false;
for (Annotation ann : paramAnnos) {
if (ann instanceof PathParam) {
String pname = ((PathParam) ann).value();
boolean hasName = false;
// fill the path param type
for (ParamSpec pspec : paramSpecList) {
if (pspec.name.equals(pname)) {
Class<?> type = paramTypes[pc];
pspec.type = type;
hasName = true;
hasCapture = true;
break;
}
}
if (!hasName) {
throw new RuntimeException(
"no capturing spec on the Path annotation for the parameter at the method position: "
+ m.getName() + ":" + pc);
}
} else if (ann instanceof QueryParam) {
hasCapture = true;
} else {
// hmm something unknown
}
}
if (!hasCapture) {
throw new RuntimeException(
"no capturing annotations(@PathParam/@QueryParam) for the parameter at the method position: "
+ m.getName() + ":" + pc);
}
pc++;
}
// check that all path param has been set up with proper types
for (ParamSpec pspec : paramSpecList) {
if (pspec.type == null) {
throw new RuntimeException("cannot match the path param with the parameter list of method: "
+ this.meth);
}
}
}
} else {
// auto-routing mechanism:
// 1. use method name as the first part
this.autoRouting = true;
pathSpec = pathPrefix + "\\." + m.getName();
int pos = 0; // path param position
int ppos = 0; // natural parameter
for (Annotation[] pa : parameterAnnotations) {
boolean isQueryParam = false;
for (Annotation a : pa) {
if (a instanceof PathParam) {
error("cannot take @PathParam when no @Path specified to the method: " + m.getName());
} else if (a instanceof QueryParam) {
isQueryParam = true;
break;
} else {
}
}
if (!isQueryParam) {
// decorate a PathParam
String s = "_" + pos++;
ParamSpec ps = new ParamSpec(s);
ps.type = paramTypes[ppos];
paramSpecList.add(ps);
pathSpec += "/" + "{" + s + "}";
}
ppos++;
}
pathSpecPattern = Pattern.compile(pathSpec.replaceAll(JaxrsRouter.urlParamCapture, "\\\\{(.*)\\\\}"));
}
valueExtractionPattern = Pattern.compile(pathSpec.replaceAll(JaxrsRouter.urlParamCapture, "(.*)"));
Produces produces = m.getAnnotation(Produces.class);
if (produces == null)
produce = null;
else if (produces.value().length != 1) {
throw new RuntimeException("Currently the PRODUCES annotation can only take one type of result.");
} else {
produce = produces.value()[0];
}
}
/**
* @author Bing Ran (bing.ran@gmail.com)
* @param string
*/
private static void error(String string) {
throw new RuntimeException(string);
}
public Object[] extractArguments(play.mvc.Http.RequestHeader r) {
String path = r.path();
if (path.endsWith(withExtension))
path = path.substring(0, path.lastIndexOf(withExtension));
List<RegMatch> rootParamValueMatches = RegMatch.findAllMatchesIn(valueExtractionPattern, path);
List<String> rootParamValues = new ArrayList<String>();
for (RegMatch rm : rootParamValueMatches) {
rootParamValues.addAll(rm.subgroups);
}
if (rootParamValues.size() != paramSpecList.size()) {
throw new RuntimeException("param spec number does not match that from URI capturing. Spec contains: "
+ paramSpecList.size() + " while the URI contains: " + rootParamValues.size() + ". The route entry is: " + this.toString());
}
Map<String, Object> args = new java.util.HashMap<String, Object>();
int c = 0;
for (ParamSpec paramSpec : paramSpecList) {
String name = paramSpec.name;
String value = rootParamValues.get(c++);
if (!paramSpec.formatPattern.matcher(value).matches()) {
throw new IllegalArgumentException("format mismatch for : (" + name + ")" + value
+ ". The route entry is: " + this.toString());
}
Class<?> type = paramSpec.type;
Object val = convertArgType(c, name, value, type);
args.put(name, val);
}
//
Object[] argValues = new Object[0];
List<Object> argVals = new ArrayList<Object>();
Annotation[][] annos = meth.getParameterAnnotations();
c = 0;
int pos = 0;
for (Annotation[] ans : annos) {
PathParam pathParam = null;
QueryParam queryParam = null;
for (Annotation an : ans) {
if (an instanceof PathParam)
pathParam = (PathParam) an;
else if (an instanceof QueryParam)
queryParam = (QueryParam) an;
}
if (pathParam != null) {
Object v = args.get(pathParam.value());
if (v != null)
argVals.add(v);
else
throw new IllegalArgumentException("can not find annotation value for argument "
+ pathParam.value() + "in " + meth.getDeclaringClass() + "#" + meth);
} else if (queryParam != null) {
String name = queryParam.value();
String queryString = r.getQueryString(name); // XXX should this
// be of
// String[]?
argVals.add(convertArgType(c, name, queryString, meth.getParameterTypes()[c]));
} else if (autoRouting) {
Object v = args.get("_" + pos++);
if (v != null)
argVals.add(v);
else
throw new IllegalArgumentException("can not find value for param No. " + c + " in "
+ meth.getDeclaringClass() + "#" + meth);
} else
throw new IllegalArgumentException("can not find how to map the value for an argument for method:"
+ meth.getDeclaringClass() + "#" + meth + ". The parameter position is(0-based): " + c);
c++;
}
argValues = argVals.toArray(argValues);
return argValues;
}
private Object convertArgType(int c, String name, String value, Class<?> type) {
Object val = null;
if (type == Boolean.class || type == boolean.class) {
val = Boolean.valueOf(value);
} else if (type == Byte.class || type == byte.class) {
val = Byte.valueOf(value);
} else if (type == Character.class || type == char.class) {
if (value.length() != 1) {
throw new IllegalArgumentException("cannot convert to a character: (" + name + ")" + value);
}
val = value.charAt(0);
} else if (type == Double.class || type == double.class) {
val = Double.valueOf(value);
} else if (type == Float.class || type == float.class) {
val = Float.valueOf(value);
} else if (type == Integer.class || type == int.class) {
val = Integer.valueOf(value);
} else if (type == Long.class || type == long.class) {
val = Long.valueOf(value);
} else if (type == Short.class || type == short.class) {
val = Short.valueOf(value);
} else if (type == String.class) {
// try {
// val = URLDecoder.decode(value, "UTF-8");// not necessary. Seems
// already decoded
val = value;
// } catch (UnsupportedEncodingException e) {
// e.printStackTrace();
// }
} else {
throw new RuntimeException(
"this version supports capturing primitive parameters, their object wrappers or strings. This param is not of primitive type: "
+ meth.getName() + ":" + c + "(0-based)");
}
return val;
}
/**
* @author Bing Ran (bing.ran@gmail.com)
* @param contentType
* @return
*/
public boolean containsConsumeType(String contentType) {
if (consumeTypes.length == 0) {
return true;
} else {
for (String c : consumeTypes) {
if (c.equals(contentType))
return true;
}
}
return false;
}
/**
* @author Bing Ran (bing.ran@gmail.com)
* @param uri
* @return
*/
public boolean matchURI(String uri) {
if (pathSpec.equals(uri))
return true;
else
return valueExtractionPattern.matcher(uri).matches();
}
public boolean supportHttpMethod(String ms) {
Class<? extends Annotation> httpMethodClass = RouterUtils.findHttpMethodAnnotation(ms.toUpperCase());
if (httpMethodAnnotations.size() == 0)
return true; // take any
for (Annotation a : httpMethodAnnotations) {
if (httpMethodClass.isInstance(a))
return true;
}
return false;
}
@Override
public String toString() {
String meths = getVerb();
String path = getPath();
String action = getAction();
return meths + " \t" + path + "\t -> \t" + action;
}
private String getAction() {
return meth.getDeclaringClass().getCanonicalName() + "." + meth.getName() + "(" + getParamListString() + ")" + (produce != null? " : " + produce : "");
}
private String getPath() {
return pathSpec.replaceAll("\\\\", "") + withExtension;
}
private String getVerb() {
String meths = "";
for (Annotation an : httpMethodAnnotations) {
String string = an.toString();
// clean it
for (int i = 0; i < string.length(); i++) {
if (Character.isUpperCase(string.charAt(i)))
meths += string.charAt(i);
}
meths += "|";
}
if (meths.endsWith("|"))
meths = meths.substring(0, meths.length() - 1);
else
meths = "*";
return meths;
}
private String getParamListString() {
return StringUtils.join(paramSpecList, ",");
}
/**
* @author Bing Ran (bing.ran@gmail.com)
* @return
*/
public RouteEntry getRouteEntry() {
return new RouteEntry(getVerb(), getPath(), getAction());
}
}