Java tutorial
/* * 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<String> 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<String, TransferClass> jsonClasses = Maps.newHashMap(); private final Map<String, ResourceClass> resourceClasses = Maps.newHashMap(); private final List<String> 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<ResourceArgument> 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<ResourceArgument> computeMethodArguments(final ExecutableElement ee) { final List<ResourceArgument> 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<ResourceArgument> 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.<ResourceMethod>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.<TransferMember>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.<TypeDescriptor>of()); } final DeclaredType dt = (DeclaredType) type; final String plainType = processingEnv.getTypeUtils().erasure(type).toString(); final List<TypeDescriptor> 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<ResourceMethod> 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(); } } }