com.spotify.docgenerator.JacksonJerseyAnnotationProcessor Maven / Gradle / Ivy
The newest version!
/*
* Copyright (c) 2014 Spotify AB.
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.spotify.docgenerator;
import com.google.auto.service.AutoService;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectWriter;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import java.io.IOException;
import java.io.OutputStream;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Filer;
import javax.annotation.processing.Processor;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedOptions;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.tools.Diagnostic.Kind;
import javax.tools.FileObject;
import javax.tools.StandardLocation;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import static com.fasterxml.jackson.databind.MapperFeature.SORT_PROPERTIES_ALPHABETICALLY;
import static com.fasterxml.jackson.databind.SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS;
import static com.fasterxml.jackson.databind.SerializationFeature.WRITE_DATES_AS_TIMESTAMPS;
@SupportedAnnotationTypes({
"com.fasterxml.jackson.annotation.JsonProperty",
"com.fasterxml.jackson.databind.annotation.JsonSerialize",
"javax.ws.rs.GET",
"javax.ws.rs.POST",
"javax.ws.rs.PUT",
"javax.ws.rs.DELETE",
"com.spotify.helios.master.http.PATCH"
})
@SupportedSourceVersion(SourceVersion.RELEASE_7)
@SupportedOptions({ "debug", "verify" })
@AutoService(Processor.class)
public class JacksonJerseyAnnotationProcessor extends AbstractProcessor {
private static final List METHOD_ANNOTATIONS = Lists.newArrayList(
"javax.ws.rs.GET",
"javax.ws.rs.POST",
"javax.ws.rs.PUT",
"javax.ws.rs.DELETE",
"com.spotify.helios.master.http.PATCH");
private static final ObjectWriter NORMALIZING_OBJECT_WRITER = new ObjectMapper()
.setSerializationInclusion(JsonInclude.Include.NON_EMPTY)
.configure(SORT_PROPERTIES_ALPHABETICALLY, true)
.configure(ORDER_MAP_ENTRIES_BY_KEYS, true)
.configure(WRITE_DATES_AS_TIMESTAMPS, false)
.writer();
private final Map jsonClasses = Maps.newHashMap();
private final Map resourceClasses = Maps.newHashMap();
private final List debugMessages = Lists.newArrayList();
@Override
public boolean process(Set extends TypeElement> annotations, RoundEnvironment roundEnv) {
if (roundEnv.processingOver()) {
generateOutput();
} else {
processAnnotations(annotations, roundEnv);
}
return true;
}
private void processAnnotations(Set extends TypeElement> annotations,
RoundEnvironment roundEnv) {
processJacksonAnnotations(roundEnv);
processRESTEndpointAnnotations(annotations, roundEnv);
}
private void processRESTEndpointAnnotations(final Set extends TypeElement> annotations,
final RoundEnvironment roundEnv) {
for (String methodAnnotation : METHOD_ANNOTATIONS) {
for (TypeElement foundAnnotations : annotations) {
if (foundAnnotations.toString().equals(methodAnnotation)) {
processFoundRestAnnotations(foundAnnotations, roundEnv);
}
}
}
}
/**
* Go through found REST Annotations and produce {@link ResourceClass}es from what we find.
*/
private void processFoundRestAnnotations(final TypeElement foundAnnotations,
final RoundEnvironment roundEnv) {
final Set extends Element> elements = roundEnv.getElementsAnnotatedWith(foundAnnotations);
for (final Element e : elements) {
// should always be METHOD, but just being paranoid
if (e.getKind() != ElementKind.METHOD) {
continue;
}
final ExecutableElement ee = (ExecutableElement) e;
final List arguments = computeMethodArguments(ee);
final ResourceMethod method = computeMethod(ee, arguments);
final ResourceClass klass = getParentResourceClass(e);
klass.getMembers().add(method);
}
}
/**
* Given an {@link ExecutableElement} representing the method, compute it's arguments.
*/
private List computeMethodArguments(final ExecutableElement ee) {
final List arguments = Lists.newArrayList();
for (VariableElement ve : ee.getParameters()) {
final PathParam pathAnnotation = ve.getAnnotation(PathParam.class);
final String argName;
if (pathAnnotation != null) {
argName = pathAnnotation.value();
} else {
argName = ve.getSimpleName().toString();
}
arguments.add(new ResourceArgument(argName, makeTypeDescriptor(ve.asType())));
}
return arguments;
}
/**
* Given an {@link ExecutableElement} representing the method, and the already computed list
* of arguments to the method, produce a {@link ResourceMethod}.
*/
private ResourceMethod computeMethod(ExecutableElement ee, List arguments) {
final String javaDoc = processingEnv.getElementUtils().getDocComment(ee);
final Path pathAnnotation = ee.getAnnotation(Path.class);
final Produces producesAnnotation = ee.getAnnotation(Produces.class);
return new ResourceMethod(
ee.getSimpleName().toString(),
computeRequestMethod(ee),
(pathAnnotation == null) ? null : pathAnnotation.value(),
(producesAnnotation == null) ? null : Joiner.on(",").join(producesAnnotation.value()),
makeTypeDescriptor(ee.getReturnType()),
arguments,
javaDoc);
}
/**
* Find the request method annotation the method was annotated with and return a string
* representing the request method.
*/
private String computeRequestMethod(Element e) {
for (AnnotationMirror am : e.getAnnotationMirrors()) {
final String typeString = am.getAnnotationType().toString();
if (typeString.endsWith(".GET")) {
return "GET";
} else if (typeString.endsWith(".PUT")) {
return "PUT";
} else if (typeString.endsWith(".POST")) {
return "POST";
} else if (typeString.endsWith(".PATCH")) {
return "PATCH";
} else if (typeString.endsWith(".DELETE")) {
return "DELETE";
}
}
return null;
}
/**
* Given an {@link Element} representing the method get either a cached {@link ResourceClass} or
* produce a new one.
*/
private ResourceClass getParentResourceClass(final Element e) {
final String parentClassName = e.getEnclosingElement().toString();
final ResourceClass klass = resourceClasses.get(parentClassName);
if (klass != null) {
return klass;
}
final Path klassPath = e.getEnclosingElement().getAnnotation(Path.class);
final ResourceClass newKlass = new ResourceClass(
(klassPath == null) ? null : klassPath.value(),
Lists.newArrayList());
resourceClasses.put(parentClassName, newKlass);
return newKlass;
}
private void processJacksonAnnotations(final RoundEnvironment roundEnv) {
processJsonPropertyAnnotations(roundEnv);
processJsonSerializeAnnotations(roundEnv);
}
/**
* Go through a Jackson-annotated constructor, and produce {@link TransferClass}es representing
* what we found.
*/
private void processJsonPropertyAnnotations(final RoundEnvironment roundEnv) {
final Set extends Element> elements = roundEnv.getElementsAnnotatedWith(JsonProperty.class);
for (final Element e : elements) {
if (e.getEnclosingElement() == null) {
continue;
}
final Element parentElement = e.getEnclosingElement().getEnclosingElement();
if (parentElement == null) {
continue;
}
if (!(parentElement instanceof TypeElement)) {
continue;
}
final TypeElement parent = (TypeElement) parentElement;
final String parentJavaDoc = processingEnv.getElementUtils().getDocComment(parent);
final String parentName = parent.getQualifiedName().toString();
final TransferClass klass = getOrCreateTransferClass(parentName, parentJavaDoc);
klass.add(e.toString(), makeTypeDescriptor(e.asType()));
}
}
/**
* If we see one of these, just create an entry that the class exists (with it's javadoc),
* but don't try to do anything fancy.
*/
private void processJsonSerializeAnnotations(final RoundEnvironment roundEnv) {
final Set extends Element> elements = roundEnv.getElementsAnnotatedWith(JsonSerialize.class);
for (final Element e : elements) {
if (e.getKind() != ElementKind.CLASS) {
debugMessages.add("kind for " + e + " is not CLASS, but " + e.getKind());
continue;
}
final TypeElement te = (TypeElement) e;
final String className = te.getQualifiedName().toString();
if (jsonClasses.containsKey(className)) {
// it has already been processed by other means
continue;
}
getOrCreateTransferClass(className, processingEnv.getElementUtils().getDocComment(te));
}
}
private TransferClass getOrCreateTransferClass(final String parentName,
final String parentJavaDoc) {
final TransferClass klass = jsonClasses.get(parentName);
if (klass != null) {
return klass;
}
final TransferClass newKlass = new TransferClass(
Lists.newArrayList(), parentJavaDoc);
jsonClasses.put(parentName, newKlass);
return newKlass;
}
/**
* Make a {@link TypeDescriptor} by examining the {@link TypeMirror} and recursively looking
* at the generic arguments to the type (if they exist).
*/
private TypeDescriptor makeTypeDescriptor(final TypeMirror type) {
if (type.getKind() != TypeKind.DECLARED) {
return new TypeDescriptor(type.toString(), ImmutableList.of());
}
final DeclaredType dt = (DeclaredType) type;
final String plainType = processingEnv.getTypeUtils().erasure(type).toString();
final List typeArgumentsList = Lists.newArrayList();
final List extends TypeMirror> typeArguments = dt.getTypeArguments();
for (final TypeMirror arg : typeArguments) {
typeArgumentsList.add(makeTypeDescriptor(arg));
}
return new TypeDescriptor(plainType, typeArgumentsList);
}
private void fatalError(String msg) {
processingEnv.getMessager().printMessage(Kind.ERROR, "FATAL ERROR: " + msg);
}
/**
* Dump the contents of our discoveries.
*/
private void generateOutput() {
final Filer filer = processingEnv.getFiler();
writeJsonToFile(filer, "JSONClasses", jsonClasses);
writeJsonToFile(filer, "debugcrud", debugMessages);
final List resources = Lists.newArrayList();
for (ResourceClass klass : resourceClasses.values()) {
final String path = klass.getPath();
for (final ResourceMethod method : klass.getMembers()) {
resources.add(new ResourceMethod("", method.getMethod(),
computeDisplayPath(path, method.getPath()),
method.getReturnContentType(), method.getReturnType(), method.getArguments(),
method.getJavadoc()));
}
}
writeJsonToFile(filer, "RESTEndpoints", resources);
}
private String computeDisplayPath(String path, String methodPath) {
final String rootPath;
if (!path.startsWith("/")) {
rootPath = "/" + path;
} else {
rootPath = path;
}
if (methodPath == null) {
return rootPath;
}
// if there is a delimiting slash between the two of them, just join directly
if (rootPath.endsWith("/") != methodPath.startsWith("/")) {
return rootPath + methodPath;
}
// two slashes, trim off one
if (rootPath.endsWith("/")) {
return rootPath + methodPath.substring(1);
}
// no slashes, add one
return rootPath + "/" + methodPath;
}
private void writeJsonToFile(Filer filer, String resourceFile, Object obj) {
try {
final FileObject outputFile = filer.createResource(StandardLocation.CLASS_OUTPUT, "",
resourceFile);
try (final OutputStream out = outputFile.openOutputStream()) {
out.write(NORMALIZING_OBJECT_WRITER.writeValueAsBytes(obj));
}
} catch (IOException e) {
fatalError("Failed writing to " + resourceFile + "\n");
e.printStackTrace();
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy