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

io.avaje.jsonb.generator.APContext Maven / Gradle / Ivy

package io.avaje.jsonb.generator;

import static java.util.function.Predicate.not;

import java.io.*;
import java.net.URI;
import java.nio.file.Path;
import java.util.*;
import java.util.stream.Stream;

import javax.annotation.processing.*;
import javax.annotation.processing.Generated;
import javax.annotation.processing.Messager;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.element.Element;
import javax.lang.model.element.ModuleElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;
import javax.tools.Diagnostic;
import javax.tools.JavaFileObject;
import javax.tools.StandardLocation;

/**
 * Utiliy Class that stores the {@link ProcessingEnvironment} and provides various helper methods
 */
@Generated("avaje-prism-generator")
public final class APContext {

  private static int jdkVersion;
  private static boolean previewEnabled;
  private static final ThreadLocal CTX = new ThreadLocal<>();

  private APContext() {}

  public static final class Ctx {
    private final ProcessingEnvironment processingEnv;
    private final Messager messager;
    private final Filer filer;
    private final Elements elementUtils;
    private final Types typeUtils;
    private ModuleElement module;
    private final boolean isTestCompilation;
    private ModuleInfoReader moduleReader;

    private Ctx(ProcessingEnvironment processingEnv) {

      this.processingEnv = processingEnv;
      messager = processingEnv.getMessager();
      filer = processingEnv.getFiler();
      elementUtils = processingEnv.getElementUtils();
      typeUtils = processingEnv.getTypeUtils();
      boolean test;
      try {
        test =
            filer
                .createResource(StandardLocation.CLASS_OUTPUT, "", UUID.randomUUID().toString())
                .toUri()
                .toString()
                .contains("test-classes");

      } catch (Exception e) {
        test = false;
      }
      isTestCompilation = test;
    }
  }

  /**
   * Initialize the ThreadLocal containing the Processing Enviroment. this typically should be
   * called during the init phase of processing. Be sure to run the clear method at the last round
   * of processing
   *
   * @param processingEnv the current annotation processing enviroment
   */
  public static void init(ProcessingEnvironment processingEnv) {
    CTX.set(new Ctx(processingEnv));
    jdkVersion = processingEnv.getSourceVersion().ordinal();
    previewEnabled = jdkVersion >= 13 && initPreviewEnabled(processingEnv);
  }

  private static boolean initPreviewEnabled(ProcessingEnvironment processingEnv) {
    try {
      return (boolean)
          ProcessingEnvironment.class.getDeclaredMethod("isPreviewEnabled").invoke(processingEnv);
    } catch (final Throwable e) {
      return false;
    }
  }

  /**
   * Initialize the ThreadLocal containing the {@link ProcessingEnvironment}. Be sure to run the
   * clear method at the last round of processing
   *
   * @param context the current annotation processing enviroment
   * @param jdkVersion the JDK version number
   * @param preview whether preview features are enabled
   */
  public static void init(Ctx context, int jdkVersion, boolean preview) {
    CTX.set(context);
    APContext.jdkVersion = jdkVersion;
    previewEnabled = preview;
  }
  /** Clears the ThreadLocal containing the {@link ProcessingEnvironment}. */
  public static void clear() {
    CTX.remove();
  }

  /**
   * Returns the source version that any generated source and class files should conform to
   *
   * @return the source version as an int
   */
  public static int jdkVersion() {
    return jdkVersion;
  }

  /**
   * Returns whether {@code --preview-enabled} has been added to compiler flags.
   *
   * @return true if preview features are enabled
   */
  public static boolean previewEnabled() {
    return previewEnabled;
  }

  private static Ctx getCtx() {
    var ctx = CTX.get();
    if (ctx == null) {
      throw new IllegalStateException("APContext has not been initialized with APContext.init");
    }
    return CTX.get();
  }  /**
   * Prints an error at the location of the element.
   *
   * @param e the element to use as a position hint
   * @param msg the message, or an empty string if none
   * @param args {@code String#format} arguments
   */
  public static void logError(Element e, String msg, Object... args) {
    messager().printMessage(Diagnostic.Kind.ERROR, String.format(msg, args), e);
  }

