001package io.avaje.inject.generator;
002
003import io.avaje.inject.InjectModule;
004import io.avaje.inject.Factory;
005import io.avaje.inject.Request;
006import io.avaje.inject.spi.DependencyMeta;
007
008import javax.annotation.processing.AbstractProcessor;
009import javax.annotation.processing.FilerException;
010import javax.annotation.processing.ProcessingEnvironment;
011import javax.annotation.processing.RoundEnvironment;
012import jakarta.inject.Singleton;
013import javax.lang.model.SourceVersion;
014import javax.lang.model.element.*;
015import javax.lang.model.util.Elements;
016import java.io.IOException;
017import java.util.ArrayList;
018import java.util.Collections;
019import java.util.HashSet;
020import java.util.Iterator;
021import java.util.LinkedHashMap;
022import java.util.LinkedHashSet;
023import java.util.List;
024import java.util.Map;
025import java.util.Set;
026
027public class Processor extends AbstractProcessor {
028
029  private static final String INJECT_MODULE = "io.avaje.inject.InjectModule";
030
031  private ProcessingContext context;
032
033  private Elements elementUtils;
034
035  /**
036   * Map to merge the existing meta data with partially compiled code. Keyed by type and qualifier/name.
037   */
038  private final Map<String, MetaData> metaData = new LinkedHashMap<>();
039
040  private final List<BeanReader> beanReaders = new ArrayList<>();
041
042  private final Set<String> readBeans = new HashSet<>();
043
044  public Processor() {
045  }
046
047  @Override
048  public SourceVersion getSupportedSourceVersion() {
049    return SourceVersion.latest();
050  }
051
052  @Override
053  public synchronized void init(ProcessingEnvironment processingEnv) {
054    super.init(processingEnv);
055    this.context = new ProcessingContext(processingEnv);
056    this.elementUtils = processingEnv.getElementUtils();
057  }
058
059  @Override
060  public Set<String> getSupportedAnnotationTypes() {
061
062    Set<String> annotations = new LinkedHashSet<>();
063    annotations.add(InjectModule.class.getCanonicalName());
064    annotations.add(Factory.class.getCanonicalName());
065    annotations.add(Singleton.class.getCanonicalName());
066    annotations.add(Constants.CONTROLLER);
067    return annotations;
068  }
069
070  @Override
071  public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
072
073    Set<? extends Element> controllers = Collections.emptySet();
074    TypeElement typeElement = elementUtils.getTypeElement(Constants.CONTROLLER);
075    if (typeElement != null) {
076      controllers = roundEnv.getElementsAnnotatedWith(typeElement);
077    }
078
079    Set<? extends Element> factoryBeans = roundEnv.getElementsAnnotatedWith(Factory.class);
080    Set<? extends Element> beans = roundEnv.getElementsAnnotatedWith(Singleton.class);
081    Set<? extends Element> requestBeans = roundEnv.getElementsAnnotatedWith(Request.class);
082
083    readModule(roundEnv);
084    readChangedBeans(factoryBeans, true);
085    readChangedBeans(beans, false);
086    readChangedBeans(controllers, false);
087    readChangedBeans(requestBeans, false);
088
089    mergeMetaData();
090
091    writeBeanHelpers();
092    if (roundEnv.processingOver()) {
093      writeBeanFactory();
094    }
095    return false;
096  }
097
098  private void writeBeanHelpers() {
099    for (BeanReader beanReader : beanReaders) {
100      try {
101        if (!beanReader.isWrittenToFile()) {
102          SimpleBeanWriter writer = new SimpleBeanWriter(beanReader, context);
103          writer.write();
104          beanReader.setWrittenToFile();
105        }
106      } catch (FilerException e) {
107        context.logWarn("FilerException to write $DI class " + beanReader.getBeanType() + " " + e.getMessage());
108
109      } catch (IOException e) {
110        e.printStackTrace();
111        context.logError(beanReader.getBeanType(), "Failed to write $DI class");
112      }
113    }
114  }
115
116  private void writeBeanFactory() {
117    MetaDataOrdering ordering = new MetaDataOrdering(metaData.values(), context);
118    int remaining = ordering.processQueue();
119    if (remaining > 0) {
120      ordering.logWarnings();
121    }
122
123    try {
124      SimpleFactoryWriter factoryWriter = new SimpleFactoryWriter(ordering, context);
125      factoryWriter.write();
126    } catch (FilerException e) {
127      context.logWarn("FilerException trying to write factory " + e.getMessage());
128    } catch (IOException e) {
129      context.logError("Failed to write factory " + e.getMessage());
130    }
131  }
132
133  /**
134   * Read the beans that have changed.
135   */
136  private void readChangedBeans(Set<? extends Element> beans, boolean factory) {
137    for (Element element : beans) {
138      if (!(element instanceof TypeElement)) {
139        context.logError("unexpected type [" + element + "]");
140      } else {
141        if (readBeans.add(element.toString())) {
142          readBeanMeta((TypeElement) element, factory);
143        } else {
144          context.logDebug("skipping already processed bean " + element);
145        }
146      }
147    }
148  }
149
150  /**
151   * Merge the changed bean meta data into the existing (factory) metaData.
152   */
153  private void mergeMetaData() {
154    for (BeanReader beanReader : beanReaders) {
155      if (!beanReader.isRequestScopedController()) {
156        MetaData metaData = this.metaData.get(beanReader.getMetaKey());
157        if (metaData == null) {
158          addMeta(beanReader);
159        } else {
160          updateMeta(metaData, beanReader);
161        }
162      }
163    }
164  }
165
166  /**
167   * Add a new previously unknown bean.
168   */
169  private void addMeta(BeanReader beanReader) {
170    MetaData meta = beanReader.createMeta();
171    metaData.put(meta.getKey(), meta);
172    for (MetaData methodMeta : beanReader.createFactoryMethodMeta()) {
173      metaData.put(methodMeta.getKey(), methodMeta);
174    }
175  }
176
177  /**
178   * Update the meta data on a previously known bean.
179   */
180  private void updateMeta(MetaData metaData, BeanReader beanReader) {
181    metaData.update(beanReader);
182  }
183
184  /**
185   * Read the dependency injection meta data for the given bean.
186   */
187  private void readBeanMeta(TypeElement typeElement, boolean factory) {
188    if (typeElement.getKind() == ElementKind.ANNOTATION_TYPE) {
189      context.logDebug("skipping annotation type " + typeElement);
190      return;
191    }
192    beanReaders.add(new BeanReader(typeElement, context, factory).read());
193  }
194
195  /**
196   * Read the existing meta data from InjectModule (if found) and the factory bean (if exists).
197   */
198  private void readModule(RoundEnvironment roundEnv) {
199    String factory = context.loadMetaInfServices();
200    if (factory != null) {
201      TypeElement factoryType = elementUtils.getTypeElement(factory);
202      if (factoryType != null) {
203        readFactory(factoryType);
204      }
205    }
206
207    Set<? extends Element> elementsAnnotatedWith = roundEnv.getElementsAnnotatedWith(InjectModule.class);
208    if (!elementsAnnotatedWith.isEmpty()) {
209      Iterator<? extends Element> iterator = elementsAnnotatedWith.iterator();
210      if (iterator.hasNext()) {
211        Element element = iterator.next();
212        InjectModule annotation = element.getAnnotation(InjectModule.class);
213        if (annotation != null) {
214          context.setContextDetails(annotation.name(), annotation.provides(), annotation.dependsOn(), element);
215          context.setContextRequires(readRequires(element));
216        }
217      }
218    }
219  }
220
221  /**
222   * Read the list of required class names.
223   */
224  private List<String> readRequires(Element element) {
225    List<String> requiresList = new ArrayList<>();
226    for (AnnotationMirror annotationMirror : element.getAnnotationMirrors()) {
227      if (INJECT_MODULE.equals(annotationMirror.getAnnotationType().toString())) {
228        for (Map.Entry<? extends ExecutableElement, ? extends AnnotationValue> entry : annotationMirror.getElementValues().entrySet()) {
229          if (entry.getKey().toString().startsWith("requires")) {
230            for (Object requiresType : (List<?>) entry.getValue().getValue()) {
231              String fullName = requiresType.toString();
232              fullName = fullName.substring(0, fullName.length() - 6);
233              requiresList.add(fullName);
234            }
235          }
236        }
237      }
238    }
239    return requiresList;
240  }
241
242
243  /**
244   * Read the existing factory bean. Each of the build methods is annotated with <code>@DependencyMeta</code>
245   * which holds the information we need (to regenerate the factory with any changes).
246   */
247  private void readFactory(TypeElement factoryType) {
248    InjectModule module = factoryType.getAnnotation(InjectModule.class);
249    context.setContextDetails(module.name(), module.provides(), module.dependsOn(), factoryType);
250    context.setContextRequires(readRequires(factoryType));
251
252    List<? extends Element> elements = factoryType.getEnclosedElements();
253    if (elements != null) {
254      for (Element element : elements) {
255        if (ElementKind.METHOD == element.getKind()) {
256          readBuildMethodDependencyMeta(element);
257        }
258      }
259    }
260  }
261
262  private void readBuildMethodDependencyMeta(Element element) {
263    Name simpleName = element.getSimpleName();
264    if (simpleName.toString().startsWith("build_")) {
265      // read a build method - DependencyMeta
266      DependencyMeta meta = element.getAnnotation(DependencyMeta.class);
267      if (meta == null) {
268        context.logError("Missing @DependencyMeta on method " + simpleName);
269      } else {
270        final MetaData metaData = new MetaData(meta);
271        this.metaData.put(metaData.getKey(), metaData);
272      }
273    }
274  }
275}