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}:
+ *
+ * {@code List} - automatically aggregates all beans of type {@code T}
+ * {@code Map} - automatically aggregates all {@code @Named} beans of type {@code T},
+ * using their name as the key
+ *
+ *
+ * With {@code @Provides} (no {@code @Aggregate}):
+ *
+ * The explicit provider replaces auto-aggregation
+ * Only the provided collection is available for injection
+ *
+ *
+ * With {@code @Provides @Aggregate}:
+ *
+ * The provider contributes to auto-aggregation
+ * Multiple {@code @Aggregate} providers can coexist and all contribute
+ * If both {@code @Aggregate} and non-{@code @Aggregate} providers exist,
+ * the non-{@code @Aggregate} provider takes precedence
+ *
+ *
+ * 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:
+ *
+ * Beans with {@link Priority} annotation are ordered by priority (highest first)
+ * Beans without priority come after prioritized beans
+ * Collections contributed via {@code @Aggregate} are merged respecting these 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 extends T> 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;
+ }
+ }
}