Created
September 23, 2016 10:41
-
-
Save rafakob/a4d3f6eddcf4c09f3fe45104641b23c8 to your computer and use it in GitHub Desktop.
JavaPoet
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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(); | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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(); | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| @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); | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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