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

io.jooby.internal.apt.RouteAttributesGenerator Maven / Gradle / Ivy

There is a newer version: 3.5.3
Show newest version
/*
 * Jooby https://jooby.io
 * Apache License Version 2.0 https://jooby.io/LICENSE.txt
 * Copyright 2014 Edgar Espina
 */
package io.jooby.internal.apt;

import static io.jooby.apt.JoobyProcessor.Options.SKIP_ATTRIBUTE_ANNOTATIONS;
import static io.jooby.internal.apt.CodeBlock.indent;
import static io.jooby.internal.apt.CodeBlock.type;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.*;
import java.util.function.Predicate;

import javax.lang.model.element.*;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.Elements;
import javax.lang.model.util.SimpleAnnotationValueVisitor14;
import javax.lang.model.util.Types;

import io.jooby.apt.JoobyProcessor.Options;

public class RouteAttributesGenerator {
  private record EnumValue(String type, String value) {}

  private static final Predicate HTTP_ANNOTATION =
      it ->
          (it.startsWith("io.jooby.annotation")
                  && !it.contains("io.jooby.annotation.Transactional"))
              || it.startsWith("jakarta.ws.rs")
              || it.startsWith("javax.ws.rs");

  private static final Predicate NULL_ANNOTATION =
      it ->
          it.endsWith("NonNull")
              || it.endsWith("NotNull")
              || it.endsWith("Nonnull")
              || it.endsWith("Nullable");

  private static final Predicate KOTLIN_ANNOTATION = it -> it.equals("kotlin.Metadata");

  private static final Predicate ATTR_FILTER =
      HTTP_ANNOTATION.or(NULL_ANNOTATION).or(KOTLIN_ANNOTATION);

  private final List skip;
  private final Elements elements;
  private final Types types;

  public RouteAttributesGenerator(MvcContext context) {
    var environment = context.getProcessingEnvironment();
    this.elements = environment.getElementUtils();
    this.types = environment.getTypeUtils();
    this.skip = Options.stringListOpt(environment, SKIP_ATTRIBUTE_ANNOTATIONS);
  }

  public Optional toSourceCode(boolean kt, MvcRoute route, int indent) {
    var attributes = annotationMap(route.getMethod());
    if (attributes.isEmpty()) {
      return Optional.empty();
    } else {
      return Optional.of(toSourceCode(kt, annotationMap(route.getMethod()), indent + 6));
    }
  }

  private String toSourceCode(boolean kt, Map attributes, int indent) {
    var buffer = new StringBuilder();
    var separator = ",\n";
    var pairPrefix = "";
    var pairSuffix = "";
    var typeInfo = kt ? "" : "";
    var factoryMethod = "of" + typeInfo;
    if (attributes.size() > 10) {
      // Map.of Max size is 10
      pairPrefix = "java.util.Map.entry(";
      pairSuffix = ")";
      factoryMethod = "ofEntries" + typeInfo;
    }
    buffer.append("java.util.Map.").append(factoryMethod).append("(\n");
    for (var e : attributes.entrySet()) {
      buffer.append(indent(indent + 4));
      buffer.append(pairPrefix);
      buffer.append(CodeBlock.string(e.getKey())).append(", ");
      buffer.append(valueToSourceCode(kt, e.getValue(), indent + 4));
      buffer.append(pairSuffix).append(separator);
    }
    buffer.setLength(buffer.length() - separator.length());
    buffer.append(")");
    return buffer.toString();
  }

  private Object valueToSourceCode(boolean kt, Object value, int indent) {
    if (value instanceof String) {
      return CodeBlock.string((String) value);
    } else if (value instanceof Character) {
      return "'" + value + "'";
    } else if (value instanceof Map attributeMap) {
      return "\n  " + indent(indent) + toSourceCode(kt, attributeMap, indent + 1);
    } else if (value instanceof List list) {
      return valueToSourceCode(kt, list, indent);
    } else if (value instanceof EnumValue enumValue) {
      return enumValue.type + "." + enumValue.value;
    } else if (value instanceof TypeMirror) {
      return value + ".class";
    } else if (value instanceof Float) {
      return value + "f";
    } else if (value instanceof Double) {
      return value + "d";
    } else if (value instanceof Long) {
      return value + "L";
    } else if (value instanceof Short) {
      return "(short)" + value;
    } else if (value instanceof Byte) {
      return "(byte)" + value;
    } else {
      return value;
    }
  }

  private String valueToSourceCode(boolean kt, List values, int indent) {
    var buffer = new StringBuilder();
    buffer.append("java.util.List.of(");
    var separator = ", ";
    for (Object value : values) {
      buffer.append(valueToSourceCode(kt, value, indent)).append(separator);
    }
    buffer.setLength(buffer.length() - separator.length());
    buffer.append(")");
    return buffer.toString();
  }

  private Map annotationMap(ExecutableElement method) {
    // class
    var attributes = annotationMap(method.getEnclosingElement().getAnnotationMirrors());
    // method
    attributes.putAll(annotationMap(method.getAnnotationMirrors()));
    return attributes;
  }

  private Map annotationMap(List annotations) {
    var result = new TreeMap();
    for (var annotation : annotations) {
      var elem = annotation.getAnnotationType().asElement();
      var retention = elem.getAnnotation(Retention.class);
      var retentionPolicy = retention == null ? RetentionPolicy.CLASS : retention.value();
      var type = annotation.getAnnotationType().toString();
      if (
      // ignore annotations not available at runtime
      retentionPolicy != RetentionPolicy.RUNTIME
          // ignore core, jars annotations
          || ATTR_FILTER.test(type)
          // ignore user specified annotations
          || skip.stream().anyMatch(type::startsWith)) {

        continue;
      }
      String prefix = elem.getSimpleName().toString();
      // Set all values and then override with present values (fix for JDK 11+)
      result.putAll(toMap(annotation.getElementValues(), prefix));
      toMap(elements.getElementValuesWithDefaults(annotation), prefix).forEach(result::putIfAbsent);
    }
    return result;
  }

  private Map toMap(
      Map values, String prefix) {
    Map result = new LinkedHashMap<>();
    for (var attribute : values.entrySet()) {
      var value = annotationValue(attribute.getValue());
      if (value != null && !value.toString().isEmpty()) {
        var method = attribute.getKey().getSimpleName().toString();
        var name = method.equals("value") ? prefix : prefix + "." + method;
        // Found value is override on JDK 11 with default annotation value, we trust that spe
        result.putIfAbsent(name, value);
      }
    }
    return result;
  }

  private Object annotationValue(AnnotationValue annotationValue) {
    try {
      return annotationValue.accept(
          new SimpleAnnotationValueVisitor14() {
            @Override
            protected Object defaultAction(Object value, Void unused) {
              return value;
            }

            @Override
            public Object visitAnnotation(AnnotationMirror mirror, Void unused) {
              var annotation = annotationMap(List.of(mirror));
              return annotation.isEmpty() ? null : annotation;
            }

            @Override
            public Object visitEnumConstant(VariableElement enumeration, Void unused) {
              var typeMirror = enumeration.asType();
              var element = types.asElement(typeMirror);
              return new EnumValue(element.toString(), enumeration.toString());
            }

            @Override
            public Object visitArray(List values, Void unused) {
              if (!values.isEmpty()) {
                var result = new ArrayList<>();
                for (var it : values) {
                  result.add(annotationValue(it));
                }
                return result;
              }
              return null;
            }
          },
          null);
    } catch (UnsupportedOperationException x) {
      // See https://github.com/jooby-project/jooby/issues/2417
      return null;
    }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy