All Downloads are FREE. Search and download functionalities are using the official Maven repository.

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 annotations, RoundEnvironment roundEnv) {
    if (roundEnv.processingOver()) {
      generateOutput();
    } else {
      processAnnotations(annotations, roundEnv);
    }
    return true;
  }

  private void processAnnotations(Set annotations,
                                  RoundEnvironment roundEnv) {
    processJacksonAnnotations(roundEnv);
    processRESTEndpointAnnotations(annotations, roundEnv);
  }

  private void processRESTEndpointAnnotations(final Set 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 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 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 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 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