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.avaje avaje-inject-parent - 6.2 + 6.5-RC0 avaje-inject-generator @@ -16,7 +16,7 @@ io.avaje avaje-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 annotations, RoundEnvironment Set factoryBeans = roundEnv.getElementsAnnotatedWith(Factory.class); Set beans = roundEnv.getElementsAnnotatedWith(Singleton.class); - Set requestBeans = roundEnv.getElementsAnnotatedWith(Request.class); - + Set 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 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 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 elementsAnnotatedWith = roundEnv.getElementsAnnotatedWith(InjectModule.class); if (!elementsAnnotatedWith.isEmpty()) { Iterator 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 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 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 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 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" + " *

\n" + " */"; - private final MetaDataOrdering ordering; private final ProcessingContext context; - private final String factoryPackage; - private final String factoryShortName; - private final String factoryFullName; + private final String modulePackage; + private final String shortName; + private final String fullName; + private final ScopeInfo scopeInfo; + private final MetaDataOrdering ordering; private Append writer; - SimpleFactoryWriter(MetaDataOrdering ordering, ProcessingContext context) { + SimpleModuleWriter(MetaDataOrdering ordering, ProcessingContext context, ScopeInfo scopeInfo) { this.ordering = ordering; this.context = context; - - String pkg = context.getContextPackage(); - this.factoryPackage = (pkg != null) ? pkg : ordering.getTopPackage(); - context.deriveContextName(factoryPackage); - this.factoryShortName = "_DI$BeanScopeFactory"; - this.factoryFullName = factoryPackage + "." + factoryShortName; + this.scopeInfo = scopeInfo; + this.modulePackage = scopeInfo.modulePackage(); + this.shortName = scopeInfo.moduleShortName(); + this.fullName = scopeInfo.moduleFullName(); } - void write() throws IOException { + void write(boolean includeServicesFile) throws IOException { writer = new Append(createFileWriter()); writePackage(); writeStartClass(); - - writeCreateMethod(); + writeBuildMethod(); writeBuildMethods(); - writeEndClass(); writer.close(); - - writeServicesFile(); + if (includeServicesFile) { + writeServicesFile(); + } } private void writeServicesFile() { @@ -80,26 +77,16 @@ private void writeServicesFile() { FileObject jfo = context.createMetaInfWriter(); if (jfo != null) { Writer writer = jfo.openWriter(); - writer.write(factoryFullName); + writer.write(fullName); writer.close(); } - } catch (IOException e) { e.printStackTrace(); context.logError("Failed to write services file " + e.getMessage()); } } - private void writeBuildMethods() { - for (MetaData metaData : ordering.getOrdered()) { - writer.append(metaData.buildMethod(ordering)).eol(); - } - for (MetaData metaData : ordering.getRequestScope()) { - writer.append(metaData.buildMethod(ordering)).eol(); - } - } - - private void writeCreateMethod() { + private void writeBuildMethod() { writer.append(CODE_COMMENT_CREATE_CONTEXT).eol(); writer.append(" @Override").eol(); writer.append(" public void build(Builder builder) {").eol(); @@ -121,8 +108,17 @@ private void writeCreateMethod() { writer.eol(); } + private void writeBuildMethods() { + for (MetaData metaData : ordering.getOrdered()) { + writer.append(metaData.buildMethod(ordering)).eol(); + } + for (MetaData metaData : ordering.getRequestScope()) { + writer.append(metaData.buildMethod(ordering)).eol(); + } + } + private void writePackage() { - writer.append("package %s;", factoryPackage).eol().eol(); + writer.append("package %s;", modulePackage).eol().eol(); for (String type : factoryImportTypes()) { writer.append("import %s;", type).eol(); } @@ -140,38 +136,27 @@ private Set factoryImportTypes() { importTypes.add(Constants.BEANCONTEXT); importTypes.add(Constants.INJECTMODULE); importTypes.add(Constants.DEPENDENCYMETA); - importTypes.add(Constants.BEANSCOPEFACTORY); + importTypes.add(Constants.MODULE); importTypes.add(Constants.BUILDER); return importTypes; } private void writeStartClass() { - writer.append(CODE_COMMENT_FACTORY, context.contextName(), factoryPackage).eol(); - context.buildAtInjectModule(writer); + writer.append(CODE_COMMENT_FACTORY, scopeInfo.name(), modulePackage, shortName).eol(); + scopeInfo.buildAtInjectModule(writer); - writer.append("public class %s implements BeanScopeFactory {", factoryShortName).eol().eol(); - writer.append(" private final String name;").eol(); - writer.append(" private final String[] provides;").eol(); - writer.append(" private final String[] dependsOn;").eol(); - writer.append(" private Builder builder;").eol().eol(); - - writer.append(" public %s() {", factoryShortName).eol(); - context.buildNewBuilder(writer); - writer.append(" }").eol().eol(); - - writer.append(" @Override").eol(); - writer.append(" public String getName() {").eol(); - writer.append(" return name;").eol(); - writer.append(" }").eol().eol(); + String custom = scopeInfo.isDefaultScope() ? "" : ".Custom"; + writer.append("public class %s implements Module%s {", shortName, custom).eol().eol(); + scopeInfo.buildFields(writer); writer.append(" @Override").eol(); - writer.append(" public String[] getProvides() {").eol(); + writer.append(" public Class[] provides() {").eol(); writer.append(" return provides;").eol(); writer.append(" }").eol().eol(); writer.append(" @Override").eol(); - writer.append(" public String[] getDependsOn() {").eol(); - writer.append(" return dependsOn;").eol(); + writer.append(" public Class[] requires() {").eol(); + writer.append(" return requires;").eol(); writer.append(" }").eol().eol(); } @@ -180,8 +165,7 @@ private void writeEndClass() { } private Writer createFileWriter() throws IOException { - JavaFileObject jfo = context.createWriter(factoryFullName); - return jfo.openWriter(); + return scopeInfo.moduleFile().openWriter(); } } diff --git a/inject-generator/src/test/java/io/avaje/inject/generator/ScopeUtilTest.java b/inject-generator/src/test/java/io/avaje/inject/generator/ScopeUtilTest.java new file mode 100644 index 000000000..7d933108e --- /dev/null +++ b/inject-generator/src/test/java/io/avaje/inject/generator/ScopeUtilTest.java @@ -0,0 +1,36 @@ +package io.avaje.inject.generator; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class ScopeUtilTest { + + @Test + void name() { + assertEquals("Example", ScopeUtil.name("org.example")); + assertEquals("Example", ScopeUtil.name("org.Example")); + assertEquals("Example", ScopeUtil.name("example")); + assertEquals("Example", ScopeUtil.name("Example")); + } + + @Test + void name_withSpace() { + assertEquals("ExAmple", ScopeUtil.name("org.ex ample")); + assertEquals("ExAmple", ScopeUtil.name("org.ex_ample")); + assertEquals("ExAmple", ScopeUtil.name("ex-ample")); + assertEquals("Example", ScopeUtil.name("ex$ample")); + } + + @Test + void name_withDigits() { + assertEquals("Example1", ScopeUtil.name("org.example1")); + assertEquals("Ex42ample", ScopeUtil.name("org.ex42ample")); + } + + @Test + void name_withSuffix() { + assertEquals("MyCustom", ScopeUtil.name("MyCustomScope")); + assertEquals("MyCustom", ScopeUtil.name("MyCustomModule")); + } +} diff --git a/inject-test/pom.xml b/inject-test/pom.xml index a3940e9b2..f8dcf598f 100644 --- a/inject-test/pom.xml +++ b/inject-test/pom.xml @@ -4,7 +4,7 @@ io.avaje avaje-inject-parent - 6.2 + 6.5-RC0 avaje-inject-test diff --git a/inject-test/src/main/java/io/avaje/inject/test/MetaReader.java b/inject-test/src/main/java/io/avaje/inject/test/MetaReader.java index d0ac033a2..4180fd506 100644 --- a/inject-test/src/main/java/io/avaje/inject/test/MetaReader.java +++ b/inject-test/src/main/java/io/avaje/inject/test/MetaReader.java @@ -108,11 +108,12 @@ private Object captorFor(Field field) { } void build(BeanScopeBuilder builder) { + final BeanScopeBuilder.ForTesting forTesting = builder.forTesting(); for (FieldTarget target : mocks) { - builder.withMock(target.type(), target.name()); + forTesting.withMock(target.type(), target.name()); } for (FieldTarget target : spies) { - builder.withSpy(target.type(), target.name()); + forTesting.withSpy(target.type(), target.name()); } } diff --git a/inject/src/main/java/io/avaje/inject/ApplicationScope.java b/inject-test/src/test/java/io/avaje/inject/ApplicationScope.java similarity index 78% rename from inject/src/main/java/io/avaje/inject/ApplicationScope.java rename to inject-test/src/test/java/io/avaje/inject/ApplicationScope.java index 0d6901bbd..7d722d75a 100644 --- a/inject/src/main/java/io/avaje/inject/ApplicationScope.java +++ b/inject-test/src/test/java/io/avaje/inject/ApplicationScope.java @@ -137,41 +137,4 @@ public static List listByAnnotation(Class annotation) { return appScope.listByAnnotation(annotation); } - /** - * Start building a RequestScope. - * - *
{@code
-   *
-   *   try (RequestScope requestScope = ApplicationScope.newRequestScope()
-   *       // supply some instances
-   *       .withBean(HttpRequest.class, request)
-   *       .withBean(HttpResponse.class, response)
-   *       .build()) {
-   *
-   *       MyController controller = requestScope.get(MyController.class);
-   *       controller.process();
-   *
-   *   }
-   *
-   *   ...
-   *
-   *   // define request scoped beans
-   *   @Request
-   *   MyController {
-   *
-   *     // can depend on supplied instances, singletons and other request scope beans
-   *     @Inject
-   *     MyController(HttpRequest request, HttpResponse response, MyService myService) {
-   *       ...
-   *     }
-   *
-   *   }
-   *
-   * }
- * - * @return The request scope builder - */ - public static RequestScopeBuilder newRequestScope() { - return appScope.newRequestScope(); - } } diff --git a/inject-test/src/test/java/io/avaje/inject/BeanScopeBuilderTest.java b/inject-test/src/test/java/io/avaje/inject/BeanScopeBuilderTest.java deleted file mode 100644 index be0bf9520..000000000 --- a/inject-test/src/test/java/io/avaje/inject/BeanScopeBuilderTest.java +++ /dev/null @@ -1,135 +0,0 @@ -package io.avaje.inject; - -import io.avaje.inject.spi.BeanScopeFactory; -import io.avaje.inject.spi.Builder; -import org.junit.jupiter.api.Test; - -import java.util.Collections; -import java.util.List; -import java.util.stream.Collectors; - -import static org.assertj.core.api.Assertions.assertThat; - -public class BeanScopeBuilderTest { - - @Test - public void noDepends() { - - DBeanScopeBuilder.FactoryOrder factoryOrder = new DBeanScopeBuilder.FactoryOrder(Collections.emptySet(), true, true); - factoryOrder.add(bc("1", null, null)); - factoryOrder.add(bc("2", null, null)); - factoryOrder.add(bc("3", null, null)); - factoryOrder.orderFactories(); - - assertThat(names(factoryOrder.factories())).containsExactly("1", "2", "3"); - } - - @Test - public void name_depends() { - - DBeanScopeBuilder.FactoryOrder factoryOrder = new DBeanScopeBuilder.FactoryOrder(Collections.emptySet(), true, true); - factoryOrder.add(bc("two", null, "one")); - factoryOrder.add(bc("one", null, null)); - factoryOrder.orderFactories(); - - assertThat(names(factoryOrder.factories())).containsExactly("one", "two"); - } - - @Test - public void name_depends4() { - - DBeanScopeBuilder.FactoryOrder factoryOrder = new DBeanScopeBuilder.FactoryOrder(Collections.emptySet(), true, true); - factoryOrder.add(bc("1", null, "3")); - factoryOrder.add(bc("2", null, "4")); - factoryOrder.add(bc("3", null, "4")); - factoryOrder.add(bc("4", null, null)); - - factoryOrder.orderFactories(); - - assertThat(names(factoryOrder.factories())).containsExactly("4", "2", "3", "1"); - } - - @Test - public void nameFeature_depends() { - - DBeanScopeBuilder.FactoryOrder factoryOrder = new DBeanScopeBuilder.FactoryOrder(Collections.emptySet(), true, true); - factoryOrder.add(bc("1", "a", "3")); - factoryOrder.add(bc("2", null, "4,a")); - factoryOrder.add(bc("3", null, "4")); - factoryOrder.add(bc("4", null, null)); - - factoryOrder.orderFactories(); - - assertThat(names(factoryOrder.factories())).containsExactly("4", "3", "1", "2"); - } - - @Test - public void feature_depends() { - - DBeanScopeBuilder.FactoryOrder factoryOrder = new DBeanScopeBuilder.FactoryOrder(Collections.emptySet(), true, true); - factoryOrder.add(bc("two", null, "myfeature")); - factoryOrder.add(bc("one", "myfeature", null)); - factoryOrder.orderFactories(); - - assertThat(names(factoryOrder.factories())).containsExactly("one", "two"); - } - - @Test - public void feature_depends2() { - - DBeanScopeBuilder.FactoryOrder factoryOrder = new DBeanScopeBuilder.FactoryOrder(Collections.emptySet(), true, true); - factoryOrder.add(bc("two", null, "myfeature")); - factoryOrder.add(bc("one", "myfeature", null)); - factoryOrder.add(bc("three", "myfeature", null)); - factoryOrder.orderFactories(); - - assertThat(names(factoryOrder.factories())).containsExactly("one", "three", "two"); - } - - private List names(List factories) { - return factories.stream() - .map(BeanScopeFactory::getName) - .collect(Collectors.toList()); - } - - private TDBeanScope bc(String name, String provides, String dependsOn) { - return new TDBeanScope(name, split(provides), split(dependsOn)); - } - - private String[] split(String val) { - return val == null ? null : val.split(","); - } - - private static class TDBeanScope implements BeanScopeFactory { - - final String name; - final String[] provides; - final String[] dependsOn; - - private TDBeanScope(String name, String[] provides, String[] dependsOn) { - this.name = name; - this.provides = provides; - this.dependsOn = dependsOn; - } - - @Override - public String getName() { - return name; - } - - @Override - public String[] getProvides() { - return provides; - } - - @Override - public String[] getDependsOn() { - return dependsOn; - } - - @Override - public void build(Builder parent) { - - } - } -} diff --git a/inject-test/src/test/java/io/avaje/inject/SystemContextTest.java b/inject-test/src/test/java/io/avaje/inject/SystemContextTest.java index 912870c99..57b6cdadf 100644 --- a/inject-test/src/test/java/io/avaje/inject/SystemContextTest.java +++ b/inject-test/src/test/java/io/avaje/inject/SystemContextTest.java @@ -20,38 +20,42 @@ public class SystemContextTest { @Test public void getBeansByPriority() { - - final List beans = SystemContext.getBeansByPriority(BaseIface.class); - assertThat(beans).hasSize(3); - - assertThat(beans.get(0)).isInstanceOf(CBasei.class); - assertThat(beans.get(1)).isInstanceOf(BBasei.class); - assertThat(beans.get(2)).isInstanceOf(ABasei.class); + try (BeanScope context = BeanScope.newBuilder().build()) { + final List beans = context.listByPriority(BaseIface.class); + assertThat(beans).hasSize(3); + + assertThat(beans.get(0)).isInstanceOf(CBasei.class); + assertThat(beans.get(1)).isInstanceOf(BBasei.class); + assertThat(beans.get(2)).isInstanceOf(ABasei.class); + } } @Test public void getBeansByPriority_withAnnotation() { - - final List beans = ApplicationScope.scope().getBeansByPriority(Somei.class, Priority.class); - assertThat(beans).hasSize(3); - - assertThat(beans.get(0)).isInstanceOf(BSomei.class); - assertThat(beans.get(1)).isInstanceOf(ASomei.class); - assertThat(beans.get(2)).isInstanceOf(A2Somei.class); + try (BeanScope context = BeanScope.newBuilder().build()) { + final List beans = context.listByPriority(Somei.class, Priority.class); + assertThat(beans).hasSize(3); + + assertThat(beans.get(0)).isInstanceOf(BSomei.class); + assertThat(beans.get(1)).isInstanceOf(ASomei.class); + assertThat(beans.get(2)).isInstanceOf(A2Somei.class); + } } @Test public void getBeansUnsorted_withPriority() { - - final List beans = ApplicationScope.list(Somei.class); - assertThat(beans).hasSize(3); - // can't assert bean order + try (BeanScope context = BeanScope.newBuilder().build()) { + final List beans = context.list(Somei.class); + assertThat(beans).hasSize(3); + // can't assert bean order + } } @Test public void getBeansWithAnnotation() { - - final List beans = SystemContext.getBeansWithAnnotation(Fruit.class); - assertThat(beans).hasSize(2); + try (BeanScope context = BeanScope.newBuilder().build()) { + final List beans = context.listByAnnotation(Fruit.class); + assertThat(beans).hasSize(2); + } } } diff --git a/inject-test/src/test/java/org/example/MyCustomScope.java b/inject-test/src/test/java/org/example/MyCustomScope.java new file mode 100644 index 000000000..3edfb9ef5 --- /dev/null +++ b/inject-test/src/test/java/org/example/MyCustomScope.java @@ -0,0 +1,10 @@ +package org.example; + +import io.avaje.inject.InjectModule; +import jakarta.inject.Scope; +import org.example.custom.LocalExternal; + +@Scope +@InjectModule(requires = {System.class, LocalExternal.class}) +public @interface MyCustomScope { +} diff --git a/inject-test/src/test/java/org/example/coffee/BeanScopeBuilderAddTest.java b/inject-test/src/test/java/org/example/coffee/BeanScopeBuilderAddTest.java index 80e4ca506..0de15b392 100644 --- a/inject-test/src/test/java/org/example/coffee/BeanScopeBuilderAddTest.java +++ b/inject-test/src/test/java/org/example/coffee/BeanScopeBuilderAddTest.java @@ -1,34 +1,48 @@ package org.example.coffee; import io.avaje.inject.BeanScope; +import io.avaje.inject.spi.Builder; +import io.avaje.inject.spi.Module; import org.junit.jupiter.api.Test; import org.mockito.Mockito; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Mockito.verify; public class BeanScopeBuilderAddTest { @Test - public void withModules_exludingThisOne() { - assertThrows(IllegalStateException.class, () -> { - TDPump testDoublePump = new TDPump(); - - try (BeanScope context = BeanScope.newBuilder() - .withBeans(testDoublePump) - // our module is "org.example.coffee" - // so this effectively includes no modules - .withModules("other") - .withIgnoreMissingModuleDependencies() - .build()) { - - CoffeeMaker coffeeMaker = context.getBean(CoffeeMaker.class); - assertThat(coffeeMaker).isNull(); - } - }); + public void withModules_excludingThisOne() { + TDPump testDoublePump = new TDPump(); + try (BeanScope context = BeanScope.newBuilder() + .withBeans(testDoublePump) + // our module is "org.example.coffee" + // so this effectively includes no modules + .withModules(new SillyModule()) + .build()) { + + CoffeeMaker coffeeMaker = context.get(CoffeeMaker.class); + assertThat(coffeeMaker).isNull(); + } } + static class SillyModule implements Module { + + @Override + public Class[] requires() { + return new Class[0]; + } + + @Override + public Class[] provides() { + return new Class[0]; + } + + @Override + public void build(Builder builder) { + // do nothing + } + } @Test public void withModules_includeThisOne() { @@ -37,10 +51,10 @@ public void withModules_includeThisOne() { try (BeanScope context = BeanScope.newBuilder() .withBeans(testDoublePump) - .withModules("org.example") + .withModules(new org.example.ExampleModule()) .build()) { - String makeIt = context.getBean(CoffeeMaker.class).makeIt(); + String makeIt = context.get(CoffeeMaker.class).makeIt(); assertThat(makeIt).isEqualTo("done"); assertThat(testDoublePump.steam).isEqualTo(1); @@ -57,7 +71,7 @@ public void withBean_expect_testDoublePumpUsed() { .withBeans(testDoublePump) .build()) { - String makeIt = context.getBean(CoffeeMaker.class).makeIt(); + String makeIt = context.get(CoffeeMaker.class).makeIt(); assertThat(makeIt).isEqualTo("done"); assertThat(testDoublePump.steam).isEqualTo(1); @@ -74,10 +88,10 @@ public void withMockitoMock_expect_mockUsed() { .withBean(Pump.class, mock) .build()) { - Pump pump = context.getBean(Pump.class); + Pump pump = context.get(Pump.class); assertThat(pump).isSameAs(mock); - CoffeeMaker coffeeMaker = context.getBean(CoffeeMaker.class); + CoffeeMaker coffeeMaker = context.get(CoffeeMaker.class); assertThat(coffeeMaker).isNotNull(); coffeeMaker.makeIt(); diff --git a/inject-test/src/test/java/org/example/coffee/BeanScope_Builder_mockitoSpyTest.java b/inject-test/src/test/java/org/example/coffee/BeanScope_Builder_mockitoSpyTest.java index fcdd5bfca..8f45dd54c 100644 --- a/inject-test/src/test/java/org/example/coffee/BeanScope_Builder_mockitoSpyTest.java +++ b/inject-test/src/test/java/org/example/coffee/BeanScope_Builder_mockitoSpyTest.java @@ -30,11 +30,11 @@ public void withBeans_asMocks() { .withBeans(pump, grinder) .build()) { - CoffeeMaker coffeeMaker = context.getBean(CoffeeMaker.class); + CoffeeMaker coffeeMaker = context.get(CoffeeMaker.class); coffeeMaker.makeIt(); - Pump pump1 = context.getBean(Pump.class); - Grinder grinder1 = context.getBean(Grinder.class); + Pump pump1 = context.get(Pump.class); + Grinder grinder1 = context.get(Grinder.class); assertThat(pump1).isSameAs(pump); assertThat(grinder1).isSameAs(grinder); @@ -48,14 +48,15 @@ public void withBeans_asMocks() { public void withMockitoSpy_noSetup_expect_spyUsed() { try (BeanScope context = BeanScope.newBuilder() + .forTesting() .withSpy(Pump.class) .build()) { - CoffeeMaker coffeeMaker = context.getBean(CoffeeMaker.class); + CoffeeMaker coffeeMaker = context.get(CoffeeMaker.class); assertThat(coffeeMaker).isNotNull(); coffeeMaker.makeIt(); - Pump pump = context.getBean(Pump.class); + Pump pump = context.get(Pump.class); verify(pump).pumpWater(); } } @@ -64,21 +65,22 @@ public void withMockitoSpy_noSetup_expect_spyUsed() { public void withMockitoSpy_postLoadSetup_expect_spyUsed() { try (BeanScope context = BeanScope.newBuilder() + .forTesting() .withSpy(Pump.class) .withSpy(Grinder.class) .build()) { // setup after load() - Pump pump = context.getBean(Pump.class); + Pump pump = context.get(Pump.class); doNothing().when(pump).pumpWater(); - CoffeeMaker coffeeMaker = context.getBean(CoffeeMaker.class); + CoffeeMaker coffeeMaker = context.get(CoffeeMaker.class); assertThat(coffeeMaker).isNotNull(); coffeeMaker.makeIt(); verify(pump).pumpWater(); - Grinder grinder = context.getBean(Grinder.class); + Grinder grinder = context.get(Grinder.class); verify(grinder).grindBeans(); } } @@ -87,6 +89,7 @@ public void withMockitoSpy_postLoadSetup_expect_spyUsed() { public void withMockitoSpy_expect_spyUsed() { try (BeanScope context = BeanScope.newBuilder() + .forTesting() .withSpy(Pump.class, pump -> { // setup the spy doNothing().when(pump).pumpWater(); @@ -94,11 +97,11 @@ public void withMockitoSpy_expect_spyUsed() { .build()) { // or setup here ... - Pump pump = context.getBean(Pump.class); + Pump pump = context.get(Pump.class); doNothing().when(pump).pumpSteam(); // act - CoffeeMaker coffeeMaker = context.getBean(CoffeeMaker.class); + CoffeeMaker coffeeMaker = context.get(CoffeeMaker.class); coffeeMaker.makeIt(); verify(pump).pumpWater(); @@ -110,11 +113,12 @@ public void withMockitoSpy_expect_spyUsed() { public void withMockitoSpy_whenPrimary_expect_spyUsed() { try (BeanScope context = BeanScope.newBuilder() + .forTesting() .withSpy(PEmailer.class) // has a primary .build()) { - UserOfPEmailer user = context.getBean(UserOfPEmailer.class); - PEmailer emailer = context.getBean(PEmailer.class); + UserOfPEmailer user = context.get(UserOfPEmailer.class); + PEmailer emailer = context.get(PEmailer.class); user.email(); verify(emailer).email(); @@ -125,18 +129,19 @@ public void withMockitoSpy_whenPrimary_expect_spyUsed() { public void withMockitoSpy_whenOnlySecondary_expect_spyUsed() { try (BeanScope context = BeanScope.newBuilder() + .forTesting() .withSpy(Widget.class) // only secondary .build()) { - WidgetUser widgetUser = context.getBean(WidgetUser.class); + WidgetUser widgetUser = context.get(WidgetUser.class); String val = widgetUser.wid(); assertThat(val).isEqualTo("second"); - Widget widget = context.getBean(Widget.class); + Widget widget = context.get(Widget.class); verify(widget).wid(); // these are the same (secondary only) - WidgetSecondary widgetSecondary = context.getBean(WidgetSecondary.class); + WidgetSecondary widgetSecondary = context.get(WidgetSecondary.class); assertThat(widget).isSameAs(widgetSecondary); } } @@ -145,20 +150,21 @@ public void withMockitoSpy_whenOnlySecondary_expect_spyUsed() { public void withMockitoSpy_whenSecondary_expect_spyUsed() { try (BeanScope context = BeanScope.newBuilder() + .forTesting() .withSpy(Something.class) // has a secondary and a normal .build()) { - Unused unused = context.getBean(Unused.class); - Something something = context.getBean(Something.class); + Unused unused = context.get(Unused.class); + Something something = context.get(Something.class); String result = unused.doSomething(); verify(something).doStuff(); // someImpl has higher precedence than the Secondary assertThat(result).isEqualTo("SomeImpl"); - SomeImpl someImpl = context.getBean(SomeImpl.class); + SomeImpl someImpl = context.get(SomeImpl.class); assertThat(someImpl).isNull(); - SomeImplBean someImplBean = context.getBean(SomeImplBean.class); + SomeImplBean someImplBean = context.get(SomeImplBean.class); assertThat(something).isNotSameAs(someImplBean); } } @@ -169,6 +175,7 @@ public void withMockitoMock_expect_mockUsed() { AtomicReference mock = new AtomicReference<>(); try (BeanScope context = BeanScope.newBuilder() + .forTesting() .withMock(Pump.class) .withMock(Grinder.class, grinder -> { // setup the mock @@ -177,10 +184,10 @@ public void withMockitoMock_expect_mockUsed() { }) .build()) { - Grinder grinder = context.getBean(Grinder.class); + Grinder grinder = context.get(Grinder.class); assertThat(grinder).isSameAs(mock.get()); - CoffeeMaker coffeeMaker = context.getBean(CoffeeMaker.class); + CoffeeMaker coffeeMaker = context.get(CoffeeMaker.class); assertThat(coffeeMaker).isNotNull(); coffeeMaker.makeIt(); diff --git a/inject-test/src/test/java/org/example/coffee/CoffeeMakerTest.java b/inject-test/src/test/java/org/example/coffee/CoffeeMakerTest.java index 329520d8a..860065276 100644 --- a/inject-test/src/test/java/org/example/coffee/CoffeeMakerTest.java +++ b/inject-test/src/test/java/org/example/coffee/CoffeeMakerTest.java @@ -1,46 +1,67 @@ package org.example.coffee; -import io.avaje.inject.ApplicationScope; +import io.avaje.inject.BeanEntry; import io.avaje.inject.BeanScope; -import io.avaje.inject.SystemContext; import org.example.coffee.core.DuperPump; +import org.example.coffee.list.BSomei; +import org.example.coffee.list.Somei; import org.junit.jupiter.api.Test; +import java.util.List; +import java.util.Optional; + +import static java.util.stream.Collectors.toList; import static org.assertj.core.api.Assertions.assertThat; public class CoffeeMakerTest { @Test public void makeIt_via_SystemContext() { + try (BeanScope context = BeanScope.newBuilder().build()) { + String makeIt = context.get(CoffeeMaker.class).makeIt(); + assertThat(makeIt).isEqualTo("done"); - String makeIt = ApplicationScope.get(CoffeeMaker.class).makeIt(); - assertThat(makeIt).isEqualTo("done"); - - Pump pump = ApplicationScope.get(Pump.class); - assertThat(pump).isInstanceOf(DuperPump.class); - - Pump pump2 = SystemContext.context().getBean(Pump.class); - assertThat(pump2).isSameAs(pump); + Pump pump = context.get(Pump.class); + assertThat(pump).isInstanceOf(DuperPump.class); + } } @Test public void makeIt_via_BootContext_withNoShutdownHook() { - try (BeanScope context = BeanScope.newBuilder() - .withNoShutdownHook() + .withShutdownHook(false) .build()) { - String makeIt = context.getBean(CoffeeMaker.class).makeIt(); + String makeIt = context.get(CoffeeMaker.class).makeIt(); assertThat(makeIt).isEqualTo("done"); } } @Test public void makeIt_via_BootContext() { - try (BeanScope context = BeanScope.newBuilder().build()) { - String makeIt = context.getBean(CoffeeMaker.class).makeIt(); + String makeIt = context.get(CoffeeMaker.class).makeIt(); assertThat(makeIt).isEqualTo("done"); + + final List beanEntries = context.all(); + assertThat(beanEntries).hasSizeGreaterThan(10); + + // all entries for an interface + final List someiEntries = beanEntries.stream() + .filter(beanEntry -> beanEntry.hasKey(Somei.class)) + .collect(toList()); + assertThat(someiEntries).hasSize(3); + + final Optional bsomeEntry = someiEntries.stream().filter(entry -> entry.type().equals(BSomei.class)).findFirst(); + assertThat(bsomeEntry).isPresent(); + + final BeanEntry entry = bsomeEntry.get(); + assertThat(entry.qualifierName()).isEqualTo("b"); + assertThat(entry.keys()).containsExactly(BSomei.class.getCanonicalName(), Somei.class.getCanonicalName()); + assertThat(entry.type()).isEqualTo(BSomei.class); + assertThat(entry.priority()).isEqualTo(0); + assertThat(entry.bean()).isEqualTo(context.get(Somei.class, "b")); + assertThat(entry.bean()).isEqualTo(context.get(BSomei.class)); } } diff --git a/inject-test/src/test/java/org/example/coffee/ExtensionExample.java b/inject-test/src/test/java/org/example/coffee/ExtensionExample.java index 76f804155..15177a917 100644 --- a/inject-test/src/test/java/org/example/coffee/ExtensionExample.java +++ b/inject-test/src/test/java/org/example/coffee/ExtensionExample.java @@ -16,7 +16,7 @@ class ExtensionExample { } BeanScope build() { - BeanScopeBuilder bootContext = BeanScope.newBuilder(); + BeanScopeBuilder.ForTesting bootContext = BeanScope.newBuilder().forTesting(); withMocks.forEach(bootContext::withMock); withSpies.forEach(bootContext::withSpy); return bootContext.build(); diff --git a/inject-test/src/test/java/org/example/coffee/ExtensionExampleTest.java b/inject-test/src/test/java/org/example/coffee/ExtensionExampleTest.java index c79a06706..8c7a30e20 100644 --- a/inject-test/src/test/java/org/example/coffee/ExtensionExampleTest.java +++ b/inject-test/src/test/java/org/example/coffee/ExtensionExampleTest.java @@ -22,6 +22,7 @@ public void checkForCompilerWarningsOnly_notATestThatRuns() { Class cls1 = SEmailer.class; BeanScopeBuilder bootContext = BeanScope.newBuilder() + .forTesting() .withSpy(cls0) .withSpy(cls1) .withMock(cls0) diff --git a/inject-test/src/test/java/org/example/coffee/FactoryTest.java b/inject-test/src/test/java/org/example/coffee/FactoryTest.java index 556108e34..59cb0d39a 100644 --- a/inject-test/src/test/java/org/example/coffee/FactoryTest.java +++ b/inject-test/src/test/java/org/example/coffee/FactoryTest.java @@ -12,7 +12,7 @@ public class FactoryTest { public void test() { try (BeanScope context = BeanScope.newBuilder().build()) { - BFact bean = context.getBean(BFact.class); + BFact bean = context.get(BFact.class); String b = bean.b(); assertThat(b).isNotNull(); } diff --git a/inject-test/src/test/java/org/example/coffee/InjectListTest.java b/inject-test/src/test/java/org/example/coffee/InjectListTest.java index 27107a933..b6050d480 100644 --- a/inject-test/src/test/java/org/example/coffee/InjectListTest.java +++ b/inject-test/src/test/java/org/example/coffee/InjectListTest.java @@ -14,7 +14,7 @@ public class InjectListTest { @Test public void test() { try (BeanScope context = BeanScope.newBuilder().build()) { - CombinedSomei bean = context.getBean(CombinedSomei.class); + CombinedSomei bean = context.get(CombinedSomei.class); List somes = bean.lotsOfSomes(); assertThat(somes).containsOnly("a", "b", "a2"); } @@ -23,7 +23,7 @@ public void test() { @Test public void test_set() { try (BeanScope context = BeanScope.newBuilder().build()) { - CombinedSetSomei bean = context.getBean(CombinedSetSomei.class); + CombinedSetSomei bean = context.get(CombinedSetSomei.class); List somes = bean.lotsOfSomes(); assertThat(somes).containsOnly("a", "b", "a2"); } diff --git a/inject-test/src/test/java/org/example/coffee/ProviderTest.java b/inject-test/src/test/java/org/example/coffee/ProviderTest.java index e50a6cbb6..911fc3e9a 100644 --- a/inject-test/src/test/java/org/example/coffee/ProviderTest.java +++ b/inject-test/src/test/java/org/example/coffee/ProviderTest.java @@ -14,11 +14,11 @@ public void test() { try (BeanScope context = BeanScope.newBuilder().build()) { - ProvOther bean = context.getBean(ProvOther.class); + ProvOther bean = context.get(ProvOther.class); String other = bean.other(); assertThat(other).isEqualTo("mush mush beans"); - ProvOther2 bean2 = context.getBean(ProvOther2.class); + ProvOther2 bean2 = context.get(ProvOther2.class); assertThat(bean2.getaProv()).isSameAs(bean.getaProv()); } diff --git a/inject-test/src/test/java/org/example/coffee/circular/CircularDependencyTest.java b/inject-test/src/test/java/org/example/coffee/circular/CircularDependencyTest.java index 913cb19bb..93f83231f 100644 --- a/inject-test/src/test/java/org/example/coffee/circular/CircularDependencyTest.java +++ b/inject-test/src/test/java/org/example/coffee/circular/CircularDependencyTest.java @@ -10,13 +10,13 @@ class CircularDependencyTest { @Test void wire() { try (BeanScope context = BeanScope.newBuilder().build()) { - assertThat(context.getBean(CircA.class)).isNotNull(); - assertThat(context.getBean(CircB.class)).isNotNull(); - assertThat(context.getBean(CircC.class)).isNotNull(); + assertThat(context.get(CircA.class)).isNotNull(); + assertThat(context.get(CircB.class)).isNotNull(); + assertThat(context.get(CircC.class)).isNotNull(); - final CircA circA = context.getBean(CircA.class); - final CircB circB = context.getBean(CircB.class); - final CircC circC = context.getBean(CircC.class); + final CircA circA = context.get(CircA.class); + final CircB circB = context.get(CircB.class); + final CircC circC = context.get(CircC.class); assertThat(circC.gen()).isEqualTo("C+CircA-CircB-CircC-Stop"); assertThat(circC.circA).isSameAs(circA); diff --git a/inject-test/src/test/java/org/example/coffee/factory/BFactTest.java b/inject-test/src/test/java/org/example/coffee/factory/BFactTest.java index 9a4d50717..558dc138b 100644 --- a/inject-test/src/test/java/org/example/coffee/factory/BFactTest.java +++ b/inject-test/src/test/java/org/example/coffee/factory/BFactTest.java @@ -12,7 +12,7 @@ public void getCountInit() { BFact bFact; try (BeanScope context = BeanScope.newBuilder().build()) { - bFact = context.getBean(BFact.class); + bFact = context.get(BFact.class); assertThat(bFact.getCountInit()).isEqualTo(1); assertThat(bFact.getCountClose()).isEqualTo(0); } diff --git a/inject-test/src/test/java/org/example/coffee/factory/ConfigurationTest.java b/inject-test/src/test/java/org/example/coffee/factory/ConfigurationTest.java index d1e5d8091..ba8792f02 100644 --- a/inject-test/src/test/java/org/example/coffee/factory/ConfigurationTest.java +++ b/inject-test/src/test/java/org/example/coffee/factory/ConfigurationTest.java @@ -12,7 +12,7 @@ public void getCountInit() { Configuration configuration; try (BeanScope context = BeanScope.newBuilder().build()) { - configuration = context.getBean(Configuration.class); + configuration = context.get(Configuration.class); assertEquals(configuration.getCountInit(), 1); assertEquals(configuration.getCountClose(), 0); } diff --git a/inject-test/src/test/java/org/example/coffee/factory/MultipleOtherThingsTest.java b/inject-test/src/test/java/org/example/coffee/factory/MultipleOtherThingsTest.java index f39a82743..e1b8fd4d9 100644 --- a/inject-test/src/test/java/org/example/coffee/factory/MultipleOtherThingsTest.java +++ b/inject-test/src/test/java/org/example/coffee/factory/MultipleOtherThingsTest.java @@ -12,7 +12,7 @@ class MultipleOtherThingsTest { @Test void test() { try (BeanScope context = BeanScope.newBuilder().build()) { - final MultipleOtherThings combined = context.getBean(MultipleOtherThings.class); + final MultipleOtherThings combined = context.get(MultipleOtherThings.class); assertEquals("blue", combined.blue()); assertEquals("red", combined.red()); assertEquals("green", combined.green()); diff --git a/inject-test/src/test/java/org/example/coffee/factory/MyFactoryTest.java b/inject-test/src/test/java/org/example/coffee/factory/MyFactoryTest.java index 7b0a7991c..7353efb88 100644 --- a/inject-test/src/test/java/org/example/coffee/factory/MyFactoryTest.java +++ b/inject-test/src/test/java/org/example/coffee/factory/MyFactoryTest.java @@ -12,7 +12,7 @@ public class MyFactoryTest { @Test public void methodsCalled() { try (BeanScope context = BeanScope.newBuilder().build()) { - final MyFactory myFactory = context.getBean(MyFactory.class); + final MyFactory myFactory = context.get(MyFactory.class); assertThat(myFactory.methodsCalled()).contains("|useCFact", "|anotherCFact", "|buildEngi"); } } diff --git a/inject-test/src/test/java/org/example/coffee/fruit/AppleServiceTest.java b/inject-test/src/test/java/org/example/coffee/fruit/AppleServiceTest.java index b461ee799..1b646ea99 100644 --- a/inject-test/src/test/java/org/example/coffee/fruit/AppleServiceTest.java +++ b/inject-test/src/test/java/org/example/coffee/fruit/AppleServiceTest.java @@ -14,11 +14,11 @@ public class AppleServiceTest { public void test_spyWithFieldInjection() { BeanScopeBuilder contextBuilder = BeanScope.newBuilder(); - contextBuilder.withSpy(AppleService.class); + contextBuilder.forTesting().withSpy(AppleService.class); try (BeanScope beanScope = contextBuilder.build()) { - AppleService appleService = beanScope.getBean(AppleService.class); + AppleService appleService = beanScope.get(AppleService.class); doNothing() .when(appleService) @@ -39,7 +39,7 @@ public void test_whenNoMockOrSpy() { try (BeanScope beanScope = BeanScope.newBuilder().build()) { - AppleService appleService = beanScope.getBean(AppleService.class); + AppleService appleService = beanScope.get(AppleService.class); assertThat(appleService.bananaService).isNotNull(); assertThat(appleService.peachService).isNotNull(); diff --git a/inject-test/src/test/java/org/example/coffee/generic/HazManagerTest.java b/inject-test/src/test/java/org/example/coffee/generic/HazManagerTest.java index 508245e34..911169cf3 100644 --- a/inject-test/src/test/java/org/example/coffee/generic/HazManagerTest.java +++ b/inject-test/src/test/java/org/example/coffee/generic/HazManagerTest.java @@ -23,10 +23,11 @@ public void find_when_allWired() { public void fin_with_mockHaz() { try (BeanScope context = BeanScope.newBuilder() + .forTesting() .withMock(HazRepo.class) .build()) { - HazManager hazManager = context.getBean(HazManager.class); + HazManager hazManager = context.get(HazManager.class); Haz haz = hazManager.find(42L); assertThat(haz).isNull(); @@ -37,12 +38,13 @@ public void fin_with_mockHaz() { public void find_with_stubHazUsingMockito() { try (BeanScope context = BeanScope.newBuilder() + .forTesting() .withMock(HazRepo.class, hazRepo -> { when(hazRepo.findById(anyLong())).thenReturn(new Haz(-23L)); }) .build()) { - HazManager hazManager = context.getBean(HazManager.class); + HazManager hazManager = context.get(HazManager.class); Haz haz = hazManager.find(42L); assertThat(haz.id).isEqualTo(-23L); @@ -58,7 +60,7 @@ public void find_with_testDouble() { .withBeans(testDouble) .build()) { - HazManager hazManager = context.getBean(HazManager.class); + HazManager hazManager = context.get(HazManager.class); Haz haz = hazManager.find(42L); assertThat(haz.id).isEqualTo(64L); diff --git a/inject-test/src/test/java/org/example/coffee/grind/AMusherTest.java b/inject-test/src/test/java/org/example/coffee/grind/AMusherTest.java index 9295d5c89..9e334207d 100644 --- a/inject-test/src/test/java/org/example/coffee/grind/AMusherTest.java +++ b/inject-test/src/test/java/org/example/coffee/grind/AMusherTest.java @@ -12,7 +12,7 @@ public void getCountInit() { AMusher aMusher; try (BeanScope context = BeanScope.newBuilder().build()) { - aMusher = context.getBean(AMusher.class); + aMusher = context.get(AMusher.class); assertEquals(aMusher.getCountInit(), 1); assertEquals(aMusher.getCountClose(), 0); } diff --git a/inject-test/src/test/java/org/example/coffee/grind/BMusherTest.java b/inject-test/src/test/java/org/example/coffee/grind/BMusherTest.java index 077c843ea..9690b88e0 100644 --- a/inject-test/src/test/java/org/example/coffee/grind/BMusherTest.java +++ b/inject-test/src/test/java/org/example/coffee/grind/BMusherTest.java @@ -12,7 +12,7 @@ public void init() { BMusher bMusher; try (BeanScope context = BeanScope.newBuilder().build()) { - bMusher = context.getBean(BMusher.class); + bMusher = context.get(BMusher.class); assertEquals(bMusher.getCountInit(), 1); assertEquals(bMusher.getCountClose(), 0); } diff --git a/inject-test/src/test/java/org/example/coffee/qualifier/StoreManagerWithFieldQualifierTest.java b/inject-test/src/test/java/org/example/coffee/qualifier/StoreManagerWithFieldQualifierTest.java index b87fb8a68..7fe5b7a5b 100644 --- a/inject-test/src/test/java/org/example/coffee/qualifier/StoreManagerWithFieldQualifierTest.java +++ b/inject-test/src/test/java/org/example/coffee/qualifier/StoreManagerWithFieldQualifierTest.java @@ -11,7 +11,7 @@ public class StoreManagerWithFieldQualifierTest { public void test() { try (BeanScope context = BeanScope.newBuilder().build()) { - StoreManagerWithFieldQualifier manager = context.getBean(StoreManagerWithFieldQualifier.class); + StoreManagerWithFieldQualifier manager = context.get(StoreManagerWithFieldQualifier.class); String store = manager.store(); assertThat(store).isEqualTo("blue"); } diff --git a/inject-test/src/test/java/org/example/coffee/qualifier/StoreManagerWithNamedTest.java b/inject-test/src/test/java/org/example/coffee/qualifier/StoreManagerWithNamedTest.java index 704bc7fa5..1b7c32614 100644 --- a/inject-test/src/test/java/org/example/coffee/qualifier/StoreManagerWithNamedTest.java +++ b/inject-test/src/test/java/org/example/coffee/qualifier/StoreManagerWithNamedTest.java @@ -11,7 +11,7 @@ public class StoreManagerWithNamedTest { public void test() { try (BeanScope context = BeanScope.newBuilder().build()) { - StoreManagerWithNamed manager = context.getBean(StoreManagerWithNamed.class); + StoreManagerWithNamed manager = context.get(StoreManagerWithNamed.class); String store = manager.store(); assertThat(store).isEqualTo("blue"); } diff --git a/inject-test/src/test/java/org/example/coffee/qualifier/StoreManagerWithQualifierTest.java b/inject-test/src/test/java/org/example/coffee/qualifier/StoreManagerWithQualifierTest.java index d5fed776e..8f18827ab 100644 --- a/inject-test/src/test/java/org/example/coffee/qualifier/StoreManagerWithQualifierTest.java +++ b/inject-test/src/test/java/org/example/coffee/qualifier/StoreManagerWithQualifierTest.java @@ -11,7 +11,7 @@ public class StoreManagerWithQualifierTest { public void test() { try (BeanScope context = BeanScope.newBuilder().build()) { - StoreManagerWithQualifier manager = context.getBean(StoreManagerWithQualifier.class); + StoreManagerWithQualifier manager = context.get(StoreManagerWithQualifier.class); String store = manager.store(); assertThat(store).isEqualTo("blue"); } diff --git a/inject-test/src/test/java/org/example/coffee/qualifier/StoreManagerWithSetterQualifierTest.java b/inject-test/src/test/java/org/example/coffee/qualifier/StoreManagerWithSetterQualifierTest.java index c5ce60b7c..92653847c 100644 --- a/inject-test/src/test/java/org/example/coffee/qualifier/StoreManagerWithSetterQualifierTest.java +++ b/inject-test/src/test/java/org/example/coffee/qualifier/StoreManagerWithSetterQualifierTest.java @@ -10,7 +10,7 @@ class StoreManagerWithSetterQualifierTest { @Test void redStore() { try (BeanScope context = BeanScope.newBuilder().build()) { - StoreManagerWithSetterQualifier manager = context.getBean(StoreManagerWithSetterQualifier.class); + StoreManagerWithSetterQualifier manager = context.get(StoreManagerWithSetterQualifier.class); assertThat(manager.blueStore()).isEqualTo("blue"); assertThat(manager.greenStore()).isEqualTo("green"); } @@ -23,7 +23,7 @@ void namedTestDouble() { .withBean("Green", SomeStore.class, () -> "TD Green") .build()) { - StoreManagerWithSetterQualifier manager = context.getBean(StoreManagerWithSetterQualifier.class); + StoreManagerWithSetterQualifier manager = context.get(StoreManagerWithSetterQualifier.class); assertThat(manager.blueStore()).isEqualTo("TD Blue"); assertThat(manager.greenStore()).isEqualTo("TD Green"); } @@ -36,7 +36,7 @@ void namedTestDouble_expect_otherNamedStillWired() { // with GreenStore still wired .build()) { - StoreManagerWithSetterQualifier manager = context.getBean(StoreManagerWithSetterQualifier.class); + StoreManagerWithSetterQualifier manager = context.get(StoreManagerWithSetterQualifier.class); assertThat(manager.blueStore()).isEqualTo("TD Blue Only"); assertThat(manager.greenStore()).isEqualTo("green"); } diff --git a/inject-test/src/test/java/org/example/custom/CustomBean.java b/inject-test/src/test/java/org/example/custom/CustomBean.java new file mode 100644 index 000000000..f02c7da4e --- /dev/null +++ b/inject-test/src/test/java/org/example/custom/CustomBean.java @@ -0,0 +1,10 @@ +package org.example.custom; + +import jakarta.inject.Named; +import org.example.MyCustomScope; + +@Named("Hello") +@MyCustomScope +public class CustomBean { + +} diff --git a/inject-test/src/test/java/org/example/custom/CustomScopeTest.java b/inject-test/src/test/java/org/example/custom/CustomScopeTest.java new file mode 100644 index 000000000..664906e52 --- /dev/null +++ b/inject-test/src/test/java/org/example/custom/CustomScopeTest.java @@ -0,0 +1,128 @@ +package org.example.custom; + +import io.avaje.inject.BeanEntry; +import io.avaje.inject.BeanScope; +import org.example.MyCustomScope; +import org.example.coffee.CoffeeMaker; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import java.util.List; +import java.util.Optional; + +import static java.util.stream.Collectors.toList; +import static org.assertj.core.api.Assertions.assertThat; + +class CustomScopeTest { + + @Test + void customScopeWithParent() { + try (final BeanScope parentScope = BeanScope.newBuilder().build()) { + final CoffeeMaker parentMaker = parentScope.get(CoffeeMaker.class); + assertThat(parentMaker).isNotNull(); + + LocalExt ext = new LocalExt(); + + try (BeanScope beanScope = BeanScope.newBuilder() + .withParent(parentScope) + .withModules(new MyCustomModule()) + .withBeans(LocalExternal.class, ext) + .build()) { + + final CoffeeMaker coffeeMaker = beanScope.get(CoffeeMaker.class); + assertThat(coffeeMaker).isNotNull(); + + final CustomBean bean = beanScope.get(CustomBean.class); + final OtherCBean cBean = beanScope.get(OtherCBean.class); + assertThat(bean).isNotNull(); + assertThat(cBean.dependency()).isSameAs(bean); + + final LocalExternal externallyProvided = beanScope.get(LocalExternal.class); + assertThat(externallyProvided).isSameAs(ext); + } + } + } + + @Test + void buildSimpleCustomScope() { + + LocalExt ext = new LocalExt(); + CoffeeMaker suppliedCoffeeMaker = Mockito.mock(CoffeeMaker.class); + + try (BeanScope beanScope = BeanScope.newBuilder() + .withModules(new MyCustomModule()) + .withBean(LocalExternal.class, ext) + .withBean(CoffeeMaker.class, suppliedCoffeeMaker) + .build()) { + + final CustomBean bean = beanScope.get(CustomBean.class); + final OtherCBean cBean = beanScope.get(OtherCBean.class); + assertThat(bean).isNotNull(); + assertThat(cBean.dependency()).isSameAs(bean); + + final LocalExternal externallyProvided = beanScope.get(LocalExternal.class); + assertThat(externallyProvided).isSameAs(ext); + + final CoffeeMaker coffeeMaker = beanScope.get(CoffeeMaker.class); + assertThat(coffeeMaker).isSameAs(suppliedCoffeeMaker); + } + } + + @Test + void customScopeAll() { + + LocalExt ext = new LocalExt(); + CoffeeMaker suppliedCoffeeMaker = Mockito.mock(CoffeeMaker.class); + + try (BeanScope beanScope = BeanScope.newBuilder() + .withModules(new MyCustomModule()) + .withBean(LocalExternal.class, ext) + .withBean(CoffeeMaker.class, suppliedCoffeeMaker) + .build()) { + + // includes the 2 supplied beans + final List all = beanScope.all(); + assertThat(all).hasSize(5); + + final CustomBean customBean = beanScope.get(CustomBean.class); + + final Optional customBeanEntry = all.stream() + .filter(beanEntry -> beanEntry.hasKey(CustomBean.class)) + .findFirst(); + + assertThat(customBeanEntry).isPresent(); + assertThat(customBeanEntry.get().bean()).isSameAs(customBean); + assertThat(customBeanEntry.get().qualifierName()).isEqualTo("hello"); + assertThat(customBeanEntry.get().priority()).isEqualTo(0); + assertThat(customBeanEntry.get().keys()).containsExactly(CustomBean.class.getCanonicalName(), MyCustomScope.class.getCanonicalName()); + + + final Optional fooCustomEntry = all.stream() + .filter(beanEntry -> beanEntry.hasKey(FooCustom.class)) + .findFirst(); + + assertThat(fooCustomEntry).isPresent(); + assertThat(fooCustomEntry.get().qualifierName()).isNull(); + + // only the beans with MyCustomScope annotation + final List myCustomScopeBeans = all.stream() + .filter(beanEntry -> beanEntry.hasKey(MyCustomScope.class)) + .collect(toList()); + assertThat(myCustomScopeBeans).hasSize(3); + + final OtherCBean cBean = beanScope.get(OtherCBean.class); + assertThat(customBean).isNotNull(); + assertThat(cBean.dependency()).isSameAs(customBean); + + final LocalExternal externallyProvided = beanScope.get(LocalExternal.class); + assertThat(externallyProvided).isSameAs(ext); + + final CoffeeMaker coffeeMaker = beanScope.get(CoffeeMaker.class); + assertThat(coffeeMaker).isSameAs(suppliedCoffeeMaker); + } + } + + static class LocalExt implements LocalExternal { + + } +} diff --git a/inject-test/src/test/java/org/example/custom/FooCustom.java b/inject-test/src/test/java/org/example/custom/FooCustom.java new file mode 100644 index 000000000..d81787180 --- /dev/null +++ b/inject-test/src/test/java/org/example/custom/FooCustom.java @@ -0,0 +1,19 @@ +package org.example.custom; + +import org.example.MyCustomScope; +import org.example.coffee.CoffeeMaker; + +@MyCustomScope +public class FooCustom { + + /** + * CoffeeMaker is provided by the "default" scope and that is "good enough" for now (could be better/tighter here). + */ + final CoffeeMaker coffeeMaker; + final LocalExternal external; + + public FooCustom(CoffeeMaker coffeeMaker, LocalExternal external) { + this.coffeeMaker = coffeeMaker; + this.external = external; + } +} diff --git a/inject-test/src/test/java/org/example/custom/LocalExternal.java b/inject-test/src/test/java/org/example/custom/LocalExternal.java new file mode 100644 index 000000000..4147b7704 --- /dev/null +++ b/inject-test/src/test/java/org/example/custom/LocalExternal.java @@ -0,0 +1,4 @@ +package org.example.custom; + +public interface LocalExternal { +} diff --git a/inject-test/src/test/java/org/example/custom/OtherCBean.java b/inject-test/src/test/java/org/example/custom/OtherCBean.java new file mode 100644 index 000000000..f413313e0 --- /dev/null +++ b/inject-test/src/test/java/org/example/custom/OtherCBean.java @@ -0,0 +1,17 @@ +package org.example.custom; + +import org.example.MyCustomScope; + +@MyCustomScope +public class OtherCBean { + + final CustomBean dependency; + + public OtherCBean(CustomBean dependency) { + this.dependency = dependency; + } + + public CustomBean dependency() { + return dependency; + } +} diff --git a/inject-test/src/test/java/org/example/custom2/OciMarker.java b/inject-test/src/test/java/org/example/custom2/OciMarker.java new file mode 100644 index 000000000..f2fd8cd04 --- /dev/null +++ b/inject-test/src/test/java/org/example/custom2/OciMarker.java @@ -0,0 +1,4 @@ +package org.example.custom2; + +public @interface OciMarker { +} diff --git a/inject-test/src/test/java/org/example/custom2/OciPlant.java b/inject-test/src/test/java/org/example/custom2/OciPlant.java new file mode 100644 index 000000000..a58d4f805 --- /dev/null +++ b/inject-test/src/test/java/org/example/custom2/OciPlant.java @@ -0,0 +1,4 @@ +package org.example.custom2; + +public interface OciPlant { +} diff --git a/inject-test/src/test/java/org/example/custom2/OciRock.java b/inject-test/src/test/java/org/example/custom2/OciRock.java new file mode 100644 index 000000000..4922d324c --- /dev/null +++ b/inject-test/src/test/java/org/example/custom2/OciRock.java @@ -0,0 +1,4 @@ +package org.example.custom2; + +public interface OciRock { +} diff --git a/inject-test/src/test/java/org/example/custom2/OcsOne.java b/inject-test/src/test/java/org/example/custom2/OcsOne.java new file mode 100644 index 000000000..d8b8ed510 --- /dev/null +++ b/inject-test/src/test/java/org/example/custom2/OcsOne.java @@ -0,0 +1,6 @@ +package org.example.custom2; + +@OtherScope +public class OcsOne implements OciPlant, OciRock { + +} diff --git a/inject-test/src/test/java/org/example/custom2/OcsThree.java b/inject-test/src/test/java/org/example/custom2/OcsThree.java new file mode 100644 index 000000000..0f78c1c38 --- /dev/null +++ b/inject-test/src/test/java/org/example/custom2/OcsThree.java @@ -0,0 +1,5 @@ +package org.example.custom2; + +@OtherScope +public class OcsThree { +} diff --git a/inject-test/src/test/java/org/example/custom2/OcsTwo.java b/inject-test/src/test/java/org/example/custom2/OcsTwo.java new file mode 100644 index 000000000..2b13c6476 --- /dev/null +++ b/inject-test/src/test/java/org/example/custom2/OcsTwo.java @@ -0,0 +1,12 @@ +package org.example.custom2; + +@OciMarker +@OtherScope +public class OcsTwo implements OciRock { + + final OcsOne one; + + OcsTwo(OcsOne one) { + this.one = one; + } +} diff --git a/inject-test/src/test/java/org/example/custom2/OtherScope.java b/inject-test/src/test/java/org/example/custom2/OtherScope.java new file mode 100644 index 000000000..23bbde407 --- /dev/null +++ b/inject-test/src/test/java/org/example/custom2/OtherScope.java @@ -0,0 +1,7 @@ +package org.example.custom2; + +import jakarta.inject.Scope; + +@Scope +public @interface OtherScope { +} diff --git a/inject-test/src/test/java/org/example/custom3/MyThreeScope.java b/inject-test/src/test/java/org/example/custom3/MyThreeScope.java new file mode 100644 index 000000000..68be8a214 --- /dev/null +++ b/inject-test/src/test/java/org/example/custom3/MyThreeScope.java @@ -0,0 +1,13 @@ +package org.example.custom3; + +import io.avaje.inject.InjectModule; +import jakarta.inject.Scope; +import org.example.custom2.OtherScope; + +/** + * This scope has a requires on another scope - requires = OtherScope + */ +@Scope +@InjectModule(requires = OtherScope.class) +public @interface MyThreeScope { +} diff --git a/inject-test/src/test/java/org/example/custom3/ParentScopeTest.java b/inject-test/src/test/java/org/example/custom3/ParentScopeTest.java new file mode 100644 index 000000000..95c6d6048 --- /dev/null +++ b/inject-test/src/test/java/org/example/custom3/ParentScopeTest.java @@ -0,0 +1,47 @@ +package org.example.custom3; + +import io.avaje.inject.BeanScope; +import org.example.custom2.*; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +class ParentScopeTest { + + @Test + void parentScope() { + + final BeanScope parent = BeanScope.newBuilder() + .withModules(new OtherModule()) + .build(); + + final BeanScope scope = BeanScope.newBuilder() + .withModules(new MyThreeModule()) + .withParent(parent) + .build(); + + // factory for custom scope + final TcsGreen green = scope.get(TcsGreen.class); + assertThat(green).isNotNull(); + + final TcsRed red = scope.get(TcsRed.class); + final TcsBlue blue = scope.get(TcsBlue.class); + final OcsOne ocsOne = scope.get(OcsOne.class); + final OcsTwo ocsTwo = scope.get(OcsTwo.class); + final OcsThree ocsThree = scope.get(OcsThree.class); + + // combined from both scopes + final List allRocks = scope.list(OciRock.class); + assertThat(allRocks).containsOnly(red, ocsOne, ocsTwo); + + // combined from both scopes + final List marked = scope.listByAnnotation(OciMarker.class); + assertThat(marked).containsOnly(red, ocsTwo); + + // dependency wired from parent + assertThat(blue).isNotNull(); + assertThat(blue.getDependency()).isSameAs(ocsThree); + } +} diff --git a/inject-test/src/test/java/org/example/custom3/TcsBlue.java b/inject-test/src/test/java/org/example/custom3/TcsBlue.java new file mode 100644 index 000000000..d7ac0da08 --- /dev/null +++ b/inject-test/src/test/java/org/example/custom3/TcsBlue.java @@ -0,0 +1,20 @@ +package org.example.custom3; + +import org.example.custom2.OcsThree; + +@MyThreeScope +public class TcsBlue { + + /** + * Wire this dependency from the parent BeanScope. + */ + final OcsThree ocs; + + public TcsBlue(OcsThree ocs) { + this.ocs = ocs; + } + + public OcsThree getDependency() { + return ocs; + } +} diff --git a/inject-test/src/test/java/org/example/custom3/TcsFactory.java b/inject-test/src/test/java/org/example/custom3/TcsFactory.java new file mode 100644 index 000000000..9dcb930a4 --- /dev/null +++ b/inject-test/src/test/java/org/example/custom3/TcsFactory.java @@ -0,0 +1,14 @@ +package org.example.custom3; + +import io.avaje.inject.Bean; +import io.avaje.inject.Factory; + +@MyThreeScope +@Factory +public class TcsFactory { + + @Bean + TcsGreen green() { + return new TcsGreen(); + } +} diff --git a/inject-test/src/test/java/org/example/custom3/TcsGreen.java b/inject-test/src/test/java/org/example/custom3/TcsGreen.java new file mode 100644 index 000000000..804f2be52 --- /dev/null +++ b/inject-test/src/test/java/org/example/custom3/TcsGreen.java @@ -0,0 +1,7 @@ +package org.example.custom3; + +/** + * Created by TcsFactory + */ +public class TcsGreen { +} diff --git a/inject-test/src/test/java/org/example/custom3/TcsRed.java b/inject-test/src/test/java/org/example/custom3/TcsRed.java new file mode 100644 index 000000000..1822828db --- /dev/null +++ b/inject-test/src/test/java/org/example/custom3/TcsRed.java @@ -0,0 +1,9 @@ +package org.example.custom3; + +import org.example.custom2.OciMarker; +import org.example.custom2.OciRock; + +@OciMarker +@MyThreeScope +public class TcsRed implements OciRock { +} diff --git a/inject-test/src/test/java/org/example/iface/NestedInterfaceTest.java b/inject-test/src/test/java/org/example/iface/NestedInterfaceTest.java index 7ae99dda3..68864d70b 100644 --- a/inject-test/src/test/java/org/example/iface/NestedInterfaceTest.java +++ b/inject-test/src/test/java/org/example/iface/NestedInterfaceTest.java @@ -22,10 +22,11 @@ void test() { void test_provided() { try (BeanScope context = BeanScope.newBuilder() + .forTesting() .withMock(Some.Nested.class, nested -> when(nested.doNested()).thenReturn("myMock")) .build()) { - final Some.Nested nestedInterface = context.getBean(Some.Nested.class); + final Some.Nested nestedInterface = context.get(Some.Nested.class); assertNotNull(nestedInterface); assertEquals("myMock", nestedInterface.doNested()); } diff --git a/inject-test/src/test/java/org/example/injectextension/WithExtnNamedMocksTest.java b/inject-test/src/test/java/org/example/injectextension/WithExtnNamedMocksTest.java index a79bd5380..e2fc09b88 100644 --- a/inject-test/src/test/java/org/example/injectextension/WithExtnNamedMocksTest.java +++ b/inject-test/src/test/java/org/example/injectextension/WithExtnNamedMocksTest.java @@ -38,6 +38,7 @@ static class ProgrammaticTest { void test() { try (BeanScope beanScope = BeanScope.newBuilder() + .forTesting() .withMock(SomeStore.class, "Blue") .withMock(SomeStore.class, "green") .build()) { diff --git a/inject-test/src/test/java/org/example/injectextension/WithExtnNamedSpyTest.java b/inject-test/src/test/java/org/example/injectextension/WithExtnNamedSpyTest.java index 0873ee2ca..f7be30710 100644 --- a/inject-test/src/test/java/org/example/injectextension/WithExtnNamedSpyTest.java +++ b/inject-test/src/test/java/org/example/injectextension/WithExtnNamedSpyTest.java @@ -42,6 +42,7 @@ static class ProgrammaticTest { void test() { try (BeanScope beanScope = BeanScope.newBuilder() + .forTesting() .withSpy(SomeStore.class, "blue") .withSpy(SomeStore.class, "Green") .build()) { diff --git a/inject-test/src/test/java/org/example/optional/AllQue.java b/inject-test/src/test/java/org/example/optional/AllQue.java index 59de651de..02000dac8 100644 --- a/inject-test/src/test/java/org/example/optional/AllQue.java +++ b/inject-test/src/test/java/org/example/optional/AllQue.java @@ -19,6 +19,7 @@ public class AllQue { this.frodo = frodo; this.sam = sam; this.bilbo = bilbo; + System.out.println("bazz"); } String whichSet() { diff --git a/inject-test/src/test/java/org/example/requestscope/BluesRStuff.java b/inject-test/src/test/java/org/example/requestscope/BluesRStuff.java deleted file mode 100644 index 416d192bd..000000000 --- a/inject-test/src/test/java/org/example/requestscope/BluesRStuff.java +++ /dev/null @@ -1,25 +0,0 @@ -package org.example.requestscope; - -import io.avaje.inject.Request; -import jakarta.inject.Named; - -//@Named("blues") -// "blues" is derived qualifier name based on short name relative to interface short name -@Request -public class BluesRStuff implements RStuff { - - private final RPump pump; - - public BluesRStuff(@Named("blue") RPump pump) { - this.pump = pump; - } - - String pump() { - return pump.pump(); - } - - @Override - public String stuff() { - return "stuff_"+pump(); - } -} diff --git a/inject-test/src/test/java/org/example/requestscope/ComboRStuff.java b/inject-test/src/test/java/org/example/requestscope/ComboRStuff.java deleted file mode 100644 index 64029080e..000000000 --- a/inject-test/src/test/java/org/example/requestscope/ComboRStuff.java +++ /dev/null @@ -1,21 +0,0 @@ -package org.example.requestscope; - -import io.avaje.inject.Request; -import jakarta.inject.Named; - -@Request -public class ComboRStuff { - - private final RStuff red; - private final RStuff blue; - - public ComboRStuff(@Named("reds") RStuff red, @Named("blues") RStuff blue) { - this.red = red; - this.blue = blue; - } - - - String stuff() { - return red.stuff() + " " + blue.stuff(); - } -} diff --git a/inject-test/src/test/java/org/example/requestscope/MyReqThingWithContext.java b/inject-test/src/test/java/org/example/requestscope/MyReqThingWithContext.java deleted file mode 100644 index d8767e634..000000000 --- a/inject-test/src/test/java/org/example/requestscope/MyReqThingWithContext.java +++ /dev/null @@ -1,15 +0,0 @@ -package org.example.requestscope; - -import io.avaje.inject.Request; -import io.javalin.http.Context; - -@Request -public class MyReqThingWithContext { - - final Context javalinContext; - - public MyReqThingWithContext(Context javalinContext) { - this.javalinContext = javalinContext; - } - -} diff --git a/inject-test/src/test/java/org/example/requestscope/MyRequestOne.java b/inject-test/src/test/java/org/example/requestscope/MyRequestOne.java deleted file mode 100644 index 8404c2d0c..000000000 --- a/inject-test/src/test/java/org/example/requestscope/MyRequestOne.java +++ /dev/null @@ -1,94 +0,0 @@ -package org.example.requestscope; - -import io.avaje.inject.Request; -import jakarta.inject.Inject; -import org.example.request.AService; - -import javax.annotation.PostConstruct; -import java.io.Closeable; -import java.util.Optional; - -@Request -class MyRequestOne implements Closeable { - - /** - * Wiring a bean scope singleton. - */ - private final AService service; - /** - * Wiring an bean provided to the request scope. - */ - private final ReqThing reqThing; - - private AService otherService; - private ReqThing otherReqThing; - - /** - * Field injection on request scoped bean. - */ - @Inject - Optional myopt; - - private boolean firedPostConstruct; - private boolean firedMethodInjection; - private boolean firedClose; - - MyRequestOne(AService service, ReqThing reqThing) { - this.service = service; - this.reqThing = reqThing; - } - - /** - * Method injection on request scoped bean. - */ - @Inject - void foo(AService other, ReqThing otherThing) { - firedMethodInjection = true; - otherService = other; - otherReqThing = otherThing; - } - - /** - * Run immediately after field/method injection of request scoped bean. - */ - @PostConstruct - void init() { - firedPostConstruct = true; - } - - /** - * Closable automatically treated as PreDestroy for request scoped beans. - */ - @Override - public void close() { - firedClose = true; - } - - AService getService() { - return service; - } - - ReqThing getReqThing() { - return reqThing; - } - - AService getOtherService() { - return otherService; - } - - ReqThing getOtherReqThing() { - return otherReqThing; - } - - boolean isFiredPostConstruct() { - return firedPostConstruct; - } - - boolean isFiredMethodInjection() { - return firedMethodInjection; - } - - boolean isFiredClose() { - return firedClose; - } -} diff --git a/inject-test/src/test/java/org/example/requestscope/MyRequestPreDestroy.java b/inject-test/src/test/java/org/example/requestscope/MyRequestPreDestroy.java deleted file mode 100644 index 193acfb33..000000000 --- a/inject-test/src/test/java/org/example/requestscope/MyRequestPreDestroy.java +++ /dev/null @@ -1,22 +0,0 @@ -package org.example.requestscope; - -import io.avaje.inject.PreDestroy; -import io.avaje.inject.Request; - -@Request -public class MyRequestPreDestroy { - - private boolean firedPreDestroy; - - /** - * Using PreDestroy rather than implementing Closable. - */ - @PreDestroy - void destroy() { - firedPreDestroy = true; - } - - boolean isFiredPreDestroy() { - return firedPreDestroy; - } -} diff --git a/inject-test/src/test/java/org/example/requestscope/NotReqBean.java b/inject-test/src/test/java/org/example/requestscope/NotReqBean.java deleted file mode 100644 index 8d46b86f6..000000000 --- a/inject-test/src/test/java/org/example/requestscope/NotReqBean.java +++ /dev/null @@ -1,14 +0,0 @@ -package org.example.requestscope; - -import jakarta.inject.Singleton; - -@Singleton -public class NotReqBean { - - //final MyReqThingWithContext myReq; - - public NotReqBean() {//MyReqThingWithContext myReq) { - //this.myReq = myReq; - } - -} diff --git a/inject-test/src/test/java/org/example/requestscope/RPump.java b/inject-test/src/test/java/org/example/requestscope/RPump.java deleted file mode 100644 index c7a8f392d..000000000 --- a/inject-test/src/test/java/org/example/requestscope/RPump.java +++ /dev/null @@ -1,6 +0,0 @@ -package org.example.requestscope; - -public interface RPump { - - String pump(); -} diff --git a/inject-test/src/test/java/org/example/requestscope/RPumpF.java b/inject-test/src/test/java/org/example/requestscope/RPumpF.java deleted file mode 100644 index 9a5511f03..000000000 --- a/inject-test/src/test/java/org/example/requestscope/RPumpF.java +++ /dev/null @@ -1,15 +0,0 @@ -package org.example.requestscope; - -class RPumpF implements RPump { - - final String data; - - RPumpF(String data) { - this.data = data; - } - - @Override - public String pump() { - return data; - } -} diff --git a/inject-test/src/test/java/org/example/requestscope/RStuff.java b/inject-test/src/test/java/org/example/requestscope/RStuff.java deleted file mode 100644 index d0e2e1f39..000000000 --- a/inject-test/src/test/java/org/example/requestscope/RStuff.java +++ /dev/null @@ -1,6 +0,0 @@ -package org.example.requestscope; - -public interface RStuff { - - String stuff(); -} diff --git a/inject-test/src/test/java/org/example/requestscope/RedsRStuff.java b/inject-test/src/test/java/org/example/requestscope/RedsRStuff.java deleted file mode 100644 index 507204f01..000000000 --- a/inject-test/src/test/java/org/example/requestscope/RedsRStuff.java +++ /dev/null @@ -1,24 +0,0 @@ -package org.example.requestscope; - -import io.avaje.inject.Request; -import jakarta.inject.Named; - -@Named("reds") -@Request -public class RedsRStuff implements RStuff { - - private final RPump pump; - - public RedsRStuff(@Named("red") RPump pump) { - this.pump = pump; - } - - String pump() { - return pump.pump(); - } - - @Override - public String stuff() { - return "stuff_"+pump(); - } -} diff --git a/inject-test/src/test/java/org/example/requestscope/ReqThing.java b/inject-test/src/test/java/org/example/requestscope/ReqThing.java deleted file mode 100644 index 26455434f..000000000 --- a/inject-test/src/test/java/org/example/requestscope/ReqThing.java +++ /dev/null @@ -1,5 +0,0 @@ -package org.example.requestscope; - -public interface ReqThing { - String hello(); -} diff --git a/inject-test/src/test/java/org/example/requestscope/RequestScopedBeanTest.java b/inject-test/src/test/java/org/example/requestscope/RequestScopedBeanTest.java deleted file mode 100644 index e66ba52c4..000000000 --- a/inject-test/src/test/java/org/example/requestscope/RequestScopedBeanTest.java +++ /dev/null @@ -1,95 +0,0 @@ -package org.example.requestscope; - -import io.avaje.inject.ApplicationScope; -import io.avaje.inject.RequestScope; -import org.example.request.AService; -import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.Assertions.assertThat; - -class RequestScopedBeanTest { - - @Test - void preDestroy() { - - MyRequestPreDestroy bean; - try (RequestScope requestScope = ApplicationScope.newRequestScope() - .build()) { - bean = requestScope.get(MyRequestPreDestroy.class); - assertThat(bean.isFiredPreDestroy()).isFalse(); - } - assertThat(bean.isFiredPreDestroy()).isTrue(); - } - - @Test - void newRequestScope() { - - MyReqThing myReqThing = new MyReqThing(); - - MyRequestOne myRequestOne; - try (RequestScope requestScope = ApplicationScope.newRequestScope() - .withBean(ReqThing.class, myReqThing) - .build()) { - - myRequestOne = requestScope.get(MyRequestOne.class); - assertThat(myRequestOne.getReqThing().hello()).isEqualTo("MyReqThing"); - - assertThat(myRequestOne.isFiredPostConstruct()).isTrue(); - assertThat(myRequestOne.isFiredMethodInjection()).isTrue(); - - assertThat(myRequestOne).isNotNull(); - assertThat(myRequestOne.getReqThing()).isSameAs(myReqThing); - - AService service = requestScope.get(AService.class); - assertThat(myRequestOne.getService()).isSameAs(service); - assertThat(myRequestOne.getOtherService()).isSameAs(service); - assertThat(myRequestOne.getOtherReqThing()).isSameAs(myReqThing); - - // assert we get back the same instance - MyRequestOne myRequestOneAgain = requestScope.get(MyRequestOne.class); - assertThat(myRequestOneAgain).isSameAs(myRequestOne); - } - - // assert request scoped closable was closed - assertThat(myRequestOne.isFiredClose()).isTrue(); - } - - @Test - void provideNamedForRequestBeans() { - - // providing named implementations of RPump - RPump redPump = new RPumpF("red"); - RPump bluePump = new RPumpF("blue"); - - try (RequestScope requestScope = ApplicationScope.newRequestScope() - .withBean("red", RPump.class, redPump) - .withBean("blue", RPump.class, bluePump) - .build()) { - - final RedsRStuff redsRStuff = requestScope.get(RedsRStuff.class); - assertThat(redsRStuff.pump()).isEqualTo("red"); - - final RStuff redsRStuff2 = requestScope.get(RStuff.class, "reds"); - assertThat(redsRStuff2.stuff()).isEqualTo("stuff_red"); - assertThat(redsRStuff2).isSameAs(redsRStuff); - - - final BluesRStuff bluesRStuff = requestScope.get(BluesRStuff.class); - assertThat(bluesRStuff.pump()).isEqualTo("blue"); - - final RStuff blues2 = requestScope.get(RStuff.class, "blues"); - assertThat(blues2.stuff()).isEqualTo("stuff_blue"); - assertThat(blues2).isSameAs(bluesRStuff); - - final ComboRStuff comboRStuff = requestScope.get(ComboRStuff.class); - assertThat(comboRStuff.stuff()).isEqualTo("stuff_red stuff_blue"); - } - } - - static class MyReqThing implements ReqThing { - @Override - public String hello() { - return "MyReqThing"; - } - } -} diff --git a/inject-test/src/test/java/org/example/requestscope/RequestScopedWithControllerDependencyTest.java b/inject-test/src/test/java/org/example/requestscope/RequestScopedWithControllerDependencyTest.java deleted file mode 100644 index 6c84913f0..000000000 --- a/inject-test/src/test/java/org/example/requestscope/RequestScopedWithControllerDependencyTest.java +++ /dev/null @@ -1,28 +0,0 @@ -package org.example.requestscope; - -import io.avaje.inject.ApplicationScope; -import io.avaje.inject.RequestScope; -import io.javalin.http.Context; -import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; - -/** - * Test that the factory generation is suppressed for the case where - * the request scoped bean has a dependency on one of the special - * 'request scope controller dependencies' (like Javalin Context). - */ -class RequestScopedWithControllerDependencyTest { - - @Test - void test() { - try (RequestScope scope = ApplicationScope.newRequestScope() - .withBean(Context.class, mock(Context.class)) - .build()) { - - final MyReqThingWithContext reqThing = scope.get(MyReqThingWithContext.class); - assertThat(reqThing.javalinContext).isNotNull(); - } - } -} diff --git a/inject/README.md b/inject/README.md index 7c3f05d90..7872466a4 100644 --- a/inject/README.md +++ b/inject/README.md @@ -4,14 +4,13 @@ APT based dependency injection for server side developers - https://avaje.io/inj ### Example module use ```java -import io.avaje.inject.spi.BeanScopeFactory; +import io.avaje.inject.spi.Module; 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/pom.xml b/inject/pom.xml index a629775ac..79c1bc2c8 100644 --- a/inject/pom.xml +++ b/inject/pom.xml @@ -4,7 +4,7 @@ io.avaje avaje-inject-parent - 6.2 + 6.5-RC0 avaje-inject diff --git a/inject/src/main/java/io/avaje/inject/BeanEntry.java b/inject/src/main/java/io/avaje/inject/BeanEntry.java index cc614fa08..889757e4c 100644 --- a/inject/src/main/java/io/avaje/inject/BeanEntry.java +++ b/inject/src/main/java/io/avaje/inject/BeanEntry.java @@ -1,107 +1,68 @@ package io.avaje.inject; +import java.util.Set; + /** * A bean entry with priority and optional name. + * + * @see BeanScope#all() */ -public class BeanEntry { +public interface BeanEntry { /** - * An explicitly supplied bean. See BeanScopeBuilder. + * Priority of externally supplied bean. */ - public static final int SUPPLIED = 2; + int SUPPLIED = 2; /** - * An @Primary bean. + * Priority of @Primary bean. */ - public static final int PRIMARY = 1; + int PRIMARY = 1; /** - * A normal priority bean. + * Priority of normal bean. */ - public static final int NORMAL = 0; + int NORMAL = 0; /** - * A @Secondary bean. + * Priority of @Secondary bean. */ - public static final int SECONDARY = -1; - - private final int priority; - - private final T bean; - - private final String name; + int SECONDARY = -1; /** - * Construct with priority, name and the bean. - */ - public BeanEntry(int priority, T bean, String name) { - this.priority = priority; - this.bean = bean; - this.name = name; - } - - /** - * Return the priority (Primary, Normal and Secondary). + * Return the bean name. */ - public int getPriority() { - return priority; - } + String qualifierName(); /** - * Return the bean. + * Return the bean instance. */ - public T getBean() { - return bean; - } + Object bean(); /** - * Return the bean name. + * The bean instance type. */ - public String getName() { - return name; - } + Class type(); /** - * Return true if this entry has Supplied priority. + * Return the priority indicating if the bean is Supplied Primary, Normal or Secondary. */ - public boolean isSupplied() { - return priority == SUPPLIED; - } + int priority(); /** - * Return true if this entry has Primary priority. + * Return the type keys for this bean. + *

+ * This is the set of type, interface types and annotation types that the entry is registered for. */ - public boolean isPrimary() { - return priority == PRIMARY; - } + Set keys(); /** - * Return true if this entry has Secondary priority. + * Return true if the entry has a key for this type. + *

+ * This is true if the keys contains the canonical name of the given type. + * + * @param type The type to match. Can be any type including concrete, interface or annotation type. */ - public boolean isSecondary() { - return priority == SECONDARY; - } + boolean hasKey(Class type); - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - sb.append("{bean:").append(bean); - if (name != null) { - sb.append(", name:").append(name); - } - switch (priority) { - case SUPPLIED: - sb.append(", Supplied"); - break; - case PRIMARY: - sb.append(", @Primary"); - break; - case SECONDARY: - sb.append(", @Secondary"); - break; - default: - sb.append(", Normal"); - } - return sb.append("}").toString(); - } } diff --git a/inject/src/main/java/io/avaje/inject/BeanScope.java b/inject/src/main/java/io/avaje/inject/BeanScope.java index 7b9138bc6..b5631c227 100644 --- a/inject/src/main/java/io/avaje/inject/BeanScope.java +++ b/inject/src/main/java/io/avaje/inject/BeanScope.java @@ -1,6 +1,5 @@ package io.avaje.inject; -import java.io.Closeable; import java.lang.annotation.Annotation; import java.util.List; @@ -13,7 +12,7 @@ * *

Create a BeanScope

*

- * We can programmatically create a BeanScope via BeanScopeBuilder. + * We can programmatically create a BeanScope via {@code BeanScope.newBuilder()}. *

*
{@code
  *
@@ -22,94 +21,86 @@
  *   try (BeanScope scope = BeanScope.newBuilder()
  *     .build()) {
  *
- *     CoffeeMaker coffeeMaker = context.getBean(CoffeeMaker.class);
+ *     CoffeeMaker coffeeMaker = context.get(CoffeeMaker.class);
  *     coffeeMaker.makeIt()
  *   }
  *
  * }
* - *

Implicitly used

+ *

External dependencies

*

- * The BeanScope is implicitly used by ApplicationScope. It will be created as needed and - * a shutdown hook will close the underlying BeanScope on JVM shutdown. + * We can supporting external dependencies when creating the BeanScope. We need to do 2 things. + * we need to specify these via *

+ *
    + *
  • + * 1. Specify the external dependency via {@code @InjectModule(requires=...)}. + * Otherwise at compile time the annotation processor detects it as a missing dependency and we can't compile. + *
  • + *
  • + * 2. Provide the dependency when creating the BeanScope + *
  • + *
+ *

+ * For example, given we have Pump as an externally provided dependency. + * *

{@code
  *
- *   // BeanScope created as needed under the hood
+ *   // tell the annotation processor Pump is provided externally
+ *   // otherwise it thinks we have a missing dependency
  *
- *   CoffeeMaker coffeeMaker = ApplicationScope.get(CoffeeMaker.class);
- *   coffeeMaker.brew();
+ *   @InjectModule(requires=Pump.class)
+ *
+ * }
+ *

+ * When we build the BeanScope provide the dependency via {@link BeanScopeBuilder#withBean(Class, Object)}. + * + *

{@code
+ *
+ *   // provide external dependencies ...
+ *   Pump pump = ...
+ *
+ *   try (BeanScope scope = BeanScope.newBuilder()
+ *     .withBean(Pump.class, pump)
+ *     .build()) {
+ *
+ *     CoffeeMaker coffeeMaker = context.get(CoffeeMaker.class);
+ *     coffeeMaker.makeIt()
+ *   }
  *
  * }
*/ -public interface BeanScope extends Closeable { +public interface BeanScope extends AutoCloseable { /** - * Build a bean scope with options for shutdown hook and supplying test doubles. + * Build a bean scope with options for shutdown hook and supplying external dependencies. *

- * We would choose to use BeanScopeBuilder in test code (for component testing) - * as it gives us the ability to inject test doubles, mocks, spy's etc. - *

+ * We can optionally: + *
    + *
  • Provide external dependencies
  • + *
  • Specify a parent BeanScope
  • + *
  • Specify specific modules to wire
  • + *
  • Specify to include a shutdown hook (to fire preDestroy lifecycle methods)
  • + *
  • Use {@code forTesting()} to specify mocks and spies to use when wiring tests
  • + *
* *
{@code
    *
-   *   @Test
-   *   public void someComponentTest() {
-   *
-   *     MyRedisApi mockRedis = mock(MyRedisApi.class);
-   *     MyDbApi mockDatabase = mock(MyDbApi.class);
+   *   // create a BeanScope ...
    *
-   *     try (BeanScope scope = BeanScope.newBuilder()
-   *       .withBeans(mockRedis, mockDatabase)
-   *       .build()) {
+   *   try (BeanScope scope = BeanScope.newBuilder()
+   *     .build()) {
    *
-   *       // built with test doubles injected ...
-   *       CoffeeMaker coffeeMaker = scope.get(CoffeeMaker.class);
-   *       coffeeMaker.makeIt();
-   *
-   *       assertThat(...
-   *     }
+   *     CoffeeMaker coffeeMaker = context.get(CoffeeMaker.class);
+   *     coffeeMaker.makeIt()
    *   }
+   *
    * }
*/ static BeanScopeBuilder newBuilder() { return new DBeanScopeBuilder(); } - /** - * Create a RequestScope via builder where we provide extra instances - * that can be used/included in wiring request scoped beans. - * - *
{@code
-   *
-   *   try (RequestScope requestScope = beanScope.newRequestScope()
-   *       // supply some instances
-   *       .withBean(HttpRequest.class, request)
-   *       .withBean(HttpResponse.class, response)
-   *       .build()) {
-   *
-   *       MyController controller = requestScope.get(MyController.class);
-   *       controller.process();
-   *   }
-   *
-   *   ...
-   *
-   *   // define request scoped beans
-   *   @Request
-   *   MyController {
-   *
-   *     // can depend on supplied instances, singletons and other request scope beans
-   *     @Inject
-   *     MyController(HttpRequest request, HttpResponse response, MyService myService) {
-   *       ...
-   *     }
-   *
-   *   }
-   *
-   * }
- */ - RequestScopeBuilder newRequestScope(); - /** * Return a single bean given the type. * @@ -124,14 +115,6 @@ static BeanScopeBuilder newBuilder() { */ T get(Class type); - /** - * Deprecated - migrate to get(type) - */ - @Deprecated - default T getBean(Class type) { - return get(type); - } - /** * Return a single bean given the type and name. * @@ -147,14 +130,6 @@ default T getBean(Class type) { */ T get(Class type, String name); - /** - * Deprecated - migrate to get(type, name). - */ - @Deprecated - default T getBean(Class type, String name) { - return get(type, name); - } - /** * Return the list of beans that have an annotation. * @@ -163,26 +138,14 @@ default T getBean(Class type, String name) { * // e.g. register all controllers with web a framework * // .. where Controller is an annotation on the beans * - * List controllers = ApplicationScope.listByAnnotation(Controller.class); + * List controllers = beanScope.listByAnnotation(Controller.class); * * } * - *

- * The classic use case for this is registering controllers or routes to - * web frameworks like Sparkjava, Javlin, Rapidoid etc. - * * @param annotation An annotation class. */ List listByAnnotation(Class annotation); - /** - * Deprecated - migrate to listByAnnotation() - */ - @Deprecated - default List getBeansWithAnnotation(Class annotation) { - return listByAnnotation(annotation); - } - /** * Return the list of beans that implement the interface. * @@ -190,7 +153,7 @@ default List getBeansWithAnnotation(Class annotation) { * * // e.g. register all routes for a web framework * - * List routes = ApplicationScope.list(WebRoute.class); + * List routes = beanScope.list(WebRoute.class); * * } * @@ -198,27 +161,11 @@ default List getBeansWithAnnotation(Class annotation) { */ List list(Class interfaceType); - /** - * Deprecated - migrate to list(interfaceType). - */ - @Deprecated - default List getBeans(Class interfaceType) { - return list(interfaceType); - } - /** * Return the list of beans that implement the interface sorting by priority. */ List listByPriority(Class interfaceType); - /** - * Deprecated - migrate to listByPriority(interfaceType). - */ - @Deprecated - default List getBeansByPriority(Class interfaceType) { - return listByPriority(interfaceType); - } - /** * Return the beans that implement the interface sorting by the priority annotation used. *

@@ -231,24 +178,16 @@ default List getBeansByPriority(Class interfaceType) { List listByPriority(Class interfaceType, Class priority); /** - * Deprecated - migrate to listByPriority(). - */ - @Deprecated - default List getBeansByPriority(Class interfaceType, Class priority) { - return listByPriority(interfaceType, priority); - } - - /** - * Return a request scoped provided for the specific type and name. + * Return all the bean entries from the scope. + *

+ * The bean entries include entries from the parent scope if it has one. * - * @param type The type of the request scoped bean - * @param name The optional qualifier name - * @return The request scope provider or null + * @return All bean entries from the scope. */ - RequestScopeMatch requestProvider(Class type, String name); + List all(); /** - * Close the scope firing any @PreDestroy methods. + * Close the scope firing any @PreDestroy lifecycle methods. */ void close(); } diff --git a/inject/src/main/java/io/avaje/inject/BeanScopeBuilder.java b/inject/src/main/java/io/avaje/inject/BeanScopeBuilder.java index 063e4109e..e1fc90dfc 100644 --- a/inject/src/main/java/io/avaje/inject/BeanScopeBuilder.java +++ b/inject/src/main/java/io/avaje/inject/BeanScopeBuilder.java @@ -1,60 +1,47 @@ package io.avaje.inject; +import io.avaje.inject.spi.Module; + import java.util.function.Consumer; /** - * Build a bean scope with options for shutdown hook and supplying test doubles. + * Build a bean scope with options for shutdown hook and supplying external dependencies. *

- * We would choose to use BeanScopeBuilder in test code (for component testing) as it gives us - * the ability to inject test doubles, mocks, spy's etc. + * We can provide external dependencies that are then used in wiring the components. *

* *
{@code
  *
- *   @Test
- *   public void someComponentTest() {
- *
- *     MyRedisApi mockRedis = mock(MyRedisApi.class);
- *     MyDbApi mockDatabase = mock(MyDbApi.class);
+ *   // external dependencies
+ *   Pump pump = ...
  *
- *     try (BeanScope scope = BeanScope.newBuilder()
- *       .withBeans(mockRedis, mockDatabase)
- *       .build()) {
+ *   BeanScope scope = BeanScope.newBuilder()
+ *     .withBean(pump)
+ *     .build();
  *
- *       // built with test doubles injected ...
- *       CoffeeMaker coffeeMaker = scope.get(CoffeeMaker.class);
- *       coffeeMaker.makeIt();
- *
- *       assertThat(...
- *     }
- *   }
+ *   CoffeeMaker coffeeMaker = scope.get(CoffeeMaker.class);
+ *   coffeeMaker.makeIt();
  *
  * }
*/ public interface BeanScopeBuilder { - /** - * Deprecated - migrate to {@link #withShutdownHook(boolean)} - */ - @Deprecated - BeanScopeBuilder withNoShutdownHook(); - /** * Create the bean scope registering a shutdown hook (defaults to false, no shutdown hook). *

- * The expectation is that the BeanScopeBuilder is closed via code or via using - * try with resources. + * With {@code withShutdownHook(true)} a shutdown hook will be registered with the Runtime + * and executed when the JVM initiates a shutdown. This then will run the {@code preDestroy} + * lifecycle methods. *

*
{@code
    *
    *   // automatically closed via try with resources
    *
-   *   try (BeanScope scope = BeanScope.newBuilder()
+   *   BeanScope scope = BeanScope.newBuilder()
    *     .withShutdownHook(true)
-   *     .build()) {
+   *     .build());
    *
-   *     String makeIt = scope.get(CoffeeMaker.class).makeIt();
-   *   }
+   *   // on JVM shutdown the preDestroy lifecycle methods are executed
    *
    * }
* @@ -64,46 +51,27 @@ public interface BeanScopeBuilder { /** * Specify the modules to include in dependency injection. - *

- * This is effectively a "whitelist" of modules names to include in the injection excluding - * any other modules that might otherwise exist in the classpath. - *

- * We typically want to use this in component testing where we wish to exclude any other - * modules that exist on the classpath. + *

+ * Only beans related to the module are included in the BeanScope that is built. + *

+ * When we do not explicitly specify modules then all modules that are not "custom scoped" + * are found and used via service loading. * *

{@code
    *
-   *   @Test
-   *   public void someComponentTest() {
-   *
-   *     EmailServiceApi mockEmailService = mock(EmailServiceApi.class);
+   *   BeanScope scope = BeanScope.newBuilder()
+   *     .withModules(new CustomModule())
+   *     .build());
    *
-   *     try (BeanScope scope = BeanScope.newBuilder()
-   *       .withBeans(mockEmailService)
-   *       .withModules("coffee")
-   *       .withIgnoreMissingModuleDependencies()
-   *       .build()) {
-   *
-   *       // built with test doubles injected ...
-   *       CoffeeMaker coffeeMaker = scope.get(CoffeeMaker.class);
-   *       coffeeMaker.makeIt();
-   *
-   *       assertThat(...
-   *     }
-   *   }
+   *   CoffeeMaker coffeeMaker = scope.get(CoffeeMaker.class);
+   *   coffeeMaker.makeIt();
    *
    * }
* - * @param modules The names of modules that we want to include in dependency injection. + * @param modules The modules that we want to include in dependency injection. * @return This BeanScopeBuilder */ - BeanScopeBuilder withModules(String... modules); - - /** - * Set this when building a BeanScope (typically for testing) and supplied beans replace module dependencies. - * This means we don't need the usual module dependencies as supplied beans are used instead. - */ - BeanScopeBuilder withIgnoreMissingModuleDependencies(); + BeanScopeBuilder withModules(Module... modules); /** * Supply a bean to the scope that will be used instead of any @@ -115,56 +83,40 @@ public interface BeanScopeBuilder { * *
{@code
    *
-   *   Pump pump = mock(Pump.class);
-   *   Grinder grinder = mock(Grinder.class);
+   *   // external dependencies
+   *   Pump pump = ...
+   *   Grinder grinder = ...
    *
-   *   try (BeanScope scope = BeanScope.newBuilder()
+   *   BeanScope scope = BeanScope.newBuilder()
    *     .withBeans(pump, grinder)
-   *     .build()) {
-   *
-   *     CoffeeMaker coffeeMaker = scope.get(CoffeeMaker.class);
-   *     coffeeMaker.makeIt();
+   *     .build();
    *
-   *     Pump pump1 = scope.get(Pump.class);
-   *     Grinder grinder1 = scope.get(Grinder.class);
-   *
-   *     assertThat(pump1).isSameAs(pump);
-   *     assertThat(grinder1).isSameAs(grinder);
-   *
-   *     verify(pump).pumpWater();
-   *     verify(grinder).grindBeans();
-   *   }
+   *   CoffeeMaker coffeeMaker = scope.get(CoffeeMaker.class);
+   *   coffeeMaker.makeIt();
    *
    * }
* - * @param beans The bean used when injecting a dependency for this bean or the interface(s) it implements + * @param beans Externally provided beans used when injecting a dependency for the bean or the interface(s) it implements * @return This BeanScopeBuilder */ BeanScopeBuilder withBeans(Object... beans); /** - * Add a supplied bean instance with the given injection type. - *

- * This is typically a test double often created by Mockito or similar. - *

+ * Add a supplied bean instance with the given injection type (typically an interface type). * *
{@code
    *
-   *   Pump mockPump = ...
+   *   Pump externalDependency = ...
    *
    *   try (BeanScope scope = BeanScope.newBuilder()
-   *     .withBean(Pump.class, mockPump)
+   *     .withBean(Pump.class, externalDependency)
    *     .build()) {
    *
-   *     Pump pump = scope.get(Pump.class);
-   *     assertThat(pump).isSameAs(mock);
-   *
-   *     // act
    *     CoffeeMaker coffeeMaker = scope.get(CoffeeMaker.class);
    *     coffeeMaker.makeIt();
    *
-   *     verify(pump).pumpSteam();
-   *     verify(pump).pumpWater();
+   *     Pump pump = scope.get(Pump.class);
+   *     assertThat(pump).isSameAs(externalDependency);
    *   }
    *
    * }
@@ -184,151 +136,182 @@ public interface BeanScopeBuilder { BeanScopeBuilder withBean(String name, Class type, D bean); /** - * Use a mockito mock when injecting this bean type. - * - *
{@code
-   *
-   *   try (BeanScope scope = BeanScope.newBuilder()
-   *     .withMock(Pump.class)
-   *     .withMock(Grinder.class, grinder -> {
-   *       // setup the mock
-   *       when(grinder.grindBeans()).thenReturn("stub response");
-   *     })
-   *     .build()) {
+   * Use the given BeanScope as the parent. This becomes an additional
+   * source of beans that can be wired and accessed in this scope.
    *
-   *
-   *     CoffeeMaker coffeeMaker = scope.get(CoffeeMaker.class);
-   *     coffeeMaker.makeIt();
-   *
-   *     // this is a mockito mock
-   *     Grinder grinder = scope.get(Grinder.class);
-   *     verify(grinder).grindBeans();
-   *   }
-   *
-   * }
+ * @param parent The BeanScope that acts as the parent */ - BeanScopeBuilder withMock(Class type); + BeanScopeBuilder withParent(BeanScope parent); /** - * Register as a Mockito mock with a qualifier name. - * - *
{@code
+   * Extend the builder to support testing using mockito with
+   * withMock() and withSpy() methods.
    *
-   *   try (BeanScope scope = BeanScope.newBuilder()
-   *     .withMock(Store.class, "red")
-   *     .withMock(Store.class, "blue")
-   *     .build()) {
-   *
-   *     ...
-   *   }
-   *
-   * }
+ * @return The builder with extra testing support for mockito mocks and spies */ - BeanScopeBuilder withMock(Class type, String name); + BeanScopeBuilder.ForTesting forTesting(); /** - * Use a mockito mock when injecting this bean type additionally - * running setup on the mock instance. - * - *
{@code
-   *
-   *   try (BeanScope scope = BeanScope.newBuilder()
-   *     .withMock(Pump.class)
-   *     .withMock(Grinder.class, grinder -> {
-   *
-   *       // setup the mock
-   *       when(grinder.grindBeans()).thenReturn("stub response");
-   *     })
-   *     .build()) {
-   *
-   *
-   *     CoffeeMaker coffeeMaker = scope.get(CoffeeMaker.class);
-   *     coffeeMaker.makeIt();
-   *
-   *     // this is a mockito mock
-   *     Grinder grinder = scope.get(Grinder.class);
-   *     verify(grinder).grindBeans();
-   *   }
+   * Build and return the bean scope.
+   * 

+ * The BeanScope is effectively immutable in that all components are created + * and all PostConstruct lifecycle methods have been invoked. + *

+ * The beanScope effectively contains eager singletons. * - * }

+ * @return The BeanScope */ - BeanScopeBuilder withMock(Class type, Consumer consumer); + BeanScope build(); /** - * Use a mockito spy when injecting this bean type. - * - *
{@code
-   *
-   *   try (BeanScope scope = BeanScope.newBuilder()
-   *     .withSpy(Pump.class)
-   *     .build()) {
-   *
-   *     // setup spy here ...
-   *     Pump pump = scope.get(Pump.class);
-   *     doNothing().when(pump).pumpSteam();
-   *
-   *     // act
-   *     CoffeeMaker coffeeMaker = scope.get(CoffeeMaker.class);
-   *     coffeeMaker.makeIt();
-   *
-   *     verify(pump).pumpWater();
-   *     verify(pump).pumpSteam();
-   *   }
-   *
-   * }
+ * Extends the building with testing specific support for mocks and spies. */ - BeanScopeBuilder withSpy(Class type); + interface ForTesting extends BeanScopeBuilder { - /** - * Register a Mockito spy with a qualifier name. - * - *
{@code
-   *
-   *   try (BeanScope scope = BeanScope.newBuilder()
-   *     .withSpy(Store.class, "red")
-   *     .withSpy(Store.class, "blue")
-   *     .build()) {
-   *
-   *     ...
-   *   }
-   *
-   * }
- */ - BeanScopeBuilder withSpy(Class type, String name); + /** + * Use a mockito mock when injecting this bean type. + * + *
{@code
+     *
+     *   try (BeanScope scope = BeanScope.newBuilder()
+     *     .forTesting()
+     *     .withMock(Pump.class)
+     *     .withMock(Grinder.class)
+     *     .build()) {
+     *
+     *
+     *     CoffeeMaker coffeeMaker = scope.get(CoffeeMaker.class);
+     *     coffeeMaker.makeIt();
+     *
+     *     // this is a mockito mock
+     *     Grinder grinder = scope.get(Grinder.class);
+     *     verify(grinder).grindBeans();
+     *   }
+     *
+     * }
+ */ + BeanScopeBuilder.ForTesting withMock(Class type); - /** - * Use a mockito spy when injecting this bean type additionally - * running setup on the spy instance. - * - *
{@code
-   *
-   *   try (BeanScope scope = BeanScope.newBuilder()
-   *     .withSpy(Pump.class, pump -> {
-   *       // setup the spy
-   *       doNothing().when(pump).pumpWater();
-   *     })
-   *     .build()) {
-   *
-   *     // or setup here ...
-   *     Pump pump = scope.get(Pump.class);
-   *     doNothing().when(pump).pumpSteam();
-   *
-   *     // act
-   *     CoffeeMaker coffeeMaker = scope.get(CoffeeMaker.class);
-   *     coffeeMaker.makeIt();
-   *
-   *     verify(pump).pumpWater();
-   *     verify(pump).pumpSteam();
-   *   }
-   *
-   * }
- */ - BeanScopeBuilder withSpy(Class type, Consumer consumer); + /** + * Register as a Mockito mock with a qualifier name. + * + *
{@code
+     *
+     *   try (BeanScope scope = BeanScope.newBuilder()
+     *     .forTesting()
+     *     .withMock(Store.class, "red")
+     *     .withMock(Store.class, "blue")
+     *     .build()) {
+     *
+     *     ...
+     *   }
+     *
+     * }
+ */ + BeanScopeBuilder.ForTesting withMock(Class type, String name); - /** - * Build and return the bean scope. - * - * @return The BeanScope - */ - BeanScope build(); + /** + * Use a mockito mock when injecting this bean type additionally + * running setup on the mock instance. + * + *
{@code
+     *
+     *   try (BeanScope scope = BeanScope.newBuilder()
+     *     .forTesting()
+     *     .withMock(Pump.class)
+     *     .withMock(Grinder.class, grinder -> {
+     *
+     *       // setup the mock
+     *       when(grinder.grindBeans()).thenReturn("stub response");
+     *     })
+     *     .build()) {
+     *
+     *
+     *     CoffeeMaker coffeeMaker = scope.get(CoffeeMaker.class);
+     *     coffeeMaker.makeIt();
+     *
+     *     // this is a mockito mock
+     *     Grinder grinder = scope.get(Grinder.class);
+     *     verify(grinder).grindBeans();
+     *   }
+     *
+     * }
+ */ + BeanScopeBuilder.ForTesting withMock(Class type, Consumer consumer); + + /** + * Use a mockito spy when injecting this bean type. + * + *
{@code
+     *
+     *   try (BeanScope scope = BeanScope.newBuilder()
+     *     .forTesting()
+     *     .withSpy(Pump.class)
+     *     .build()) {
+     *
+     *     // setup spy here ...
+     *     Pump pump = scope.get(Pump.class);
+     *     doNothing().when(pump).pumpSteam();
+     *
+     *     // act
+     *     CoffeeMaker coffeeMaker = scope.get(CoffeeMaker.class);
+     *     coffeeMaker.makeIt();
+     *
+     *     verify(pump).pumpWater();
+     *     verify(pump).pumpSteam();
+     *   }
+     *
+     * }
+ */ + BeanScopeBuilder.ForTesting withSpy(Class type); + + /** + * Register a Mockito spy with a qualifier name. + * + *
{@code
+     *
+     *   try (BeanScope scope = BeanScope.newBuilder()
+     *     .forTesting()
+     *     .withSpy(Store.class, "red")
+     *     .withSpy(Store.class, "blue")
+     *     .build()) {
+     *
+     *     ...
+     *   }
+     *
+     * }
+ */ + BeanScopeBuilder.ForTesting withSpy(Class type, String name); + + /** + * Use a mockito spy when injecting this bean type additionally + * running setup on the spy instance. + * + *
{@code
+     *
+     *   try (BeanScope scope = BeanScope.newBuilder()
+     *     .forTesting()
+     *     .withSpy(Pump.class, pump -> {
+     *       // setup the spy
+     *       doNothing().when(pump).pumpWater();
+     *     })
+     *     .build()) {
+     *
+     *     // or setup here ...
+     *     Pump pump = scope.get(Pump.class);
+     *     doNothing().when(pump).pumpSteam();
+     *
+     *     // act
+     *     CoffeeMaker coffeeMaker = scope.get(CoffeeMaker.class);
+     *     coffeeMaker.makeIt();
+     *
+     *     verify(pump).pumpWater();
+     *     verify(pump).pumpSteam();
+     *   }
+     *
+     * }
+ */ + BeanScopeBuilder.ForTesting withSpy(Class type, Consumer consumer); + + } } diff --git a/inject/src/main/java/io/avaje/inject/DBeanScopeBuilder.java b/inject/src/main/java/io/avaje/inject/DBeanScopeBuilder.java index 850106e2d..ada4fe40a 100644 --- a/inject/src/main/java/io/avaje/inject/DBeanScopeBuilder.java +++ b/inject/src/main/java/io/avaje/inject/DBeanScopeBuilder.java @@ -1,6 +1,6 @@ package io.avaje.inject; -import io.avaje.inject.spi.BeanScopeFactory; +import io.avaje.inject.spi.Module; import io.avaje.inject.spi.Builder; import io.avaje.inject.spi.EnrichBean; import io.avaje.inject.spi.SuppliedBean; @@ -13,21 +13,21 @@ /** * Build a bean scope with options for shutdown hook and supplying test doubles. */ -class DBeanScopeBuilder implements BeanScopeBuilder { +class DBeanScopeBuilder implements BeanScopeBuilder.ForTesting { private static final Logger log = LoggerFactory.getLogger(DBeanScopeBuilder.class); - private boolean shutdownHook = false; - @SuppressWarnings("rawtypes") private final List suppliedBeans = new ArrayList<>(); @SuppressWarnings("rawtypes") private final List enrichBeans = new ArrayList<>(); - private final Set includeModules = new LinkedHashSet<>(); + private final Set includeModules = new LinkedHashSet<>(); + + private BeanScope parent; - private boolean ignoreMissingModuleDependencies; + private boolean shutdownHook; /** * Create a BeanScopeBuilder to ultimately load and return a new BeanScope. @@ -36,8 +36,7 @@ class DBeanScopeBuilder implements BeanScopeBuilder { } @Override - public BeanScopeBuilder withNoShutdownHook() { - this.shutdownHook = false; + public ForTesting forTesting() { return this; } @@ -48,17 +47,11 @@ public BeanScopeBuilder withShutdownHook(boolean shutdownHook) { } @Override - public BeanScopeBuilder withModules(String... modules) { + public BeanScopeBuilder withModules(Module... modules) { this.includeModules.addAll(Arrays.asList(modules)); return this; } - @Override - public BeanScopeBuilder withIgnoreMissingModuleDependencies() { - this.ignoreMissingModuleDependencies = true; - return this; - } - @Override @SuppressWarnings({"unchecked", "rawtypes"}) public BeanScopeBuilder withBeans(Object... beans) { @@ -80,39 +73,45 @@ public BeanScopeBuilder withBean(String name, Class type, D bean) { } @Override - public BeanScopeBuilder withMock(Class type) { + public BeanScopeBuilder withParent(BeanScope parent) { + this.parent = parent; + return this; + } + + @Override + public BeanScopeBuilder.ForTesting withMock(Class type) { return withMock(type, null, null); } - public BeanScopeBuilder withMock(Class type, String name) { + public BeanScopeBuilder.ForTesting withMock(Class type, String name) { return withMock(type, name, null); } @Override - public BeanScopeBuilder withMock(Class type, Consumer consumer) { + public BeanScopeBuilder.ForTesting withMock(Class type, Consumer consumer) { return withMock(type, null, consumer); } - private BeanScopeBuilder withMock(Class type, String name, Consumer consumer) { + private BeanScopeBuilder.ForTesting withMock(Class type, String name, Consumer consumer) { suppliedBeans.add(new SuppliedBean<>(name, type, null, consumer)); return this; } @Override - public BeanScopeBuilder withSpy(Class type) { + public BeanScopeBuilder.ForTesting withSpy(Class type) { return spy(type, null, null); } - public BeanScopeBuilder withSpy(Class type, String name) { + public BeanScopeBuilder.ForTesting withSpy(Class type, String name) { return spy(type, name, null); } @Override - public DBeanScopeBuilder withSpy(Class type, Consumer consumer) { + public BeanScopeBuilder.ForTesting withSpy(Class type, Consumer consumer) { return spy(type, null, consumer); } - private DBeanScopeBuilder spy(Class type, String name, Consumer consumer) { + private BeanScopeBuilder.ForTesting spy(Class type, String name, Consumer consumer) { enrichBeans.add(new EnrichBean<>(type, name, consumer)); return this; } @@ -120,8 +119,10 @@ private DBeanScopeBuilder spy(Class type, String name, Consumer consum @Override public BeanScope build() { // sort factories by dependsOn - FactoryOrder factoryOrder = new FactoryOrder(includeModules, !suppliedBeans.isEmpty(), ignoreMissingModuleDependencies); - ServiceLoader.load(BeanScopeFactory.class).forEach(factoryOrder::add); + FactoryOrder factoryOrder = new FactoryOrder(includeModules, !suppliedBeans.isEmpty()); + if (factoryOrder.isEmpty()) { + ServiceLoader.load(Module.class).forEach(factoryOrder::add); + } Set moduleNames = factoryOrder.orderFactories(); if (moduleNames.isEmpty()) { @@ -132,8 +133,8 @@ public BeanScope build() { " Refer to https://avaje.io/inject#gradle"); } log.debug("building with modules {}", moduleNames); - Builder builder = Builder.newBuilder(suppliedBeans, enrichBeans); - for (BeanScopeFactory factory : factoryOrder.factories()) { + Builder builder = Builder.newBuilder(suppliedBeans, enrichBeans, parent); + for (Module factory : factoryOrder.factories()) { factory.build(builder); } return builder.build(shutdownHook); @@ -157,66 +158,55 @@ private Class suppliedType(Class suppliedClass) { */ static class FactoryOrder { - private final Set includeModules; private final boolean suppliedBeans; - private final boolean ignoreMissingModuleDependencies; - private final Set moduleNames = new LinkedHashSet<>(); - private final List factories = new ArrayList<>(); + private final List factories = new ArrayList<>(); private final List queue = new ArrayList<>(); private final List queueNoDependencies = new ArrayList<>(); private final Map providesMap = new HashMap<>(); - FactoryOrder(Set includeModules, boolean suppliedBeans, boolean ignoreMissingModuleDependencies) { - this.includeModules = includeModules; + FactoryOrder(Set includeModules, boolean suppliedBeans) { + this.factories.addAll(includeModules); this.suppliedBeans = suppliedBeans; - this.ignoreMissingModuleDependencies = ignoreMissingModuleDependencies; + for (Module includeModule : includeModules) { + moduleNames.add(includeModule.getClass().getCanonicalName()); + } } - void add(BeanScopeFactory factory) { - - if (includeModule(factory)) { - FactoryState wrappedFactory = new FactoryState(factory); - providesMap.computeIfAbsent(factory.getName(), s -> new FactoryList()).add(wrappedFactory); - if (!isEmpty(factory.getProvides())) { - for (String feature : factory.getProvides()) { - providesMap.computeIfAbsent(feature, s -> new FactoryList()).add(wrappedFactory); - } + void add(Module factory) { + FactoryState wrappedFactory = new FactoryState(factory); + providesMap.computeIfAbsent(factory.getClass().getCanonicalName(), s -> new FactoryList()).add(wrappedFactory); + if (!isEmpty(factory.provides())) { + for (Class feature : factory.provides()) { + providesMap.computeIfAbsent(feature.getCanonicalName(), s -> new FactoryList()).add(wrappedFactory); } - if (isEmpty(factory.getDependsOn())) { - if (!isEmpty(factory.getProvides())) { - // only has 'provides' so we can push this - push(wrappedFactory); - } else { - // hold until after all the 'provides only' modules are added - queueNoDependencies.add(wrappedFactory); - } + } + if (isEmpty(factory.requires())) { + if (!isEmpty(factory.provides())) { + // only has 'provides' so we can push this + push(wrappedFactory); } else { - // queue it to process by dependency ordering - queue.add(wrappedFactory); + // hold until after all the 'provides only' modules are added + queueNoDependencies.add(wrappedFactory); } + } else { + // queue it to process by dependency ordering + queue.add(wrappedFactory); } } - private boolean isEmpty(String[] values) { + private boolean isEmpty(Class[] values) { return values == null || values.length == 0; } - /** - * Return true of the factory (for the module) should be included. - */ - private boolean includeModule(BeanScopeFactory factory) { - return includeModules.isEmpty() || includeModules.contains(factory.getName()); - } - /** * Push the factory onto the build order (the wiring order for modules). */ private void push(FactoryState factory) { factory.setPushed(); - factories.add(factory.getFactory()); - moduleNames.add(factory.getName()); + factories.add(factory.factory()); + moduleNames.add(factory.getClass().getCanonicalName()); } /** @@ -235,7 +225,7 @@ Set orderFactories() { /** * Return the list of factories in the order they should be built. */ - List factories() { + List factories() { return factories; } @@ -249,7 +239,7 @@ private void processQueue() { count = processQueuedFactories(); } while (count > 0); - if (suppliedBeans || ignoreMissingModuleDependencies) { + if (suppliedBeans) { // just push everything left assuming supplied beans // will satisfy the required dependencies for (FactoryState factoryState : queue) { @@ -259,8 +249,8 @@ private void processQueue() { } else if (!queue.isEmpty()) { StringBuilder sb = new StringBuilder(); for (FactoryState factory : queue) { - sb.append("Module [").append(factory.getName()).append("] has unsatisfied dependencies on modules:"); - for (String depModuleName : factory.getDependsOn()) { + sb.append("Module [").append(factory.getClass().getCanonicalName()).append("] has unsatisfied dependencies on modules:"); + for (Class depModuleName : factory.requires()) { if (!moduleNames.contains(depModuleName)) { sb.append(String.format(" [%s]", depModuleName)); } @@ -268,7 +258,7 @@ private void processQueue() { } sb.append(". Modules that were loaded ok are:").append(moduleNames); - sb.append(". Consider using BeanScopeBuilder.withIgnoreMissingModuleDependencies() or BeanScopeBuilder.withSuppliedBeans(...)"); + sb.append(". Maybe need to add external dependencies via BeanScopeBuilder.withBean()?"); throw new IllegalStateException(sb.toString()); } } @@ -280,7 +270,6 @@ private void processQueue() { * This returns the number of factories added so once this returns 0 it is done. */ private int processQueuedFactories() { - int count = 0; Iterator it = queue.iterator(); while (it.hasNext()) { @@ -299,14 +288,18 @@ private int processQueuedFactories() { * Return true if the (module) dependencies are satisfied for this factory. */ private boolean satisfiedDependencies(FactoryState factory) { - for (String moduleOrFeature : factory.getDependsOn()) { - FactoryList factories = providesMap.get(moduleOrFeature); + for (Class moduleOrFeature : factory.requires()) { + FactoryList factories = providesMap.get(moduleOrFeature.getCanonicalName()); if (factories == null || !factories.allPushed()) { return false; } } return true; } + + boolean isEmpty() { + return factories.isEmpty(); + } } /** @@ -314,10 +307,10 @@ private boolean satisfiedDependencies(FactoryState factory) { */ private static class FactoryState { - private final BeanScopeFactory factory; + private final Module factory; private boolean pushed; - private FactoryState(BeanScopeFactory factory) { + private FactoryState(Module factory) { this.factory = factory; } @@ -332,16 +325,12 @@ boolean isPushed() { return pushed; } - BeanScopeFactory getFactory() { + Module factory() { return factory; } - String getName() { - return factory.getName(); - } - - String[] getDependsOn() { - return factory.getDependsOn(); + Class[] requires() { + return factory.requires(); } } diff --git a/inject/src/main/java/io/avaje/inject/InjectModule.java b/inject/src/main/java/io/avaje/inject/InjectModule.java index a61d83e26..c37d36689 100644 --- a/inject/src/main/java/io/avaje/inject/InjectModule.java +++ b/inject/src/main/java/io/avaje/inject/InjectModule.java @@ -1,111 +1,71 @@ package io.avaje.inject; /** - * Used to explicitly name a bean scope and optionally specify if it depends on other bean scopes. - *

- * If this annotation is not present then the name will be derived as the "top level package name" - * e.g. "org.example.featuretoggle" - *

- * - *

- * Typically there is a single bean scope per Jar (module). In that sense the name is the "module name" and - * the dependsOn specifies the names of modules that this depends on (provide beans that are used to wire this module). - *

+ * Used to explicitly specify if it depends on externally provided beans or provides. * + *

External dependencies

*

- * This annotation is typically placed on a top level interface or package-info in the module. - *

+ * Use {@code requires} to specify dependencies that will be provided externally. * *
{@code
  *
- * package org.example.featuretoggle;
+ *   // tell the annotation processor Pump and Grinder are provided externally
+ *   // otherwise it will think we have missing dependencies at compile time
  *
- * import io.avaje.inject.InjectModule;
- *
- * @InjectModule(name = "feature-toggle")
- * public interface FeatureToggle {
- *
- *   boolean isEnabled(String key);
- * }
+ *   @InjectModule(requires = {Pump.class, Grinder.class})
  *
  * }
* - *

dependsOn

+ *

Custom scope depending on another scope

*

- * We specify dependsOn when we have a module that depends on beans that - * will be supplied by another module (jar). - *

+ * When using custom scopes we can have the case where we desire one scope to depend + * on another. In this case we put the custom scope annotation in requires. *

- * In the example below we have the "Job System" which depends on the common "Feature Toggle" module. - * When wiring the Job system module we expect some beans to be provided by the feature toggle module (jar). - *

+ * For example lets say we have a custom scope called {@code StoreComponent} and that + * depends on {@code QueueComponent} custom scope. * *
{@code
  *
- * package org.example.jobsystem;
- *
- * import io.avaje.inject.InjectModule;
+ *   @Scope
+ *   @InjectModule(requires = {QueueComponent.class})
+ *   public @interface StoreComponent {
+ *   }
  *
- * @InjectModule(name = "job-system", dependsOn = {"feature-toggle"})
- * public interface JobSystem {
- *
- *   ...
- * }
  *
  * }
*/ public @interface InjectModule { /** - * The name of this context/module. - *

- * Other modules can then depend on this name and when they do they should wire after than module. - *

+ * Explicitly specify the name of the module. */ String name() default ""; /** - * Additional module features that is provided by this module. + * Explicitly define features that are provided by this module and required by other modules. *

- * These names are an addition to the module name and can be used in the dependsOn of other modules. - *

- * - *
{@code
-   *
-   * // A module that provides 'email-service' and also 'health-check'.
-   * // ie. it has bean(s) that implement a health check interface
-   * @InjectModule(name="email-service", provides={"health-checks"})
-   *
-   * // provides beans that implement a health check interface
-   * // ... wires after 'email-service'
-   * @InjectModule(name="main", provides={"health-checks"}, dependsOn={"email-service"})
-   *
-   * // wire this after all modules that provide 'health-checks'
-   * @InjectModule(name="health-check-service", dependsOn={"health-checks"})
-   *
-   * }
+ * This is used to order wiring across multiple modules. Modules that provide dependencies + * should be wired before modules that require dependencies. */ - String[] provides() default {}; + Class[] provides() default {}; /** - * The list of modules this scope depends on. + * The dependencies that are provided externally or by other modules and that are required + * when wiring this module. *

- * Effectively dependsOn specifies the modules that must wire before this module. - *

- *
{@code
-   *
-   * // wire after a module that is called 'email-service'
-   * // ... or any module that provides 'email-service'
-   *
-   * @InjectModule(name="...", dependsOn={"email-service"})
-   *
-   * }
+ * This effectively tells the annotation processor that these types are expected to be + * provided and to not treat them as missing dependencies. If we don't do this the annotation + * processor thinks the dependency is missing and will error the compilation saying there is + * a missing dependency. */ - String[] dependsOn() default {}; + Class[] requires() default {}; /** - * The dependencies that are provided externally and required when wiring this module. + * Internal use only - identifies the custom scope annotation associated to this module. + *

+ * When a module is generated for a custom scope this is set to link the module back to the + * custom scope annotation and support partial compilation. */ - Class[] requires() default {}; + String customScopeType() default ""; } diff --git a/inject/src/main/java/io/avaje/inject/Priority.java b/inject/src/main/java/io/avaje/inject/Priority.java index 41de7583e..937d9b142 100644 --- a/inject/src/main/java/io/avaje/inject/Priority.java +++ b/inject/src/main/java/io/avaje/inject/Priority.java @@ -9,7 +9,7 @@ /** * The Priority annotation can be applied to classes to indicate - * in what order they should be returned via @{@link ApplicationScope#listByPriority(Class)}. + * in what order they should be returned via @{@link BeanScope#listByPriority(Class)}. *

* Beans can be returned using other Priority annotation such as javax.annotation.Priority * or any custom priority annotation that has an int value() attribute. diff --git a/inject/src/main/java/io/avaje/inject/Request.java b/inject/src/main/java/io/avaje/inject/Request.java deleted file mode 100644 index e5835efad..000000000 --- a/inject/src/main/java/io/avaje/inject/Request.java +++ /dev/null @@ -1,19 +0,0 @@ -package io.avaje.inject; - -import jakarta.inject.Scope; - -import java.lang.annotation.Documented; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; - -/** - * Marks a request scoped bean. - *

- * Request scoped beans are only available via {@link RequestScope}. - */ -@Scope -@Documented -@Retention(RetentionPolicy.RUNTIME) -public @interface Request { - -} diff --git a/inject/src/main/java/io/avaje/inject/RequestScope.java b/inject/src/main/java/io/avaje/inject/RequestScope.java deleted file mode 100644 index 8758ae79b..000000000 --- a/inject/src/main/java/io/avaje/inject/RequestScope.java +++ /dev/null @@ -1,77 +0,0 @@ -package io.avaje.inject; - -import jakarta.inject.Provider; - -import java.io.Closeable; -import java.util.List; -import java.util.Optional; -import java.util.Set; - -/** - * Provides request scoped beans in addition to beans from the underlying bean scope. - */ -public interface RequestScope extends Closeable { - - /** - * Get a dependency. - */ - T get(Class type); - - /** - * Get a named dependency. - */ - T get(Class type, String name); - - /** - * Get an optional dependency. - */ - Optional getOptional(Class type); - - /** - * Get an optional named dependency. - */ - Optional getOptional(Class type, String name); - - /** - * Get an optional dependency potentially returning null. - */ - T getNullable(Class type); - - /** - * Get an optional named dependency potentially returning null. - */ - T getNullable(Class type, String name); - - /** - * Return Provider of T given the type. - */ - Provider getProvider(Class type); - - /** - * Return Provider of T given the type and name. - */ - Provider getProvider(Class type, String name); - - /** - * Get a list of dependencies for the interface type . - */ - List list(Class interfaceType); - - /** - * Get a set of dependencies for the interface type . - */ - Set set(Class interfaceType); - - /** - * Register a closable with the request scope. - *

- * All closeables registered here are closed at the end of the request scope. - */ - void addClosable(Closeable closeable); - - /** - * Close the scope firing any @PreDestroy methods. - */ - void close(); - -} diff --git a/inject/src/main/java/io/avaje/inject/RequestScopeBuilder.java b/inject/src/main/java/io/avaje/inject/RequestScopeBuilder.java deleted file mode 100644 index ec783f83f..000000000 --- a/inject/src/main/java/io/avaje/inject/RequestScopeBuilder.java +++ /dev/null @@ -1,34 +0,0 @@ -package io.avaje.inject; - -/** - * Builder for RequestScope. - *

- * When building a request scope we can provide additional instances that can then we wired as - * dependencies to request scoped beans. - */ -public interface RequestScopeBuilder { - - /** - * Provide a bean that can be dependency of a request scoped bean. - * - * @param type The type of provided bean - * @param bean The instance being provided - * @return The builder - */ - RequestScopeBuilder withBean(Class type, D bean); - - /** - * Provide a bean that can be dependency of a request scoped bean. - * - * @param name The qualifier name of the provided bean - * @param type The type of provided bean - * @param bean The instance being provided - * @return The builder - */ - RequestScopeBuilder withBean(String name, Class type, D bean); - - /** - * Build and return the request scope. - */ - RequestScope build(); -} diff --git a/inject/src/main/java/io/avaje/inject/RequestScopeMatch.java b/inject/src/main/java/io/avaje/inject/RequestScopeMatch.java deleted file mode 100644 index 09f42d3d1..000000000 --- a/inject/src/main/java/io/avaje/inject/RequestScopeMatch.java +++ /dev/null @@ -1,19 +0,0 @@ -package io.avaje.inject; - -import java.util.List; - -/** - * Match for request scope provider. - */ -public interface RequestScopeMatch { - - /** - * Return all the keys that match the provider. - */ - List keys(); - - /** - * Return the provider. - */ - RequestScopeProvider provider(); -} diff --git a/inject/src/main/java/io/avaje/inject/RequestScopeProvider.java b/inject/src/main/java/io/avaje/inject/RequestScopeProvider.java deleted file mode 100644 index 61ab6c3a1..000000000 --- a/inject/src/main/java/io/avaje/inject/RequestScopeProvider.java +++ /dev/null @@ -1,16 +0,0 @@ -package io.avaje.inject; - -/** - * Provides request scoped beans. - */ -public interface RequestScopeProvider { - - /** - * Create and return the request scope bean instance. - * - * @param scope The request scope when creating the bean - * @return The request scoped bean instance - */ - T provide(RequestScope scope); - -} diff --git a/inject/src/main/java/io/avaje/inject/SystemContext.java b/inject/src/main/java/io/avaje/inject/SystemContext.java deleted file mode 100644 index 84899e7ac..000000000 --- a/inject/src/main/java/io/avaje/inject/SystemContext.java +++ /dev/null @@ -1,77 +0,0 @@ -package io.avaje.inject; - -import java.util.List; - -/** - * Deprecated - migrate to ApplicationScope. - *

- * Provides a global system wide BeanScope that contains all the beans. - *

- * This will automatically get all the beans and wire them all as necessary. It will use - * a shutdown hook to fire any @PreDestroy methods on beans. - *

- * - *

Example: get a bean

- *
{@code
- *
- *   CoffeeMaker coffeeMaker = ApplicationScope.get(CoffeeMaker.class);
- *   coffeeMaker.brew();
- *
- * }
- */ -@Deprecated -public class SystemContext { - - private SystemContext() { - // hide - } - - /** - * Deprecated - migrate to ApplicationScope.scope(). - */ - @Deprecated - public static BeanScope context() { - return ApplicationScope.scope(); - } - - /** - * Deprecated - migrate to ApplicationScope.get(type). - */ - @Deprecated - public static T getBean(Class type) { - return ApplicationScope.get(type); - } - - /** - * Deprecated - migrate to ApplicationScope.get(type, name). - */ - @Deprecated - public static T getBean(Class type, String name) { - return ApplicationScope.get(type, name); - } - - /** - * Deprecated - migrate to ApplicationScope.listByAnnotation(annotation). - */ - @Deprecated - public static List getBeansWithAnnotation(Class annotation) { - return ApplicationScope.listByAnnotation(annotation); - } - - /** - * Deprecated - migrate to ApplicationScope.list(). - */ - @Deprecated - public static List getBeans(Class interfaceType) { - return ApplicationScope.list(interfaceType); - } - - /** - * Deprecated - migrate to ApplicationScope.listByPriority(). - */ - @Deprecated - public static List getBeansByPriority(Class interfaceType) { - return ApplicationScope.listByPriority(interfaceType); - } - -} diff --git a/inject/src/main/java/io/avaje/inject/package-info.java b/inject/src/main/java/io/avaje/inject/package-info.java index 102be2151..9a4c405c0 100644 --- a/inject/src/main/java/io/avaje/inject/package-info.java +++ b/inject/src/main/java/io/avaje/inject/package-info.java @@ -1,4 +1,159 @@ /** - * Avaje Inject API - see {@link io.avaje.inject.ApplicationScope} and {@link io.avaje.inject.BeanScope}. + * Avaje Inject API - see {@link io.avaje.inject.BeanScope}. + * + *

Overview

+ * + *

1. @Singleton / @Factory

+ *

+ * Put @Singleton on beans that we want avaje-inject to wire. + *

+ * We can use @Factory/@Bean to programmatically create dependencies + * when they have interesting construction logic (e.g. construction depends on + * system/environment properties etc - like Spring @Configuration). + * + *

{@code
+ *
+ *   @Singleton
+ *   public class CoffeeMaker { ... }
+ *
+ *   @Singleton
+ *   public class Pump { ... }
+ *
+ *   // Use @Factory to programmatically build dependencies
+ *
+ *   @Factory
+ *   public class MyFactory {
+ *
+ *     @Bean
+ *     Grinder buildGrinder(Pump pump) { // interesting construction logic ... }
+ *
+ *   }
+ *
+ * }
+ * + *

2. Create and use BeanScope

+ *

+ * Create {@link io.avaje.inject.BeanScope} using a builder. + * Obtain beans from the scope and use them. + *

+ * We should ensure the BeanScope is closed in order to fire + * preDestroy lifecycle methods. We can do this via a shutdown + * hook, or try with resource block or explicitly via application code. + * + *

{@code
+ *
+ *   // create BeanScope
+ *   BeanScope scope = BeanScope.newBuilder()
+ *     .build();
+ *
+ *   // use it
+ *   CoffeeMaker coffeeMaker = scope.get(CoffeeMaker.class);
+ *   coffeeMaker.makeIt();
+ *
+ *   // close it to fire preDestroy lifecycle methods
+ *   scope.close();
+ *
+ * }
+ * + *

Default scope

+ *

+ * All beans annotated with {@code @Singleton} and {@code @Factory} are by default included in + * the "default scope". + *

+ * When we create a BeanScope and do not specify any modules then all the beans in the default + * scope are included. This is done via ServiceLoader and includes all 'default scope' modules + * that are in the classpath (other jars can have default scope modules and these are all included). + * + *

Generated code

+ *

+ * avaje-inject will generate a $DI class for each bean that it is going to + * wire - this has the code that instantiates the bean, performs field and method injection and + * lifecycle support (PostConstruct and PreDestroy). + *

+ * avaje-inject will generate a Module class for the default scope. The main job of + * the module code is to specify the ordering of how all the beans are instantiated. + *

+ * We typically find the generated source code in target/generate-sources/annotations. + * + * + *

Custom scopes

+ *

+ * We can create custom scopes that only contain the beans/components that we want to include in + * that scope. These beans/components in the custom scope are not included in the default scope. + *

+ * To do this we: + * + *

1. Create the scope annotation

+ *

+ * Create an annotation that has the {@code Scope} annotation. In the example below we create the + * @StoreComponent annotation. We will put this annotation on beans that we want + * included in the scope. + * + *

{@code
+ *
+ *    @Scope
+ *    public @interface StoreComponent {
+ *    }
+ *
+ * }
+ * + *

+ * Note that if this scope depends on external dependencies or another scope we specify that via + * {@code @InjectModule requires}. In the example below our StoreComponent depends on another + * scope called QueueComponent and an external dependency - SomeExternalDependency. + * + *

{@code
+ *
+ *    @Scope
+ *    @InjectModule(requires = {QueueComponent.class, SomeExternalDependency.class})
+ *    public @interface StoreComponent {
+ *    }
+ *
+ * }
+ * + *

2. Use the annotation

+ *

+ * Put the @StoreComponent annotation on the beans that we want included in the + * custom scope. + * + *

{@code
+ *
+ *   @StoreComponent
+ *   public class StoreLoader {
+ *     ...
+ *   }
+ *
+ * }
+ * + * + *

3. Generated Module

+ *

+ * avaje-inject will generate a StoreComponentModule with the appropriate + * code to create/wire all the beans in the custom scope. + *

+ * StoreComponentModule is typically found in target/generate-sources/annotations. + * For each component we will also see a $DI class which is the generated code + * that creates the component. + * + *

4. Use the custom scope

+ *

+ * To use the custom scope we specify the StoreComponentModule when creating the + * BeanScope. Only the components in our module will be create/wired into the BeanScope. + *

+ * If the scope depends on another scope then we specify that using {@code withParent()}. + * + *

{@code
+ *
+ *   BeanScope parentScope = ...
+ *
+ *   BeanScope scope = BeanScope.newBuilder()
+ *     .withModules(new StoreComponentModule())
+ *     .withParent(parentScope)
+ *     .build());
+ *
+ *   StoreLoader storeLoader = scope.get(StoreLoader.class);
+ *   storeLoader.load();
+ *
+ * }
*/ package io.avaje.inject; diff --git a/inject/src/main/java/io/avaje/inject/spi/BeanScopeFactory.java b/inject/src/main/java/io/avaje/inject/spi/BeanScopeFactory.java deleted file mode 100644 index 4d4a2f7a2..000000000 --- a/inject/src/main/java/io/avaje/inject/spi/BeanScopeFactory.java +++ /dev/null @@ -1,27 +0,0 @@ -package io.avaje.inject.spi; - -/** - * This is the service loader interface defining the bean scope. - */ -public interface BeanScopeFactory { - - /** - * Return the name of the bean scope (module) this will create. - */ - String getName(); - - /** - * Return the name of module features this module provides. - */ - String[] getProvides(); - - /** - * Return the names of bean scopes (modules) that this is dependent on (they need to be built before this one). - */ - String[] getDependsOn(); - - /** - * Build all the beans. - */ - void build(Builder builder); -} diff --git a/inject/src/main/java/io/avaje/inject/spi/Builder.java b/inject/src/main/java/io/avaje/inject/spi/Builder.java index fba3ff65e..d6db7157c 100644 --- a/inject/src/main/java/io/avaje/inject/spi/Builder.java +++ b/inject/src/main/java/io/avaje/inject/spi/Builder.java @@ -1,7 +1,6 @@ package io.avaje.inject.spi; import io.avaje.inject.BeanScope; -import io.avaje.inject.RequestScopeProvider; import jakarta.inject.Provider; import java.util.List; @@ -19,14 +18,15 @@ public interface Builder { * * @param suppliedBeans The list of beans (typically test doubles) supplied when building the context. * @param enrichBeans The list of classes we want to have with mockito spy enhancement + * @param parent The parent BeanScope */ @SuppressWarnings("rawtypes") - static Builder newBuilder(List suppliedBeans, List enrichBeans) { + static Builder newBuilder(List suppliedBeans, List enrichBeans, BeanScope parent) { if (suppliedBeans.isEmpty() && enrichBeans.isEmpty()) { // simple case, no mocks or spies - return new DBuilder(); + return new DBuilder(parent); } - return new DBuilderExtn(suppliedBeans, enrichBeans); + return new DBuilderExtn(parent, suppliedBeans, enrichBeans); } /** @@ -50,23 +50,6 @@ static Builder newBuilder(List suppliedBeans, List enr */ boolean isAddBeanFor(Class... types); - /** - * Register a request scoped bean provider. - * - * @param type The type of the bean being provided - * @param provider The provider - */ - void requestScope(Class type, RequestScopeProvider provider); - - /** - * Register a request scoped bean provider. - * - * @param name The qualifier name of the bean being provided - * @param type The type of the bean being provided - * @param provider The provider - */ - void requestScope(Class type, RequestScopeProvider provider, String name, Class... interfaceTypes); - /** * Register the bean instance into the context. * diff --git a/inject/src/main/java/io/avaje/inject/spi/DBeanMap.java b/inject/src/main/java/io/avaje/inject/spi/DBeanMap.java index 8984abe35..b85a87a57 100644 --- a/inject/src/main/java/io/avaje/inject/spi/DBeanMap.java +++ b/inject/src/main/java/io/avaje/inject/spi/DBeanMap.java @@ -1,10 +1,11 @@ package io.avaje.inject.spi; -import io.avaje.inject.BeanEntry; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; -import java.util.*; - -import static io.avaje.inject.BeanEntry.*; +import static io.avaje.inject.BeanEntry.SUPPLIED; /** * Map of types (class types, interfaces and annotations) to a DContextEntry where the @@ -19,6 +20,17 @@ class DBeanMap { DBeanMap() { } + /** + * Add to the map of entries. + */ + void addAll(Map map) { + for (Map.Entry entry : beans.entrySet()) { + for (DContextEntryBean contentEntry : entry.getValue().entries()) { + map.computeIfAbsent(contentEntry, dContextEntryBean -> contentEntry.entry()).addKey(entry.getKey()); + } + } + } + /** * Add test double supplied beans. */ diff --git a/inject/src/main/java/io/avaje/inject/spi/DBeanScope.java b/inject/src/main/java/io/avaje/inject/spi/DBeanScope.java index ed619b206..8de6d0b1c 100644 --- a/inject/src/main/java/io/avaje/inject/spi/DBeanScope.java +++ b/inject/src/main/java/io/avaje/inject/spi/DBeanScope.java @@ -1,18 +1,15 @@ package io.avaje.inject.spi; -import io.avaje.inject.*; +import io.avaje.inject.BeanEntry; +import io.avaje.inject.BeanScope; +import io.avaje.inject.Priority; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.lang.annotation.Annotation; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.concurrent.locks.ReentrantLock; -import static io.avaje.inject.spi.KeyUtil.key; - class DBeanScope implements BeanScope { private static final Logger log = LoggerFactory.getLogger(DBeanScope.class); @@ -21,16 +18,16 @@ class DBeanScope implements BeanScope { private final List postConstruct; private final List preDestroy; private final DBeanMap beans; - private final Map> reqScopeProviders; private final ShutdownHook shutdownHook; + private final BeanScope parent; private boolean shutdown; private boolean closed; - DBeanScope(boolean withShutdownHook, List preDestroy, List postConstruct, DBeanMap beans, Map> reqScopeProviders) { + DBeanScope(boolean withShutdownHook, List preDestroy, List postConstruct, DBeanMap beans, BeanScope parent) { this.preDestroy = preDestroy; this.postConstruct = postConstruct; this.beans = beans; - this.reqScopeProviders = reqScopeProviders; + this.parent = parent; if (withShutdownHook) { this.shutdownHook = new ShutdownHook(this); Runtime.getRuntime().addShutdownHook(shutdownHook); @@ -40,14 +37,17 @@ class DBeanScope implements BeanScope { } @Override - public RequestScopeBuilder newRequestScope() { - return new DRequestScopeBuilder(this); + public List all() { + IdentityHashMap map = new IdentityHashMap<>(); + if (parent != null) { + ((DBeanScope) parent).addAll(map); + } + addAll(map); + return new ArrayList<>(map.values()); } - @SuppressWarnings("unchecked") - @Override - public RequestScopeMatch requestProvider(Class type, String name) { - return (RequestScopeMatch) reqScopeProviders.get(key(type, name)); + void addAll(Map map) { + beans.addAll(map); } @Override @@ -57,13 +57,32 @@ public T get(Class beanClass) { @Override public T get(Class beanClass, String name) { - return beans.get(beanClass, name); + final T bean = beans.get(beanClass, name); + if (bean != null) { + return bean; + } + return (parent == null) ? null : parent.get(beanClass, name); } @SuppressWarnings("unchecked") @Override public List list(Class interfaceType) { - return (List) beans.all(interfaceType); + List values = (List) beans.all(interfaceType); + if (parent == null) { + return values; + } + return combine(values, parent.list(interfaceType)); + } + + static List combine(List values, List parentValues) { + if (values.isEmpty()) { + return parentValues; + } else if (parentValues.isEmpty()) { + return values; + } else { + values.addAll(parentValues); + return values; + } } @Override @@ -102,7 +121,11 @@ private List sortByPriority(List list, final Class listByAnnotation(Class annotation) { - return beans.all(annotation); + final List values = beans.all(annotation); + if (parent == null) { + return values; + } + return combine(values, parent.listByAnnotation(annotation)); } DBeanScope start() { @@ -165,7 +188,6 @@ public void run() { } } - private static class SortBean implements Comparable> { private final T bean; diff --git a/inject/src/main/java/io/avaje/inject/spi/DBuilder.java b/inject/src/main/java/io/avaje/inject/spi/DBuilder.java index 8ae4832b9..c4dcd71ea 100644 --- a/inject/src/main/java/io/avaje/inject/spi/DBuilder.java +++ b/inject/src/main/java/io/avaje/inject/spi/DBuilder.java @@ -2,13 +2,13 @@ import io.avaje.inject.BeanEntry; import io.avaje.inject.BeanScope; -import io.avaje.inject.RequestScopeMatch; -import io.avaje.inject.RequestScopeProvider; import jakarta.inject.Provider; import java.util.*; import java.util.function.Consumer; +import static io.avaje.inject.spi.DBeanScope.combine; + class DBuilder implements Builder { /** @@ -22,15 +22,12 @@ class DBuilder implements Builder { */ private final List> injectors = new ArrayList<>(); - /** - * Map of request scope providers and their associated keys. - */ - private final Map> reqScopeProviders = new HashMap<>(); - /** * The beans created and added to the scope during building. */ - final DBeanMap beanMap = new DBeanMap(); + protected final DBeanMap beanMap = new DBeanMap(); + + private final BeanScope parent; /** * Debug of the current bean being wired - used in injection errors. @@ -42,6 +39,10 @@ class DBuilder implements Builder { */ private boolean runningPostConstruct; + DBuilder(BeanScope parent) { + this.parent = parent; + } + @Override public boolean isAddBeanFor(Class... types) { return isAddBeanFor(null, types); @@ -58,19 +59,6 @@ protected void next(String name, Class... types) { beanMap.nextBean(name, types); } - @Override - public void requestScope(Class type, RequestScopeProvider provider) { - requestScope(type, provider, null, null); - } - - @Override - public void requestScope(Class type, RequestScopeProvider provider, String name, Class... types) { - final DRequestScopeMatch match = DRequestScopeMatch.of(provider, type, name, types); - for (String key : match.keys()) { - reqScopeProviders.put(key, match); - } - } - private Class firstOf(Class[] types) { return types != null && types.length > 0 ? types[0] : null; } @@ -83,11 +71,19 @@ public Set set(Class interfaceType) { @SuppressWarnings({"unchecked"}) @Override public List list(Class interfaceType) { - return (List) beanMap.all(interfaceType); + List values = (List) beanMap.all(interfaceType); + if (parent == null) { + return values; + } + return combine(values, parent.list(interfaceType)); } private T getMaybe(Class beanClass, String name) { - return beanMap.get(beanClass, name); + T bean = beanMap.get(beanClass, name); + if (bean != null) { + return bean; + } + return (parent == null) ? null : parent.get(beanClass, name); } /** @@ -202,6 +198,6 @@ private void runInjectors() { public BeanScope build(boolean withShutdownHook) { runInjectors(); - return new DBeanScope(withShutdownHook, preDestroy, postConstruct, beanMap, reqScopeProviders).start(); + return new DBeanScope(withShutdownHook, preDestroy, postConstruct, beanMap, parent).start(); } } diff --git a/inject/src/main/java/io/avaje/inject/spi/DBuilderExtn.java b/inject/src/main/java/io/avaje/inject/spi/DBuilderExtn.java index 04aaa84a1..ee7f99361 100644 --- a/inject/src/main/java/io/avaje/inject/spi/DBuilderExtn.java +++ b/inject/src/main/java/io/avaje/inject/spi/DBuilderExtn.java @@ -1,5 +1,7 @@ package io.avaje.inject.spi; +import io.avaje.inject.BeanScope; + import java.util.HashMap; import java.util.List; import java.util.Map; @@ -15,8 +17,8 @@ class DBuilderExtn extends DBuilder { private final boolean hasSuppliedBeans; @SuppressWarnings("rawtypes") - DBuilderExtn(List suppliedBeans, List enrichBeans) { - super(); + DBuilderExtn(BeanScope parent, List suppliedBeans, List enrichBeans) { + super(parent); this.hasSuppliedBeans = (suppliedBeans != null && !suppliedBeans.isEmpty()); if (hasSuppliedBeans) { beanMap.add(suppliedBeans); diff --git a/inject/src/main/java/io/avaje/inject/spi/DContextEntry.java b/inject/src/main/java/io/avaje/inject/spi/DContextEntry.java index 23770dff9..d89cf6bb9 100644 --- a/inject/src/main/java/io/avaje/inject/spi/DContextEntry.java +++ b/inject/src/main/java/io/avaje/inject/spi/DContextEntry.java @@ -12,6 +12,10 @@ class DContextEntry { private final List entries = new ArrayList<>(5); + List entries() { + return entries; + } + void add(DContextEntryBean entryBean) { entries.add(entryBean); } @@ -102,7 +106,8 @@ private void checkMatch(DContextEntryBean entry) { match = entry; return; } - throw new IllegalStateException("Expecting only 1 bean match but have multiple matching beans " + match.getBean() + " and " + entry.getBean()); + throw new IllegalStateException("Expecting only 1 bean match but have multiple matching beans " + match.getBean() + + " and " + entry.getBean() + ". Maybe need a rebuild is required after adding a @Named qualifier?"); } private Object candidate() { diff --git a/inject/src/main/java/io/avaje/inject/spi/DContextEntryBean.java b/inject/src/main/java/io/avaje/inject/spi/DContextEntryBean.java index 75ed5db22..817c9634e 100644 --- a/inject/src/main/java/io/avaje/inject/spi/DContextEntryBean.java +++ b/inject/src/main/java/io/avaje/inject/spi/DContextEntryBean.java @@ -25,7 +25,7 @@ public static DContextEntryBean of(Object bean, String name, int flag) { } } - final Object source; + protected final Object source; private final String name; private final int flag; @@ -35,6 +35,10 @@ private DContextEntryBean(Object source, String name, int flag) { this.flag = flag; } + DEntry entry() { + return new DEntry(name, flag, getBean()); + } + /** * Return true if qualifierName is null or matched. */ diff --git a/inject/src/main/java/io/avaje/inject/spi/DEntry.java b/inject/src/main/java/io/avaje/inject/spi/DEntry.java new file mode 100644 index 000000000..de9486011 --- /dev/null +++ b/inject/src/main/java/io/avaje/inject/spi/DEntry.java @@ -0,0 +1,54 @@ +package io.avaje.inject.spi; + +import io.avaje.inject.BeanEntry; + +import java.util.LinkedHashSet; +import java.util.Set; + +class DEntry implements BeanEntry { + + private final String qualifierName; + private final int priority; + private final Object bean; + private Set keys = new LinkedHashSet<>(); + + DEntry(String qualifierName, int priority, Object bean) { + this.qualifierName = qualifierName; + this.priority = priority; + this.bean = bean; + } + + void addKey(String key) { + keys.add(key); + } + + @Override + public int priority() { + return priority; + } + + @Override + public Object bean() { + return bean; + } + + @Override + public Class type() { + return bean.getClass(); + } + + @Override + public String qualifierName() { + return qualifierName; + } + + @Override + public Set keys() { + return keys; + } + + @Override + public boolean hasKey(Class type) { + return keys.contains(type.getCanonicalName()); + } +} diff --git a/inject/src/main/java/io/avaje/inject/spi/DRequestScope.java b/inject/src/main/java/io/avaje/inject/spi/DRequestScope.java deleted file mode 100644 index 740f3c994..000000000 --- a/inject/src/main/java/io/avaje/inject/spi/DRequestScope.java +++ /dev/null @@ -1,120 +0,0 @@ -package io.avaje.inject.spi; - -import io.avaje.inject.BeanScope; -import io.avaje.inject.RequestScope; -import io.avaje.inject.RequestScopeMatch; -import jakarta.inject.Provider; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.Closeable; -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; - -import static io.avaje.inject.spi.KeyUtil.key; - -/** - * Implementation of RequestScope. - */ -class DRequestScope implements RequestScope { - - private static final Logger log = LoggerFactory.getLogger(RequestScope.class); - - private final BeanScope beanScope; - private final ConcurrentHashMap supplied; - private final List closeables = new ArrayList<>(); - - DRequestScope(BeanScope beanScope, ConcurrentHashMap supplied) { - this.beanScope = beanScope; - this.supplied = supplied; - } - - @SuppressWarnings("unchecked") - @Override - public T getNullable(Class type, String name) { - Object suppliedBean = supplied.get(key(type, name)); - if (suppliedBean != null) { - return (T) suppliedBean; - } - RequestScopeMatch reqMatch = beanScope.requestProvider(type, name); - if (reqMatch != null) { - return createRequestScopeBean(reqMatch); - } - return beanScope.get(type, name); - } - - @SuppressWarnings("unchecked") - private T createRequestScopeBean(RequestScopeMatch reqMatch) { - Object createdBean = reqMatch.provider().provide(this); - for (String matchKey : reqMatch.keys()) { - supplied.put(matchKey, createdBean); - } - return (T) createdBean; - } - - @Override - public T getNullable(Class type) { - return getNullable(type, null); - } - - @Override - public Optional getOptional(Class type) { - return Optional.of(getNullable(type)); - } - - @Override - public Optional getOptional(Class type, String name) { - return Optional.of(getNullable(type, name)); - } - - @Override - public T get(Class type) { - return get(type, null); - } - - @Override - public T get(Class type, String name) { - T maybe = getNullable(type, name); - if (maybe == null) { - throw new IllegalStateException("Missing bean for type:" + type + " name:" + name); - } - return maybe; - } - - @Override - public Provider getProvider(Class type) { - return getProvider(type, null); - } - - @Override - public Provider getProvider(Class type, String name) { - // in request scope everything is already wired, there is no delay - return () -> get(type, name); - } - - @Override - public List list(Class interfaceType) { - return beanScope.list(interfaceType); - } - - @Override - public Set set(Class interfaceType) { - return new LinkedHashSet<>(beanScope.list(interfaceType)); - } - - @Override - public void addClosable(Closeable closeable) { - closeables.add(closeable); - } - - @Override - public void close() { - for (Closeable closeable : closeables) { - try { - closeable.close(); - } catch (Exception e) { - log.error("Error closing request scoped bean", e); - } - } - } -} diff --git a/inject/src/main/java/io/avaje/inject/spi/DRequestScopeBuilder.java b/inject/src/main/java/io/avaje/inject/spi/DRequestScopeBuilder.java deleted file mode 100644 index 0250df716..000000000 --- a/inject/src/main/java/io/avaje/inject/spi/DRequestScopeBuilder.java +++ /dev/null @@ -1,39 +0,0 @@ -package io.avaje.inject.spi; - -import io.avaje.inject.BeanScope; -import io.avaje.inject.RequestScope; -import io.avaje.inject.RequestScopeBuilder; - -import java.util.concurrent.ConcurrentHashMap; - -import static io.avaje.inject.spi.KeyUtil.key; - -/** - * Builder for request scope. - */ -class DRequestScopeBuilder implements RequestScopeBuilder { - - private final ConcurrentHashMap supplied = new ConcurrentHashMap<>(); - - private final BeanScope beanScope; - - DRequestScopeBuilder(BeanScope beanScope) { - this.beanScope = beanScope; - } - - @Override - public RequestScopeBuilder withBean(Class type, D bean) { - return withBean(null, type, bean); - } - - @Override - public RequestScopeBuilder withBean(String name, Class type, D bean) { - supplied.put(key(type, name), bean); - return this; - } - - @Override - public RequestScope build() { - return new DRequestScope(beanScope, supplied); - } -} diff --git a/inject/src/main/java/io/avaje/inject/spi/DRequestScopeMatch.java b/inject/src/main/java/io/avaje/inject/spi/DRequestScopeMatch.java deleted file mode 100644 index 813e8339a..000000000 --- a/inject/src/main/java/io/avaje/inject/spi/DRequestScopeMatch.java +++ /dev/null @@ -1,49 +0,0 @@ -package io.avaje.inject.spi; - -import io.avaje.inject.RequestScopeMatch; -import io.avaje.inject.RequestScopeProvider; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -import static io.avaje.inject.spi.KeyUtil.key; - -class DRequestScopeMatch implements RequestScopeMatch { - - private final List keys; - private final RequestScopeProvider provider; - - static DRequestScopeMatch of(RequestScopeProvider provider, Class type, String name, Class[] types) { - if (name == null || types == null) { - return new DRequestScopeMatch<>(key(type, null), provider); - } - List keys = new ArrayList<>(types.length + 1); - keys.add(key(type, null)); - // add the alternate keys that the provider also matches to - for (Class aClass : types) { - keys.add(key(aClass, name)); - } - return new DRequestScopeMatch<>(keys, provider); - } - - private DRequestScopeMatch(List keys, RequestScopeProvider provider) { - this.keys = keys; - this.provider = provider; - } - - private DRequestScopeMatch(String key, RequestScopeProvider provider) { - this.keys = Collections.singletonList(key); - this.provider = provider; - } - - @Override - public List keys() { - return keys; - } - - @Override - public RequestScopeProvider provider() { - return provider; - } -} diff --git a/inject/src/main/java/io/avaje/inject/spi/Module.java b/inject/src/main/java/io/avaje/inject/spi/Module.java new file mode 100644 index 000000000..2cdf07c61 --- /dev/null +++ b/inject/src/main/java/io/avaje/inject/spi/Module.java @@ -0,0 +1,29 @@ +package io.avaje.inject.spi; + +/** + * A Module that can be included in BeanScope. + */ +public interface Module { + + /** + * Return the types this module needs to be provided externally or via other modules. + */ + Class[] requires(); + + /** + * Return the set of types this module explicitly provides to other modules. + */ + Class[] provides(); + + /** + * Build all the beans. + */ + void build(Builder builder); + + /** + * Marker for custom scoped modules. + */ + interface Custom extends Module { + + } +} diff --git a/inject/src/test/java/io/avaje/inject/BeanScopeBuilderTest.java b/inject/src/test/java/io/avaje/inject/BeanScopeBuilderTest.java index be0bf9520..326bab20d 100644 --- a/inject/src/test/java/io/avaje/inject/BeanScopeBuilderTest.java +++ b/inject/src/test/java/io/avaje/inject/BeanScopeBuilderTest.java @@ -1,11 +1,10 @@ package io.avaje.inject; -import io.avaje.inject.spi.BeanScopeFactory; +import io.avaje.inject.spi.Module; import io.avaje.inject.spi.Builder; import org.junit.jupiter.api.Test; -import java.util.Collections; -import java.util.List; +import java.util.*; import java.util.stream.Collectors; import static org.assertj.core.api.Assertions.assertThat; @@ -15,7 +14,7 @@ public class BeanScopeBuilderTest { @Test public void noDepends() { - DBeanScopeBuilder.FactoryOrder factoryOrder = new DBeanScopeBuilder.FactoryOrder(Collections.emptySet(), true, true); + DBeanScopeBuilder.FactoryOrder factoryOrder = new DBeanScopeBuilder.FactoryOrder(Collections.emptySet(), true); factoryOrder.add(bc("1", null, null)); factoryOrder.add(bc("2", null, null)); factoryOrder.add(bc("3", null, null)); @@ -27,8 +26,8 @@ public void noDepends() { @Test public void name_depends() { - DBeanScopeBuilder.FactoryOrder factoryOrder = new DBeanScopeBuilder.FactoryOrder(Collections.emptySet(), true, true); - factoryOrder.add(bc("two", null, "one")); + DBeanScopeBuilder.FactoryOrder factoryOrder = new DBeanScopeBuilder.FactoryOrder(Collections.emptySet(), true); + factoryOrder.add(bc("two", null, of(Mod3.class))); factoryOrder.add(bc("one", null, null)); factoryOrder.orderFactories(); @@ -38,11 +37,11 @@ public void name_depends() { @Test public void name_depends4() { - DBeanScopeBuilder.FactoryOrder factoryOrder = new DBeanScopeBuilder.FactoryOrder(Collections.emptySet(), true, true); - factoryOrder.add(bc("1", null, "3")); - factoryOrder.add(bc("2", null, "4")); - factoryOrder.add(bc("3", null, "4")); - factoryOrder.add(bc("4", null, null)); + DBeanScopeBuilder.FactoryOrder factoryOrder = new DBeanScopeBuilder.FactoryOrder(Collections.emptySet(), true); + factoryOrder.add(bc("1", null, of(Mod3.class))); + factoryOrder.add(bc("2", null, of(Mod4.class))); + factoryOrder.add(bc("3", of(Mod3.class), of(Mod4.class))); + factoryOrder.add(bc("4", of(Mod4.class), null)); factoryOrder.orderFactories(); @@ -52,11 +51,11 @@ public void name_depends4() { @Test public void nameFeature_depends() { - DBeanScopeBuilder.FactoryOrder factoryOrder = new DBeanScopeBuilder.FactoryOrder(Collections.emptySet(), true, true); - factoryOrder.add(bc("1", "a", "3")); - factoryOrder.add(bc("2", null, "4,a")); - factoryOrder.add(bc("3", null, "4")); - factoryOrder.add(bc("4", null, null)); + DBeanScopeBuilder.FactoryOrder factoryOrder = new DBeanScopeBuilder.FactoryOrder(Collections.emptySet(), true); + factoryOrder.add(bc("1", of(FeatureA.class), of(Mod3.class))); + factoryOrder.add(bc("2", null, of(Mod4.class, FeatureA.class))); + factoryOrder.add(bc("3", of(Mod3.class), of(Mod4.class))); + factoryOrder.add(bc("4", of(Mod4.class), null)); factoryOrder.orderFactories(); @@ -66,9 +65,9 @@ public void nameFeature_depends() { @Test public void feature_depends() { - DBeanScopeBuilder.FactoryOrder factoryOrder = new DBeanScopeBuilder.FactoryOrder(Collections.emptySet(), true, true); - factoryOrder.add(bc("two", null, "myfeature")); - factoryOrder.add(bc("one", "myfeature", null)); + DBeanScopeBuilder.FactoryOrder factoryOrder = new DBeanScopeBuilder.FactoryOrder(Collections.emptySet(), true); + factoryOrder.add(bc("two", null, of(MyFeature.class))); + factoryOrder.add(bc("one", of(MyFeature.class), null)); factoryOrder.orderFactories(); assertThat(names(factoryOrder.factories())).containsExactly("one", "two"); @@ -77,54 +76,50 @@ public void feature_depends() { @Test public void feature_depends2() { - DBeanScopeBuilder.FactoryOrder factoryOrder = new DBeanScopeBuilder.FactoryOrder(Collections.emptySet(), true, true); - factoryOrder.add(bc("two", null, "myfeature")); - factoryOrder.add(bc("one", "myfeature", null)); - factoryOrder.add(bc("three", "myfeature", null)); + DBeanScopeBuilder.FactoryOrder factoryOrder = new DBeanScopeBuilder.FactoryOrder(Collections.emptySet(), true); + factoryOrder.add(bc("two", null, of(MyFeature.class))); + factoryOrder.add(bc("one", of(MyFeature.class), null)); + factoryOrder.add(bc("three", of(MyFeature.class), null)); factoryOrder.orderFactories(); assertThat(names(factoryOrder.factories())).containsExactly("one", "three", "two"); } - private List names(List factories) { + private List names(List factories) { return factories.stream() - .map(BeanScopeFactory::getName) + .map(Module::toString) .collect(Collectors.toList()); } - private TDBeanScope bc(String name, String provides, String dependsOn) { - return new TDBeanScope(name, split(provides), split(dependsOn)); + private TDBeanScope bc(String name, Class[] provides, Class[] dependsOn) { + return new TDBeanScope(name, provides, dependsOn); } - private String[] split(String val) { - return val == null ? null : val.split(","); - } - - private static class TDBeanScope implements BeanScopeFactory { + private static class TDBeanScope implements Module { final String name; - final String[] provides; - final String[] dependsOn; + final Class[] provides; + final Class[] requires; - private TDBeanScope(String name, String[] provides, String[] dependsOn) { + private TDBeanScope(String name, Class[] provides, Class[] requires) { this.name = name; this.provides = provides; - this.dependsOn = dependsOn; + this.requires = requires; } @Override - public String getName() { + public String toString() { return name; } @Override - public String[] getProvides() { + public Class[] provides() { return provides; } @Override - public String[] getDependsOn() { - return dependsOn; + public Class[] requires() { + return requires; } @Override @@ -132,4 +127,17 @@ public void build(Builder parent) { } } + + Class[] of(Class... cls) { + return cls != null ? cls : new Class[0]; + } + + class MyFeature { + } + class FeatureA { + } + class Mod3 { + } + class Mod4 { + } } diff --git a/inject/src/test/java/io/avaje/inject/spi/DBeanScopeTest.java b/inject/src/test/java/io/avaje/inject/spi/DBeanScopeTest.java new file mode 100644 index 000000000..244c9445c --- /dev/null +++ b/inject/src/test/java/io/avaje/inject/spi/DBeanScopeTest.java @@ -0,0 +1,34 @@ +package io.avaje.inject.spi; + +import org.assertj.core.util.Arrays; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static java.util.Collections.emptyList; +import static org.assertj.core.api.Assertions.assertThat; + +class DBeanScopeTest { + + @Test + void combine_asFirst() { + final List result = DBeanScope.combine(list("A", "B"), emptyList()); + assertThat(result).containsExactly("A", "B"); + } + + @Test + void combine_asLast() { + final List result = DBeanScope.combine(emptyList(), list("A", "B")); + assertThat(result).containsExactly("A", "B"); + } + @Test + void combine_both() { + final List result = DBeanScope.combine(list("A", "B"),list("C")); + assertThat(result).containsExactly("A", "B", "C"); + } + + + List list(String... vals) { + return Arrays.asList(vals); + } +} diff --git a/pom.xml b/pom.xml index 512992656..2c371f156 100644 --- a/pom.xml +++ b/pom.xml @@ -9,7 +9,7 @@ io.avaje avaje-inject-parent - 6.2 + 6.5-RC0 pom avaje inject parent parent pom for avaje inject library