diff --git a/api/maven-api-di/src/main/java/org/apache/maven/api/di/Aggregate.java b/api/maven-api-di/src/main/java/org/apache/maven/api/di/Aggregate.java new file mode 100644 index 000000000000..c22a0e7f52db --- /dev/null +++ b/api/maven-api-di/src/main/java/org/apache/maven/api/di/Aggregate.java @@ -0,0 +1,181 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.maven.api.di; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Indicates that a {@link Provides} method contributes to an aggregated collection + * rather than replacing it. + * + *

Maven DI automatically aggregates beans into collections when injecting {@code List} + * or {@code Map}. By default, if an explicit {@code @Provides} method returns + * a collection of the same type, it replaces the auto-aggregation. + * The {@code @Aggregate} annotation changes this behavior to contribute + * entries to the aggregated collection instead.

+ * + *

Collection Aggregation Rules

+ * + *

Without explicit {@code @Provides}:

+ * + * + *

With {@code @Provides} (no {@code @Aggregate}):

+ * + * + *

With {@code @Provides @Aggregate}:

+ * + * + *

Usage Examples

+ * + *

Contributing to a Map

+ *
{@code
+ * @Provides
+ * @Aggregate
+ * Map corePlugins() {
+ *     Map plugins = new HashMap<>();
+ *     plugins.put("compile", new CompilePlugin());
+ *     plugins.put("test", new TestPlugin());
+ *     return plugins;
+ * }
+ *
+ * @Provides
+ * @Aggregate
+ * Map extraPlugins() {
+ *     Map plugins = new HashMap<>();
+ *     plugins.put("deploy", new DeployPlugin());
+ *     return plugins;
+ * }
+ *
+ * // Injection point receives all entries
+ * @Inject
+ * Map allPlugins; // Contains: compile, test, deploy
+ * }
+ * + *

Contributing to a List

+ *
{@code
+ * @Provides
+ * @Aggregate
+ * List customValidators() {
+ *     return Arrays.asList(
+ *         new PomValidator(),
+ *         new DependencyValidator()
+ *     );
+ * }
+ *
+ * @Inject
+ * List allValidators; // Contains all @Named Validator beans + custom ones
+ * }
+ * + *

Contributing Single Beans

+ *
{@code
+ * // Single bean contributions are implicitly aggregated (no @Aggregate needed)
+ * @Provides
+ * @Named("foo")
+ * MyService foo() {
+ *     return new FooService();
+ * }
+ *
+ * @Provides
+ * @Named("bar")
+ * MyService bar() {
+ *     return new BarService();
+ * }
+ *
+ * @Inject
+ * Map services; // Contains: foo, bar
+ * }
+ * + *

Replacing Auto-Aggregation

+ *
{@code
+ * @Named("service1")
+ * class Service1 implements MyService {}
+ *
+ * @Named("service2")
+ * class Service2 implements MyService {}
+ *
+ * // Without @Aggregate, this REPLACES the auto-aggregated map
+ * @Provides
+ * Map customMap() {
+ *     return Map.of("only", new OnlyService());
+ * }
+ *
+ * @Inject
+ * Map services; // Contains only: "only" -> OnlyService
+ * }
+ * + *

Priority and Ordering

+ *

When contributing to {@code List}, entries follow priority ordering rules:

+ * + * + *

Duplicate Keys

+ *

When multiple {@code @Aggregate} providers contribute the same key to a {@code Map}, + * the behavior is last-write-wins (though this may result in a warning or error in future versions).

+ * + *

Plugin Architecture Pattern

+ *

This annotation is particularly useful for Maven's plugin architecture, where different + * modules need to register their services without depending on a central registry:

