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