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

io.avaje.inject.generator.InjectProcessor-e Maven / Gradle / Ivy

package io.avaje.inject.generator;

import io.avaje.prism.GenerateAPContext;
import io.avaje.prism.GenerateModuleInfoReader;
import io.avaje.prism.GenerateUtils;

import javax.annotation.processing.*;
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.util.ElementFilter;
import javax.lang.model.util.Elements;

import static java.util.stream.Collectors.joining;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.StandardOpenOption;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static io.avaje.inject.generator.APContext.*;
import static io.avaje.inject.generator.ProcessingContext.*;

@GenerateUtils
@GenerateAPContext
@GenerateModuleInfoReader
@SupportedOptions("mergeServices")
@SupportedAnnotationTypes({
  AspectImportPrism.PRISM_TYPE,
  AssistFactoryPrism.PRISM_TYPE,
  ComponentPrism.PRISM_TYPE,
  Constants.TESTSCOPE,
  ControllerPrism.PRISM_TYPE,
  ExternalPrism.PRISM_TYPE,
  FactoryPrism.PRISM_TYPE,
  ImportPrism.PRISM_TYPE,
  InjectModulePrism.PRISM_TYPE,
  PluginProvidesPrism.PRISM_TYPE,
  PrototypePrism.PRISM_TYPE,
  QualifierPrism.PRISM_TYPE,
  ScopePrism.PRISM_TYPE,
  SingletonPrism.PRISM_TYPE,
  ServiceProviderPrism.PRISM_TYPE
})
public final class InjectProcessor extends AbstractProcessor {

  private Elements elementUtils;
  private ScopeInfo defaultScope;
  private AllScopes allScopes;
  private boolean readModuleInfo;
  private final Set pluginFileProvided = new HashSet<>();
  private final Set moduleFileProvided = new HashSet<>();
  private final List moduleData = new ArrayList<>();

  @Override
  public SourceVersion getSupportedSourceVersion() {
    return SourceVersion.latest();
  }

  @Override
  public synchronized void init(ProcessingEnvironment processingEnv) {
    super.init(processingEnv);
    APContext.init(processingEnv);
    loadProvidedFiles();
    ProcessingContext.registerProvidedTypes(moduleFileProvided);
    moduleData.forEach(ProcessingContext::addModule);
    this.elementUtils = processingEnv.getElementUtils();
    this.allScopes = new AllScopes();
    this.defaultScope = allScopes.defaultScope();
    ExternalProvider.registerPluginProvidedTypes(defaultScope);
    pluginFileProvided.forEach(defaultScope::pluginProvided);

    // write a note in target so that other apts can know inject is running
    try {
      var file = APContext.getBuildResource("avaje-processors.txt");
      var addition = new StringBuilder();
      //if file exists, dedup and append current processor
      if (file.toFile().exists()) {
        var result = Stream.concat(Files.lines(file), Stream.of("avaje-inject-generator"))
          .distinct()
          .collect(joining("\n"));
        addition.append(result);
      } else {
        addition.append("avaje-inject-generator");
      }
      Files.writeString(file, addition.toString(), StandardOpenOption.CREATE, StandardOpenOption.WRITE);
    } catch (IOException e) {
      // not an issue worth failing over
    }
  }

  /**
   * Loads provider files generated by avaje-inject-maven-plugin
   */
  void loadProvidedFiles() {
    lines("avaje-module-dependencies.csv").stream()
      .filter(s -> s.contains("|") && !s.startsWith("External Module Type"))
      .distinct()
      .map(l -> l.split("\\|"))
      .map(ModuleData::of)
      .flatMap(Optional::stream)
      .forEach(m -> {
        ExternalProvider.registerExternalMetaData(m.name());
        ExternalProvider.readMetaDataProvides(moduleFileProvided);
        this.moduleData.add(m);
      });
    lines("avaje-plugins.csv").stream()
      .filter(s -> s.contains("|") && !s.startsWith("External Plugin Type"))
      .distinct()
      .map(l -> l.split("\\|")[1])
      .forEach(pluginFileProvided::add);
  }

  private List lines(String relativeName) {
    try {
      return Files.readAllLines(APContext.getBuildResource(relativeName));
    } catch (final Exception e) {
      return Collections.emptyList();
    }
  }