  /**
   * Prints an error.
   *
   * @param msg the message, or an empty string if none
   * @param args {@code String#format} arguments
   */
  public static void logError(String msg, Object... args) {
    messager().printMessage(Diagnostic.Kind.ERROR, String.format(msg, args));
  }

  /**
   * Prints an warning at the location of the element.
   *
   * @param e the element to use as a position hint
   * @param msg the message, or an empty string if none
   * @param args {@code String#format} arguments
   */
  public static void logWarn(Element e, String msg, Object... args) {
    messager().printMessage(Diagnostic.Kind.WARNING, String.format(msg, args), e);
  }

  /**
   * Prints a warning.
   *
   * @param msg the message, or an empty string if none
   * @param args {@code String#format} arguments
   */
  public static void logWarn(String msg, Object... args) {
    messager().printMessage(Diagnostic.Kind.WARNING, String.format(msg, args));
  }

  /**
   * Prints a note.
   *
   * @param msg the message, or an empty string if none
   * @param args {@code String#format} arguments
   */
  public static void logNote(Element e, String msg, Object... args) {
    messager().printMessage(Diagnostic.Kind.NOTE, String.format(msg, args), e);
  }

  /**
   * Prints a note at the location of the element.
   *
   * @param e the element to use as a position hint
   * @param msg the message, or an empty string if none
   * @param args {@code String#format} arguments
   */
  public static void logNote(String msg, Object... args) {
    messager().printMessage(Diagnostic.Kind.NOTE, String.format(msg, args));
  }

  /**
   * Returns the elements annotated with the given annotation interface.
   *
   * @param round RoundEnviroment to extract the elements
   * @param annotationFQN the fqn of the annotation
   * @return the elements annotated with the given annotation interface,or an empty set if there are
   *     none
   */
  public static Set elementsAnnotatedWith(
      RoundEnvironment round, String annotationFQN) {

    return Optional.ofNullable(typeElement(annotationFQN))
        .map(round::getElementsAnnotatedWith)
        .orElse(Set.of());
  }

  /**
   * Create a file writer for the given class name.
   *
   * @param name canonical (fully qualified) name of the principal class or interface being declared
   *     in this file or a package name followed by {@code ".package-info"} for a package
   *     information file
   * @param originatingElements class, interface, package, or module elements causally associated
   *     with the creation of this file, may be elided or {@code null}
   * @return a JavaFileObject to write the new source file
   */
  public static JavaFileObject createSourceFile(CharSequence name, Element... originatingElements)
      throws IOException {
    return filer().createSourceFile(name, originatingElements);
  }

  /**
   * Returns a type element given its canonical name.
   *
   * @param name the canonical name
   * @return the named type element, or null if no type element can be uniquely determined
   */
  public static TypeElement typeElement(String name) {
    return elements().getTypeElement(name);
  }

  /**
   * Returns the element corresponding to a type.The type may be a DeclaredType or
   * TypeVariable.Returns null if the type is not one with a corresponding element.
   *
   * @param t the type to map to an element
   * @return the element corresponding to the given type
   */
  public static TypeElement asTypeElement(TypeMirror t) {

    return (TypeElement) types().asElement(t);
  }

  /**
   * Get current {@link ProcessingEnvironment}
   *
   * @return the enviroment
   */
  public static ProcessingEnvironment processingEnv() {
    return getCtx().processingEnv;
  }

  /**
   * Get current {@link Filer} from the {@link ProcessingEnvironment}
   *
   * @return the filer
   */
  public static Filer filer() {
    return getCtx().filer;
  }

  /**
   * Get current {@link Elements} from the {@link ProcessingEnvironment}
   *
   * @return the filer
   */
  public static Elements elements() {
    return getCtx().elementUtils;
  }

  /**
   * Get current {@link Messager} from the {@link ProcessingEnvironment}
   *
   * @return the messager
   */
  public static Messager messager() {
    return getCtx().messager;
  }

  /**
   * Get current {@link Types} from the {@link ProcessingEnvironment}
   *
   * @return the types
   */
  public static Types types() {
    return getCtx().typeUtils;
  }

  /**
   * Determine whether the first type can be assigned to the second
   *
   * @param type string type to check
   * @param superType the type that should be assignable to.
   * @return true if type can be assinged to supertype
   */
  public static boolean isAssignable(String type, String superType) {
    return type.equals(superType) || isAssignable(typeElement(type), superType);
  }

