Repository: avaje/avaje-inject
Pull request #231 (2022-06-08 09:18:24)

#230 - ENH: Add support for injecting into Map<String,T> (by qualifier)


Patch:
diff --git a/inject-generator/src/main/java/io/avaje/inject/generator/Util.java b/inject-generator/src/main/java/io/avaje/inject/generator/Util.java
index 6282737a..ca48dc13 100644
--- a/inject-generator/src/main/java/io/avaje/inject/generator/Util.java
+++ b/inject-generator/src/main/java/io/avaje/inject/generator/Util.java
@@ -129,6 +129,14 @@ class Util {
     return setType;
   }
 
+  static String extractMap(String rawType) {
+    String valType = rawType.substring(31, rawType.length() - 1);
+    if (valType.startsWith("? extends")) {
+      return valType.substring(10);
+    }
+    return valType;
+  }
+
   static UtilType determineType(TypeMirror rawType) {
     return UtilType.of(rawType.toString());
   }
diff --git a/inject-generator/src/main/java/io/avaje/inject/generator/UtilType.java b/inject-generator/src/main/java/io/avaje/inject/generator/UtilType.java
index 9d435eb4..3f14fe30 100644
--- a/inject-generator/src/main/java/io/avaje/inject/generator/UtilType.java
+++ b/inject-generator/src/main/java/io/avaje/inject/generator/UtilType.java
@@ -5,6 +5,7 @@ class UtilType {
   private enum Type {
     LIST,
     SET,
+    MAP,
     OPTIONAL,
     PROVIDER,
     OTHER
@@ -23,6 +24,8 @@ class UtilType {
       return new UtilType(Type.LIST, rawType);
     } else if (rawType.startsWith("java.util.Set<")) {
       return new UtilType(Type.SET, rawType);
+    } else if (rawType.startsWith("java.util.Map<java.lang.String,")) {
+      return new UtilType(Type.MAP, rawType);
     } else if (rawType.startsWith("java.util.Optional<")) {
       return new UtilType(Type.OPTIONAL, rawType);
     } else if (Util.isProvider(rawType)) {
@@ -49,6 +52,8 @@ class UtilType {
         return Util.extractSet(rawType);
       case LIST:
         return Util.extractList(rawType);
+      case MAP:
+        return Util.extractMap(rawType);
       case OPTIONAL:
         return Util.extractOptionalType(rawType);
       default:
@@ -62,6 +67,8 @@ class UtilType {
         return "set(";
       case LIST:
         return "list(";
+      case MAP:
+        return "map(";
       case OPTIONAL:
         return "getOptional(";
       case PROVIDER:
diff --git a/inject/src/main/java/io/avaje/inject/BeanScope.java b/inject/src/main/java/io/avaje/inject/BeanScope.java
index c222a052..532766cb 100644
--- a/inject/src/main/java/io/avaje/inject/BeanScope.java
+++ b/inject/src/main/java/io/avaje/inject/BeanScope.java
@@ -6,6 +6,7 @@ import io.avaje.lang.Nullable;
 import java.lang.annotation.Annotation;
 import java.lang.reflect.Type;
 import java.util.List;
+import java.util.Map;
 import java.util.Optional;
 
 /**
@@ -187,7 +188,7 @@ public interface BeanScope extends AutoCloseable {
   List<Object> listByAnnotation(Class<?> annotation);
 
   /**
-   * Return the list of beans that implement the interface.
+   * Return the list of beans for a given type.
    *
    * <pre>{@code
    *
@@ -197,19 +198,19 @@ public interface BeanScope extends AutoCloseable {
    *
    * }</pre>
    *
-   * @param interfaceType An interface class.
+   * @param type The type of beans to return.
    */
-  <T> List<T> list(Class<T> interfaceType);
+  <T> List<T> list(Class<T> type);
 
   /**
    * Return the list of beans that implement the given type.
    */
-  <T> List<T> list(Type interfaceType);
+  <T> List<T> list(Type type);
 
   /**
    * Return the list of beans that implement the interface sorting by priority.
    */
-  <T> List<T> listByPriority(Class<T> interfaceType);
+  <T> List<T> listByPriority(Class<T> type);
 
   /**
    * Return the beans that implement the interface sorting by the priority annotation used.
@@ -217,10 +218,17 @@ public interface BeanScope extends AutoCloseable {
    * The priority annotation will typically be either <code>javax.annotation.Priority</code>
    * or <code>jakarta.annotation.Priority</code>.
    *
-   * @param interfaceType The interface type of the beans to return
-   * @param priority      The priority annotation used to sort the beans
+   * @param type     The interface type of the beans to return
+   * @param priority The priority annotation used to sort the beans
    */
-  <T> List<T> listByPriority(Class<T> interfaceType, Class<? extends Annotation> priority);
+  <T> List<T> listByPriority(Class<T> type, Class<? extends Annotation> priority);
+
+  /**
+   * Return the beans for this type mapped by their qualifier name.
+   * <p>
+   * Beans with no qualifier name get a generated unique key to use instead.
+   */
+  <T> Map<String, T> map(Type type);
 
   /**
    * Return all the bean entries from the scope.
diff --git a/inject/src/main/java/io/avaje/inject/spi/Builder.java b/inject/src/main/java/io/avaje/inject/spi/Builder.java
index 483c2889..f8ada4b1 100644
--- a/inject/src/main/java/io/avaje/inject/spi/Builder.java
+++ b/inject/src/main/java/io/avaje/inject/spi/Builder.java
@@ -5,6 +5,7 @@ import jakarta.inject.Provider;
 
 import java.lang.reflect.Type;
 import java.util.List;
+import java.util.Map;
 import java.util.Optional;
 import java.util.Set;
 import java.util.function.Consumer;
@@ -146,15 +147,20 @@ public interface Builder {
   <T> T get(Type cls, String name);
 
   /**
-   * Get a list of dependencies for the interface type .
+   * Get a list of dependencies for the type.
    */
   <T> List<T> list(Type interfaceType);
 
   /**
-   * Get a set of dependencies for the interface type .
+   * Get a set of dependencies for the type.
    */
   <T> Set<T> set(Type interfaceType);
 
+  /**
+   * Return a map of dependencies keyed by qualifier name.
+   */
+  <T> Map<String, T> map(Type interfaceType);
+
   /**
    * Build and return the bean scope.
    */
diff --git a/inject/src/main/java/io/avaje/inject/spi/DBeanMap.java b/inject/src/main/java/io/avaje/inject/spi/DBeanMap.java
index a223eff3..e7490d43 100644
--- a/inject/src/main/java/io/avaje/inject/spi/DBeanMap.java
+++ b/inject/src/main/java/io/avaje/inject/spi/DBeanMap.java
@@ -1,5 +1,6 @@
 package io.avaje.inject.spi;
 
+import io.avaje.inject.BeanScope;
 import jakarta.inject.Provider;
 
 import java.lang.reflect.Type;
@@ -118,6 +119,23 @@ class DBeanMap {
     return entry != null ? entry.all() : Collections.emptyList();
   }
 
+  /**
+   * Return a map of bean instances keyed by qualifier name.
+   */
+  Map<String, Object> map(Type type, BeanScope parent) {
+    if (parent == null) {
+      return map(type);
+    }
+    Map<String, Object> result = parent.map(type);
+    result.putAll(map(type));
+    return result;
+  }
+
+  private Map<String, Object> map(Type type) {
+    DContextEntry entry = beans.get(type.getTypeName());
+    return entry != null ? entry.map() : Collections.emptyMap();
+  }
+
   /**
    * Return true if there is a supplied bean for the name and types.
    */
diff --git a/inject/src/main/java/io/avaje/inject/spi/DBeanScope.java b/inject/src/main/java/io/avaje/inject/spi/DBeanScope.java
index bde6963a..5c21093a 100644
--- a/inject/src/main/java/io/avaje/inject/spi/DBeanScope.java
+++ b/inject/src/main/java/io/avaje/inject/spi/DBeanScope.java
@@ -6,7 +6,6 @@ import io.avaje.inject.Priority;
 import io.avaje.lang.NonNullApi;
 import io.avaje.lang.Nullable;
 
-
 import java.lang.System.Logger.Level;
 import java.lang.annotation.Annotation;
 import java.lang.reflect.Type;
@@ -84,7 +83,7 @@ class DBeanScope implements BeanScope {
     return getByType(type, name);
   }
 
-  private  <T> T getByType(Type type, @Nullable String name) {
+  private <T> T getByType(Type type, @Nullable String name) {
     final T bean = beans.get(type, name);
     if (bean != null) {
       return bean;
@@ -107,7 +106,7 @@ class DBeanScope implements BeanScope {
       }
     }
     if (parent instanceof DBeanScope) {
-      DBeanScope dParent = (DBeanScope)parent;
+      DBeanScope dParent = (DBeanScope) parent;
       return dParent.getStrict(name, types);
     }
     return null;
@@ -134,23 +133,29 @@ class DBeanScope implements BeanScope {
     return parent.getOptional(type, name);
   }
 
+  @SuppressWarnings("unchecked")
+  @Override
+  public <T> Map<String, T> map(Type type) {
+    return (Map<String, T>) beans.map(type, parent);
+  }
+
   @Override
-  public <T> List<T> list(Class<T> interfaceType) {
-    return listOf(interfaceType);
+  public <T> List<T> list(Class<T> type) {
+    return listOf(type);
   }
 
   @Override
-  public <T> List<T> list(Type interfaceType) {
-    return listOf(interfaceType);
+  public <T> List<T> list(Type type) {
+    return listOf(type);
   }
 
   @SuppressWarnings("unchecked")
-  private  <T> List<T> listOf(Type interfaceType) {
-    List<T> values = (List<T>) beans.all(interfaceType);
+  private <T> List<T> listOf(Type type) {
+    List<T> values = (List<T>) beans.all(type);
     if (parent == null) {
       return values;
     }
-    return combine(values, parent.list(interfaceType));
+    return combine(values, parent.list(type));
   }
 
   static <T> List<T> combine(List<T> values, List<T> parentValues) {
@@ -165,13 +170,13 @@ class DBeanScope implements BeanScope {
   }
 
   @Override
-  public <T> List<T> listByPriority(Class<T> interfaceType) {
-    return listByPriority(interfaceType, Priority.class);
+  public <T> List<T> listByPriority(Class<T> type) {
+    return listByPriority(type, Priority.class);
   }
 
   @Override
-  public <T> List<T> listByPriority(Class<T> interfaceType, Class<? extends Annotation> priorityAnnotation) {
-    List<T> list = list(interfaceType);
+  public <T> List<T> listByPriority(Class<T> type, Class<? extends Annotation> priorityAnnotation) {
+    List<T> list = list(type);
     return list.size() > 1 ? sortByPriority(list, priorityAnnotation) : list;
   }
 
diff --git a/inject/src/main/java/io/avaje/inject/spi/DBuilder.java b/inject/src/main/java/io/avaje/inject/spi/DBuilder.java
index 01744bc3..d31f9f0f 100644
--- a/inject/src/main/java/io/avaje/inject/spi/DBuilder.java
+++ b/inject/src/main/java/io/avaje/inject/spi/DBuilder.java
@@ -116,6 +116,12 @@ class DBuilder implements Builder {
     return combine(values, parent.list(interfaceType));
   }
 
+  @Override
+  @SuppressWarnings("unchecked")
+  public <T> Map<String, T> map(Type type) {
+    return (Map<String, T>) beanMap.map(type, parent);
+  }
+
   private <T> T getMaybe(Type type, String name) {
     T bean = beanMap.get(type, name);
     if (bean != null) {
diff --git a/inject/src/main/java/io/avaje/inject/spi/DContextEntry.java b/inject/src/main/java/io/avaje/inject/spi/DContextEntry.java
index 44515c01..4f3610a0 100644
--- a/inject/src/main/java/io/avaje/inject/spi/DContextEntry.java
+++ b/inject/src/main/java/io/avaje/inject/spi/DContextEntry.java
@@ -3,7 +3,9 @@ package io.avaje.inject.spi;
 import jakarta.inject.Provider;
 
 import java.util.ArrayList;
+import java.util.LinkedHashMap;
 import java.util.List;
+import java.util.Map;
 
 /**
  * Entry for a given key (bean class, interface class or annotation class).
@@ -62,6 +64,22 @@ class DContextEntry {
     return list;
   }
 
+  /**
+   * Return a map of beans keyed by qualifier name.
+   */
+  Map<String, Object> map() {
+    Map<String, Object> map = new LinkedHashMap<>();
+    for (DContextEntryBean entry : entries) {
+      Object bean = entry.bean();
+      String nm = entry.name();
+      if (nm == null) {
+        nm = "$Unnamed-" + System.identityHashCode(bean) + "-" + bean;
+      }
+      map.put(nm, bean);
+    }
+    return map;
+  }
+
   /**
    * Return a supplied bean is one of the entries.
    */
diff --git a/inject/src/main/java/io/avaje/inject/spi/DContextEntryBean.java b/inject/src/main/java/io/avaje/inject/spi/DContextEntryBean.java
index afdf9fa1..68f29129 100644
--- a/inject/src/main/java/io/avaje/inject/spi/DContextEntryBean.java
+++ b/inject/src/main/java/io/avaje/inject/spi/DContextEntryBean.java
@@ -30,7 +30,7 @@ class DContextEntryBean {
   }
 
   protected final Object source;
-  private final String name;
+  protected final String name;
   private final int flag;
 
   private DContextEntryBean(Object source, String name, int flag) {
@@ -73,6 +73,10 @@ class DContextEntryBean {
     return isNameMatch(name) ? bean() : null;
   }
 
+  String name() {
+    return name;
+  }
+
   Object bean() {
     return source;
   }


Test patch:
diff --git a/inject-generator/src/test/java/io/avaje/inject/generator/UtilTest.java b/inject-generator/src/test/java/io/avaje/inject/generator/UtilTest.java
index 0d4e01e9..e77ab4f0 100644
--- a/inject-generator/src/test/java/io/avaje/inject/generator/UtilTest.java
+++ b/inject-generator/src/test/java/io/avaje/inject/generator/UtilTest.java
@@ -39,16 +39,26 @@ class UtilTest {
     assertEquals(Util.unwrapProvider("jakarta.inject.Provider<org.Foo<com.Bazz>>"), "org.Foo<com.Bazz>");
   }
 
+  @Test
+  void extractMap() {
+    assertEquals("Foo", Util.extractMap("java.util.Map<java.lang.String,? extends Foo>"));
+    assertEquals("org.foo.Bar", Util.extractMap("java.util.Map<java.lang.String,? extends org.foo.Bar>"));
+    assertEquals("org.foo.Bar", Util.extractMap("java.util.Map<java.lang.String,org.foo.Bar>"));
+  }
+
   @Test
   void extractList() {
-    assertEquals("Foo", Util.extractList("List<? extends Foo>"));
-    assertEquals("org.foo.Bar", Util.extractList("List<? extends org.foo.Bar>"));
+    assertEquals("Foo", Util.extractList("java.util.List<? extends Foo>"));
+    assertEquals("org.foo.Bar", Util.extractList("java.util.List<? extends org.foo.Bar>"));
+    assertEquals("org.foo.Bar", Util.extractList("java.util.List<org.foo.Bar>"));
+
   }
 
   @Test
   void extractSet() {
-    assertEquals("Foo", Util.extractSet("Set<? extends Foo>"));
-    assertEquals("org.foo.Bar", Util.extractSet("Set<? extends org.foo.Bar>"));
+    assertEquals("Foo", Util.extractSet("java.util.Set<? extends Foo>"));
+    assertEquals("org.foo.Bar", Util.extractSet("java.util.Set<? extends org.foo.Bar>"));
+    assertEquals("org.foo.Bar", Util.extractSet("java.util.Set<org.foo.Bar>"));
   }
 
   @Test
diff --git a/inject-test/src/test/java/org/example/coffee/InjectListTest.java b/inject-test/src/test/java/org/example/coffee/InjectListTest.java
index 6e93f96f..47e9c30f 100644
--- a/inject-test/src/test/java/org/example/coffee/InjectListTest.java
+++ b/inject-test/src/test/java/org/example/coffee/InjectListTest.java
@@ -1,6 +1,7 @@
 package org.example.coffee;
 
 import io.avaje.inject.BeanScope;
+import org.example.coffee.list.CombinedMapSomei;
 import org.example.coffee.list.CombinedSetSomei;
 import org.example.coffee.list.CombinedSomei;
 import org.example.coffee.list.SomeInterfaceConsumer;
@@ -30,6 +31,17 @@ class InjectListTest {
     }
   }
 
+  @Test
+  void test_map() {
+    try (BeanScope context = BeanScope.builder().build()) {
+      CombinedMapSomei bean = context.get(CombinedMapSomei.class);
+      List<String> keys = bean.someKeys();
+      assertThat(keys).containsOnly("a", "b", "a2");
+      List<String> vals = bean.someVals();
+      assertThat(vals).containsOnly("a", "b", "a2");
+    }
+  }
+
   @Test
   void testEmptyList() {
     try (BeanScope context = BeanScope.builder().build()) {
diff --git a/inject-test/src/test/java/org/example/coffee/list/CombinedMapSomei.java b/inject-test/src/test/java/org/example/coffee/list/CombinedMapSomei.java
new file mode 100644
index 00000000..534ecfa5
--- /dev/null
+++ b/inject-test/src/test/java/org/example/coffee/list/CombinedMapSomei.java
@@ -0,0 +1,33 @@
+package org.example.coffee.list;
+
+import jakarta.inject.Inject;
+import jakarta.inject.Singleton;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+@Singleton
+public class CombinedMapSomei {
+
+  private final Map<String, Somei> somes;
+
+  /**
+   * Inject map of beans keyed by qualifier name.
+   */
+  @Inject
+  public CombinedMapSomei(Map<String, Somei> somes) {
+    this.somes = somes;
+  }
+
+  public List<String> someKeys() {
+    return new ArrayList<>(somes.keySet());
+  }
+
+  public List<String> someVals() {
+    return somes.values().stream()
+      .map(Somei::some)
+      .collect(Collectors.toList());
+  }
+}
diff --git a/inject-test/src/test/java/org/example/coffee/qualifier/StoreManagerWithNamedTest.java b/inject-test/src/test/java/org/example/coffee/qualifier/StoreManagerWithNamedTest.java
index 258926d5..769ecc11 100644
--- a/inject-test/src/test/java/org/example/coffee/qualifier/StoreManagerWithNamedTest.java
+++ b/inject-test/src/test/java/org/example/coffee/qualifier/StoreManagerWithNamedTest.java
@@ -1,19 +1,34 @@
 package org.example.coffee.qualifier;
 
 import io.avaje.inject.BeanScope;
+import org.example.autonamed.MyAutoB2;
 import org.junit.jupiter.api.Test;
 
+import java.util.Map;
+
 import static org.assertj.core.api.Assertions.assertThat;
 
-public class StoreManagerWithNamedTest {
+class StoreManagerWithNamedTest {
 
   @Test
-  public void test() {
-
-    try (BeanScope context = BeanScope.builder().build()) {
-      StoreManagerWithNamed manager = context.get(StoreManagerWithNamed.class);
+  void test() {
+    try (BeanScope beanScope = BeanScope.builder().build()) {
+      StoreManagerWithNamed manager = beanScope.get(StoreManagerWithNamed.class);
       String store = manager.store();
       assertThat(store).isEqualTo("blue");
+
+      SomeStore greenStore = beanScope.get(SomeStore.class, "green");
+      SomeStore blueStore = beanScope.get(SomeStore.class, "blue");
+      Map<String, SomeStore> stores = beanScope.map(SomeStore.class);
+
+      SomeStore green = stores.get("green");
+      assertThat(green).isSameAs(greenStore);
+      SomeStore blue = stores.get("blue");
+      assertThat(blue).isSameAs(blueStore);
+
+      // a map with unnamed component
+      Map<String, MyAutoB2> mapWithUnnamed = beanScope.map(MyAutoB2.class);
+      assertThat(mapWithUnnamed).hasSize(1);
     }
   }
 }
diff --git a/inject-test/src/test/java/org/example/generic/MzCrudService.java b/inject-test/src/test/java/org/example/generic/MzCrudService.java
new file mode 100644
index 00000000..6d92adad
--- /dev/null
+++ b/inject-test/src/test/java/org/example/generic/MzCrudService.java
@@ -0,0 +1,24 @@
+package org.example.generic;
+
+import io.avaje.inject.Component;
+
+import java.util.Optional;
+
+@Component
+public class MzCrudService implements CRUDService<MzObj, Integer> {
+
+  @Override
+  public String iamCrud() {
+    return "MzCrud";
+  }
+
+  @Override
+  public Integer create(MzObj bean) {
+    return 92;
+  }
+
+  @Override
+  public Optional<MzObj> get(Integer id) {
+    return Optional.of(new MzObj());
+  }
+}
diff --git a/inject-test/src/test/java/org/example/generic/MzCrudServiceTest.java b/inject-test/src/test/java/org/example/generic/MzCrudServiceTest.java
new file mode 100644
index 00000000..a8f936db
--- /dev/null
+++ b/inject-test/src/test/java/org/example/generic/MzCrudServiceTest.java
@@ -0,0 +1,27 @@
+package org.example.generic;
+
+import io.avaje.inject.BeanScope;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+class MzCrudServiceTest {
+
+  @Test
+  void test() {
+    try (BeanScope beanScope = BeanScope.builder().build()) {
+      MzCrudService crud = beanScope.get(MzCrudService.class);
+      ReadService<MzObj,Integer> read = beanScope.get(MzCrudService$DI.TYPE_ReadServiceMzObjInteger, null);
+      CreateService<MzObj,Integer> create = beanScope.get(MzCrudService$DI.TYPE_CreateServiceMzObjInteger, null);
+
+      assertNotNull(crud);
+      assertThat(crud).isSameAs(read);
+      assertThat(crud).isSameAs(create);
+
+      assertThat(crud.iamCrud()).isEqualTo("MzCrud");
+      assertThat(create.create(new MzObj())).isEqualTo(92);
+      assertThat(read.get(42)).isNotEmpty();
+    }
+  }
+}
diff --git a/inject-test/src/test/java/org/example/generic/MzObj.java b/inject-test/src/test/java/org/example/generic/MzObj.java
new file mode 100644
index 00000000..1db187f9
--- /dev/null
+++ b/inject-test/src/test/java/org/example/generic/MzObj.java
@@ -0,0 +1,4 @@
+package org.example.generic;
+
+public class MzObj {
+}
diff --git a/inject/src/test/java/io/avaje/inject/BeanScopeBuilderTest.java b/inject/src/test/java/io/avaje/inject/BeanScopeBuilderTest.java
index 4db07933..1e454d3b 100644
--- a/inject/src/test/java/io/avaje/inject/BeanScopeBuilderTest.java
+++ b/inject/src/test/java/io/avaje/inject/BeanScopeBuilderTest.java
@@ -8,6 +8,7 @@ import java.lang.annotation.Annotation;
 import java.lang.reflect.Type;
 import java.util.Collections;
 import java.util.List;
+import java.util.Map;
 import java.util.Optional;
 import java.util.stream.Collectors;
 
@@ -268,25 +269,30 @@ class BeanScopeBuilderTest {
     }
 
     @Override
-    public <T> List<T> list(Class<T> interfaceType) {
+    public <T> List<T> list(Class<T> type) {
       return null;
     }
 
     @Override
-    public <T> List<T> list(Type interfaceType) {
+    public <T> List<T> list(Type type) {
       return null;
     }
 
     @Override
-    public <T> List<T> listByPriority(Class<T> interfaceType) {
+    public <T> List<T> listByPriority(Class<T> type) {
       return null;
     }
 
     @Override
-    public <T> List<T> listByPriority(Class<T> interfaceType, Class<? extends Annotation> priority) {
+    public <T> List<T> listByPriority(Class<T> type, Class<? extends Annotation> priority) {
       return null;
     }
 
+    @Override
+    public <T> Map<String, T> map(Type type) {
+      return Collections.emptyMap();
+    }
+
     @Override
     public List<BeanEntry> all() {
       return null;