Repository: avaje/avaje-inject (java)

Issue:
Usage of @RequiresProperty on interface with different implementations
Is it possible to use @RequiresProperty to create beans of same interface in a factory? 
I'm using avaje 11.3

I'm trying something like:

```java
@Factory
class MyServiceFactory {

    @Bean
    @RequiresProperty(value = "service.type", equalTo = "one")
    MyService myServiceOne() {
        return new MyServiceOne();
    }

    @Bean
    @RequiresProperty(value = "service.type", equalTo = "two")
    MyService myServiceTwo() {
        return new MyServiceTwo();
    }
}
```

But I see in the generated code that only the 2nd bean creation will be ever called making instanciation of bean with property `service.type=one` never happening.

Generated module code:
```java
@Override
  public void build(Builder builder) {
    // create beans in order based on constructor dependencies
    // i.e. "provides" followed by "dependsOn"
    build_example_MyServiceFactory(builder);
    build_example_MyService(builder);
    build_example_MyApp(builder);
  }

  @DependencyMeta(type = "example.MyServiceFactory")
  private void build_example_MyServiceFactory(Builder builder) {
    MyServiceFactory$DI.build(builder);
  }

  @DependencyMeta(
      type = "example.MyService",
      method = "example.MyServiceFactory$DI.build_myServiceTwo",
      dependsOn = {"example.MyServiceFactory"},
      autoProvides = {"example.MyService"})
  private void build_example_MyService(Builder builder) {
    MyServiceFactory$DI.build_myServiceTwo(builder);
  }

  @DependencyMeta(
      type = "example.MyApp",
      dependsOn = {"example.MyService"})
  private void build_example_MyApp(Builder builder) {
    MyApp$DI.build(builder);
  }
```

stack trace when running with service.type=one
```
Exception in thread "main" java.lang.IllegalStateException: Injecting null for example.MyService name:!myService when creating class example.MyApp - potential beans to inject: []. Check for missing module? [ missing beanScopeBuilder.modules() ]. If it is expected to be externally provided, missing beanScopeBuilder.bean() ?
	at io.avaje.inject.spi.DBuilder.getBean(DBuilder.java:386)
	at io.avaje.inject.spi.DBuilder.get(DBuilder.java:333)
	at example.MyApp$DI.build(MyApp$DI.java:11)
	at example.ExampleModule.build_example_MyApp(ExampleModule.java:78)
	at example.ExampleModule.build(ExampleModule.java:57)
	at io.avaje.inject.DBeanScopeBuilder.build(DBeanScopeBuilder.java:272)
	at example.MyApp.main(MyApp.java:21)
```


Pull request description:
Fix conditional factories
Now this:
```java
@Factory
class MyServiceFactory {

  @Bean
  @RequiresProperty(value = "service.type", equalTo = "one")
  MyService myServiceOne() {
        return new MyServiceOne();
    }

  @Bean
  @RequiresProperty(value = "service.type", equalTo = "two")
  MyService myServiceTwo() {
        return new MyServiceTwo();
    }
}
```


will generate

```java
  @Override
  public void build(Builder builder) {
    // create beans in order based on constructor dependencies
    // i.e. "provides" followed by "dependsOn"
    build_requires_MyServiceFactory(builder);
    build_requires_MyService(builder);
    build_requires_MyService__MyServiceFactory_build_myServiceTwo(builder);
  }

  @DependencyMeta(type = "io.avaje.inject.generator.models.valid.requires.MyServiceFactory")
  private void build_requires_MyServiceFactory(Builder builder) {
    MyServiceFactory$DI.build(builder);
  }

  @DependencyMeta(
      type = "io.avaje.inject.generator.models.valid.requires.MyService",
      method = "io.avaje.inject.generator.models.valid.requires.MyServiceFactory$DI.build_myServiceOne",
      dependsOn = {"io.avaje.inject.generator.models.valid.requires.MyServiceFactory"},
      autoProvides = {"io.avaje.inject.generator.models.valid.requires.MyService"})
  private void build_requires_MyService(Builder builder) {
    MyServiceFactory$DI.build_myServiceOne(builder);
  }

  @DependencyMeta(
      type = "io.avaje.inject.generator.models.valid.requires.MyService",
      method = "io.avaje.inject.generator.models.valid.requires.MyServiceFactory$DI.build_myServiceTwo",
      dependsOn = {"io.avaje.inject.generator.models.valid.requires.MyServiceFactory"},
      autoProvides = {"io.avaje.inject.generator.models.valid.requires.MyService"})
  private void build_requires_MyService__MyServiceFactory_build_myServiceTwo(Builder builder) {
    MyServiceFactory$DI.build_myServiceTwo(builder);
  }
```
Resolves #784

