Last active
August 26, 2016 06:28
-
-
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
This file contains 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
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(); | |
} | |
} |
This file contains 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
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; | |
} | |
} |
This file contains 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
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