  /**
   * Determine whether the first type can be assigned to the second
   *
   * @param type type to check
   * @param superType the type that should be assignable to.
   * @return true if type can be assinged to supertype
   */
  public static boolean isAssignable(TypeElement type, String superType) {
    return Optional.ofNullable(type).stream()
        .flatMap(APContext::superTypes)
        .anyMatch(superType::equals);
  }

  private static Stream superTypes(TypeElement element) {
    final var types = types();
    return types.directSupertypes(element.asType()).stream()
        .filter(type -> !type.toString().contains("java.lang.Object"))
        .map(superType -> (TypeElement) types.asElement(superType))
        .flatMap(e -> Stream.concat(superTypes(e), Stream.of(e)))
        .map(Object::toString);
  }

  /**
   * Discover the {@link ModuleElement} for the project being processed and set in the context.
   *
   * @param annotations the annotation interfaces requested to be processed
   * @param roundEnv environment for information about the current and prior round
   */
  public static void setProjectModuleElement(
      Set annotations, RoundEnvironment roundEnv) {
    if (getCtx().module == null) {
      getCtx().module =
          annotations.stream()
              .map(roundEnv::getElementsAnnotatedWith)
              .filter(not(Collection::isEmpty))
              .findAny()
              .map(s -> s.iterator().next())
              .map(elements()::getModuleOf)
              .orElse(null);
    }
  }

  /**
   * Retrieve the project's {@link ModuleElement}. {@code setProjectModuleElement} must be called
   * before this.
   *
   * @return the {@link ModuleElement} associated with the current project
   */
  public static ModuleElement getProjectModuleElement() {
    return getCtx().module;
  }

  /** Retrieve the root module-info reader if it can be read */
  public static Optional moduleInfoReader() {
    if (getCtx().moduleReader == null) {
      try {
        getCtx().moduleReader = new ModuleInfoReader();
      } catch (Exception e) {
        // could not retrieve
      }
    }
    return Optional.ofNullable(getCtx().moduleReader);
  }

  /**
   * Gets a {@link BufferedReader} for the project's {@code module-info.java} source file.
   *
   * 

Calling {@link ModuleElement}'s {@code getDirectives()} method has a chance of making * compilation fail in certain situations. Therefore, manually parsing {@code module-info.java} * seems to be the safest way to get module information. * * @return * @throws IOException if unable to read the module-info */ public static BufferedReader getModuleInfoReader() throws IOException { var modulePath = isTestCompilation() ? "src/main/test" : "src/main/java"; // some JVM implementations do not implement SOURCE_PATH so gotta find the module path by trying // to find the src folder var path = Path.of(filer().createResource(StandardLocation.CLASS_OUTPUT, "", UUID.randomUUID().toString()).toUri()); var i = 0; while (i < 5 && path != null && !path.resolve(modulePath).toFile().exists()) { i++; path = path.getParent(); } var moduleFile = path.resolve(modulePath + "/module-info.java"); if (moduleFile.toFile().exists()) { return new BufferedReader(new InputStreamReader(moduleFile.toUri().toURL().openStream())); } // if that fails try via SOURCE_PATH var sourcePath = Path.of(filer().getResource(StandardLocation.SOURCE_PATH, "", "module-info.java").toUri()); return new BufferedReader(new InputStreamReader(sourcePath.toUri().toURL().openStream())); } /** * Given the relative path, gets a {@link Path} from the Maven {@code target}/Gradle {@code build} folder. * @param path the relative path of the file in the target/build folder * * @return the file object * @throws IOException if unable to retrieve the file */ public static Path getBuildResource(String path) throws IOException { var id = UUID.randomUUID().toString(); final var uri = filer() .createResource(StandardLocation.CLASS_OUTPUT, "", path + id) .toUri() .toString() .replaceFirst(id, "") .replaceFirst("/classes", "") .replaceFirst("/classes/java/main", ""); var updatedPath = Path.of(URI.create(uri)); if (path.contains("/")) { updatedPath.getParent().toFile().mkdirs(); } return updatedPath; } /** * Return true if the compiler is creating test classes. * * @return Whether the current apt compilation is for test-compile. */ public static boolean isTestCompilation() { return getCtx().isTestCompilation; }}





© 2015 - 2024 Weber Informatics LLC | Privacy Policy