Patch:
diff --git a/inject-generator/src/main/java/io/avaje/inject/generator/MetaData.java b/inject-generator/src/main/java/io/avaje/inject/generator/MetaData.java
index 153b278f..f2845fe2 100644
--- a/inject-generator/src/main/java/io/avaje/inject/generator/MetaData.java
+++ b/inject-generator/src/main/java/io/avaje/inject/generator/MetaData.java
@@ -1,9 +1,15 @@
 package io.avaje.inject.generator;
 
 import static io.avaje.inject.generator.APContext.typeElement;
-import java.util.*;
-import java.util.stream.Collectors;
 
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
 
 /**
  * Holds the data as per <code>@DependencyMeta</code>
@@ -15,13 +21,17 @@ final class MetaData implements Comparable<MetaData> {
       .thenComparing(MetaData::name, Comparator.nullsFirst(Comparator.naturalOrder()))
       .thenComparing(MetaData::compareProvides);
 
+  private static final Map<String, Integer> FACTORY_FREQUENCY = new HashMap<>();
+
   private static final String INDENT = "      ";
   private static final String NEWLINE = "\n";
 
   private final String type;
   private final String shortType;
   private final String name;
-  private String method;
+  private final String buildName;
+  private final String method;
+  private final String key;
   private boolean wired;
   private String providesAspect;
 
@@ -55,19 +65,28 @@ final class MetaData implements Comparable<MetaData> {
     this.provides = Util.addQualifierSuffix(meta.provides(), name);
     this.autoProvides = Util.addQualifierSuffix(meta.autoProvides(), name);
     this.importedComponent = meta.importedComponent();
+    this.key = createKey();
+    this.buildName = createBuildName();
   }
 
   MetaData(String type, String name) {
+    this(type, name, null);
+  }
+
+  MetaData(String type, String name, String method) {
     this.type = type;
     this.name = trimName(name);
     this.shortType = Util.shortName(type);
     this.provides = new ArrayList<>();
     this.dependsOn = new ArrayList<>();
+    this.method = method;
+    this.key = createKey();
+    this.buildName = createBuildName();
   }
 
   @Override
   public String toString() {
-    return (name == null) ? type : type + ":" + name;
+    return name == null ? type : type + ":" + name;
   }
 
   boolean importedComponent() {
@@ -90,18 +109,21 @@ final class MetaData implements Comparable<MetaData> {
   }
 
   String buildName() {
+    return buildName;
+  }
+
+  private String createBuildName() {
     if (Util.isVoid(type)) {
       return "void_" + Util.trimMethod(method);
     } else {
       final String trimType = Util.trimMethod(Util.unwrapProvider(type));
+
       if (name != null) {
         return trimType + "_" + name.replaceAll("[^a-zA-Z0-9_$]+", "_");
-      } else {
-        if (buildNameIncludeMethod()) {
-          return trimType + "__" + Util.trimMethod(method);
-        }
-        return trimType;
+      } else if (buildNameIncludeMethod() || hasMethod() && FACTORY_FREQUENCY.get(type) > 0) {
+        return trimType + "__" + Util.trimMethod(method);
       }
+      return trimType;
     }
   }
 
@@ -110,15 +132,20 @@ final class MetaData implements Comparable<MetaData> {
     return type.contains("<") && hasMethod();
   }
 
-  public String key() {
+  String key() {
+    return key;
+  }
+
+  private String createKey() {
     if (Util.isVoid(type)) {
       return "method:" + method;
     }
-    if (name != null) {
-      return type + ":" + name;
-    } else {
-      return type;
+    String keyString = name != null ? type + ":" + name : type;
+    if (hasMethod() && FACTORY_FREQUENCY.compute(type, (k, v) -> v == null ? 0 : ++v) > 0) {
+      // allow multiple factory methods that produce the same types, typically conditional
+      keyString += FACTORY_FREQUENCY.get(type);
     }
+    return keyString;
   }
 
   boolean noDepends() {
@@ -294,10 +321,6 @@ final class MetaData implements Comparable<MetaData> {
     this.dependsOn = dependsOn.stream().map(Dependency::new).collect(Collectors.toList());
   }
 
-  void setMethod(String method) {
-    this.method = method;
-  }
-
   void setAutoProvides(List<String> autoProvides) {
     this.autoProvides = autoProvides;
   }
diff --git a/inject-generator/src/main/java/io/avaje/inject/generator/MethodReader.java b/inject-generator/src/main/java/io/avaje/inject/generator/MethodReader.java
index 493a911d..92f6a29f 100644
--- a/inject-generator/src/main/java/io/avaje/inject/generator/MethodReader.java
+++ b/inject-generator/src/main/java/io/avaje/inject/generator/MethodReader.java
@@ -84,10 +84,10 @@ final class MethodReader {
     this.factoryType = beanType.getQualifiedName().toString();
     this.factoryShortName = Util.shortName(factoryType);
     this.isVoid = Util.isVoid(mainType);
-    String initMethod = (bean == null) ? null : bean.initMethod();
-    String destroyMethod = (bean == null) ? null : bean.destroyMethod();
-    this.destroyPriority = (bean == null) ? null : bean.destroyPriority();
-    this.beanCloseable = (bean != null) && bean.autoCloseable();
+    String initMethod = bean == null ? null : bean.initMethod();
+    String destroyMethod = bean == null ? null : bean.destroyMethod();
+    this.destroyPriority = bean == null ? null : bean.destroyPriority();
+    this.beanCloseable = bean != null && bean.autoCloseable();
     // for multiRegister we desire a qualifier name such that builder.isBeanAbsent() uses it and allows
     // other beans of the same type to also register, otherwise it defaults to slightly confusing behaviour
     this.name = multiRegister && qualifierName == null ? "multi" : qualifierName;
@@ -151,8 +151,7 @@ final class MethodReader {
   }
 
   MetaData createMeta() {
-    MetaData metaData = new MetaData(returnTypeRaw, name);
-    metaData.setMethod(fullBuildMethod());
+    MetaData metaData = new MetaData(returnTypeRaw, name, fullBuildMethod());
 
     List<String> dependsOn = new ArrayList<>(params.size() + 1);
     dependsOn.add(factoryType);
@@ -374,6 +373,10 @@ final class MethodReader {
     return typeReader == null ? Collections.emptySet() : typeReader.genericTypes();
   }
 
+  boolean hasConditions() {
+    return !conditions.isEmpty();
+  }
+
   void buildConditional(Append writer) {
     new ConditionalWriter(writer, conditions).write();
   }
diff --git a/inject-generator/src/main/java/io/avaje/inject/generator/TypeExtendsInjection.java b/inject-generator/src/main/java/io/avaje/inject/generator/TypeExtendsInjection.java
index 621cb5f7..e03029f9 100644
--- a/inject-generator/src/main/java/io/avaje/inject/generator/TypeExtendsInjection.java
+++ b/inject-generator/src/main/java/io/avaje/inject/generator/TypeExtendsInjection.java
@@ -262,7 +262,7 @@ final class TypeExtendsInjection {
   void validateFactoryMethodDuplicates() {
     var map = new HashMap<String, MethodReader>();
     for (MethodReader method : factoryMethods) {
-      if (!method.isVoid()) {
+      if (!method.isVoid() && !method.hasConditions()) {
         var clashMethod = map.put(method.qualifiedKey(), method);
         if (clashMethod != null) {
           var msg = String.format("@Bean method %s() returns the same type as with method %s() without a unique name qualifier." +


Test patch:
diff --git a/blackbox-test-inject/src/main/java/org/example/myapp/conditional/BirdFactory.java b/blackbox-test-inject/src/main/java/org/example/myapp/conditional/BirdFactory.java
index 8ffef2bb..9f509b99 100644
--- a/blackbox-test-inject/src/main/java/org/example/myapp/conditional/BirdFactory.java
+++ b/blackbox-test-inject/src/main/java/org/example/myapp/conditional/BirdFactory.java
@@ -17,19 +17,19 @@ public class BirdFactory {
   @Bean
   @NoKiwi
   @RequiresProperty(missing = "finch-time")
-  public BlueJay jay() {
+  public Bird jay() {
     return new BlueJay();
   }
 
   @Bean
   @Secondary
-  public Cassowary dinosaur() {
+  public Bird dinosaur() {
     return new Cassowary();
   }
 
   @Bean
   @Finches
-  public StrawberryFinch finch() {
+  public Bird finch() {
     return new StrawberryFinch();
   }
 }
diff --git a/blackbox-test-inject/src/test/java/org/example/myapp/conditional/ConditionalTests.java b/blackbox-test-inject/src/test/java/org/example/myapp/conditional/ConditionalTests.java
index 4c0c53aa..a9ef3da7 100644
--- a/blackbox-test-inject/src/test/java/org/example/myapp/conditional/ConditionalTests.java
+++ b/blackbox-test-inject/src/test/java/org/example/myapp/conditional/ConditionalTests.java
@@ -1,16 +1,11 @@
 package org.example.myapp.conditional;
 
+import static org.assertj.core.api.Assertions.assertThat;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
-import java.io.IOException;
-
-import org.example.myapp.conditional.Bird;
 import org.example.myapp.conditional.Bird.BlueJay;
 import org.example.myapp.conditional.Bird.StrawberryFinch;
-import org.example.myapp.conditional.BirdFactory;
-import org.example.myapp.conditional.BirdWatcher;
-import org.example.myapp.conditional.QualifiedBirdWatcher;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 
@@ -44,7 +39,7 @@ class ConditionalTests {
     final BeanScope beanScope = BeanScope.builder().build();
 
     assertTrue(beanScope.getOptional(BirdFactory.class).isPresent());
-    assertTrue(beanScope.getOptional(BlueJay.class).isPresent());
+    assertThat(beanScope.get(Bird.class)).isInstanceOf(BlueJay.class);
     assertTrue(beanScope.getOptional(BirdWatcher.class).isEmpty());
   }
 
diff --git a/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/conditions/MyService.java b/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/conditions/MyService.java
new file mode 100644
index 00000000..3c60f09c
--- /dev/null
+++ b/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/conditions/MyService.java
@@ -0,0 +1,3 @@
+package io.avaje.inject.generator.models.valid.conditions;
+
+public interface MyService {}
diff --git a/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/conditions/MyServiceFactory.java b/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/conditions/MyServiceFactory.java
new file mode 100644
index 00000000..c5127d44
--- /dev/null
+++ b/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/conditions/MyServiceFactory.java
@@ -0,0 +1,21 @@
+package io.avaje.inject.generator.models.valid.conditions;
+
+import io.avaje.inject.Bean;
+import io.avaje.inject.Factory;
+import io.avaje.inject.RequiresProperty;
+
+@Factory
+class MyServiceFactory {
+
+  @Bean
+  @RequiresProperty(value = "service.type", equalTo = "one")
+  MyService myServiceOne() {
+        return new MyServiceOne();
+    }
+
+  @Bean
+  @RequiresProperty(value = "service.type", equalTo = "two")
+  MyService myServiceTwo() {
+        return new MyServiceTwo();
+    }
+}
\ No newline at end of file
diff --git a/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/conditions/MyServiceOne.java b/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/conditions/MyServiceOne.java
new file mode 100644
index 00000000..304cafd6
--- /dev/null
+++ b/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/conditions/MyServiceOne.java
@@ -0,0 +1,3 @@
+package io.avaje.inject.generator.models.valid.conditions;
+
+public class MyServiceOne implements MyService {}
diff --git a/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/conditions/MyServiceTwo.java b/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/conditions/MyServiceTwo.java
new file mode 100644
index 00000000..d7d44b27
--- /dev/null
+++ b/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/conditions/MyServiceTwo.java
@@ -0,0 +1,3 @@
+package io.avaje.inject.generator.models.valid.conditions;
+
+public class MyServiceTwo implements MyService {}