Skip to content

Instantly share code, notes, and snippets.

@rafakob
Created September 23, 2016 10:41
Show Gist options
  • Save rafakob/a4d3f6eddcf4c09f3fe45104641b23c8 to your computer and use it in GitHub Desktop.
Save rafakob/a4d3f6eddcf4c09f3fe45104641b23c8 to your computer and use it in GitHub Desktop.
JavaPoet
public class AllApis {
private final List<InterfaceElement> apis;
private final Filer filer;
private final Elements elements;
public AllApis(List<InterfaceElement> apis, Filer filer, Elements elements) {
this.apis = apis;
this.filer = filer;
this.elements = elements;
}
public void generate() {
TypeSpec.Builder classBuilder = TypeSpec.classBuilder("SynoApi").addModifiers(Modifier.PUBLIC);
classBuilder.addField(FieldSpec.builder(TypeName.get(Retrofit.class), "retrofit", Modifier.PRIVATE, Modifier.FINAL).build());
classBuilder.addMethod(MethodSpec.constructorBuilder().addParameter(TypeName.get(Retrofit.class), "retrofit").addModifiers(Modifier.PUBLIC)
.addStatement("this.$N = $N", "retrofit", "retrofit")
.build());
for (InterfaceElement api : apis) {
classBuilder.addField(createField(api));
classBuilder.addMethod(createMethod(api));
}
saveGeneratedFile(classBuilder.build());
}
private FieldSpec createField(InterfaceElement api) {
return FieldSpec.builder(ClassName.bestGuess(api.IMPL_NAME), "m" + api.IMPL_NAME)
.addModifiers(Modifier.PRIVATE)
.build();
}
private MethodSpec createMethod(InterfaceElement api) {
MethodSpec.Builder builder = MethodSpec.methodBuilder(Character.toLowerCase(api.name.charAt(0)) + api.name.substring(1))
.addModifiers(Modifier.PUBLIC)
.addStatement("if($N == null) $N = new $N(retrofit)", "m" + api.IMPL_NAME, "m" + api.IMPL_NAME, api.IMPL_NAME)
.addStatement("return $N", "m" + api.IMPL_NAME)
.returns(ClassName.bestGuess(api.IMPL_NAME));
return builder.build();
}
private void saveGeneratedFile(TypeSpec typeSpec) {
if (apis.isEmpty()) return;
PackageElement pkg = elements.getPackageOf(apis.get(0).typeElement);
String packageName = pkg.isUnnamed() ? null : pkg.getQualifiedName().toString();
try {
JavaFile.builder(getPackageName(), typeSpec).build().writeTo(filer);
} catch (IOException e) {
e.printStackTrace();
}
}
private String getPackageName() {
if (apis.isEmpty()) return null;
PackageElement pkg = elements.getPackageOf(apis.get(0).typeElement);
return pkg.isUnnamed() ? null : pkg.getQualifiedName().toString();
}
}
public class InterfaceElement {
public final String INTERFACE_NAME;
public final String IMPL_NAME;
public final String name;
public final TypeElement typeElement;
public final List<ExecutableElement> postMethods;
public final List<ExecutableElement> getMethods;
public final String apiName;
public final String apiPath;
public final int apiVersion;
private final Filer filer;
private final Elements elements;
public InterfaceElement(TypeElement typeElement, Filer filer, Elements elements) {
this.getMethods = new ArrayList<>();
this.postMethods = new ArrayList<>();
this.typeElement = typeElement;
this.name = typeElement.getSimpleName().toString();
final SynoApiDef annotation = typeElement.getAnnotation(SynoApiDef.class);
this.apiName = annotation.name();
this.apiPath = annotation.path();
this.apiVersion = annotation.version();
this.filer = filer;
this.elements = elements;
INTERFACE_NAME = "Generated" + name;
IMPL_NAME = "Generated" + name + "Impl";
}
public void generate() {
saveGeneratedFile(createRetrofitInterface());
saveGeneratedFile(createRetrofitImplementation());
}
/**
* Creates Retrofit interface. "Generated{ClassName}"
*/
private TypeSpec createRetrofitInterface() {
TypeSpec.Builder interfaceBuilder = TypeSpec.interfaceBuilder(INTERFACE_NAME).addModifiers(Modifier.PUBLIC);
for (ExecutableElement executableElement : getMethods)
interfaceBuilder.addMethod(createRetrofitMethod(executableElement, GET.class, Query.class, executableElement.getAnnotation(SynoApiGET.class).value(), executableElement.getAnnotation(SynoApiGET.class).wrap()));
for (ExecutableElement executableElement : postMethods)
interfaceBuilder.addMethod(createRetrofitMethod(executableElement, POST.class, Body.class, executableElement.getAnnotation(SynoApiPOST.class).value(), executableElement.getAnnotation(SynoApiPOST.class).wrap()));
return interfaceBuilder.build();
}
/**
* Creates Retrofit implementation class which contains modified observables (auto added observeOn, subscribeOn etc.). "Generated{ClassName}Impl"
*/
private TypeSpec createRetrofitImplementation() {
TypeSpec.Builder classBuilder = TypeSpec.classBuilder(IMPL_NAME).addModifiers(Modifier.PUBLIC);
classBuilder.addField(FieldSpec.builder(ClassName.get(getPackageName(), INTERFACE_NAME), "api", Modifier.PRIVATE, Modifier.FINAL).build());
classBuilder.addMethod(MethodSpec.constructorBuilder().addParameter(TypeName.get(Retrofit.class), "retrofit").addModifiers(Modifier.PUBLIC)
.addStatement("this.$N = retrofit.create($N.class)", "api", INTERFACE_NAME)
.build());
for (ExecutableElement executableElement : getMethods)
classBuilder.addMethod(createRetrofitImplementationMethod(executableElement, executableElement.getAnnotation(SynoApiGET.class).wrap()));
for (ExecutableElement executableElement : postMethods)
classBuilder.addMethod(createRetrofitImplementationMethod(executableElement, executableElement.getAnnotation(SynoApiPOST.class).wrap()));
// Add info methods
classBuilder.addMethod(
MethodSpec.methodBuilder("thisApiName").addModifiers(Modifier.PUBLIC).returns(String.class)
.addStatement("return $S", apiName).build());
classBuilder.addMethod(
MethodSpec.methodBuilder("thisApiVersion").addModifiers(Modifier.PUBLIC).returns(int.class)
.addStatement("return " + apiVersion).build());
return classBuilder.build();
}
/**
* Methods for Retrofit interface.
*/
private MethodSpec createRetrofitMethod(ExecutableElement el, Class httpMethodAnnotation, Class parameterAnnotation, String httpMethodValue, boolean wrap) {
// Define relative path which will be in Retrofit's GET/POST annotation, eg: GET("/users")
// By default it uses SynologyApi path format
String path;
if (Strings.isNullOrEmpty(httpMethodValue))
// path = "/webapi" + apiPath + "?api=" + apiName + "&version=" + apiVersion + "&method=" + el.getSimpleName().toString().toLowerCase();
path = "/webapi" + apiPath ;
else
path = httpMethodValue;
// By default (wrap = true) return type is wrapped with ApiResponse, eg: Observable<ApiResponse<String>>
ParameterizedTypeName returnType;
if (wrap)
returnType = ParameterizedTypeName.get(ClassName.get(Observable.class), ParameterizedTypeName.get(ClassName.get(ApiResponse.class), TypeName.get(el.getReturnType())));
else
returnType = ParameterizedTypeName.get(ClassName.get(Observable.class), TypeName.get(el.getReturnType()));
MethodSpec.Builder builder = MethodSpec.methodBuilder(el.getSimpleName().toString())
.addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT)
.addAnnotation(AnnotationSpec.builder(httpMethodAnnotation).addMember("value", "$S", path).build())
.addParameters(createMethodParameters(el, FieldMap.class))
.returns(returnType);
return builder.build();
}
/**
* Methods - implementation
*/
private MethodSpec createRetrofitImplementationMethod(ExecutableElement el, boolean wrap) {
Iterable<ParameterSpec> params = createMethodParameters(el, null);
// If wrap is false, don't add any additional data mapping (extracting 'data' and 'error' from ApiResponse).
if (!wrap) {
return MethodSpec.methodBuilder(el.getSimpleName().toString())
.addModifiers(Modifier.PUBLIC)
.addParameters(params)
.returns(ParameterizedTypeName.get(ClassName.get(Observable.class), TypeName.get(el.getReturnType())))
.addStatement("return api.$N($N)\n" +
".retry(1)\n" +
".subscribeOn($T.io())\n" +
".observeOn(rx.android.schedulers.AndroidSchedulers.mainThread())\n" +
".unsubscribeOn(Schedulers.io())", el.getSimpleName().toString(), getArguments(params), Schedulers.class)
.build();
}
// Anonymous class that check api response status - if error throw ApiException
TypeSpec action = TypeSpec.anonymousClassBuilder("")
.addSuperinterface(
ParameterizedTypeName.get(
ClassName.get(Action1.class),
ParameterizedTypeName.get(ClassName.get(ApiResponse.class), TypeName.get(el.getReturnType()))))
.addMethod(MethodSpec.methodBuilder("call")
.addAnnotation(Override.class)
.addModifiers(Modifier.PUBLIC)
.addParameter(ParameterizedTypeName.get(ClassName.get(ApiResponse.class), TypeName.get(el.getReturnType())), "apiResponse")
.returns(TypeName.get(void.class))
.addStatement("if(!apiResponse.success) throw new $T(apiResponse.error.code, $S)", ApiException.class, apiName)
.build())
.build();
// Anonymous class that maps ApiResponse<Class> to Class
TypeSpec func = TypeSpec.anonymousClassBuilder("")
.addSuperinterface(
ParameterizedTypeName.get(
ClassName.get(Func1.class),
ParameterizedTypeName.get(ClassName.get(ApiResponse.class), TypeName.get(el.getReturnType())),
TypeName.get(el.getReturnType())))
.addMethod(MethodSpec.methodBuilder("call")
.addAnnotation(Override.class)
.addModifiers(Modifier.PUBLIC)
.addParameter(ParameterizedTypeName.get(ClassName.get(ApiResponse.class), TypeName.get(el.getReturnType())), "apiResponse")
.returns(TypeName.get(el.getReturnType()))
.addStatement("return $N.data", "apiResponse")
.build())
.build();
// Final method spec
MethodSpec.Builder builder = MethodSpec.methodBuilder(el.getSimpleName().toString())
.addModifiers(Modifier.PUBLIC)
.addParameters(params)
.returns(ParameterizedTypeName.get(
ClassName.get(Observable.class),
TypeName.get(el.getReturnType())))
.addStatement("return api.$N($N)\n" +
".retry(1)\n" +
".subscribeOn($T.io())\n" +
".observeOn(rx.android.schedulers.AndroidSchedulers.mainThread())\n" +
".unsubscribeOn(Schedulers.io())\n" +
".doOnNext($L)\n" +
".map($L)", el.getSimpleName().toString(), getArguments(params), Schedulers.class, action, func);
return builder.build();
}
/**
* Creates Retrofit method parameters (annotated with Query/Body).
*/
private Iterable<ParameterSpec> createMethodParameters(ExecutableElement el, Class parameterAnnotation) {
List<ParameterSpec> params = new ArrayList<>();
for (VariableElement p : el.getParameters()) {
ParameterSpec.Builder builder;
if (parameterAnnotation == null) {
builder = ParameterSpec.builder(TypeName.get(p.asType()), p.getSimpleName().toString());
} else if (p.getSimpleName().toString().equals("path")) {
builder = ParameterSpec.builder(TypeName.get(p.asType()), p.getSimpleName().toString()).addAnnotation(AnnotationSpec.builder(Path.class)
.addMember("value", "$S", p.getSimpleName().toString())
.addMember("encoded", "$L", true)
.build());
} else if (parameterAnnotation.getSimpleName().equals(Query.class.getSimpleName())) {
builder = ParameterSpec.builder(TypeName.get(p.asType()), p.getSimpleName().toString()).addAnnotation(AnnotationSpec.builder(parameterAnnotation)
.addMember("value", "$S", p.getSimpleName().toString())
.build());
} else {
builder = ParameterSpec.builder(TypeName.get(p.asType()), p.getSimpleName().toString()).addAnnotation(parameterAnnotation);
}
params.add(builder.build());
}
return params;
}
/**
* Parse arguments string from method parameters.
*/
private String getArguments(Iterable<ParameterSpec> params) {
String arguments = "";
for (ParameterSpec param : params)
arguments = arguments + param.name + ",";
if (!Strings.isNullOrEmpty(arguments))
return arguments.substring(0, arguments.length() - 1);
else
return arguments;
}
private void saveGeneratedFile(TypeSpec typeSpec) {
PackageElement pkg = elements.getPackageOf(typeElement);
String packageName = pkg.isUnnamed() ? null : pkg.getQualifiedName().toString();
try {
JavaFile.builder(getPackageName(), typeSpec).build().writeTo(filer);
} catch (IOException e) {
e.printStackTrace();
}
}
private String getPackageName() {
PackageElement pkg = elements.getPackageOf(typeElement);
return pkg.isUnnamed() ? null : pkg.getQualifiedName().toString();
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
InterfaceElement that = (InterfaceElement) o;
return name.equals(that.name);
}
@Override
public int hashCode() {
return name.hashCode();
}
}
@AutoService(Processor.class)
public class MainProcessor extends AbstractProcessor {
private Messager messager;
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
messager = processingEnv.getMessager();
}
@Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> annotations = new LinkedHashSet<>();
annotations.add(SynoApiDef.class.getCanonicalName());
annotations.add(SynoApiGET.class.getCanonicalName());
annotations.add(SynoApiPOST.class.getCanonicalName());
return annotations;
}
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
// For all interfaces annotated with @SynoApiDef
ArrayList<InterfaceElement> interfaceElements = new ArrayList<>();
for (Element annotatedElement : roundEnv.getElementsAnnotatedWith(SynoApiDef.class)) {
if (annotatedElement.getKind() != ElementKind.INTERFACE) {
error(annotatedElement, "Only interfaces can be annotated with @%s", SynoApiDef.class.getSimpleName());
return true;
}
if (!annotatedElement.getAnnotation(SynoApiDef.class).path().startsWith("/")
&& !annotatedElement.getAnnotation(SynoApiDef.class).path().startsWith("http://")
&& !annotatedElement.getAnnotation(SynoApiDef.class).path().startsWith("https://")) {
error(annotatedElement, "@%s annotation parameter 'path' should start with: '/', 'http://' or 'https://'", SynoApiDef.class.getSimpleName());
return true;
}
// Create new helper class
InterfaceElement interfaceElement = new InterfaceElement((TypeElement) annotatedElement, processingEnv.getFiler(), processingEnv.getElementUtils());
// Add GET methods
for (Element element : roundEnv.getElementsAnnotatedWith(SynoApiGET.class)) {
if (element.getKind() != ElementKind.METHOD) {
error(annotatedElement, "Only methods can be annotated with @%s", SynoApiGET.class.getSimpleName());
return true;
}
if (element.getEnclosingElement().getSimpleName().toString().equals(interfaceElement.name))
interfaceElement.getMethods.add((ExecutableElement) element);
}
// Add POST methods
for (Element element : roundEnv.getElementsAnnotatedWith(SynoApiPOST.class)) {
if (element.getKind() != ElementKind.METHOD) {
error(annotatedElement, "Only methods can be annotated with @%s", SynoApiPOST.class.getSimpleName());
return true;
}
if (element.getEnclosingElement().getSimpleName().toString().equals(interfaceElement.name))
interfaceElement.postMethods.add((ExecutableElement) element);
}
// Add to list
interfaceElements.add(interfaceElement);
}
// Generate code
if (!interfaceElements.isEmpty()) {
for (InterfaceElement interfaceElement : interfaceElements)
interfaceElement.generate();
new AllApis(interfaceElements, processingEnv.getFiler(), processingEnv.getElementUtils()).generate();
interfaceElements.clear();
}
return true;
}
private void error(Element e, String msg, Object... args) {
messager.printMessage(Diagnostic.Kind.ERROR, String.format(msg, args), e);
}
private void debug(String msg) {
messager.printMessage(Diagnostic.Kind.MANDATORY_WARNING, msg);
}
}
public class Utils {
private Utils() {
}
static String getPackageName(Elements elementUtils, TypeElement type) {
PackageElement pkg = elementUtils.getPackageOf(type);
return pkg.getQualifiedName().toString();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment