package de.bitdroid.jaxrs2retrofit;
import com.squareup.javapoet.AnnotationSpec;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.JavaFile;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.ParameterSpec;
import com.squareup.javapoet.ParameterizedTypeName;
import com.squareup.javapoet.TypeName;
import com.squareup.javapoet.TypeSpec;
import com.thoughtworks.qdox.builder.impl.EvaluatingVisitor;
import com.thoughtworks.qdox.model.JavaAnnotation;
import com.thoughtworks.qdox.model.JavaClass;
import com.thoughtworks.qdox.model.JavaMethod;
import com.thoughtworks.qdox.model.JavaParameter;
import com.thoughtworks.qdox.model.JavaParameterizedType;
import com.thoughtworks.qdox.model.JavaType;
import com.thoughtworks.qdox.model.expression.Add;
import com.thoughtworks.qdox.model.expression.AnnotationValue;
import java.text.SimpleDateFormat;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.lang.model.element.Modifier;
import javax.ws.rs.Consumes;
import javax.ws.rs.Path;
import javax.ws.rs.core.MediaType;
import de.bitdroid.jaxrs2retrofit.converter.AnnotatedParam;
import de.bitdroid.jaxrs2retrofit.converter.ParamConverter;
import retrofit.client.Response;
import retrofit.http.Headers;
public final class RetrofitGenerator {
private static final SimpleDateFormat dateFormat = new SimpleDateFormat("dd.MM.yyyy 'at' HH:mm");
private final GeneratorSettings settings;
private final Date currentDate;
public RetrofitGenerator(GeneratorSettings settings) {
this.settings = settings;
this.currentDate = new Date();
}
public JavaFile createResource(JavaClass jaxRsClass) {
// find path annotation
JavaAnnotation jaxRsPath = null;
JavaAnnotation jaxRsConsumes = null;
for (JavaAnnotation annotation : jaxRsClass.getAnnotations()) {
String annotationType = annotation.getType().getFullyQualifiedName();
if (annotationType.equals(Path.class.getName())) {
jaxRsPath = annotation;
} else if (annotationType.equals(Consumes.class.getName())) {
jaxRsConsumes = annotation;
}
}
if (jaxRsPath == null) return null; // no a valid JAX RS resource
if (jaxRsClass.getName().matches(settings.getExcludedClassNamesRegex())) return null;
System.out.println(jaxRsClass.getName());
TypeSpec.Builder retrofitResourceBuilder = TypeSpec
.interfaceBuilder(jaxRsClass.getName())
.addModifiers(Modifier.PUBLIC);
addAboutJavadoc(retrofitResourceBuilder);
for (JavaMethod jaxRsMethod : jaxRsClass.getMethods()) {
Collection<MethodSpec> retrofitMethods = createMethod(jaxRsClass, jaxRsMethod, jaxRsPath, jaxRsConsumes);
if (retrofitMethods != null) {
for (MethodSpec method : retrofitMethods) {
retrofitResourceBuilder.addMethod(method);
}
}
}
return JavaFile.builder(settings.getPackageName(), retrofitResourceBuilder.build()).build();
}
private void addAboutJavadoc(TypeSpec.Builder retrofitResourceBuilder) {
StringBuilder aboutBuilder = new StringBuilder();
aboutBuilder
.append("This file was generated by ")
.append("<a href=\"https://github.com/Maddoc42/JaxRs2Retrofit\">JaxRs2Retrofit</a> on ")
.append(dateFormat.format(currentDate))
.append(".\n");
retrofitResourceBuilder.addJavadoc(aboutBuilder.toString());
}
private Collection<MethodSpec> createMethod(
JavaClass jaxRsClass,
JavaMethod jaxRsMethod,
JavaAnnotation jaxRsPath,
JavaAnnotation jaxRsConsumes) {
RetrofitMethodBuilder retrofitMethodBuilder = new RetrofitMethodBuilder(
jaxRsMethod.getName(),
settings);
// find method type and path
JavaAnnotation jaxRsMethodPath = null;
HttpMethod httpMethod = null;
for (JavaAnnotation annotation : jaxRsMethod.getAnnotations()) {
String annotationType = annotation.getType().getFullyQualifiedName();
if (annotationType.equals(Path.class.getName())) {
jaxRsMethodPath = annotation;
} else if (annotationType.equals(Consumes.class.getName())) {
jaxRsConsumes = annotation;
} else if (httpMethod == null) {
httpMethod = HttpMethod.forJaxRsClassName(annotation.getType().getFullyQualifiedName());
}
}
if (httpMethod == null) return null; // not a valid resource method
EvaluatingVisitor evaluatingVisitor = new SimpleEvaluatingVisitor(jaxRsClass);
// add path
retrofitMethodBuilder.addAnnotation(createPathAnnotation(evaluatingVisitor, httpMethod, jaxRsPath, jaxRsMethodPath));
// add content type
if (jaxRsConsumes != null) {
retrofitMethodBuilder.addAnnotation(createContentTypeAnnotation(evaluatingVisitor, jaxRsConsumes));
}
// create parameters
for (JavaParameter jaxRsParameter : jaxRsMethod.getParameters()) {
ParameterSpec spec = createParameter(jaxRsParameter);
if (spec != null) retrofitMethodBuilder.addParameter(spec);
}
// create return type
TypeName retrofitReturnType = createType(jaxRsMethod.getReturnType());
if (retrofitReturnType.equals(TypeName.VOID)) {
retrofitReturnType = ClassName.get(Response.class);
}
retrofitMethodBuilder.setReturnType(retrofitReturnType);
return retrofitMethodBuilder.build().values();
}
private ParameterSpec createParameter(
JavaParameter jaxRsParameter) {
// find first annotation which can be converted, others are ignored
JavaAnnotation jaxRsAnnotation = null;
ClassName annotationType = null;
TypeName paramType = createType(jaxRsParameter.getJavaClass());
for (JavaAnnotation annotation : jaxRsParameter.getAnnotations()) {
annotationType = (ClassName) createType(annotation.getType());
jaxRsAnnotation = annotation;
if (settings.getParamConverterManager().hasConverter(annotationType)) break;
}
// find suitable converter
ParamConverter converter = settings.getParamConverterManager().getConverter(annotationType);
// if no converter was found, ignore annotation
if (converter == null) {
annotationType = ClassName.get(Void.class);
converter = settings.getParamConverterManager().getConverter(annotationType);
}
// if still no converted is found, ignore parameter completely (should (TM) only happen when
// void converter has been explicitly removed
if (converter == null) return null;
// convert annotation and param
AnnotatedParam param = new AnnotatedParam(
paramType,
annotationType,
(jaxRsAnnotation == null) ? new HashMap<String, Object>() : jaxRsAnnotation.getNamedParameterMap());
AnnotatedParam convertedParam = converter.convert(param);
if (convertedParam == null) return null;
// create code
ParameterSpec.Builder retrofitParamBuilder = ParameterSpec
.builder(convertedParam.getParamType(), jaxRsParameter.getName());
AnnotationSpec.Builder retrofitParamAnnotationBuilder = AnnotationSpec
.builder(convertedParam.getAnnotationType());
if (jaxRsAnnotation != null) {
for (Map.Entry<String, Object> entry : convertedParam.getAnnotationParameterMap().entrySet()) {
retrofitParamAnnotationBuilder.addMember(entry.getKey(), entry.getValue().toString());
}
}
retrofitParamBuilder.addAnnotation(retrofitParamAnnotationBuilder.build());
return retrofitParamBuilder.build();
}
/**
* Regex for getting the indiviual parts of an JaxRs URL, explained from outer to most inner parts:
* \\{? ... \\}? outer { } in case of path variables
* ( ... )(: ....)? path followed by an optional JaxRx regex
* [a-zA-z0-9-_.] path regex (slightly limited to reduce complexity)
* [^\\{\\}]* JaxRs regex
*/
private final Pattern pathRegexPattern = Pattern.compile("\\{?([a-zA-z0-9-_.]+)(:[^\\{\\}]*)?\\}?");
private AnnotationSpec createPathAnnotation(
EvaluatingVisitor evaluatingVisitor,
HttpMethod method,
JavaAnnotation classPath,
JavaAnnotation methodPath) {
AnnotationValue pathExpression = classPath.getProperty("value");
if (methodPath != null) {
pathExpression = new Add(pathExpression, methodPath.getProperty("value"));
}
String value = pathExpression.accept(evaluatingVisitor).toString();
Matcher matcher = pathRegexPattern.matcher(value);
StringBuilder regexFreeValue = new StringBuilder();
while (matcher.find()) {
regexFreeValue.append("/");
String regexValue = matcher.group(0);
if (regexValue.startsWith("{")) regexFreeValue
.append("{")
.append(matcher.group(1))
.append("}");
else regexFreeValue.append(matcher.group(1));
}
return AnnotationSpec.builder(method.getRetrofitClass())
.addMember("value", "\"" + regexFreeValue.toString() + "\"")
.build();
}
private AnnotationSpec createContentTypeAnnotation(
EvaluatingVisitor evaluatingVisitor,
JavaAnnotation consumesAnnotation) {
AnnotationValue annotationValue = consumesAnnotation.getProperty("value");
String stringAnnotationValue = annotationValue.getParameterValue().toString();
String value = null;
if (stringAnnotationValue.startsWith(MediaType.class.getSimpleName() + ".")) {
String[] token = stringAnnotationValue.split("\\.");
try {
value = (String) MediaType.class.getDeclaredField(token[1]).get(null);
} catch (Exception e) {
e.printStackTrace(System.err);
}
} else {
value = consumesAnnotation.getProperty("value").accept(evaluatingVisitor).toString();
}
return AnnotationSpec.builder(Headers.class)
.addMember("value", "\"Content-type: " + value + "\"")
.build();
}
private TypeName createType(JavaType jaxRsType) {
if (void.class.getName().equals(jaxRsType.getFullyQualifiedName())) {
return TypeName.VOID;
} else if (boolean.class.getName().equals(jaxRsType.getFullyQualifiedName())) {
return TypeName.BOOLEAN;
} else if (int.class.getName().equals(jaxRsType.getFullyQualifiedName())) {
return TypeName.INT;
} else if (float.class.getName().equals(jaxRsType.getFullyQualifiedName())) {
return TypeName.FLOAT;
} else if (double.class.getName().equals(jaxRsType.getFullyQualifiedName())) {
return TypeName.DOUBLE;
} else if (short.class.getName().equals(jaxRsType.getFullyQualifiedName())) {
return TypeName.SHORT;
} else if (long.class.getName().equals(jaxRsType.getFullyQualifiedName())) {
return TypeName.LONG;
} else if (char.class.getName().equals(jaxRsType.getFullyQualifiedName())) {
return TypeName.CHAR;
} else if (byte.class.getName().equals(jaxRsType.getFullyQualifiedName())) {
return TypeName.BYTE;
// map jaxrs response objects to retrofit ones
} else if (javax.ws.rs.core.Response.class.getName().equals(jaxRsType.getFullyQualifiedName())) {
return ClassName.get(Response.class);
} else if (jaxRsType instanceof JavaParameterizedType) {
JavaParameterizedType parametrizedType = (JavaParameterizedType) jaxRsType;
if (parametrizedType.getActualTypeArguments().size() == 0) {
return ClassName.bestGuess(jaxRsType.getFullyQualifiedName());
}
ClassName outerType = ClassName.bestGuess(parametrizedType.getFullyQualifiedName());
TypeName[] paramTypes = new TypeName[parametrizedType.getActualTypeArguments().size()];
for (int i = 0; i < paramTypes.length; ++i) {
paramTypes[i] = ClassName.bestGuess(parametrizedType.getActualTypeArguments().get(i).getFullyQualifiedName());
}
return ParameterizedTypeName.get(outerType, paramTypes);
} else {
return ClassName.bestGuess(jaxRsType.getFullyQualifiedName());
}
}
}