  @Override
  public boolean process(Set annotations, RoundEnvironment roundEnv) {
    APContext.setProjectModuleElement(annotations, roundEnv);
    readModule(roundEnv);

    final var processingOver = roundEnv.processingOver();
    ProcessingContext.processingOver(processingOver);

    readBeans(delayedElements());
    addImportedAspects(importedAspects(roundEnv));
    maybeElements(roundEnv, QualifierPrism.PRISM_TYPE).stream()
      .flatMap(Set::stream)
      .flatMap(e -> ElementFilter.methodsIn(e.getEnclosedElements()).stream())
      .forEach(this::validateQualifier);

    maybeElements(roundEnv, ScopePrism.PRISM_TYPE).ifPresent(this::readScopes);
    maybeElements(roundEnv, FactoryPrism.PRISM_TYPE).ifPresent(this::readFactories);

    if (defaultScope.includeSingleton()) {
      maybeElements(roundEnv, SingletonPrism.PRISM_TYPE).ifPresent(this::readBeans);
    }
    maybeElements(roundEnv, ComponentPrism.PRISM_TYPE).ifPresent(this::readBeans);
    maybeElements(roundEnv, PrototypePrism.PRISM_TYPE).ifPresent(this::readBeans);

    readImported(importedElements(roundEnv));

    maybeElements(roundEnv, ControllerPrism.PRISM_TYPE).ifPresent(this::readBeans);
    maybeElements(roundEnv, ProxyPrism.PRISM_TYPE).ifPresent(this::readBeans);
    maybeElements(roundEnv, AssistFactoryPrism.PRISM_TYPE).ifPresent(this::readAssisted);

    maybeElements(roundEnv, ExternalPrism.PRISM_TYPE).stream()
      .flatMap(Set::stream)
      .forEach(e -> {
        var type = UType.parse(e.asType());
        type = "java.util.List".equals(type.mainType()) ? type.param0() : type;
        ProcessingContext.addOptionalType(type.fullWithoutAnnotations(), Util.named(e));
        ProcessingContext.addOptionalType(type.fullWithoutAnnotations(), null);
      });

    maybeElements(roundEnv, ServiceProviderPrism.PRISM_TYPE).ifPresent(this::registerSPI);
    maybeElements(roundEnv, PluginProvidesPrism.PRISM_TYPE).ifPresent(this::registerSPI);
    allScopes.readBeans(roundEnv);
    defaultScope.write(processingOver);
    allScopes.write(processingOver);

    if (processingOver) {
      var order =
        new FactoryOrder(ProcessingContext.modules(), defaultScope.pluginProvided())
          .orderModules();

      if (ProcessingContext.strictWiring()) {
        try {
          new SimpleOrderWriter(order, defaultScope).write();
        } catch (IOException e) {
          logError("FilerException trying to write wiring order class %s", e.getMessage());
        }
      }
      ProcessingContext.writeSPIServicesFile();
      ProcessingContext.validateModule();
      ProcessingContext.clear();
    }
    return false;
  }

  private void validateQualifier(ExecutableElement method) {
    var type = APContext.asTypeElement(method.getReturnType());
    if (type == null || type.getKind() != ElementKind.ANNOTATION_TYPE) {
      return;
    }

    var enclosedMethods = ElementFilter.methodsIn(type.getEnclosedElements());
    if (enclosedMethods.size() > 1) {
      APContext.logError(method, "Qualifier annotation members can only have a single attribute");
    }
    enclosedMethods.forEach(this::validateQualifier);
  }

  // Optional because these annotations are not guaranteed to exist
  private static Optional> maybeElements(RoundEnvironment round, String name) {
    return Optional.ofNullable(typeElement(name)).map(round::getElementsAnnotatedWith);
  }

  private Set importedElements(RoundEnvironment roundEnv) {
    return maybeElements(roundEnv, ImportPrism.PRISM_TYPE).stream()
      .flatMap(Set::stream)
      .map(ImportPrism::getInstanceOn)
      .flatMap(p -> p.value().stream())
      .map(ProcessingContext::asElement)
      .filter(this::notAlreadyProvided)
      .collect(Collectors.toSet());
  }

  private boolean notAlreadyProvided(TypeElement e) {
    final String type = e.getQualifiedName().toString();
    return !moduleFileProvided.contains(type) && !pluginFileProvided.contains(type);
  }

  private static Map importedAspects(RoundEnvironment roundEnv) {
    return maybeElements(roundEnv, AspectImportPrism.PRISM_TYPE).stream()
      .flatMap(Set::stream)
      .map(AspectImportPrism::getAllInstancesOn)
      .flatMap(List::stream)
      .collect(Collectors.toMap(p -> p.value().toString(), p -> p));
  }

