diff --git a/README.md b/README.md
index 2dc34e4cc..d730401f8 100644
--- a/README.md
+++ b/README.md
@@ -8,8 +8,7 @@ module org.example {
requires io.avaje.inject;
- // register org.example._DI$BeanScopeFactory from generated sources
- provides io.avaje.inject.spi.BeanScopeFactory with org.example._DI$BeanScopeFactory;
+ provides io.avaje.inject.spi.Module with org.example.ExampleModule;
}
```
diff --git a/inject-generator/pom.xml b/inject-generator/pom.xml
index c01c8c93d..509f6c301 100644
--- a/inject-generator/pom.xml
+++ b/inject-generator/pom.xml
@@ -4,7 +4,7 @@
io.avajeavaje-inject-parent
- 6.2
+ 6.5-RC0avaje-inject-generator
@@ -16,7 +16,7 @@
io.avajeavaje-inject
- 6.2
+ 6.5-RC0
diff --git a/inject-generator/src/main/java/io/avaje/inject/generator/AllScopes.java b/inject-generator/src/main/java/io/avaje/inject/generator/AllScopes.java
new file mode 100644
index 000000000..e9ce5634a
--- /dev/null
+++ b/inject-generator/src/main/java/io/avaje/inject/generator/AllScopes.java
@@ -0,0 +1,124 @@
+package io.avaje.inject.generator;
+
+import io.avaje.inject.InjectModule;
+
+import javax.annotation.processing.RoundEnvironment;
+import javax.lang.model.element.Element;
+import javax.lang.model.element.TypeElement;
+import javax.tools.FileObject;
+import java.io.IOException;
+import java.io.Writer;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+class AllScopes {
+
+ private final Map scopeAnnotations = new HashMap<>();
+ private final ProcessingContext context;
+ private final ScopeInfo defaultScope;
+
+ AllScopes(ProcessingContext context) {
+ this.context = context;
+ this.defaultScope = new ScopeInfo(context);
+ }
+
+ ScopeInfo defaultScope() {
+ return defaultScope;
+ }
+
+ ScopeInfo addScopeAnnotation(TypeElement type) {
+ final Data data = new Data(type, context, this);
+ scopeAnnotations.put(type.getQualifiedName().toString(), data);
+ return data.scopeInfo;
+ }
+
+ boolean providedByDefaultModule(String dependency) {
+ return defaultScope.providesDependency(dependency);
+ }
+
+ void readBeans(RoundEnvironment roundEnv) {
+ for (Data data : scopeAnnotations.values()) {
+ for (Element customBean : roundEnv.getElementsAnnotatedWith(data.type)) {
+ // context.logWarn("read custom scope bean " + customBean + " for scope " + entry.getKey());
+ data.scopeInfo.read((TypeElement) customBean, false);
+ }
+ }
+ }
+
+ void write(boolean processingOver) {
+ for (Data value : scopeAnnotations.values()) {
+ value.write(processingOver);
+ }
+ if (processingOver) {
+ writeModuleCustomServicesFile();
+ }
+ }
+
+ private void writeModuleCustomServicesFile() {
+ if (scopeAnnotations.isEmpty()) {
+ return;
+ }
+ try {
+ FileObject jfo = context.createMetaInfModuleCustom();
+ if (jfo != null) {
+ Writer writer = jfo.openWriter();
+ for (Data value : scopeAnnotations.values()) {
+ writer.write(value.moduleFullName());
+ writer.write("\n");
+ }
+ writer.close();
+ }
+
+ } catch (IOException e) {
+ e.printStackTrace();
+ context.logError("Failed to write services file " + e.getMessage());
+ }
+ }
+
+ void readModules(List customScopeModules) {
+ for (String customScopeModule : customScopeModules) {
+ final TypeElement module = context.element(customScopeModule);
+ if (module != null) {
+ final InjectModule injectModule = module.getAnnotation(InjectModule.class);
+ if (injectModule != null) {
+ final String customScopeType = injectModule.customScopeType();
+ final TypeElement scopeType = context.element(customScopeType);
+ if (scopeType == null) {
+ context.logError(module, "customScopeType [" + customScopeType + "] is invalid? on " + module);
+ } else {
+ final ScopeInfo scopeInfo = addScopeAnnotation(scopeType);
+ scopeInfo.readModuleMetaData(module);
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Find the scope by scope annotation type.
+ */
+ ScopeInfo get(String fullType) {
+ final Data data = scopeAnnotations.get(fullType);
+ return data == null ? null : data.scopeInfo;
+ }
+
+ static class Data {
+ final TypeElement type;
+ final ScopeInfo scopeInfo;
+
+ Data(TypeElement type, ProcessingContext context, AllScopes allScopes) {
+ this.type = type;
+ this.scopeInfo = new ScopeInfo(context, type, allScopes);
+ this.scopeInfo.details(null, type);
+ }
+
+ void write(boolean processingOver) {
+ scopeInfo.write(processingOver);
+ }
+
+ String moduleFullName() {
+ return scopeInfo.moduleFullName();
+ }
+ }
+}
diff --git a/inject-generator/src/main/java/io/avaje/inject/generator/BeanReader.java b/inject-generator/src/main/java/io/avaje/inject/generator/BeanReader.java
index a63fb7724..b3c82d17f 100644
--- a/inject-generator/src/main/java/io/avaje/inject/generator/BeanReader.java
+++ b/inject-generator/src/main/java/io/avaje/inject/generator/BeanReader.java
@@ -148,9 +148,6 @@ boolean isExtraInjectionRequired() {
}
void buildAddFor(Append writer) {
- if (requestParams.isRequestParam()) {
- context.logError(beanType, "@Singleton %s is not allowed to have a @Request scope dependency %s", shortName, requestParams.getRequestParamType());
- }
writer.append(" if (builder.isAddBeanFor(");
if (name != null && !name.isEmpty()) {
writer.append("\"%s\", ", name);
diff --git a/inject-generator/src/main/java/io/avaje/inject/generator/BeanRequestParams.java b/inject-generator/src/main/java/io/avaje/inject/generator/BeanRequestParams.java
index a15b5614f..3c878ec1b 100644
--- a/inject-generator/src/main/java/io/avaje/inject/generator/BeanRequestParams.java
+++ b/inject-generator/src/main/java/io/avaje/inject/generator/BeanRequestParams.java
@@ -1,8 +1,6 @@
package io.avaje.inject.generator;
-import io.avaje.inject.Request;
-import javax.lang.model.element.TypeElement;
import java.util.Set;
/**
@@ -15,7 +13,6 @@ class BeanRequestParams {
private final boolean requestScopedBean;
private RequestScope.Handler reqScopeHandler;
- private String requestParamType;
BeanRequestParams(ProcessingContext context, String parentType, boolean requestScopedBean) {
this.context = context;
@@ -23,14 +20,6 @@ class BeanRequestParams {
this.requestScopedBean = requestScopedBean;
}
- boolean isRequestParam() {
- return requestParamType != null;
- }
-
- String getRequestParamType() {
- return requestParamType;
- }
-
/**
* Return true if this type is a request scoped type (e.g. Javalin Context).
*/
@@ -45,14 +34,6 @@ boolean check(String paramType) {
}
return true;
}
- if (paramType != null && requestParamType == null) {
- final TypeElement element = context.element(paramType);
- if (element != null) {
- if (element.getAnnotation(Request.class) != null) {
- requestParamType = paramType;
- }
- }
- }
return false;
}
diff --git a/inject-generator/src/main/java/io/avaje/inject/generator/Constants.java b/inject-generator/src/main/java/io/avaje/inject/generator/Constants.java
index ec898ce96..492cfcc87 100644
--- a/inject-generator/src/main/java/io/avaje/inject/generator/Constants.java
+++ b/inject-generator/src/main/java/io/avaje/inject/generator/Constants.java
@@ -20,7 +20,8 @@ class Constants {
static final String AT_SINGLETON = "@Singleton";
static final String AT_GENERATED = "@Generated(\"io.avaje.inject.generator\")";
- static final String META_INF_FACTORY = "META-INF/services/io.avaje.inject.spi.BeanScopeFactory";
+ static final String META_INF_FACTORY = "META-INF/services/io.avaje.inject.spi.Module";
+ static final String META_INF_CUSTOM = "META-INF/services/io.avaje.inject.spi.Module.Custom";
static final String REQUESTSCOPE = "io.avaje.inject.RequestScope";
static final String BEANCONTEXT = "io.avaje.inject.BeanScope";
@@ -31,5 +32,5 @@ class Constants {
static final String BEAN_FACTORY2 = "io.avaje.inject.spi.BeanFactory2";
static final String BUILDER = "io.avaje.inject.spi.Builder";
static final String DEPENDENCYMETA = "io.avaje.inject.spi.DependencyMeta";
- static final String BEANSCOPEFACTORY = "io.avaje.inject.spi.BeanScopeFactory";
+ static final String MODULE = "io.avaje.inject.spi.Module";
}
diff --git a/inject-generator/src/main/java/io/avaje/inject/generator/MetaData.java b/inject-generator/src/main/java/io/avaje/inject/generator/MetaData.java
index c331cda26..303b0b6dd 100644
--- a/inject-generator/src/main/java/io/avaje/inject/generator/MetaData.java
+++ b/inject-generator/src/main/java/io/avaje/inject/generator/MetaData.java
@@ -17,7 +17,7 @@ class MetaData {
private final String type;
private final String shortType;
private final String name;
-
+ private final List externallyProvided = new ArrayList<>();
private String method;
private boolean wired;
private boolean requestScope;
@@ -229,4 +229,7 @@ void setMethod(String method) {
this.method = method;
}
+ void externallyProvided(String dependency) {
+ externallyProvided.add(dependency);
+ }
}
diff --git a/inject-generator/src/main/java/io/avaje/inject/generator/MetaDataOrdering.java b/inject-generator/src/main/java/io/avaje/inject/generator/MetaDataOrdering.java
index 162408fed..6fbd6cc38 100644
--- a/inject-generator/src/main/java/io/avaje/inject/generator/MetaDataOrdering.java
+++ b/inject-generator/src/main/java/io/avaje/inject/generator/MetaDataOrdering.java
@@ -11,21 +11,17 @@ class MetaDataOrdering {
"\n See https://avaje.io/inject/#circular";
private final ProcessingContext context;
-
+ private final ScopeInfo scopeInfo;
private final List orderedList = new ArrayList<>();
private final List requestScope = new ArrayList<>();
private final List queue = new ArrayList<>();
-
private final Map providers = new HashMap<>();
-
private final List circularDependencies = new ArrayList<>();
-
private final Set missingDependencyTypes = new LinkedHashSet<>();
- private String topPackage;
-
- MetaDataOrdering(Collection values, ProcessingContext context) {
+ MetaDataOrdering(Collection values, ProcessingContext context, ScopeInfo scopeInfo) {
this.context = context;
+ this.scopeInfo = scopeInfo;
for (MetaData metaData : values) {
if (metaData.isRequestScope()) {
// request scoped expected to have externally provided dependencies
@@ -37,21 +33,20 @@ class MetaDataOrdering {
} else {
queue.add(metaData);
}
- topPackage = Util.commonParent(topPackage, metaData.getTopPackage());
// register into map keyed by provider
providers.computeIfAbsent(metaData.getType(), s -> new ProviderList()).add(metaData);
for (String provide : metaData.getProvides()) {
providers.computeIfAbsent(provide, s -> new ProviderList()).add(metaData);
}
}
- externallyRequiredDependencies(context);
+ externallyRequiredDependencies();
}
/**
* These if defined are expected to be required at wiring time probably via another module.
*/
- private void externallyRequiredDependencies(ProcessingContext context) {
- for (String requireType : context.contextRequires()) {
+ private void externallyRequiredDependencies() {
+ for (String requireType : scopeInfo.requires()) {
providers.computeIfAbsent(requireType, s -> new ProviderList());
}
}
@@ -120,14 +115,8 @@ private void errorOnCircularDependencies() {
* Build list of specific dependencies that are missing.
*/
void missingDependencies() {
- for (MetaData m : queue) {
- for (String dependency : m.getDependsOn()) {
- if (providers.get(dependency) == null) {
- TypeElement element = context.elementMaybe(m.getType());
- context.logError(element, "No dependency provided for " + dependency);
- missingDependencyTypes.add(dependency);
- }
- }
+ for (MetaData metaData : queue) {
+ checkMissingDependencies(metaData);
}
if (missingDependencyTypes.isEmpty()) {
// only look for circular dependencies if there are no missing dependencies
@@ -135,17 +124,29 @@ void missingDependencies() {
}
}
+ private void checkMissingDependencies(MetaData metaData) {
+ for (String dependency : metaData.getDependsOn()) {
+ if (providers.get(dependency) == null && !scopeInfo.providedByOtherModule(dependency)) {
+ TypeElement element = context.elementMaybe(metaData.getType());
+ context.logError(element, "No dependency provided for " + dependency + " on " + metaData.getType());
+ missingDependencyTypes.add(dependency);
+ }
+ }
+ }
+
/**
* Log a warning on unsatisfied dependencies that are expected to be provided by another module.
*/
private void warnOnDependencies() {
- if (missingDependencyTypes.isEmpty()) {
- context.logWarn("There are " + queue.size() + " beans with unsatisfied dependencies (assuming external dependencies)");
- for (MetaData m : queue) {
- context.logWarn("Unsatisfied dependencies on %s dependsOn %s", m, m.getDependsOn());
- }
- } else {
+ if (!missingDependencyTypes.isEmpty()) {
context.logError("Dependencies %s are not provided - missing @Singleton or @Factory/@Bean or specify external dependency via @InjectModule requires attribute", missingDependencyTypes);
+ } else {
+ if (!queue.isEmpty()) {
+ context.logWarn("There are " + queue.size() + " beans with unsatisfied dependencies (assuming external dependencies)");
+ for (MetaData m : queue) {
+ context.logWarn("Unsatisfied dependencies on %s dependsOn %s", m, m.getDependsOn());
+ }
+ }
}
}
@@ -157,10 +158,6 @@ void logWarnings() {
}
}
- String getTopPackage() {
- return topPackage;
- }
-
private int processQueueRound() {
// loop queue looking for entry that has all provides marked as included
int count = 0;
@@ -183,8 +180,7 @@ private boolean allDependenciesWired(MetaData queuedMeta) {
// check non-provider dependency is satisfied
ProviderList providerList = providers.get(dependency);
if (providerList == null) {
- // dependency not yet satisfied
- return false;
+ return scopeInfo.providedByOtherModule(dependency);
} else {
if (!providerList.isAllWired()) {
return false;
diff --git a/inject-generator/src/main/java/io/avaje/inject/generator/MetaTopPackage.java b/inject-generator/src/main/java/io/avaje/inject/generator/MetaTopPackage.java
new file mode 100644
index 000000000..844154a9a
--- /dev/null
+++ b/inject-generator/src/main/java/io/avaje/inject/generator/MetaTopPackage.java
@@ -0,0 +1,22 @@
+package io.avaje.inject.generator;
+
+import java.util.Collection;
+
+class MetaTopPackage {
+
+ private String topPackage;
+
+ static String of(Collection values) {
+ return new MetaTopPackage(values).value();
+ }
+
+ private String value() {
+ return topPackage;
+ }
+
+ private MetaTopPackage(Collection values) {
+ for (MetaData metaData : values) {
+ topPackage = Util.commonParent(topPackage, metaData.getTopPackage());
+ }
+ }
+}
diff --git a/inject-generator/src/main/java/io/avaje/inject/generator/ProcessingContext.java b/inject-generator/src/main/java/io/avaje/inject/generator/ProcessingContext.java
index d9ed79d23..93c810610 100644
--- a/inject-generator/src/main/java/io/avaje/inject/generator/ProcessingContext.java
+++ b/inject-generator/src/main/java/io/avaje/inject/generator/ProcessingContext.java
@@ -19,9 +19,7 @@
import java.io.LineNumberReader;
import java.io.Reader;
import java.nio.file.NoSuchFileException;
-import java.util.LinkedHashSet;
-import java.util.List;
-import java.util.Set;
+import java.util.*;
class ProcessingContext {
@@ -31,13 +29,6 @@ class ProcessingContext {
private final Elements elementUtils;
private final Types typeUtils;
- private String contextName;
- private String[] contextProvides;
- private String[] contextDependsOn;
- private Set contextRequires = new LinkedHashSet<>();
- private String contextPackage;
- private String metaInfServicesLine;
-
ProcessingContext(ProcessingEnvironment processingEnv) {
this.processingEnv = processingEnv;
this.messager = processingEnv.getMessager();
@@ -66,23 +57,29 @@ void logDebug(String msg, Object... args) {
}
String loadMetaInfServices() {
- if (metaInfServicesLine == null) {
- metaInfServicesLine = loadMetaInf();
- }
- return metaInfServicesLine;
+ final List lines = loadMetaInf(Constants.META_INF_FACTORY);
+ return lines.isEmpty() ? null : lines.get(0);
}
- private String loadMetaInf() {
- // logDebug("loading metaInfServicesLine ...");
+ List loadMetaInfCustom() {
+ return loadMetaInf(Constants.META_INF_CUSTOM);
+ }
+
+ private List loadMetaInf(String fullName) {
try {
- FileObject fileObject = processingEnv.getFiler().getResource(StandardLocation.CLASS_OUTPUT, "", Constants.META_INF_FACTORY);
+ FileObject fileObject = processingEnv.getFiler().getResource(StandardLocation.CLASS_OUTPUT, "", fullName);
if (fileObject != null) {
+ List lines = new ArrayList<>();
Reader reader = fileObject.openReader(true);
LineNumberReader lineReader = new LineNumberReader(reader);
- String line = lineReader.readLine();
- if (line != null) {
- return line.trim();
+ String line;
+ while ((line = lineReader.readLine()) != null) {
+ line = line.trim();
+ if (!line.isEmpty()) {
+ lines.add(line);
+ }
}
+ return lines;
}
} catch (FileNotFoundException | NoSuchFileException e) {
@@ -95,7 +92,7 @@ private String loadMetaInf() {
e.printStackTrace();
logWarn("Error reading services file: " + e.getMessage());
}
- return null;
+ return Collections.emptyList();
}
/**
@@ -105,44 +102,16 @@ JavaFileObject createWriter(String cls) throws IOException {
return filer.createSourceFile(cls);
}
- /**
- * Create a file writer for the given class name.
- */
FileObject createMetaInfWriter() throws IOException {
- return filer.createResource(StandardLocation.CLASS_OUTPUT, "", Constants.META_INF_FACTORY);
- }
-
- void setContextDetails(String name, String[] provides, String[] dependsOn, Element contextElement) {
- this.contextName = name;
- this.contextProvides = provides;
- this.contextDependsOn = dependsOn;
-
- // determine the context package (that we put the DI Factory class into)
- PackageElement pkg = elementUtils.getPackageOf(contextElement);
- logDebug("using package from element " + pkg);
- this.contextPackage = (pkg == null) ? null : pkg.getQualifiedName().toString();
- }
-
- void setContextRequires(List contextRequires) {
- this.contextRequires.addAll(contextRequires);
- }
-
- Set contextRequires() {
- return contextRequires;
- }
-
- void deriveContextName(String factoryPackage) {
- if (contextName == null) {
- contextName = factoryPackage;
- }
+ return createMetaInfWriterFor(Constants.META_INF_FACTORY);
}
- String contextName() {
- return contextName;
+ FileObject createMetaInfModuleCustom() throws IOException {
+ return createMetaInfWriterFor(Constants.META_INF_CUSTOM);
}
- String getContextPackage() {
- return contextPackage;
+ private FileObject createMetaInfWriterFor(String interfaceType) throws IOException {
+ return filer.createResource(StandardLocation.CLASS_OUTPUT, "", interfaceType);
}
TypeElement element(String rawType) {
@@ -161,61 +130,7 @@ Element asElement(TypeMirror returnType) {
return typeUtils.asElement(returnType);
}
- void buildNewBuilder(Append writer) {
- writer.append(" this.name = \"%s\";", contextName).eol();
- writer.append(" this.provides = ", contextProvides);
- buildStringArray(writer, contextProvides, true);
- writer.append(";").eol();
- writer.append(" this.dependsOn = ", contextDependsOn);
- buildStringArray(writer, contextDependsOn, true);
- writer.append(";").eol();
- }
-
- void buildAtInjectModule(Append writer) {
- writer.append(Constants.AT_GENERATED).eol();
- writer.append("@InjectModule(name=\"%s\"", contextName);
- if (!isEmpty(contextProvides)) {
- writer.append(", provides=");
- buildStringArray(writer, contextProvides, false);
- }
- if (!isEmpty(contextDependsOn)) {
- writer.append(", dependsOn=");
- buildStringArray(writer, contextDependsOn, false);
- }
- if (!contextRequires.isEmpty()) {
- writer.append(", requires={");
- int c = 0;
- for (String value : contextRequires) {
- if (c++ > 0) {
- writer.append(",");
- }
- writer.append(value).append(".class");
- }
- writer.append("}");
- }
- writer.append(")").eol();
- }
-
- private boolean isEmpty(String[] strings) {
- return strings == null || strings.length == 0;
- }
-
- private void buildStringArray(Append writer, String[] values, boolean asArray) {
- if (isEmpty(values)) {
- writer.append("null");
- } else {
- if (asArray) {
- writer.append("new String[]");
- }
- writer.append("{");
- int c = 0;
- for (String value : values) {
- if (c++ > 0) {
- writer.append(",");
- }
- writer.append("\"").append(value).append("\"");
- }
- writer.append("}");
- }
+ PackageElement getPackageOf(Element element) {
+ return elementUtils.getPackageOf(element);
}
}
diff --git a/inject-generator/src/main/java/io/avaje/inject/generator/Processor.java b/inject-generator/src/main/java/io/avaje/inject/generator/Processor.java
index f23d410d4..cd9cee4f0 100644
--- a/inject-generator/src/main/java/io/avaje/inject/generator/Processor.java
+++ b/inject-generator/src/main/java/io/avaje/inject/generator/Processor.java
@@ -1,45 +1,25 @@
package io.avaje.inject.generator;
-import io.avaje.inject.InjectModule;
import io.avaje.inject.Factory;
-import io.avaje.inject.Request;
-import io.avaje.inject.spi.DependencyMeta;
+import io.avaje.inject.InjectModule;
+import jakarta.inject.Scope;
+import jakarta.inject.Singleton;
import javax.annotation.processing.AbstractProcessor;
-import javax.annotation.processing.FilerException;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.RoundEnvironment;
-import jakarta.inject.Singleton;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.*;
import javax.lang.model.util.Elements;
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.Iterator;
-import java.util.LinkedHashMap;
-import java.util.LinkedHashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
+import java.util.*;
public class Processor extends AbstractProcessor {
- private static final String INJECT_MODULE = "io.avaje.inject.InjectModule";
-
private ProcessingContext context;
-
private Elements elementUtils;
-
- /**
- * Map to merge the existing meta data with partially compiled code. Keyed by type and qualifier/name.
- */
- private final Map metaData = new LinkedHashMap<>();
-
- private final List beanReaders = new ArrayList<>();
-
- private final Set readBeans = new HashSet<>();
+ private ScopeInfo defaultScope;
+ private AllScopes allScopes;
+ private boolean readModuleInfo;
public Processor() {
}
@@ -54,15 +34,17 @@ public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
this.context = new ProcessingContext(processingEnv);
this.elementUtils = processingEnv.getElementUtils();
+ this.allScopes = new AllScopes(context);
+ this.defaultScope = allScopes.defaultScope();
}
@Override
public Set getSupportedAnnotationTypes() {
-
Set annotations = new LinkedHashSet<>();
annotations.add(InjectModule.class.getCanonicalName());
annotations.add(Factory.class.getCanonicalName());
annotations.add(Singleton.class.getCanonicalName());
+ annotations.add(Scope.class.getCanonicalName());
annotations.add(Constants.CONTROLLER);
return annotations;
}
@@ -78,58 +60,29 @@ public boolean process(Set extends TypeElement> annotations, RoundEnvironment
Set extends Element> factoryBeans = roundEnv.getElementsAnnotatedWith(Factory.class);
Set extends Element> beans = roundEnv.getElementsAnnotatedWith(Singleton.class);
- Set extends Element> requestBeans = roundEnv.getElementsAnnotatedWith(Request.class);
-
+ Set extends Element> scopes = roundEnv.getElementsAnnotatedWith(Scope.class);
+ readScopes(scopes);
readModule(roundEnv);
readChangedBeans(factoryBeans, true);
readChangedBeans(beans, false);
readChangedBeans(controllers, false);
- readChangedBeans(requestBeans, false);
+ allScopes.readBeans(roundEnv);
- mergeMetaData();
-
- writeBeanHelpers();
- if (roundEnv.processingOver()) {
- writeBeanFactory();
- }
+ defaultScope.write(roundEnv.processingOver());
+ allScopes.write(roundEnv.processingOver());
return false;
}
- private void writeBeanHelpers() {
- for (BeanReader beanReader : beanReaders) {
- try {
- if (!beanReader.isWrittenToFile()) {
- SimpleBeanWriter writer = new SimpleBeanWriter(beanReader, context);
- writer.write();
- beanReader.setWrittenToFile();
- }
- } catch (FilerException e) {
- context.logWarn("FilerException to write $DI class " + beanReader.getBeanType() + " " + e.getMessage());
-
- } catch (IOException e) {
- e.printStackTrace();
- context.logError(beanReader.getBeanType(), "Failed to write $DI class");
+ private void readScopes(Set extends Element> scopes) {
+ for (Element element : scopes) {
+ if (element.getKind() == ElementKind.ANNOTATION_TYPE) {
+ // context.logDebug("detected scope annotation " + element);
+ TypeElement type = (TypeElement) element;
+ allScopes.addScopeAnnotation(type);
}
}
}
- private void writeBeanFactory() {
- MetaDataOrdering ordering = new MetaDataOrdering(metaData.values(), context);
- int remaining = ordering.processQueue();
- if (remaining > 0) {
- ordering.logWarnings();
- }
-
- try {
- SimpleFactoryWriter factoryWriter = new SimpleFactoryWriter(ordering, context);
- factoryWriter.write();
- } catch (FilerException e) {
- context.logWarn("FilerException trying to write factory " + e.getMessage());
- } catch (IOException e) {
- context.logError("Failed to write factory " + e.getMessage());
- }
- }
-
/**
* Read the beans that have changed.
*/
@@ -138,138 +91,75 @@ private void readChangedBeans(Set extends Element> beans, boolean factory) {
if (!(element instanceof TypeElement)) {
context.logError("unexpected type [" + element + "]");
} else {
- if (readBeans.add(element.toString())) {
- readBeanMeta((TypeElement) element, factory);
+ TypeElement typeElement = (TypeElement) element;
+ if (!factory) {
+ defaultScope.read(typeElement, factory);
} else {
- context.logDebug("skipping already processed bean " + element);
+ final ScopeInfo scope = findScope(typeElement);
+ if (scope != null) {
+ // context.logWarn("Adding factory to custom scope "+element+" scope: "+scope);
+ scope.read(typeElement, true);
+ } else {
+ defaultScope.read(typeElement, true);
+ }
}
}
}
}
/**
- * Merge the changed bean meta data into the existing (factory) metaData.
+ * Find the scope if the Factory has a scope annotation.
*/
- private void mergeMetaData() {
- for (BeanReader beanReader : beanReaders) {
- if (!beanReader.isRequestScopedController()) {
- MetaData metaData = this.metaData.get(beanReader.getMetaKey());
- if (metaData == null) {
- addMeta(beanReader);
- } else {
- updateMeta(metaData, beanReader);
- }
+ private ScopeInfo findScope(Element element) {
+ for (AnnotationMirror annotationMirror : element.getAnnotationMirrors()) {
+ final ScopeInfo scopeInfo = allScopes.get(annotationMirror.getAnnotationType().toString());
+ if (scopeInfo != null) {
+ return scopeInfo;
}
}
- }
-
- /**
- * Add a new previously unknown bean.
- */
- private void addMeta(BeanReader beanReader) {
- MetaData meta = beanReader.createMeta();
- metaData.put(meta.getKey(), meta);
- for (MetaData methodMeta : beanReader.createFactoryMethodMeta()) {
- metaData.put(methodMeta.getKey(), methodMeta);
- }
- }
-
- /**
- * Update the meta data on a previously known bean.
- */
- private void updateMeta(MetaData metaData, BeanReader beanReader) {
- metaData.update(beanReader);
- }
-
- /**
- * Read the dependency injection meta data for the given bean.
- */
- private void readBeanMeta(TypeElement typeElement, boolean factory) {
- if (typeElement.getKind() == ElementKind.ANNOTATION_TYPE) {
- context.logDebug("skipping annotation type " + typeElement);
- return;
- }
- beanReaders.add(new BeanReader(typeElement, context, factory).read());
+ 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;
String factory = context.loadMetaInfServices();
if (factory != null) {
- TypeElement factoryType = elementUtils.getTypeElement(factory);
- if (factoryType != null) {
- readFactory(factoryType);
+ TypeElement moduleType = elementUtils.getTypeElement(factory);
+ if (moduleType != null) {
+ defaultScope.readModuleMetaData(moduleType);
}
}
+ allScopes.readModules(context.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
Set extends Element> elementsAnnotatedWith = roundEnv.getElementsAnnotatedWith(InjectModule.class);
if (!elementsAnnotatedWith.isEmpty()) {
Iterator extends Element> iterator = elementsAnnotatedWith.iterator();
if (iterator.hasNext()) {
Element element = iterator.next();
- InjectModule annotation = element.getAnnotation(InjectModule.class);
- if (annotation != null) {
- context.setContextDetails(annotation.name(), annotation.provides(), annotation.dependsOn(), element);
- context.setContextRequires(readRequires(element));
- }
- }
- }
- }
-
- /**
- * Read the list of required class names.
- */
- private List readRequires(Element element) {
- List requiresList = new ArrayList<>();
- for (AnnotationMirror annotationMirror : element.getAnnotationMirrors()) {
- if (INJECT_MODULE.equals(annotationMirror.getAnnotationType().toString())) {
- for (Map.Entry extends ExecutableElement, ? extends AnnotationValue> entry : annotationMirror.getElementValues().entrySet()) {
- if (entry.getKey().toString().startsWith("requires")) {
- for (Object requiresType : (List>) entry.getValue().getValue()) {
- String fullName = requiresType.toString();
- fullName = fullName.substring(0, fullName.length() - 6);
- requiresList.add(fullName);
- }
+ Scope scope = element.getAnnotation(Scope.class);
+ if (scope == null) {
+ // it it not a custom scope annotation
+ InjectModule annotation = element.getAnnotation(InjectModule.class);
+ if (annotation != null) {
+ defaultScope.details(annotation.name(), element);
}
}
}
}
- return requiresList;
- }
-
-
- /**
- * Read the existing factory bean. Each of the build methods is annotated with @DependencyMeta
- * which holds the information we need (to regenerate the factory with any changes).
- */
- private void readFactory(TypeElement factoryType) {
- InjectModule module = factoryType.getAnnotation(InjectModule.class);
- context.setContextDetails(module.name(), module.provides(), module.dependsOn(), factoryType);
- context.setContextRequires(readRequires(factoryType));
-
- List extends Element> elements = factoryType.getEnclosedElements();
- if (elements != null) {
- for (Element element : elements) {
- if (ElementKind.METHOD == element.getKind()) {
- readBuildMethodDependencyMeta(element);
- }
- }
- }
}
- private void readBuildMethodDependencyMeta(Element element) {
- Name simpleName = element.getSimpleName();
- if (simpleName.toString().startsWith("build_")) {
- // read a build method - DependencyMeta
- DependencyMeta meta = element.getAnnotation(DependencyMeta.class);
- if (meta == null) {
- context.logError("Missing @DependencyMeta on method " + simpleName);
- } else {
- final MetaData metaData = new MetaData(meta);
- this.metaData.put(metaData.getKey(), metaData);
- }
- }
- }
}
diff --git a/inject-generator/src/main/java/io/avaje/inject/generator/ScopeInfo.java b/inject-generator/src/main/java/io/avaje/inject/generator/ScopeInfo.java
new file mode 100644
index 000000000..d0fb279c6
--- /dev/null
+++ b/inject-generator/src/main/java/io/avaje/inject/generator/ScopeInfo.java
@@ -0,0 +1,384 @@
+package io.avaje.inject.generator;
+
+import io.avaje.inject.InjectModule;
+import io.avaje.inject.spi.DependencyMeta;
+
+import javax.annotation.processing.FilerException;
+import javax.lang.model.element.*;
+import javax.tools.JavaFileObject;
+import java.io.IOException;
+import java.util.*;
+
+class ScopeInfo {
+
+ /**
+ * Map to merge the existing meta data with partially compiled code. Keyed by type and qualifier/name.
+ */
+ private final Map metaData = new LinkedHashMap<>();
+ private final List beanReaders = new ArrayList<>();
+ private final Set readBeans = new HashSet<>();
+ private final ProcessingContext context;
+ private final Set requires = new LinkedHashSet<>();
+ private final Set provides = new LinkedHashSet<>();
+ private final boolean defaultScope;
+ private final TypeElement annotationType;
+ private final AllScopes scopes;
+ private boolean moduleInitialised;
+ private boolean moduleWritten;
+ private String name;
+ private String modulePackage;
+ private String moduleFullName;
+ private String moduleShortName;
+ private JavaFileObject moduleFile;
+ private boolean emptyModule;
+
+ /**
+ * Create for the main/global module scope.
+ */
+ ScopeInfo(ProcessingContext context) {
+ this.scopes = null;
+ this.context = context;
+ this.defaultScope = true;
+ this.annotationType = null;
+ }
+
+ /**
+ * Create for custom scope.
+ */
+ ScopeInfo(ProcessingContext context, TypeElement type, AllScopes scopes) {
+ this.scopes = scopes;
+ this.context = context;
+ this.defaultScope = false;
+ this.annotationType = type;
+ }
+
+ void details(String name, Element contextElement) {
+ if (name == null || name.isEmpty()) {
+ final String simpleName = contextElement.getSimpleName().toString();
+ this.name = ScopeUtil.name(simpleName);
+ } else {
+ this.name = ScopeUtil.name(name);
+ }
+ read(contextElement);
+ }
+
+ private void read(Element element) {
+ requires(ScopeUtil.readRequires(element));
+ provides(ScopeUtil.readProvides(element));
+ }
+
+ private String initName(String topPackage) {
+ if (name == null || name.isEmpty()) {
+ name = ScopeUtil.name(topPackage);
+ }
+ return name;
+ }
+
+ void initialiseName(String topPackage) throws IOException {
+ emptyModule = topPackage == null;
+ if (!emptyModule) {
+ modulePackage = topPackage;
+ final String name = initName(modulePackage);
+ moduleShortName = name + "Module";
+ moduleFullName = modulePackage + "." + moduleShortName;
+ moduleFile = context.createWriter(moduleFullName);
+ }
+ }
+
+ JavaFileObject moduleFile() {
+ return moduleFile;
+ }
+
+ String modulePackage() {
+ return modulePackage;
+ }
+
+ String moduleFullName() {
+ return moduleFullName;
+ }
+
+ String moduleShortName() {
+ return moduleShortName;
+ }
+
+ boolean isDefaultScope() {
+ return defaultScope;
+ }
+
+ String name() {
+ return name;
+ }
+
+ private void provides(List provides) {
+ this.provides.addAll(provides);
+ }
+
+ private void requires(List contextRequires) {
+ this.requires.addAll(contextRequires);
+ }
+
+ Set requires() {
+ return requires;
+ }
+
+ Set provides() {
+ return provides;
+ }
+
+ void writeBeanHelpers() {
+ for (BeanReader beanReader : beanReaders) {
+ try {
+ if (!beanReader.isWrittenToFile()) {
+ SimpleBeanWriter writer = new SimpleBeanWriter(beanReader, context);
+ writer.write();
+ beanReader.setWrittenToFile();
+ }
+ } catch (FilerException e) {
+ context.logWarn("FilerException to write $DI class " + beanReader.getBeanType() + " " + e.getMessage());
+
+ } catch (IOException e) {
+ e.printStackTrace();
+ context.logError(beanReader.getBeanType(), "Failed to write $DI class");
+ }
+ }
+ }
+
+ private void initialiseModule() {
+ if (!moduleInitialised) {
+ try {
+ initialiseName(MetaTopPackage.of(metaData.values()));
+ moduleInitialised = true;
+ } catch (IOException e) {
+ context.logError("Failed to create module filer " + e.getMessage());
+ }
+ }
+ }
+
+ void writeModule() {
+ if (moduleWritten) {
+ context.logError("already written module " + name);
+ return;
+ }
+ final Collection meta = metaData.values();
+ if (emptyModule) {
+ // typically nothing in the default scope, only custom scopes
+ if (meta.size() > 0) {
+ context.logWarn("Empty module but meta is not empty? " + meta);
+ }
+ return;
+ }
+ MetaDataOrdering ordering = new MetaDataOrdering(meta, context, this);
+ int remaining = ordering.processQueue();
+ if (remaining > 0) {
+ ordering.logWarnings();
+ }
+ try {
+ SimpleModuleWriter factoryWriter = new SimpleModuleWriter(ordering, context, this);
+ factoryWriter.write(defaultScope);
+ moduleWritten = true;
+ } catch (FilerException e) {
+ context.logWarn("FilerException trying to write factory " + e.getMessage());
+ } catch (IOException e) {
+ context.logError("Failed to write factory " + e.getMessage());
+ }
+ }
+
+ /**
+ * Merge the changed bean meta data into the existing (factory) metaData.
+ */
+ void mergeMetaData() {
+ for (BeanReader beanReader : beanReaders) {
+ if (!beanReader.isRequestScopedController()) {
+ MetaData metaData = this.metaData.get(beanReader.getMetaKey());
+ if (metaData == null) {
+ addMeta(beanReader);
+ } else {
+ updateMeta(metaData, beanReader);
+ }
+ }
+ }
+ }
+
+ /**
+ * Add a new previously unknown bean.
+ */
+ private void addMeta(BeanReader beanReader) {
+ MetaData meta = beanReader.createMeta();
+ metaData.put(meta.getKey(), meta);
+ for (MetaData methodMeta : beanReader.createFactoryMethodMeta()) {
+ metaData.put(methodMeta.getKey(), methodMeta);
+ }
+ }
+
+ /**
+ * Update the meta data on a previously known bean.
+ */
+ private void updateMeta(MetaData metaData, BeanReader beanReader) {
+ metaData.update(beanReader);
+ }
+
+ /**
+ * Read the dependency injection meta data for the given bean.
+ */
+ private void readBeanMeta(TypeElement typeElement, boolean factory) {
+ if (typeElement.getKind() == ElementKind.ANNOTATION_TYPE) {
+ context.logDebug("skipping annotation type " + typeElement);
+ return;
+ }
+ beanReaders.add(new BeanReader(typeElement, context, factory).read());
+ }
+
+ void readBuildMethodDependencyMeta(Element element) {
+ Name simpleName = element.getSimpleName();
+ if (simpleName.toString().startsWith("build_")) {
+ // read a build method - DependencyMeta
+ DependencyMeta meta = element.getAnnotation(DependencyMeta.class);
+ if (meta == null) {
+ context.logError("Missing @DependencyMeta on method " + simpleName);
+ } else {
+ final MetaData metaData = new MetaData(meta);
+ this.metaData.put(metaData.getKey(), metaData);
+ }
+ }
+ }
+
+ void read(TypeElement element, boolean factory) {
+ if (readBeans.add(element.toString())) {
+ readBeanMeta(element, factory);
+ } else {
+ context.logDebug("skipping already processed bean " + element);
+ }
+ }
+
+ void write(boolean processingOver) {
+ mergeMetaData();
+ writeBeanHelpers();
+ initialiseModule();
+ if (processingOver) {
+ writeModule();
+ }
+ }
+
+ void buildAtInjectModule(Append writer) {
+ writer.append(Constants.AT_GENERATED).eol();
+ writer.append("@InjectModule(");
+ boolean leadingComma = false;
+ if (!provides.isEmpty()) {
+ attributeClasses(leadingComma, writer, "provides", provides);
+ leadingComma = true;
+ }
+ if (!requires.isEmpty()) {
+ attributeClasses(leadingComma, writer, "requires", requires);
+ leadingComma = true;
+ }
+ if (annotationType != null) {
+ if (leadingComma) {
+ writer.append(", ");
+ }
+ writer.append("customScopeType=\"%s\"", annotationType.getQualifiedName().toString());
+ }
+ writer.append(")").eol();
+ }
+
+ private void attributeClasses(boolean leadingComma, Append writer, String prefix, Set classNames) {
+ if (leadingComma) {
+ writer.append(", ");
+ }
+ writer.append("%s={", prefix);
+ int c = 0;
+ for (String value : classNames) {
+ if (c++ > 0) {
+ writer.append(",");
+ }
+ writer.append(value).append(".class");
+ }
+ writer.append("}");
+ }
+
+ private void buildClassArray(Append writer, Set values) {
+ writer.append("new Class>[]");
+ writer.append("{");
+ if (!values.isEmpty()) {
+ int c = 0;
+ for (String value : values) {
+ if (c++ > 0) {
+ writer.append(",");
+ }
+ writer.append(value).append(".class");
+ }
+ }
+ writer.append("}");
+ }
+
+ void buildFields(Append writer) {
+ writer.append(" private final Class>[] provides = ");
+ buildClassArray(writer, provides);
+ writer.append(";").eol();
+ writer.append(" private final Class>[] requires = ");
+ buildClassArray(writer, requires);
+ writer.append(";").eol();
+ writer.append(" private Builder builder;").eol().eol();
+ }
+
+ void readModuleMetaData(TypeElement moduleType) {
+ context.logDebug("Reading module info for " + moduleType);
+ InjectModule module = moduleType.getAnnotation(InjectModule.class);
+ details(module.name(), moduleType);
+ readFactoryMetaData(moduleType);
+ }
+
+ private void readFactoryMetaData(TypeElement moduleType) {
+ List extends Element> elements = moduleType.getEnclosedElements();
+ if (elements != null) {
+ for (Element element : elements) {
+ if (ElementKind.METHOD == element.getKind()) {
+ readBuildMethodDependencyMeta(element);
+ }
+ }
+ }
+ }
+
+ /**
+ * Return true if the scope is a custom scope and the dependency is provided
+ * by the "default" module. We could/should move to be tighter here at some point.
+ */
+ boolean providedByOtherModule(String dependency) {
+ if (defaultScope) {
+ return false;
+ }
+ if (scopes.providedByDefaultModule(dependency)) {
+ return true;
+ }
+ // look for required scopes ...
+ for (String require : requires) {
+ final ScopeInfo requiredScope = scopes.get(require);
+ if (requiredScope != null) {
+ if (requiredScope.providesDependency(dependency)) {
+ // context.logWarn("dependency "+dependency+" provided by other scope "+requiredScope.name);
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Return true if this module provides the dependency.
+ */
+ boolean providesDependency(String dependency) {
+ for (MetaData meta : metaData.values()) {
+ if (dependency.equals(meta.getType())) {
+ return true;
+ }
+ final List provides = meta.getProvides();
+ if (provides != null && !provides.isEmpty()) {
+ for (String provide : provides) {
+ if (dependency.equals(provide)) {
+ return true;
+ }
+ }
+ }
+ }
+ return false;
+ }
+}
diff --git a/inject-generator/src/main/java/io/avaje/inject/generator/ScopeUtil.java b/inject-generator/src/main/java/io/avaje/inject/generator/ScopeUtil.java
new file mode 100644
index 000000000..61d39d39d
--- /dev/null
+++ b/inject-generator/src/main/java/io/avaje/inject/generator/ScopeUtil.java
@@ -0,0 +1,79 @@
+package io.avaje.inject.generator;
+
+import javax.lang.model.element.*;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+class ScopeUtil {
+
+ private static final String INJECT_MODULE = "io.avaje.inject.InjectModule";
+
+ static List readProvides(Element element) {
+ return readClasses(element, "provides");
+ }
+
+ static List readRequires(Element element) {
+ return readClasses(element, "requires");
+ }
+
+ static List readClasses(Element element, String attributeName) {
+ if (element == null) {
+ return Collections.emptyList();
+ }
+ List requiresList = new ArrayList<>();
+ for (AnnotationMirror annotationMirror : element.getAnnotationMirrors()) {
+ if (INJECT_MODULE.equals(annotationMirror.getAnnotationType().toString())) {
+ for (Map.Entry extends ExecutableElement, ? extends AnnotationValue> entry : annotationMirror.getElementValues().entrySet()) {
+ if (entry.getKey().toString().startsWith(attributeName)) {
+ for (Object requiresType : (List>) entry.getValue().getValue()) {
+ String fullName = requiresType.toString();
+ fullName = fullName.substring(0, fullName.length() - 6);
+ requiresList.add(fullName);
+ }
+ }
+ }
+ }
+ }
+ return requiresList;
+ }
+
+ static String name(String name) {
+ if (name == null) {
+ return null;
+ }
+ final int pos = name.lastIndexOf('.');
+ if (pos > -1) {
+ name = name.substring(pos + 1);
+ }
+ if (name.endsWith("Scope")) {
+ name = name.substring(0, name.length() - 5);
+ }
+ if (name.endsWith("Module")) {
+ name = name.substring(0, name.length() - 6);
+ }
+ return camelCase(name);
+ }
+
+ private static String camelCase(String name) {
+ StringBuilder sb = new StringBuilder(name.length());
+ boolean upper = true;
+ for (char aChar : name.toCharArray()) {
+ if (Character.isLetterOrDigit(aChar)) {
+ if (upper) {
+ aChar = Character.toUpperCase(aChar);
+ upper = false;
+ }
+ sb.append(aChar);
+ } else if (toUpperOn(aChar)) {
+ upper = true;
+ }
+ }
+ return sb.toString();
+ }
+
+ private static boolean toUpperOn(char aChar) {
+ return aChar == ' ' || aChar == '-' || aChar == '_';
+ }
+}
diff --git a/inject-generator/src/main/java/io/avaje/inject/generator/SimpleFactoryWriter.java b/inject-generator/src/main/java/io/avaje/inject/generator/SimpleModuleWriter.java
similarity index 64%
rename from inject-generator/src/main/java/io/avaje/inject/generator/SimpleFactoryWriter.java
rename to inject-generator/src/main/java/io/avaje/inject/generator/SimpleModuleWriter.java
index 4122e41b8..a555b8a1e 100644
--- a/inject-generator/src/main/java/io/avaje/inject/generator/SimpleFactoryWriter.java
+++ b/inject-generator/src/main/java/io/avaje/inject/generator/SimpleModuleWriter.java
@@ -1,7 +1,6 @@
package io.avaje.inject.generator;
import javax.tools.FileObject;
-import javax.tools.JavaFileObject;
import java.io.IOException;
import java.io.Writer;
import java.util.List;
@@ -11,11 +10,11 @@
/**
* Write the source code for the factory.
*/
-class SimpleFactoryWriter {
+class SimpleModuleWriter {
private static final String CODE_COMMENT_FACTORY =
"/**\n" +
- " * Generated source - Creates the BeanScope for the %s module.\n" +
+ " * Generated source - avaje inject module for %s.\n" +
" * \n" +
" * With JPMS Java module system this generated class should be explicitly\n" +
" * registered in module-info via a provides clause like:\n" +
@@ -25,7 +24,7 @@ class SimpleFactoryWriter {
" * module example {\n" +
" * requires io.avaje.inject;\n" +
" * \n" +
- " * provides io.avaje.inject.spi.BeanScopeFactory with %s._DI$BeanScopeFactory;\n" +
+ " * provides io.avaje.inject.spi.Module with %s.%s;\n" +
" * \n" +
" * }\n" +
" * \n" +
@@ -36,43 +35,41 @@ class SimpleFactoryWriter {
" /**\n" +
" * Create the beans.\n" +
" *
\n" +
- " * Creates all the beans in order based on constuctor dependencies.\n" +
+ " * Creates all the beans in order based on constructor dependencies.\n" +
" * The beans are registered into the builder along with callbacks for\n" +
" * field injection, method injection and lifecycle support.\n" +
" *