Skip to content

Instantly share code, notes, and snippets.

@asm0dey
Last active August 26, 2016 06:28
Show Gist options
  • Save asm0dey/d9e4a93c8c35f46051f6 to your computer and use it in GitHub Desktop.
Save asm0dey/d9e4a93c8c35f46051f6 to your computer and use it in GitHub Desktop.
Simple processor, which goes thru interfaces, which are annotated with @RequestMapping annotation and creates interfaces from them
package ru.atol.asmcloud.apt.processor;
import com.squareup.javapoet.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.client.RestTemplate;
import javax.lang.model.element.Element;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.TypeMirror;
import java.util.List;
import java.util.Objects;
import static com.squareup.javapoet.MethodSpec.methodBuilder;
import static java.util.stream.Collectors.toList;
import static javax.lang.model.element.Modifier.*;
/**
* Created by finkel on 04.06.16.
*/
public class ClientFactory {
public static TypeSpec generateClient(TypeElement clazz, List<? extends Element> methods) {
return TypeSpec.classBuilder(clazz.getSimpleName().toString().replaceAll("[Pp]roto", "") + "Client")
.addModifiers(ABSTRACT, PUBLIC)
.addMethod(apiUrlMethod())
.addField(restTemplate())
.addMethods(interfaceMethodsToImplementations(methods))
.build();
}
private static FieldSpec restTemplate() {
return FieldSpec
.builder(TypeName.get(RestTemplate.class), "restTemplate", PRIVATE)
.addAnnotation(AnnotationSpec.builder(Autowired.class).build())
.build();
}
private static Iterable<MethodSpec> interfaceMethodsToImplementations(List<? extends Element> methods) {
return methods
.stream()
.map(it -> (ExecutableElement) it)
.map(ClientFactory::interfaceToImpl)
.collect(toList());
}
private static MethodSpec apiUrlMethod() {
return methodBuilder("apiUrl")
.returns(TypeName.get(String.class))
.addModifiers(ABSTRACT, PROTECTED)
.build();
}
private static MethodSpec interfaceToImpl(ExecutableElement sourceMethod) {
RequestMapping mapping = sourceMethod.getAnnotation(RequestMapping.class);
RequestMethod httpMethod = mapping.method()[0];
String path = mapping.path().length > 0 ? mapping.path()[0] : mapping.value()[0];
String methodName = sourceMethod.getSimpleName().toString();
String[] consumeHeaders = mapping.consumes();
String[] produceHeaders = mapping.produces();
TypeName returnType = responseEntityOf(sourceMethod.getReturnType());
return methodBuilder(methodName)
.returns(returnType)
.addModifiers(PUBLIC)
.addCode(generateRequestCode(httpMethod, path, sourceMethod.getParameters(), returnType,consumeHeaders,produceHeaders))
.addParameters(extractTargetArguments(sourceMethod))
.build();
}
private static TypeName responseEntityOf(TypeMirror returnType) {
ClassName responseEntityClassName = ClassName.get(ResponseEntity.class);
TypeName returnTypeClassName = ClassName.get(returnType);
if (TypeName.VOID == returnTypeClassName || Objects.equals(TypeName.VOID.box(), returnTypeClassName))
return TypeName.get(ResponseEntity.class);
if (returnTypeClassName instanceof ParameterizedTypeName) {
ParameterizedTypeName typeClassName = (ParameterizedTypeName) returnTypeClassName;
if (typeClassName.rawType.equals(responseEntityClassName)){
TypeName insideResponse = typeClassName.typeArguments.get(0);
if (insideResponse.equals(TypeName.VOID.box())) {
return responseEntityClassName;
}
return returnTypeClassName;
}
}
return ParameterizedTypeName.get(responseEntityClassName, returnTypeClassName.box());
}
private static CodeBlock generateRequestCode(RequestMethod httpMethod, String requestPath, List<? extends VariableElement> sourceParams, TypeName returnType, String[] consumeHeaders, String[] produceHeaders) {
return RequestBuilder.generate(requestPath, sourceParams, returnType, httpMethod,consumeHeaders,produceHeaders);
}
private static Iterable<ParameterSpec> extractTargetArguments(ExecutableElement sourceMethod) {
return sourceMethod.getParameters()
.stream()
.map(ClientFactory::argToParameterSpec)
.collect(toList());
}
private static ParameterSpec argToParameterSpec(VariableElement variableElement) {
return ParameterSpec
.builder(TypeName.get(variableElement.asType()), variableElement.getSimpleName().toString(), FINAL)
.build();
}
}
package ru.atol.asmcloud.apt.processor;
import com.squareup.javapoet.*;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.element.*;
import javax.lang.model.type.TypeMirror;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static com.squareup.javapoet.MethodSpec.methodBuilder;
import static java.util.stream.Collectors.groupingBy;
import static javax.lang.model.SourceVersion.RELEASE_8;
import static javax.lang.model.element.Modifier.ABSTRACT;
import static javax.lang.model.element.Modifier.PUBLIC;
/**
* Created by finkel on 12.02.16.
*/
@SupportedAnnotationTypes("org.springframework.web.bind.annotation.RequestMapping")
@SupportedSourceVersion(RELEASE_8)
public class ProtoProcessor extends AbstractProcessor {
private static final String ENDPOINT_SUFFIX = "Endpoint";
private static final String CLIENT_SUFFIX = "Client";
private static void generateInterfaces(TypeElement clazz, List<? extends Element> methods) {
Stream.of(CLIENT_SUFFIX, ENDPOINT_SUFFIX)
.map(suffix -> getTypeSpec(clazz, methods, suffix))
.map(typeSpec -> toJavaFile(clazz, typeSpec))
.forEach(ProtoProcessor::printToFiles);
}
private static void printToFiles(JavaFile it) {
try {
String path = System.getProperty("generated.path", "target/generated-sources/java");
it.writeTo(new File(path));
} catch (IOException e) {
e.printStackTrace();
}
}
private static JavaFile toJavaFile(TypeElement clazz, TypeSpec typeSpec) {
boolean isEndpoint = typeSpec.toString().contains(ENDPOINT_SUFFIX);
return JavaFile.builder(clazz.getEnclosingElement().toString() + (isEndpoint ? ".endpoint" : ".client"), typeSpec).build();
}
private static TypeSpec getTypeSpec(TypeElement clazz, List<? extends Element> methods, String suffix) {
List<MethodSpec> methodSpecs = new ArrayList<>();
if (ENDPOINT_SUFFIX.equals(suffix)) {
for (Element element : methods) {
ExecutableElement method = (ExecutableElement) element;
MethodSpec.Builder methodBuilder = methodBuilder(element.getSimpleName().toString());
methodBuilder.returns(callableOf(method));
method.getParameters()
.stream()
.map(ProtoProcessor::getParameterSpecByVariable)
.forEach(methodBuilder::addParameter);
methodSpecs.add(methodBuilder
.addModifiers(ABSTRACT, PUBLIC)
.addAnnotations(annotationSpecsByAnnotations(method))
.build());
}
return TypeSpec.interfaceBuilder(clazz.getSimpleName().toString().replaceAll("[Pp]roto", "") + suffix)
.addModifiers(PUBLIC)
.addMethods(methodSpecs)
.build();
} else {
return ClientFactory.generateClient(clazz, methods);
}
}
private static List<AnnotationSpec> annotationSpecsByAnnotations(ExecutableElement func) {
return func.getAnnotationMirrors()
.stream()
.map(AnnotationSpec::get)
.collect(Collectors.toList());
}
private static ParameterizedTypeName callableOf(ExecutableElement func) {
if (!func.getReturnType().toString().equals(void.class.getName())) {
TypeMirror actualType = func.getReturnType();
TypeName wrapperType = TypeName.get(actualType).box();
return ParameterizedTypeName.get(ClassName.get(Callable.class), wrapperType == null ? TypeName.get(actualType) : wrapperType);
} else
return ParameterizedTypeName.get(ClassName.get(Callable.class), ClassName.get(Void.class));
}
private static ParameterSpec getParameterSpecByVariable(VariableElement variableElement) {
List<? extends AnnotationMirror> annotationMirrors = variableElement.getAnnotationMirrors();
ParameterSpec.Builder paramBuilder = ParameterSpec
.builder(TypeName.get(variableElement.asType()), variableElement.getSimpleName().toString());
annotationMirrors.forEach(it -> paramBuilder.addAnnotation(AnnotationSpec.get(it)));
return paramBuilder.build();
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
System.out.println(processingEnv.getOptions());
for (TypeElement annotation : annotations) {
roundEnv.getElementsAnnotatedWith(annotation)
.stream()
.collect(groupingBy(elem -> (TypeElement) elem.getEnclosingElement()))
.forEach(ProtoProcessor::generateInterfaces);
}
return true;
}
}
package ru.atol.asmcloud.apt.processor;
import com.squareup.javapoet.CodeBlock;
import com.squareup.javapoet.ParameterizedTypeName;
import com.squareup.javapoet.TypeName;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.util.UriComponentsBuilder;
import javax.lang.model.element.VariableElement;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static java.util.stream.Collectors.toMap;
/**
* Created by finkel on 04.06.16.
*/
public class RequestBuilder {
public static CodeBlock generate(String requestPath, List<? extends VariableElement> sourceParams, TypeName returnType, RequestMethod httpMethod, String[] consumeHeaders, String[] produceHeaders) {
if (returnType instanceof ParameterizedTypeName) {
ParameterizedTypeName type = (ParameterizedTypeName) returnType;
if (type.rawType.equals(TypeName.get(ResponseEntity.class)))
returnType = type.typeArguments.get(0);
} else if (returnType.equals(TypeName.get(ResponseEntity.class))) {
returnType = TypeName.VOID.box();
}
CodeBlock.Builder codeBuilder = CodeBlock.builder();
codeBuilder.addStatement("String url = apiUrl() + $S", requestPath);
addRequestParams(sourceParams, codeBuilder);
addPathParams(sourceParams, codeBuilder);
constructHttpEntity(sourceParams, codeBuilder, consumeHeaders, produceHeaders);
if (returnType instanceof ParameterizedTypeName) {
ParameterizedTypeName type = (ParameterizedTypeName) returnType;
codeBuilder.addStatement("$T<$T> ref = new $T<$T>(){}", ParameterizedTypeReference.class, type, ParameterizedTypeReference.class, type);
codeBuilder.addStatement("return restTemplate.exchange(url, $T.$L, toSend, ref, vars)", HttpMethod.class, httpMethod);
} else {
codeBuilder.addStatement("return restTemplate.exchange(url, $T.$L, toSend, $T.class, vars)", HttpMethod.class, httpMethod, returnType.box());
}
return codeBuilder.build();
}
private static void constructHttpEntity(List<? extends VariableElement> sourceParams, CodeBlock.Builder codeBuilder, String[] consumeHeaders, String[] produceHeaders) {
Map<String, String> headerParams = extractHeaderParams(sourceParams);
Map<? extends String, ? extends String> consumeAndProduceHeaders = consumeAndProduceHeaders(consumeHeaders, produceHeaders);
boolean hasHeaders = !headerParams.isEmpty() || !consumeAndProduceHeaders.isEmpty();
if (hasHeaders) {
codeBuilder.addStatement("$T<String, String> headers = new $T<String, String>()", MultiValueMap.class, LinkedMultiValueMap.class);
for (Map.Entry<String, String> headerParam : headerParams.entrySet()) {
codeBuilder.addStatement("headers.add($S, $L)", headerParam.getKey(), headerParam.getValue());
}
consumeAndProduceHeaders
.forEach((headerParam, value) -> codeBuilder.addStatement("headers.add($S, $S)", headerParam, value));
}
String bodyParam = extracetBodyParam(sourceParams);
boolean hasBody = bodyParam != null;
if (hasHeaders && hasBody)
codeBuilder.addStatement("$T toSend = new $T($L, headers)", HttpEntity.class, HttpEntity.class, bodyParam);
else if (hasHeaders)
codeBuilder.addStatement("$T toSend = new $T(headers)", HttpEntity.class, HttpEntity.class);
else if (hasBody)
codeBuilder.addStatement("$T toSend = new $T($L)", HttpEntity.class, HttpEntity.class, bodyParam);
else
codeBuilder.addStatement("$T toSend = $T.EMPTY", HttpEntity.class, HttpEntity.class);
}
private static Map<? extends String, ? extends String> consumeAndProduceHeaders(String[] consumeHeaders, String[] produceHeaders) {
Map<String, String> headers = new HashMap<>();
if (consumeHeaders.length == 0 && produceHeaders.length == 0) return Collections.emptyMap();
if (consumeHeaders.length > 0) headers.put("Content-Type", consumeHeaders[0]);
if (produceHeaders.length > 0) headers.put("Accept", produceHeaders[0]);
return headers;
}
private static String extracetBodyParam(List<? extends VariableElement> sourceParams) {
return sourceParams
.stream()
.filter(RequestBuilder::isRequestBodyParam)
.map(RequestBuilder::argToItsName)
.findFirst()
.orElse(null);
}
private static String argToItsName(VariableElement arg) {
return arg.getSimpleName().toString();
}
private static boolean isRequestBodyParam(VariableElement variableElement) {
return variableElement.getAnnotation(RequestBody.class) != null;
}
private static void addPathParams(List<? extends VariableElement> sourceParams, CodeBlock.Builder codeBuilder) {
Map<String, String> pathParams = extractPathParams(sourceParams);
codeBuilder.addStatement("$T<String, String> vars = new $T<>()", Map.class, HashMap.class);
if (!pathParams.isEmpty()) {
pathParams
.forEach((header, arg) -> codeBuilder.addStatement("vars.put($S, String.valueOf($L))", header, arg));
}
}
private static void addRequestParams(List<? extends VariableElement> sourceParams, CodeBlock.Builder codeBuilder) {
Map<String, String> requestParams = extractQueryParams(sourceParams);
if (!requestParams.isEmpty()) {
codeBuilder.add("url = $T.fromHttpUrl(url)", UriComponentsBuilder.class);
requestParams
.forEach((headerName, paramName) -> codeBuilder.add("\n\t.queryParam($S, $L)", headerName, paramName));
codeBuilder.add("\n\t.build()\n\t.encode()\n\t.toUri()\n\t.toString();\n");
}
}
private static Map<String, String> extractHeaderParams(List<? extends VariableElement> sourceParams) {
return sourceParams
.stream()
.filter(RequestBuilder::isHeaderParam)
.collect(toMap(RequestBuilder::getHeaderName, RequestBuilder::getParamName));
}
private static String getParamName(VariableElement variableElement) {
return variableElement.getSimpleName().toString();
}
private static Map<String, String> extractQueryParams(List<? extends VariableElement> sourceParams) {
return sourceParams
.stream()
.filter(RequestBuilder::isRequestParam)
.collect(toMap(RequestBuilder::getRequestParamName, RequestBuilder::getParamName));
}
private static Map<String, String> extractPathParams(List<? extends VariableElement> sourceParams) {
return sourceParams
.stream()
.filter(RequestBuilder::isPathParam)
.collect(toMap(RequestBuilder::getPathParamName, RequestBuilder::getParamName));
}
private static String getHeaderName(VariableElement variableElement) {
return variableElement.getAnnotation(RequestHeader.class).value();
}
private static boolean isHeaderParam(VariableElement variableElement) {
return variableElement.getAnnotation(RequestHeader.class) != null;
}
private static String getRequestParamName(VariableElement variableElement) {
return variableElement.getAnnotation(RequestParam.class).value();
}
private static boolean isRequestParam(VariableElement variableElement) {
return variableElement.getAnnotation(RequestParam.class) != null;
}
private static String getPathParamName(VariableElement variableElement) {
String value = variableElement.getAnnotation(PathVariable.class).value();
return "".equals(value) ? variableElement.getSimpleName().toString() : value;
}
private static boolean isPathParam(VariableElement variableElement) {
return variableElement.getAnnotation(PathVariable.class) != null;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment