package io.myweb.processor;
import android.content.Context;
import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.base.Predicate;
import io.myweb.api.Before;
import io.myweb.http.Method;
import io.myweb.http.Request;
import io.myweb.http.Response;
import io.myweb.processor.model.ParsedFilter;
import io.myweb.processor.model.ParsedParam;
import io.myweb.processor.model.ServiceParam;
import org.apache.commons.lang3.StringUtils;
import org.json.JSONArray;
import org.json.JSONObject;
import javax.annotation.processing.Messager;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.VariableElement;
import java.io.InvalidObjectException;
import java.util.Collection;
import java.util.List;
import static com.google.common.collect.Collections2.transform;
import static com.google.common.collect.Iterables.filter;
import static com.google.common.collect.Iterables.size;
public class MyValidator extends AnnotationMessagerAware {
private final static String TAB = " ";
private static final String CONSOLE_COLOR_ORANGE = "\u001B[33m";
private static final String CONSOLE_COLOR_RESET = "\u001B[0m";
private static final String CONSOLE_COLOR_RED = "\u001B[31m";
private static final String[] TYPE_NAMES = {
String.class.getName(), "int", "long", "double", "float", "boolean",
Integer.class.getName(), Long.class.getName(), Float.class.getName(),
Double.class.getName(), Boolean.class.getName(), JSONObject.class.getName(),
JSONArray.class.getName(), Object.class.getName() // the Object is for json values
};
public MyValidator(Messager messager) {
super(messager);
}
public static boolean isTypeNameAllowed(String typeName) {
for (String t: TYPE_NAMES) {
if (t.equals(typeName)) return true;
}
return false;
}
public void validateAnnotation(Method httpMethod, String destMethodRetType, String destMethod, List<ParsedParam> params, String httpUri, ExecutableElement ee, AnnotationMirror am) {
int paramsInAnnotation = StringUtils.countMatches(httpUri, ":");
paramsInAnnotation += StringUtils.countMatches(httpUri, "*");
int paramsInMethod = size(filter(params, new Predicate<ParsedParam>() {
@Override
public boolean apply(ParsedParam param) {
return isTypeNameAllowed(param.getTypeName());
}
}));
if (paramsInAnnotation != paramsInMethod) {
error("This annotation requires different set of parameters for the method (details below)", ee, am);
error(buildErrorMsg(httpMethod, httpUri, destMethodRetType, destMethod, params));
throw new RuntimeException();
}
}
public ParsedFilter validateFilterAnnotation(String value, boolean isBefore, ExecutableElement ee) {
String destClass = ee.getEnclosingElement().toString();
String destMethod = ee.getSimpleName().toString();
// validate parameters and return type
String className = Response.class.getName();
if (isBefore) className = Request.class.getName();
String destMethodRetType = ee.getReturnType().toString();
List<? extends VariableElement> parameters = ee.getParameters();
if (!className.equals(destMethodRetType)) {
error(destMethod+"() method should return "+className, ee);
throw new RuntimeException();
}
if (parameters.size() != 1) {
error(destMethod+"() method has to have one parameter of type "+className, ee);
throw new RuntimeException();
}
String paramType = parameters.get(0).asType().toString().replaceFirst("class ", "");
if (!className.equals(paramType)) {
error(destMethod+"() method parameter is not of type "+className, parameters.get(0));
throw new RuntimeException();
}
return new ParsedFilter(value, destClass, destMethod, isBefore);
}
public ServiceParam validateBindServiceAnnotation(String value, List<ParsedParam> params, ExecutableElement ee, AnnotationMirror am) {
try {
String parameterName, componentName;
int idx = value.indexOf(":");
if (idx < 0) {
// no parameter name specified, try to guess by convention
componentName = value.trim();
parameterName = value.substring(value.lastIndexOf(".")+1);
parameterName = parameterName.substring(0,1).toLowerCase()+parameterName.substring(1);
} else {
componentName = value.substring(0, idx).trim();
parameterName = value.substring(idx).trim().substring(1);
}
if (componentName.contains("/")) { // parse
String[] parts = componentName.split("/");
if (parts.length != 2 || !isPackageName(parts[0]) ||
!parts[1].startsWith(".") || !isPackageName(parts[1].substring(1))) {
throw new RuntimeException("Invalid service name: " + componentName + ". Name in the form \"package.name/.ClassName\" is required.");
}
} else if (!isPackageName(componentName)) {
throw new RuntimeException("Invalid service class name: " + componentName);
}
// check for match in method parameters
boolean matched = false;
for (ParsedParam p : params) {
if (p.getName().equals(parameterName)) {
matched = true;
break;
}
}
if (!matched) {
throw new RuntimeException("@BindService parameter name \"" + parameterName + "\" does not match any parameter names of the method");
}
if (!componentName.contains(".")) { // no package
String packageName = ee.getEnclosingElement().toString();
packageName = packageName.substring(0, packageName.lastIndexOf(".")+1);
componentName = packageName + componentName;
}
return new ServiceParam(parameterName, componentName);
} catch (RuntimeException e) {
error(e.getMessage(), ee, am);
throw e;
}
}
private static boolean isPackageName(String name) {
String[] parts = name.split(".");
for (String part: parts) {
if (part.length() == 0) return false;
if (!part.matches("[^\\d+]\\w+")) return false;
}
return true;
}
private String buildErrorMsg(Method httpMethod, String httpUri, String destMethodRetType, String destMethod, List<ParsedParam> params) {
StringBuilder sb = new StringBuilder();
sb.append("\n");
appendAnnotation(sb, httpMethod, httpUri);
appendAnnotationUnderline(sb, httpMethod, httpUri, params);
appendMethod(sb, destMethodRetType, destMethod, params);
appendMethodUnderline(sb, destMethodRetType, destMethod, httpUri, params);
return sb.toString();
}
private void appendMethod(StringBuilder sb, String destMethodRetType, String destMethod, List<ParsedParam> params) {
sb.append(CONSOLE_COLOR_ORANGE);
sb.append(TAB);
sb.append("public ");
sb.append(simpleTypeName(destMethodRetType)).append(" ");
sb.append(destMethod);
sb.append("(");
appendMethodParams(sb, params);
sb.append(")");
sb.append(CONSOLE_COLOR_RESET);
sb.append("\n");
}
private String simpleTypeName(String destMethodRetType) {
int lastDotIndex = destMethodRetType.lastIndexOf(".");
if (lastDotIndex > 0) {
return destMethodRetType.substring(lastDotIndex + 1);
} else {
return destMethodRetType;
}
}
private void appendMethodParams(StringBuilder sb, List<ParsedParam> params) {
Collection<String> typesAndNames = transform(params, new Function<ParsedParam, String>() {
@Override
public String apply(ParsedParam pe) {
return simpleTypeName(pe.getTypeName()) + " " + pe.getName();
}
});
String paramsStr = Joiner.on(", ").join(typesAndNames);
sb.append(paramsStr);
}
private void appendAnnotation(StringBuilder sb, Method httpMethod, String httpUri) {
sb.append(CONSOLE_COLOR_ORANGE);
sb.append(TAB).append("@").append(httpMethod.toString());
sb.append("(\"").append(httpUri).append("\")");
sb.append(CONSOLE_COLOR_RESET);
sb.append("\n");
}
private void appendAnnotationUnderline(StringBuilder sb, Method httpMethod, String httpUri, List<ParsedParam> params) {
int beginOffset = 5 + httpMethod.toString().length() + 2; // TAB @NAME ("
appendSpaces(sb, beginOffset);
String[] uriSplit = httpUri.split("/");
for (String pathElem : uriSplit) {
if (pathElem.startsWith(":") || pathElem.startsWith("*")) {
String paramName = pathElem.substring(1);
if (isPathParamCorrect(paramName, params)) {
appendSpaces(sb, 2 + paramName.length()); // NAME + "/:"
} else {
appendUnderlineChars(sb, 1 + paramName.length()); // NAME + "/:"
appendSpaces(sb, 1); // NAME + "/:"
}
} else {
appendSpaces(sb, 1 + pathElem.length());
}
}
sb.append("\n");
}
private void appendMethodUnderline(StringBuilder sb, String destMethodRetType, String destMethod, String httpUri, List<ParsedParam> params) {
int beginOffset = 4 + "public ".length() + simpleTypeName(destMethodRetType).length() + 1 + destMethod.length() + 1;
appendSpaces(sb, beginOffset);
for (ParsedParam param : params) {
if (isMethodParamCorrect(httpUri, param)) {
int offset = simpleTypeName(param.getTypeName()).length() + 1; // TYPE AND SPACE
offset += param.getName().length() + 2;
appendSpaces(sb, offset); // NAME COMA SPACE
} else {
int offset = simpleTypeName(param.getTypeName()).length() + 1; // TYPE AND SPACE
offset += param.getName().length();
appendUnderlineChars(sb, offset); // NAME COMA
appendSpaces(sb, 2); // COMA SPACE
}
}
}
private boolean isMethodParamCorrect(String httpUri, ParsedParam param) {
String[] pathElems = httpUri.split("/");
boolean methodParamCorrect = false;
for (String pathElem : pathElems) {
if (pathElem.startsWith(":") || pathElem.startsWith("*")) {
String paramName = pathElem.substring(1);
if (paramName.equals(param.getName())) {
methodParamCorrect = true;
}
}
}
// TODO warn if more then one Context or Request
if (Context.class.getName().equals(param.getTypeName()) ||
Request.class.getName().equals(param.getTypeName())) {
methodParamCorrect = true;
}
return methodParamCorrect;
}
private void appendUnderlineChars(StringBuilder sb, int count) {
sb.append(CONSOLE_COLOR_RED);
for (int i = 0; i < count; i++) {
sb.append("^");
}
sb.append(CONSOLE_COLOR_RESET);
}
private void appendSpaces(StringBuilder sb, int count) {
for (int i = 0; i < count; i++) {
sb.append(" ");
}
}
private boolean isPathParamCorrect(String pathParam, List<ParsedParam> params) {
for (ParsedParam param : params) {
if (param.getName().equals(pathParam)) {
return true;
}
}
return false;
}
}