+ *
{@code
+ * // In plugin module A
+ * @Provides
+ * @Aggregate
+ * Map lifecycleProviders() {
+ *     return Map.of("clean", new CleanLifecycle());
+ * }
+ *
+ * // In plugin module B
+ * @Provides
+ * @Aggregate
+ * Map moreLifecycleProviders() {
+ *     return Map.of("deploy", new DeployLifecycle());
+ * }
+ *
+ * // In core
+ * @Inject
+ * Map allLifecycles; // Contains contributions from all modules
+ * }
+ * + * @since 4.1.0 + * @see Provides + * @see Named + * @see Inject + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface Aggregate {} diff --git a/impl/maven-di/src/main/java/org/apache/maven/di/impl/Binding.java b/impl/maven-di/src/main/java/org/apache/maven/di/impl/Binding.java index 4caa7311b82a..dd7415034e93 100644 --- a/impl/maven-di/src/main/java/org/apache/maven/di/impl/Binding.java +++ b/impl/maven-di/src/main/java/org/apache/maven/di/impl/Binding.java @@ -19,6 +19,7 @@ package org.apache.maven.di.impl; import java.lang.annotation.Annotation; +import java.lang.reflect.Method; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; @@ -74,6 +75,15 @@ public static Binding to( return new BindingToConstructor<>(originalKey, constructor, dependencies, priority); } + public static Binding toMethod( + Key originalKey, + TupleConstructorN constructor, + Dependency[] dependencies, + Method method, + int priority) { + return new BindingToMethod<>(originalKey, constructor, dependencies, method, priority); + } + // endregion public Binding scope(Annotation scope) { @@ -216,4 +226,40 @@ public String toString() { return "BindingToConstructor[" + getOriginalKey() + "]" + getDependencies(); } } + + public static class BindingToMethod extends Binding { + final TupleConstructorN constructor; + final Dependency[] args; + final Method method; + + BindingToMethod( + Key key, + TupleConstructorN constructor, + Dependency[] dependencies, + Method method, + int priority) { + super(key, new HashSet<>(Arrays.asList(dependencies)), null, priority); + this.constructor = constructor; + this.args = dependencies; + this.method = method; + } + + @Override + public Supplier compile(Function, Supplier> compiler) { + return () -> { + Object[] args = + Stream.of(this.args).map(compiler).map(Supplier::get).toArray(); + return constructor.create(args); + }; + } + + public Method getMethod() { + return method; + } + + @Override + public String toString() { + return "BindingToMethod[" + method + "]" + getDependencies(); + } + } } diff --git a/impl/maven-di/src/main/java/org/apache/maven/di/impl/InjectorImpl.java b/impl/maven-di/src/main/java/org/apache/maven/di/impl/InjectorImpl.java index b0672ff56fb9..81e2d2a116bb 100644 --- a/impl/maven-di/src/main/java/org/apache/maven/di/impl/InjectorImpl.java +++ b/impl/maven-di/src/main/java/org/apache/maven/di/impl/InjectorImpl.java @@ -24,6 +24,7 @@ import java.lang.annotation.Annotation; import java.lang.reflect.Method; import java.lang.reflect.Modifier; +import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.net.URL; import java.util.AbstractList; @@ -35,6 +36,7 @@ import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; +import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; @@ -47,6 +49,7 @@ import java.util.stream.Stream; import org.apache.maven.api.annotations.Nonnull; +import org.apache.maven.api.di.Aggregate; import org.apache.maven.api.di.Provides; import org.apache.maven.api.di.Qualifier; import org.apache.maven.api.di.Singleton; @@ -184,7 +187,7 @@ private Injector doBind(Key key, Binding binding) { } protected Injector bind(Key key, Binding b) { - Set> bindingSet = bindings.computeIfAbsent(key, $ -> new HashSet<>()); + Set> bindingSet = bindings.computeIfAbsent(key, $ -> new LinkedHashSet<>()); bindingSet.add(b); return this; } @@ -222,43 +225,28 @@ public Supplier getCompiledBinding(Dependency dep) { public Supplier doGetCompiledBinding(Dependency dep) { Key key = dep.key(); Set> res = getBindings(key); + if (res != null && !res.isEmpty()) { + // Check if this is a List/Map and all bindings are @Aggregate + // If so, use aggregation logic instead of explicit binding + if (key.getRawType() == List.class && areAllAggregateListProviders(res)) { + return handleListAggregation(key); + } + if (key.getRawType() == Map.class && areAllAggregateMapProviders(res)) { + return handleMapAggregation(key); + } + + // Use explicit binding (highest priority wins) List> bindingList = new ArrayList<>(res); bindingList.sort(getPriorityComparator()); Binding binding = bindingList.get(0); return compile(binding); } if (key.getRawType() == List.class) { - Set> res2 = getBindings(key.getTypeParameter(0)); - if (res2 != null) { - // Sort bindings by priority (highest first) for deterministic ordering - List> sortedBindings = new ArrayList<>(res2); - sortedBindings.sort(getPriorityComparator()); - - List> list = - sortedBindings.stream().map(this::compile).collect(Collectors.toList()); - //noinspection unchecked - return () -> (Q) list(list, Supplier::get); - } + return handleListAggregation(key); } if (key.getRawType() == Map.class) { - Key k = key.getTypeParameter(0); - Key v = key.getTypeParameter(1); - Set> res2 = getBindings(v); - if (k.getRawType() == String.class && res2 != null) { - Map> map = res2.stream() - .filter(b -> b.getOriginalKey() == null - || b.getOriginalKey().getQualifier() == null - || b.getOriginalKey().getQualifier() instanceof String) - .collect(Collectors.toMap( - b -> (String) - (b.getOriginalKey() != null - ? b.getOriginalKey().getQualifier() - : null), - this::compile)); - //noinspection unchecked - return () -> (Q) map(map, Supplier::get); - } + return handleMapAggregation(key); } if (dep.optional()) { return () -> null; @@ -273,6 +261,332 @@ public Supplier doGetCompiledBinding(Dependency dep) { .collect(Collectors.joining("\n - ", " - ", ""))); } + /** + * Handle aggregation for List injection. + * Rules: + * 1. If there's an explicit (non-@Aggregate) provider for List, use only that + * 2. Otherwise, aggregate all beans of type T (including @Aggregate contributions) + * 3. Sort by priority (highest first) + */ + @SuppressWarnings("unchecked") + private Supplier handleListAggregation(Key key) { + Key elementKey = key.getTypeParameter(0); + Set> elementBindings = getBindings(elementKey); + Set> listBindings = getBindings(key); + + if ((elementBindings == null || elementBindings.isEmpty()) + && (listBindings == null || listBindings.isEmpty())) { + // No elements found, return empty list + return () -> (Q) new ArrayList<>(); + } + + if (elementBindings == null) { + elementBindings = new HashSet<>(); + } + + // Check if there's an explicit (non-@Aggregate) List provider in both element and list bindings + Binding explicitListProvider = findExplicitListProvider(elementBindings); + if (explicitListProvider == null && listBindings != null) { + explicitListProvider = (Binding) listBindings.stream() + .filter(binding -> isListProvider(binding) && !isAggregate(binding)) + .findFirst() + .orElse(null); + } + if (explicitListProvider != null) { + // Use explicit provider, ignore aggregation + Supplier compiled = compile(explicitListProvider); + return () -> (Q) compiled.get(); + } + + // Aggregate all beans including @Aggregate contributions + List> allBindings = new ArrayList<>(); + + // Process element bindings (individual beans) + for (Binding binding : elementBindings) { + + if (isAggregateListProvider(binding)) { + // This is an @Aggregate List provider, expand its elements + allBindings.addAll(expandAggregateList(binding)); + } else if (!isListProvider(binding)) { + // Regular bean (not a List provider), add it directly + allBindings.add(binding); + } + } + + // Process list bindings (@Aggregate List providers) + if (listBindings != null) { + for (Binding binding : listBindings) { + Binding objBinding = (Binding) binding; + + if (isAggregateListProvider(objBinding)) { + // This is an @Aggregate List provider, expand its elements + allBindings.addAll(expandAggregateList(objBinding)); + } + } + } + + // Sort by priority (highest first) + allBindings.sort(getPriorityComparator()); + + List> suppliers = + allBindings.stream().map(this::compile).collect(Collectors.toList()); + + return () -> (Q) list(suppliers, Supplier::get); + } + + /** + * Handle aggregation for Map injection. + * Rules: + * 1. If there's an explicit (non-@Aggregate) provider for Map, use only that + * 2. Otherwise, aggregate all @Named beans of type T (including @Aggregate contributions) + * 3. Use @Named qualifier value as the key + */ + @SuppressWarnings("unchecked") + private Supplier handleMapAggregation(Key key) { + Key keyType = key.getTypeParameter(0); + Key valueType = key.getTypeParameter(1); + Set> valueBindings = getBindings(valueType); + Set> mapBindings = getBindings(key); + + if (keyType.getRawType() != String.class) { + // Only support Map + return null; + } + + if ((valueBindings == null || valueBindings.isEmpty()) && (mapBindings == null || mapBindings.isEmpty())) { + // No elements found, return empty map + return () -> (Q) new LinkedHashMap<>(); + } + + // Ensure valueBindings is not null for the loop below + if (valueBindings == null) { + valueBindings = new HashSet<>(); + } + + // Check if there's an explicit (non-@Aggregate) Map provider + // Look in both value bindings and map bindings + Binding explicitMapProvider = findExplicitMapProvider(valueBindings); + if (explicitMapProvider == null && mapBindings != null) { + explicitMapProvider = (Binding) mapBindings.stream() + .filter(binding -> isMapProvider(binding) && !isAggregate(binding)) + .findFirst() + .orElse(null); + } + if (explicitMapProvider != null) { + // Use explicit provider, ignore aggregation + Supplier compiled = compile(explicitMapProvider); + return () -> (Q) compiled.get(); + } + + // Aggregate all @Named beans including @Aggregate contributions + Map> aggregatedBindings = new LinkedHashMap<>(); + + // Process value bindings (individual @Named beans) + for (Binding binding : valueBindings) { + if (isAggregateMapProvider(binding)) { + // This is an @Aggregate Map provider, expand its entries + Map> expanded = expandAggregateMap(binding); + aggregatedBindings.putAll(expanded); + } else if (!isMapProvider(binding)) { + // Regular bean with @Named qualifier + String name = extractNamedQualifier(binding); + if (name != null) { + aggregatedBindings.put(name, binding); + } + } + } + + // Process map bindings (@Aggregate Map providers) + if (mapBindings != null) { + for (Binding binding : mapBindings) { + Binding objBinding = (Binding) binding; + if (isAggregateMapProvider(objBinding)) { + // This is an @Aggregate Map provider, expand its entries + Map> expanded = expandAggregateMap(objBinding); + aggregatedBindings.putAll(expanded); + } + } + } + + Map> supplierMap = aggregatedBindings.entrySet().stream() + .collect(Collectors.toMap( + Map.Entry::getKey, + e -> compile(e.getValue()), + (a, b) -> b, // Last write wins on duplicate keys + LinkedHashMap::new)); + + return () -> (Q) map(supplierMap, Supplier::get); + } + + /** + * Find an explicit (non-@Aggregate) List provider + */ + private Binding findExplicitListProvider(Set> bindings) { + for (Binding binding : bindings) { + if (isListProvider(binding) && !isAggregate(binding)) { + return binding; + } + } + return null; + } + + /** + * Find an explicit (non-@Aggregate) Map provider + */ + private Binding findExplicitMapProvider(Set> bindings) { + for (Binding binding : bindings) { + if (isMapProvider(binding) && !isAggregate(binding)) { + return binding; + } + } + return null; + } + + /** + * Check if a binding is for a List type + */ + private boolean isListProvider(Binding binding) { + Key originalKey = binding.getOriginalKey(); + if (originalKey == null) { + return false; + } + Type type = originalKey.getType(); + if (type instanceof ParameterizedType) { + ParameterizedType pt = (ParameterizedType) type; + return pt.getRawType() == List.class; + } + return false; + } + + /** + * Check if a binding is for a Map type + */ + private boolean isMapProvider(Binding binding) { + Key originalKey = binding.getOriginalKey(); + if (originalKey == null) { + return false; + } + Type type = originalKey.getType(); + return isMapType(type); + } + + /** + * Check if a Type is a Map type + */ + private boolean isMapType(Type type) { + if (type instanceof ParameterizedType) { + ParameterizedType pt = (ParameterizedType) type; + return pt.getRawType() == Map.class; + } + return false; + } + + /** + * Check if a Type is a List type + */ + private boolean isListType(Type type) { + if (type instanceof ParameterizedType) { + ParameterizedType pt = (ParameterizedType) type; + return pt.getRawType() == List.class; + } + return false; + } + + /** + * Check if a binding has @Aggregate annotation + */ + private boolean isAggregate(Binding binding) { + // Check if the binding's source method has @Aggregate + if (binding instanceof Binding.BindingToMethod) { + Method method = ((Binding.BindingToMethod) binding).getMethod(); + return method.isAnnotationPresent(Aggregate.class); + } + return false; + } + + /** + * Check if all bindings are @Aggregate List providers + */ + private boolean areAllAggregateListProviders(Set> bindings) { + return bindings.stream().allMatch(binding -> isAggregateListProvider((Binding) binding)); + } + + /** + * Check if all bindings are @Aggregate Map providers + */ + private boolean areAllAggregateMapProviders(Set> bindings) { + return bindings.stream().allMatch(binding -> isAggregateMapProvider((Binding) binding)); + } + + /** + * Check if this is an @Aggregate List provider + */ + private boolean isAggregateListProvider(Binding binding) { + return isListProvider(binding) && isAggregate(binding); + } + + /** + * Check if this is an @Aggregate Map provider + */ + private boolean isAggregateMapProvider(Binding binding) { + return isMapProvider(binding) && isAggregate(binding); + } + + /** + * Expand an @Aggregate List provider into individual bindings + */ + @SuppressWarnings("unchecked") + private List> expandAggregateList(Binding listBinding) { + List> result = new ArrayList<>(); + Supplier compiled = compile(listBinding); + List elements = (List) compiled.get(); + + if (elements != null) { + for (Object element : elements) { + // Create instance binding for each element, inheriting priority from the original binding + Binding elementBinding = (Binding) Binding.toInstance(element); + elementBinding = elementBinding.prioritize(listBinding.getPriority()); + result.add(elementBinding); + } + } + + return result; + } + + /** + * Expand an @Aggregate Map provider into individual bindings + */ + @SuppressWarnings("unchecked") + private Map> expandAggregateMap(Binding mapBinding) { + Map> result = new LinkedHashMap<>(); + Supplier compiled = compile(mapBinding); + Map entries = (Map) compiled.get(); + + if (entries != null) { + for (Map.Entry entry : entries.entrySet()) { + // Create instance binding for each entry + result.put(entry.getKey(), (Binding) Binding.toInstance(entry.getValue())); + } + } + + return result; + } + + /** + * Extract @Named qualifier value from a binding + */ + private String extractNamedQualifier(Binding binding) { + Key originalKey = binding.getOriginalKey(); + if (originalKey == null) { + return null; + } + Object qualifier = originalKey.getQualifier(); + if (qualifier instanceof String str && !str.isEmpty()) { + return str; + } + return null; + } + @SuppressWarnings("unchecked") protected Supplier compile(Binding binding) { Supplier compiled = binding.compile(this::getCompiledBinding); @@ -323,6 +637,7 @@ protected void doBindImplicit(Key key, Binding binding) { Type returnType = method.getGenericReturnType(); Set> types = getBoundTypes(method.getAnnotation(Typed.class), Types.getRawType(returnType)); Binding bind = ReflectionUtils.bindingFromMethod(method).scope(scope); + for (Type t : Types.getAllSuperTypes(returnType)) { if (types == null || types.contains(Types.getRawType(t))) { bind(Key.ofType(t, qualifier), bind); @@ -331,7 +646,111 @@ protected void doBindImplicit(Key key, Binding binding) { } } } + + // Special handling for @Aggregate providers + if (method.isAnnotationPresent(Aggregate.class)) { + if (isMapType(returnType)) { + // Register individual named bindings for each map entry + registerAggregateMapEntries(method, bind); + } else if (isListType(returnType)) { + // Register individual bindings for each list element + registerAggregateListEntries(method, bind); + } + } + } + } + } + + /** + * Register individual named bindings for entries in an @Aggregate Map provider + */ + @SuppressWarnings("unchecked") + private void registerAggregateMapEntries(Method method, Binding mapBinding) { + try { + // Get the value type from Map + Type returnType = method.getGenericReturnType(); + if (!(returnType instanceof ParameterizedType)) { + return; + } + ParameterizedType pt = (ParameterizedType) returnType; + Type[] typeArgs = pt.getActualTypeArguments(); + if (typeArgs.length != 2 || typeArgs[0] != String.class) { + return; // Only support Map + } + Type valueType = typeArgs[1]; + + // Execute the map provider to get the actual map + Supplier compiled = compile(mapBinding); + Map map = (Map) compiled.get(); + + if (map != null) { + // Register each map entry as a named binding + for (Map.Entry entry : map.entrySet()) { + String name = entry.getKey(); + Object value = entry.getValue(); + + if (name != null && !name.isEmpty() && value != null) { + // Create a binding for this individual service + Binding valueBinding = (Binding) Binding.toInstance(value); + + // Bind it with the @Named qualifier + Key namedKey = (Key) Key.ofType(valueType, name); + bind(namedKey, valueBinding); + } + } } + } catch (Exception e) { + // If we can't execute the provider at binding time, that's okay + // The individual services will be resolved through map aggregation at runtime + } + } + + /** + * Register individual bindings for elements in an @Aggregate List provider + * Only registers individual bindings if there are no existing individual bindings for that type + */ + @SuppressWarnings("unchecked") + private void registerAggregateListEntries(Method method, Binding listBinding) { + try { + // Get the element type from List + Type returnType = method.getGenericReturnType(); + if (!(returnType instanceof ParameterizedType)) { + return; + } + ParameterizedType pt = (ParameterizedType) returnType; + Type[] typeArgs = pt.getActualTypeArguments(); + if (typeArgs.length != 1) { + return; // Only support List + } + Type elementType = typeArgs[0]; + Key elementKey = (Key) Key.ofType(elementType); + + // Only register individual bindings if there are no existing individual bindings for this type + Set> existingBindings = getBindings(elementKey); + if (existingBindings != null && !existingBindings.isEmpty()) { + // There are already individual bindings for this type, don't add more + return; + } + + // Execute the list provider to get the actual list + Supplier compiled = compile(listBinding); + List list = (List) compiled.get(); + + if (list != null) { + // Register each list element as an individual binding + for (Object element : list) { + if (element != null) { + // Create a binding for this individual service + Binding elementBinding = (Binding) Binding.toInstance(element); + + // Bind it without any qualifier (so it can be injected as @Inject T) + bind(elementKey, elementBinding); + } + } + } + } catch (Exception e) { + // If we can't execute the provider at binding time, that's okay + // The individual services will be resolved through list aggregation at runtime } } @@ -469,7 +888,7 @@ public T get() { } /** - * Release all internal state so this Injector can be GC’d + * Release all internal state so this Injector can be GC'd * (and so that subsequent tests start from a clean slate). * @since 4.1 */ diff --git a/impl/maven-di/src/main/java/org/apache/maven/di/impl/ReflectionUtils.java b/impl/maven-di/src/main/java/org/apache/maven/di/impl/ReflectionUtils.java index 809e39e9552e..51cb8ef3922d 100644 --- a/impl/maven-di/src/main/java/org/apache/maven/di/impl/ReflectionUtils.java +++ b/impl/maven-di/src/main/java/org/apache/maven/di/impl/ReflectionUtils.java @@ -298,7 +298,12 @@ private static Dependency[] toArgDependencies(@Nullable Type container, Execu @SuppressWarnings("unchecked") public static Binding bindingFromMethod(Method method) { method.setAccessible(true); - Binding binding = Binding.to( + + // Extract priority first + Priority priority = method.getAnnotation(Priority.class); + int priorityValue = priority != null ? priority.value() : 0; + + return Binding.toMethod( Key.ofType(method.getGenericReturnType(), ReflectionUtils.qualifierOf(method)), args -> { try { @@ -323,14 +328,9 @@ public static Binding bindingFromMethod(Method method) { throw new DIException("Failed to call method " + method, e.getCause()); } }, - toDependencies(method.getDeclaringClass(), method)); - - Priority priority = method.getAnnotation(Priority.class); - if (priority != null) { - binding = binding.prioritize(priority.value()); - } - - return binding; + toDependencies(method.getDeclaringClass(), method), + method, + priorityValue); } public static Binding bindingFromConstructor(Key key, Constructor constructor) { diff --git a/impl/maven-di/src/test/java/org/apache/maven/di/impl/InjectorImplTest.java b/impl/maven-di/src/test/java/org/apache/maven/di/impl/InjectorImplTest.java index 46ea1b331818..6404321cae75 100644 --- a/impl/maven-di/src/test/java/org/apache/maven/di/impl/InjectorImplTest.java +++ b/impl/maven-di/src/test/java/org/apache/maven/di/impl/InjectorImplTest.java @@ -21,11 +21,13 @@ import java.lang.annotation.Retention; import java.util.ArrayList; import java.util.Arrays; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; import org.apache.maven.api.annotations.Nullable; +import org.apache.maven.api.di.Aggregate; import org.apache.maven.api.di.Inject; import org.apache.maven.api.di.Named; import org.apache.maven.api.di.Priority; @@ -39,6 +41,7 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -514,4 +517,1038 @@ static class Foo {} @Named static class Bar {} } + + // ============================================================================ + // Collection Aggregation Tests + // ============================================================================ + + @Test + void testListAggregationFromMultipleNamedBeans() { + Injector injector = Injector.create().bindImplicit(ListAggregationTest.class); + + List services = + injector.getInstance(new Key>() {}); + + assertNotNull(services); + assertEquals(3, services.size()); + + // Verify all three implementations are present + assertTrue(services.stream().anyMatch(s -> s instanceof ListAggregationTest.FooService)); + assertTrue(services.stream().anyMatch(s -> s instanceof ListAggregationTest.BarService)); + assertTrue(services.stream().anyMatch(s -> s instanceof ListAggregationTest.BazService)); + } + + static class ListAggregationTest { + interface MyService {} + + @Named("foo") + static class FooService implements MyService {} + + @Named("bar") + static class BarService implements MyService {} + + @Named("baz") + static class BazService implements MyService {} + } + + @Test + void testListAggregationFromProvidesMethod() { + Injector injector = Injector.create().bindImplicit(ListAggregationFromProvides.class); + + List services = + injector.getInstance(new Key>() {}); + + assertNotNull(services); + assertEquals(3, services.size()); + } + + static class ListAggregationFromProvides { + interface MyService {} + + @Provides + MyService foo() { + return new MyService() {}; + } + + @Provides + MyService bar() { + return new MyService() {}; + } + + @Provides + MyService baz() { + return new MyService() {}; + } + } + + @Test + void testListAggregationMixedNamedAndProvides() { + Injector injector = Injector.create().bindImplicit(ListAggregationMixed.class); + + List services = + injector.getInstance(new Key>() {}); + + assertNotNull(services); + assertEquals(4, services.size()); + } + + static class ListAggregationMixed { + interface MyService {} + + @Named("foo") + static class FooService implements MyService {} + + @Named("bar") + static class BarService implements MyService {} + + @Provides + MyService provided1() { + return new MyService() {}; + } + + @Provides + MyService provided2() { + return new MyService() {}; + } + } + + @Test + void testEmptyListWhenNoBeansAvailable() { + Injector injector = Injector.create().bindImplicit(EmptyListTest.class); + + List services = + injector.getInstance(new Key>() {}); + + assertNotNull(services); + assertEquals(0, services.size()); + } + + static class EmptyListTest { + interface NonExistentService {} + } + + @Test + void testMapAggregationFromMultipleNamedBeans() { + Injector injector = Injector.create().bindImplicit(MapAggregationTest.class); + + Map services = + injector.getInstance(new Key>() {}); + + assertNotNull(services); + assertEquals(3, services.size()); + + assertTrue(services.containsKey("foo")); + assertTrue(services.containsKey("bar")); + assertTrue(services.containsKey("baz")); + + assertInstanceOf(MapAggregationTest.FooService.class, services.get("foo")); + assertInstanceOf(MapAggregationTest.BarService.class, services.get("bar")); + assertInstanceOf(MapAggregationTest.BazService.class, services.get("baz")); + } + + static class MapAggregationTest { + interface MyService {} + + @Named("foo") + static class FooService implements MyService {} + + @Named("bar") + static class BarService implements MyService {} + + @Named("baz") + static class BazService implements MyService {} + } + + @Test + void testMapAggregationFromNamedProvidesMethod() { + Injector injector = Injector.create().bindImplicit(MapAggregationFromProvides.class); + + Map services = + injector.getInstance(new Key>() {}); + + assertNotNull(services); + assertEquals(3, services.size()); + + assertTrue(services.containsKey("foo")); + assertTrue(services.containsKey("bar")); + assertTrue(services.containsKey("baz")); + } + + static class MapAggregationFromProvides { + interface MyService {} + + @Provides + @Named("foo") + MyService foo() { + return new MyService() {}; + } + + @Provides + @Named("bar") + MyService bar() { + return new MyService() {}; + } + + @Provides + @Named("baz") + MyService baz() { + return new MyService() {}; + } + } + + @Test + void testMapAggregationMixedNamedAndProvides() { + Injector injector = Injector.create().bindImplicit(MapAggregationMixed.class); + + Map services = + injector.getInstance(new Key>() {}); + + assertNotNull(services); + assertEquals(4, services.size()); + + assertTrue(services.containsKey("foo")); + assertTrue(services.containsKey("bar")); + assertTrue(services.containsKey("provided1")); + assertTrue(services.containsKey("provided2")); + } + + static class MapAggregationMixed { + interface MyService {} + + @Named("foo") + static class FooService implements MyService {} + + @Named("bar") + static class BarService implements MyService {} + + @Provides + @Named("provided1") + MyService provided1() { + return new MyService() {}; + } + + @Provides + @Named("provided2") + MyService provided2() { + return new MyService() {}; + } + } + + @Test + void testEmptyMapWhenNoNamedBeansAvailable() { + Injector injector = Injector.create().bindImplicit(EmptyMapTest.class); + + Map services = + injector.getInstance(new Key>() {}); + + assertNotNull(services); + assertEquals(0, services.size()); + } + + static class EmptyMapTest { + interface NonExistentService {} + } + + @Test + void testMapIgnoresUnnamedBeans() { + Injector injector = Injector.create().bindImplicit(MapIgnoresUnnamed.class); + + Map services = + injector.getInstance(new Key>() {}); + + assertNotNull(services); + assertEquals(2, services.size()); // Only foo and bar, not unnamed + + assertTrue(services.containsKey("foo")); + assertTrue(services.containsKey("bar")); + } + + static class MapIgnoresUnnamed { + interface MyService {} + + @Named("foo") + static class FooService implements MyService {} + + @Named("bar") + static class BarService implements MyService {} + + @Named // No value, so not added to map + static class UnnamedService implements MyService {} + } + + @Test + void testListAggregationWithPriorityOrdering() { + Injector injector = Injector.create().bindImplicit(ListPriorityOrdering.class); + + List services = + injector.getInstance(new Key>() {}); + + assertNotNull(services); + assertEquals(4, services.size()); + + // Verify priority ordering: highest priority first + assertInstanceOf(ListPriorityOrdering.HighPriority.class, services.get(0)); + assertInstanceOf(ListPriorityOrdering.MediumPriority.class, services.get(1)); + assertInstanceOf(ListPriorityOrdering.LowPriority.class, services.get(2)); + assertInstanceOf(ListPriorityOrdering.NoPriority.class, services.get(3)); + } + + static class ListPriorityOrdering { + interface MyService {} + + @Named + @Priority(100) + static class HighPriority implements MyService {} + + @Named + @Priority(50) + static class MediumPriority implements MyService {} + + @Named + @Priority(10) + static class LowPriority implements MyService {} + + @Named + static class NoPriority implements MyService {} + } + + @Test + void testInjectListIntoMojo() { + Injector injector = Injector.create().bindImplicit(InjectListIntoMojo.class); + + InjectListIntoMojo.MyMojo mojo = injector.getInstance(InjectListIntoMojo.MyMojo.class); + + assertNotNull(mojo); + assertNotNull(mojo.services); + assertEquals(3, mojo.services.size()); + } + + static class InjectListIntoMojo { + interface MyService {} + + @Named("foo") + static class FooService implements MyService {} + + @Named("bar") + static class BarService implements MyService {} + + @Named("baz") + static class BazService implements MyService {} + + @Named + static class MyMojo { + @Inject + List services; + } + } + + @Test + void testInjectMapIntoMojoViaConstructor() { + Injector injector = Injector.create().bindImplicit(InjectMapConstructor.class); + + InjectMapConstructor.MyMojo mojo = injector.getInstance(InjectMapConstructor.MyMojo.class); + + assertNotNull(mojo); + assertNotNull(mojo.services); + assertEquals(2, mojo.services.size()); + assertTrue(mojo.services.containsKey("foo")); + assertTrue(mojo.services.containsKey("bar")); + } + + static class InjectMapConstructor { + interface MyService {} + + @Named("foo") + static class FooService implements MyService {} + + @Named("bar") + static class BarService implements MyService {} + + @Named + static class MyMojo { + final Map services; + + @Inject + MyMojo(Map services) { + this.services = services; + } + } + } + + @Test + void testListAggregationWithSingletonScope() { + Injector injector = Injector.create().bindImplicit(ListSingletonScope.class); + + List services1 = + injector.getInstance(new Key>() {}); + List services2 = + injector.getInstance(new Key>() {}); + + assertNotNull(services1); + assertNotNull(services2); + assertEquals(2, services1.size()); + assertEquals(2, services2.size()); + + // Singleton beans should be the same instance + ListSingletonScope.MyService singleton1a = services1.stream() + .filter(s -> s instanceof ListSingletonScope.SingletonService) + .findFirst() + .orElse(null); + ListSingletonScope.MyService singleton1b = services2.stream() + .filter(s -> s instanceof ListSingletonScope.SingletonService) + .findFirst() + .orElse(null); + + assertNotNull(singleton1a); + assertNotNull(singleton1b); + assertEquals(singleton1a, singleton1b); // Same instance + + // Non-singleton beans should be different instances + ListSingletonScope.MyService nonSingleton1a = services1.stream() + .filter(s -> s instanceof ListSingletonScope.NonSingletonService) + .findFirst() + .orElse(null); + ListSingletonScope.MyService nonSingleton1b = services2.stream() + .filter(s -> s instanceof ListSingletonScope.NonSingletonService) + .findFirst() + .orElse(null); + + assertNotNull(nonSingleton1a); + assertNotNull(nonSingleton1b); + assertNotEquals(nonSingleton1a, nonSingleton1b); // Different instances + } + + static class ListSingletonScope { + interface MyService {} + + @Named + @Singleton + static class SingletonService implements MyService {} + + @Named + static class NonSingletonService implements MyService {} + } + + @Test + void testMapAggregationWithQualifiers() { + Injector injector = Injector.create().bindImplicit(MapWithQualifiers.class); + + Map services = + injector.getInstance(new Key>() {}); + + assertNotNull(services); + // Should only include @Named beans, not other qualifiers + assertEquals(2, services.size()); + assertTrue(services.containsKey("foo")); + assertTrue(services.containsKey("bar")); + } + + static class MapWithQualifiers { + @Qualifier + @Retention(RUNTIME) + @interface CustomQualifier {} + + interface MyService {} + + @Named("foo") + static class FooService implements MyService {} + + @Named("bar") + static class BarService implements MyService {} + + @CustomQualifier + static class QualifiedService implements MyService {} + } + + @Test + void testNestedListAndMapInjection() { + Injector injector = Injector.create().bindImplicit(NestedCollections.class); + + NestedCollections.Aggregator aggregator = injector.getInstance(NestedCollections.Aggregator.class); + + assertNotNull(aggregator); + assertNotNull(aggregator.allServices); + assertNotNull(aggregator.namedServices); + + assertEquals(3, aggregator.allServices.size()); + assertEquals(3, aggregator.namedServices.size()); + } + + static class NestedCollections { + interface MyService {} + + @Named("foo") + static class FooService implements MyService {} + + @Named("bar") + static class BarService implements MyService {} + + @Named("baz") + static class BazService implements MyService {} + + @Named + static class Aggregator { + @Inject + List allServices; + + @Inject + Map namedServices; + } + } + + @Test + void testListAggregationWithTypedAnnotation() { + Injector injector = Injector.create().bindImplicit(ListWithTyped.class); + + List services = injector.getInstance(new Key>() {}); + + assertNotNull(services); + // @Typed beans should only be accessible by their explicit types + assertEquals(1, services.size()); + assertInstanceOf(ListWithTyped.RegularService.class, services.get(0)); + } + + static class ListWithTyped { + interface MyService {} + + @Named + static class RegularService implements MyService {} + + @Named + @Typed(ListWithTyped.SpecificInterface.class) + static class TypedService implements MyService, SpecificInterface {} + + interface SpecificInterface {} + } + + // ============================================================================ + // @Aggregate Annotation Tests + // ============================================================================ + + @Test + void testAggregateMapContribution() { + Injector injector = Injector.create().bindImplicit(AggregateMapTest.class); + + Map services = + injector.getInstance(new Key>() {}); + + assertNotNull(services); + assertEquals(4, services.size()); + + // Should contain all entries from both modules + assertTrue(services.containsKey("foo")); + assertTrue(services.containsKey("bar")); + assertTrue(services.containsKey("extra1")); + assertTrue(services.containsKey("extra2")); + } + + static class AggregateMapTest { + interface MyService {} + + @Named("foo") + static class FooService implements MyService {} + + @Named("bar") + static class BarService implements MyService {} + + // Bulk contribution using @Aggregate + @Provides + @Aggregate + Map extraServices() { + Map map = new java.util.HashMap<>(); + map.put("extra1", new MyService() {}); + map.put("extra2", new MyService() {}); + return map; + } + } + + @Test + void testAggregateListContribution() { + Injector injector = Injector.create().bindImplicit(AggregateListTest.class); + + List services = + injector.getInstance(new Key>() {}); + + assertNotNull(services); + assertEquals(5, services.size()); // 2 named + 3 from aggregate + } + + static class AggregateListTest { + interface MyService {} + + @Named("foo") + static class FooService implements MyService {} + + @Named("bar") + static class BarService implements MyService {} + + // Bulk contribution using @Aggregate + @Provides + @Aggregate + List extraServices() { + return Arrays.asList(new MyService() {}, new MyService() {}, new MyService() {}); + } + } + + @Test + void testExplicitMapProviderWithoutAggregate() { + Injector injector = Injector.create().bindImplicit(ExplicitMapProvider.class); + + Map services = + injector.getInstance(new Key>() {}); + + assertNotNull(services); + // Without @Aggregate, explicit provider REPLACES auto-aggregation + assertEquals(2, services.size()); + assertTrue(services.containsKey("explicit1")); + assertTrue(services.containsKey("explicit2")); + + // "foo" and "bar" should NOT be in the map + assertFalse(services.containsKey("foo")); + assertFalse(services.containsKey("bar")); + } + + static class ExplicitMapProvider { + interface MyService {} + + @Named("foo") + static class FooService implements MyService {} + + @Named("bar") + static class BarService implements MyService {} + + // Explicit provider WITHOUT @Aggregate replaces auto-aggregation + @Provides + Map explicitMap() { + Map map = new java.util.HashMap<>(); + map.put("explicit1", new MyService() {}); + map.put("explicit2", new MyService() {}); + return map; + } + } + + @Test + void testExplicitListProviderWithoutAggregate() { + Injector injector = Injector.create().bindImplicit(ExplicitListProvider.class); + + List services = + injector.getInstance(new Key>() {}); + + assertNotNull(services); + // Without @Aggregate, explicit provider REPLACES auto-aggregation + assertEquals(2, services.size()); + + // Should only contain services from the explicit provider + // Not the @Named beans + } + + static class ExplicitListProvider { + interface MyService {} + + @Named("foo") + static class FooService implements MyService {} + + @Named("bar") + static class BarService implements MyService {} + + // Explicit provider WITHOUT @Aggregate replaces auto-aggregation + @Provides + List explicitList() { + return Arrays.asList(new MyService() {}, new MyService() {}); + } + } + + @Test + void testMultipleAggregateProviders() { + Injector injector = Injector.create().bindImplicit(MultipleAggregateProviders.class); + + Map services = + injector.getInstance(new Key>() {}); + + assertNotNull(services); + assertEquals(6, services.size()); + + // Should contain entries from all sources + assertTrue(services.containsKey("foo")); + assertTrue(services.containsKey("bar")); + assertTrue(services.containsKey("module1")); + assertTrue(services.containsKey("module2")); + assertTrue(services.containsKey("module3")); + assertTrue(services.containsKey("module4")); + } + + static class MultipleAggregateProviders { + interface MyService {} + + @Named("foo") + static class FooService implements MyService {} + + @Named("bar") + static class BarService implements MyService {} + + @Provides + @Aggregate + Map moduleA() { + Map map = new java.util.HashMap<>(); + map.put("module1", new MyService() {}); + map.put("module2", new MyService() {}); + return map; + } + + @Provides + @Aggregate + Map moduleB() { + Map map = new java.util.HashMap<>(); + map.put("module3", new MyService() {}); + map.put("module4", new MyService() {}); + return map; + } + } + + @Test + void testAggregateWithDuplicateKeys() { + Injector injector = Injector.create().bindImplicit(AggregateWithDuplicates.class); + + Map services = + injector.getInstance(new Key>() {}); + + assertNotNull(services); + // When duplicate keys exist, last one wins (or you could throw an exception) + assertEquals(3, services.size()); + assertTrue(services.containsKey("foo")); + assertTrue(services.containsKey("bar")); + assertTrue(services.containsKey("duplicate")); + } + + static class AggregateWithDuplicates { + interface MyService {} + + @Named("foo") + static class FooService implements MyService {} + + @Named("bar") + static class BarService implements MyService {} + + @Provides + @Aggregate + Map module1() { + Map map = new java.util.HashMap<>(); + map.put("duplicate", new MyService() {}); + return map; + } + + @Provides + @Aggregate + Map module2() { + Map map = new java.util.HashMap<>(); + map.put("duplicate", new MyService() {}); // Same key + return map; + } + } + + @Test + void testAggregateSingleBeanToMap() { + Injector injector = Injector.create().bindImplicit(AggregateSingleToMap.class); + + Map services = + injector.getInstance(new Key>() {}); + + assertNotNull(services); + assertEquals(3, services.size()); + + assertTrue(services.containsKey("foo")); + assertTrue(services.containsKey("bar")); + assertTrue(services.containsKey("extra")); + } + + static class AggregateSingleToMap { + interface MyService {} + + @Named("foo") + static class FooService implements MyService {} + + @Named("bar") + static class BarService implements MyService {} + + // Single bean with @Aggregate and @Named - contributes to map + @Provides + @Aggregate + @Named("extra") + MyService extra() { + return new MyService() {}; + } + } + + @Test + void testAggregateSingleBeanToList() { + Injector injector = Injector.create().bindImplicit(AggregateSingleToList.class); + + List services = + injector.getInstance(new Key>() {}); + + assertNotNull(services); + assertEquals(3, services.size()); + } + + static class AggregateSingleToList { + interface MyService {} + + @Named("foo") + static class FooService implements MyService {} + + @Named("bar") + static class BarService implements MyService {} + + // Single bean with @Aggregate - contributes to list + @Provides + @Aggregate + MyService extra() { + return new MyService() {}; + } + } + + @Test + void testAggregateEmptyCollections() { + Injector injector = Injector.create().bindImplicit(AggregateEmptyCollections.class); + + Map mapServices = + injector.getInstance(new Key>() {}); + List listServices = + injector.getInstance(new Key>() {}); + + assertNotNull(mapServices); + assertNotNull(listServices); + + // Empty @Aggregate contributions should not cause errors + assertEquals(2, mapServices.size()); + assertEquals(2, listServices.size()); + } + + static class AggregateEmptyCollections { + interface MyService {} + + @Named("foo") + static class FooService implements MyService {} + + @Named("bar") + static class BarService implements MyService {} + + @Provides + @Aggregate + Map emptyMap() { + return new java.util.HashMap<>(); + } + + @Provides + @Aggregate + List emptyList() { + return new ArrayList<>(); + } + } + + @Test + void testAggregateWithPriorityInList() { + Injector injector = Injector.create().bindImplicit(AggregateWithPriority.class); + + List services = + injector.getInstance(new Key>() {}); + + assertNotNull(services); + assertEquals(5, services.size()); + + // Priority ordering should apply to aggregated items too + assertInstanceOf(AggregateWithPriority.HighPriority.class, services.get(0)); + assertInstanceOf(AggregateWithPriority.LowPriority.class, services.get(services.size() - 1)); + } + + static class AggregateWithPriority { + interface MyService {} + + @Named + @Priority(100) + static class HighPriority implements MyService {} + + @Named + @Priority(-10) + static class LowPriority implements MyService {} + + @Provides + @Aggregate + List extraServices() { + return Arrays.asList(new MyService() {}, new MyService() {}, new MyService() {}); + } + } + + @Test + void testMixedAggregateAndExplicitProviders() { + Injector injector = Injector.create().bindImplicit(MixedAggregateExplicit.class); + + // When both @Aggregate and explicit (non-@Aggregate) providers exist, + // the explicit one should win and replace everything + Map services = + injector.getInstance(new Key>() {}); + + assertNotNull(services); + assertEquals(2, services.size()); + assertTrue(services.containsKey("explicit1")); + assertTrue(services.containsKey("explicit2")); + + // Aggregate contributions should be ignored when explicit provider exists + assertFalse(services.containsKey("foo")); + assertFalse(services.containsKey("aggregate1")); + } + + static class MixedAggregateExplicit { + interface MyService {} + + @Named("foo") + static class FooService implements MyService {} + + @Provides + @Aggregate + Map aggregateMap() { + Map map = new java.util.HashMap<>(); + map.put("aggregate1", new MyService() {}); + return map; + } + + // Explicit provider (no @Aggregate) takes precedence + @Provides + Map explicitMap() { + Map map = new java.util.HashMap<>(); + map.put("explicit1", new MyService() {}); + map.put("explicit2", new MyService() {}); + return map; + } + } + + @Test + void testPriorityOnProvidesMethod() { + Injector injector = Injector.create().bindImplicit(PriorityProvidesTest.class); + + List services = + injector.getInstance(new Key>() {}); + + assertNotNull(services); + assertEquals(3, services.size()); + + // Should be ordered by priority: High (100), Medium (50), Low (10) + assertInstanceOf(PriorityProvidesTest.HighPriorityService.class, services.get(0)); + assertInstanceOf(PriorityProvidesTest.MediumPriorityService.class, services.get(1)); + assertInstanceOf(PriorityProvidesTest.LowPriorityService.class, services.get(2)); + } + + static class PriorityProvidesTest { + interface MyService {} + + static class HighPriorityService implements MyService {} + + static class MediumPriorityService implements MyService {} + + static class LowPriorityService implements MyService {} + + @Provides + @Priority(100) + @Named("high") + MyService highPriorityService() { + return new HighPriorityService(); + } + + @Provides + @Priority(50) + @Named("medium") + MyService mediumPriorityService() { + return new MediumPriorityService(); + } + + @Provides + @Priority(10) + @Named("low") + MyService lowPriorityService() { + return new LowPriorityService(); + } + } + + @Test + void testAggregateMapContributesToNamedInjection() { + Injector injector = Injector.create() + .bindImplicit(AggregateMapToNamedTest.class) + .bindImplicit(AggregateMapToNamedTest.ServiceConsumer.class); + + // Should be able to inject individual named services from @Aggregate Map provider + AggregateMapToNamedTest.ServiceConsumer consumer = + injector.getInstance(AggregateMapToNamedTest.ServiceConsumer.class); + + assertNotNull(consumer); + assertNotNull(consumer.fooService); + assertNotNull(consumer.barService); + + // Verify these are the services from the @Aggregate Map + assertEquals("foo-service", consumer.fooService.getName()); + assertEquals("bar-service", consumer.barService.getName()); + } + + static class AggregateMapToNamedTest { + interface MyService { + String getName(); + } + + static class ServiceConsumer { + @Inject + @Named("foo") + MyService fooService; + + @Inject + @Named("bar") + MyService barService; + } + + @Provides + @Aggregate + Map serviceMap() { + Map map = new LinkedHashMap<>(); + map.put("foo", () -> "foo-service"); + map.put("bar", () -> "bar-service"); + return map; + } + } + + @Test + void testAggregateListContributesToIndividualInjection() { + Injector injector = Injector.create() + .bindImplicit(AggregateListToIndividualTest.class) + .bindImplicit(AggregateListToIndividualTest.ServiceConsumer.class); + + // Should be able to inject individual services from @Aggregate List provider + AggregateListToIndividualTest.ServiceConsumer consumer = + injector.getInstance(AggregateListToIndividualTest.ServiceConsumer.class); + + assertNotNull(consumer); + assertNotNull(consumer.service); + + // Should get one of the services from the @Aggregate List + // (the exact one depends on priority/order, but it should be non-null) + assertNotNull(consumer.service.getName()); + assertTrue(consumer.service.getName().startsWith("service-")); + } + + static class AggregateListToIndividualTest { + interface MyService { + String getName(); + } + + static class ServiceConsumer { + @Inject + MyService service; + } + + @Provides + @Aggregate + List serviceList() { + List list = new ArrayList<>(); + list.add(() -> "service-1"); + list.add(() -> "service-2"); + return list; + } + } }