  private void readScopes(Set scopes) {
    for (final Element element : scopes) {
      if ((element.getKind() == ElementKind.ANNOTATION_TYPE) && (element instanceof TypeElement)) {
        final var type = (TypeElement) element;
        allScopes.addScopeAnnotation(type);
      }
    }
    addTestScope();
  }

  /**
   * Add built-in test scope for @TestScope if available.
   */
  private void addTestScope() {
    final var testScopeType = elementUtils.getTypeElement(Constants.TESTSCOPE);
    if (testScopeType != null) {
      allScopes.addScopeAnnotation(testScopeType);
    }
  }

  private void readFactories(Set beans) {
    readChangedBeans(ElementFilter.typesIn(beans), true, false);
  }

  private void readAssisted(Set beans) {
    ElementFilter.typesIn(beans).forEach(t -> {
      var reader = new AssistBeanReader(t);
      try {
        new SimpleAssistWriter(reader).write();
      } catch (IOException e) {
        e.printStackTrace();
      }
    });
  }

  private void readBeans(Set beans) {
    readChangedBeans(ElementFilter.typesIn(beans), false, false);
  }

  private void readImported(Set beans) {
    readChangedBeans(ElementFilter.typesIn(beans), false, true);
  }

  /**
   * Read the beans that have changed.
   */
  private void readChangedBeans(Set beans, boolean factory, boolean importedComponent) {
    for (final var typeElement : beans) {
      if (typeElement.getKind() == ElementKind.INTERFACE) {
        continue;
      }
      final var scope = findScope(typeElement);
      if (!factory) {
        // will be found via custom scope so effectively ignore additional @Singleton
        if (scope == null) {
          defaultScope.read(typeElement, false, importedComponent);
        }
      } else if (scope != null) {
        scope.read(typeElement, true, false);
      } else {
        defaultScope.read(typeElement, true, false);
      }
    }
  }

  /**
   * Find the scope if the Factory has a scope annotation.
   */
  private ScopeInfo findScope(Element element) {
    for (final AnnotationMirror annotationMirror : element.getAnnotationMirrors()) {
      final var scopeInfo = allScopes.get(annotationMirror.getAnnotationType().toString());
      if (scopeInfo != null) {
        return scopeInfo;
      }
    }
    return null;
  }

  /**
   * Read the existing meta-data from InjectModule (if found) and the factory bean (if exists).
   */
  private void readModule(RoundEnvironment roundEnv) {
    if (readModuleInfo) {
      // only read the module meta data once
      return;
    }
    readModuleInfo = true;
    final var factory = loadMetaInfServices();
    if (factory != null) {
      final var moduleType = elementUtils.getTypeElement(factory);
      if (moduleType != null) {
        defaultScope.readModuleMetaData(moduleType);
      }
    }
    allScopes.readModules(loadMetaInfCustom());
    readInjectModule(roundEnv);
  }

  /** Read InjectModule for things like package-info etc (not for custom scopes) */
  private void readInjectModule(RoundEnvironment roundEnv) {
    // read other that are annotated with InjectModule
    maybeElements(roundEnv, InjectModulePrism.PRISM_TYPE).stream()
      .flatMap(Set::stream)
      .forEach(element -> {
        final var scope = ScopePrism.getInstanceOn(element);
        if (scope == null) {
          // it it not a custom scope annotation
          final var annotation = InjectModulePrism.getInstanceOn(element);
          if (annotation != null) {
            defaultScope.details(annotation.name(), element);
            ProcessingContext.strictWiring(annotation.strictWiring());
          }
        }
      });
  }

  private void registerSPI(Set beans) {
    ElementFilter.typesIn(beans).stream()
      .filter(this::isExtension)
      .map(TypeElement::getQualifiedName)
      .map(Object::toString)
      .forEach(ProcessingContext::addInjectSPI);
  }

  private boolean isExtension(TypeElement te) {
    PluginProvidesPrism.getOptionalOn(te).ifPresent(t -> {
      if (!APContext.isAssignable(te, "io.avaje.inject.spi.InjectPlugin")) {
        APContext.logError(te, "PluginProvides can only be placed on io.avaje.inject.spi.InjectPlugin");
      }
    });
    ServiceProviderPrism.getOptionalOn(te).ifPresent(t -> {
      if (APContext.isAssignable(te, "io.avaje.inject.spi.InjectPlugin")) {
        APContext.logWarn(te, "PluginProvides should be used to auto register InjectPlugins");
      }
    });

    return APContext.isAssignable(te, "io.avaje.inject.spi.InjectExtension");
  }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy