Skip to content

Commit 22165f7

Browse files
SentryManrbygrave
andauthored
Fix Field Assisted Inject (#492)
* fix field assisted inject * Update AssistFactory.java * fix factory formatting * even better validation * Update AssistBeanReader.java * Update AssistBeanReader.java * Update README.md * test default methods * #492 Add test for Assisted injection fix Assert that the injection for the abstract factory ACarFactory works * Format and style only changes --------- Co-authored-by: Rob Bygrave <[email protected]>
1 parent 06915ff commit 22165f7

File tree

10 files changed

+226
-76
lines changed

10 files changed

+226
-76
lines changed

README.md

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -101,19 +101,19 @@ module org.example {
101101

102102
## Spring DI comparison
103103

104-
| Spring | Avaje
104+
| Avaje | Spring
105105
| :--- | :--- |
106-
| @Component, @Service, @Repository | [@Singleton](https://avaje.io/inject/#singleton) |
107-
| FactoryBean&lt;T> | [Provider&lt;T>](https://avaje.io/inject/#provider) |
108-
| @Inject, @Autowired | [@Inject](https://avaje.io/inject/#inject) |
109-
| @Autowired(required=false) |[@Inject @Nullable](https://avaje.io/inject/#nullable) or [@Inject Optional&lt;T>](https://avaje.io/inject/#optional) |
110-
| @PostConstruct| [@PostConstruct](https://avaje.io/inject/#post-construct) |
111-
| @PreDestroy| [@PreDestroy](https://avaje.io/inject/#pre-destroy) |
112-
| @Configuration and @Bean | [@Factory and @Bean](https://avaje.io/inject/#factory) |
113-
| @Conditional | [@RequiresBean and @RequiresProperty](https://avaje.io/inject/#conditional) |
114-
| @Primary | [@Primary](https://avaje.io/inject/#primary) |
115-
| @Secondary | [@Secondary](https://avaje.io/inject/#secondary) |
116-
| @Profile | [@Profile](https://avaje.io/inject/#profile) |
106+
| [@Singleton](https://avaje.io/inject/#singleton) | @Component, @Service, @Repository |
107+
| [Provider&lt;T>](https://avaje.io/inject/#provider) | FactoryBean&lt;T> |
108+
| [@Inject](https://avaje.io/inject/#inject) | @Inject, @Autowired |
109+
| [@Inject @Nullable](https://avaje.io/inject/#nullable) or [@Inject Optional&lt;T>](https://avaje.io/inject/#optional) | @Autowired(required=false) |
110+
| [@PostConstruct](https://avaje.io/inject/#post-construct) | @PostConstruct|
111+
| [@PreDestroy](https://avaje.io/inject/#pre-destroy) | @PreDestroy |
112+
| [@Factory and @Bean](https://avaje.io/inject/#factory) | @Configuration and @Bean |
113+
| [@RequiresBean and @RequiresProperty](https://avaje.io/inject/#conditional) | @Conditional |
114+
| [@Primary](https://avaje.io/inject/#primary) | @Primary |
115+
| [@Secondary](https://avaje.io/inject/#secondary) | @Secondary |
116+
| [@AssistFactory](https://avaje.io/inject/#assistInject) | - |
117117

118118
## Generated Code
119119

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package org.example.myapp.assist.droid;
2+
3+
import java.util.List;
4+
5+
import io.avaje.inject.AssistFactory;
6+
import io.avaje.inject.Assisted;
7+
import io.avaje.lang.Nullable;
8+
import jakarta.inject.Inject;
9+
import jakarta.inject.Named;
10+
11+
@Named("tomato")
12+
@AssistFactory(ACarFactory.class)
13+
public class ACar {
14+
15+
final Paint paint;
16+
final Engine engine;
17+
@Assisted List<String> type;
18+
@Inject Wheel wheel;
19+
Radio radio;
20+
21+
public ACar(@Assisted Paint paint, @Nullable Engine engine) {
22+
this.paint = paint;
23+
this.engine = engine;
24+
}
25+
26+
@Inject
27+
void injectMethod(@Assisted int size, Radio radio) {
28+
this.radio = radio;
29+
}
30+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package org.example.myapp.assist.droid;
2+
3+
import java.util.List;
4+
5+
public abstract class ACarFactory {
6+
7+
public abstract ACar construct(Paint paint, int size, List<String> type);
8+
9+
public void nonFactory() {}
10+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package org.example.myapp.assist.droid;
2+
3+
import jakarta.inject.Singleton;
4+
5+
import java.util.List;
6+
7+
@Singleton
8+
class ACarThing {
9+
10+
final ACarFactory factory;
11+
12+
ACarThing(ACarFactory factory) {
13+
this.factory = factory;
14+
}
15+
16+
ACar doIt(Paint paint, int size, List<String> other) {
17+
return factory.construct(paint, size, other);
18+
}
19+
}

blackbox-test-inject/src/main/java/org/example/myapp/assist/droid/DroidFactory.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ public interface DroidFactory {
44

55
Droid createDroid(int personality, Model model);
66

7+
default void defaultMethod() {}
8+
79
interface Droid {
810

911
int personality();
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package org.example.myapp.assist.droid;
2+
3+
import jakarta.inject.Singleton;
4+
5+
@Singleton
6+
public class Wheel {}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package org.example.myapp.assist.droid;
2+
3+
import io.avaje.inject.BeanScope;
4+
import org.junit.jupiter.api.Test;
5+
6+
import java.util.List;
7+
8+
import static org.assertj.core.api.Assertions.assertThat;
9+
10+
class ACarTest {
11+
12+
@Test
13+
void test() {
14+
try (BeanScope testScope = BeanScope.builder().build()) {
15+
ACarThing carThing = testScope.get(ACarThing.class);
16+
Paint p = new Paint() {};
17+
List<String> stringList = List.of("a");
18+
ACar aCar = carThing.doIt(p, 42, stringList);
19+
20+
assertThat(aCar.paint).isSameAs(p);
21+
assertThat(aCar.type).isSameAs(stringList);
22+
assertThat(aCar.engine).isNotNull();
23+
assertThat(aCar.wheel).isNotNull();
24+
assertThat(aCar.radio).isNotNull();
25+
}
26+
}
27+
}

inject-generator/src/main/java/io/avaje/inject/generator/AssistBeanReader.java

Lines changed: 67 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
11
package io.avaje.inject.generator;
22

3+
import io.avaje.inject.generator.MethodReader.MethodParam;
4+
5+
import javax.lang.model.element.*;
6+
import javax.lang.model.util.ElementFilter;
37
import java.util.ArrayList;
48
import java.util.List;
9+
import java.util.Optional;
510
import java.util.Set;
611

7-
import javax.lang.model.element.*;
8-
9-
import io.avaje.inject.generator.MethodReader.MethodParam;
12+
import static java.util.function.Predicate.not;
13+
import static java.util.stream.Collectors.joining;
14+
import static java.util.stream.Collectors.toSet;
1015

1116
final class AssistBeanReader {
1217

@@ -23,7 +28,7 @@ final class AssistBeanReader {
2328
private final TypeReader typeReader;
2429
private final TypeElement targetType;
2530
private final String qualifierName;
26-
private ExecutableElement factoryMethod;
31+
private final ExecutableElement factoryMethod;
2732

2833
AssistBeanReader(TypeElement beanType) {
2934
this.beanType = beanType;
@@ -38,14 +43,7 @@ final class AssistBeanReader {
3843
this.constructor = typeReader.constructor();
3944

4045
AssistFactoryPrism instanceOn = AssistFactoryPrism.getInstanceOn(beanType);
41-
targetType = APContext.asTypeElement(instanceOn.value());
42-
validateTarget(targetType);
43-
44-
for (Element enclosedElement : targetType.getEnclosedElements()) {
45-
if (enclosedElement.getKind() == ElementKind.METHOD) {
46-
factoryMethod = (ExecutableElement) enclosedElement;
47-
}
48-
}
46+
var factoryType = APContext.asTypeElement(instanceOn.value());
4947

5048
constructor.params().stream()
5149
.filter(MethodParam::assisted)
@@ -61,12 +59,65 @@ final class AssistBeanReader {
6159
.filter(MethodParam::assisted)
6260
.map(MethodParam::element)
6361
.forEach(assistedElements::add);
62+
63+
factoryMethod =
64+
ElementFilter.methodsIn(factoryType.getEnclosedElements()).stream()
65+
.filter(e -> e.getModifiers().contains(Modifier.ABSTRACT))
66+
.findFirst()
67+
.orElse(null);
68+
69+
validateTarget(factoryType);
70+
this.targetType = factoryType;
6471
}
6572

6673
private void validateTarget(TypeElement t) {
67-
if (t.getKind() != ElementKind.INTERFACE || !t.getModifiers().contains(Modifier.ABSTRACT)) {
68-
APContext.logError(type, "@AssistFactory targets must be abstract");
74+
var methods = ElementFilter.methodsIn(t.getEnclosedElements());
75+
if (!APContext.elements().isFunctionalInterface(t)) {
76+
if (!t.getModifiers().contains(Modifier.ABSTRACT)) {
77+
APContext.logError(type, "@AssistFactory targets must be abstract");
78+
} else if (checkAbstractMethodCount(methods)) {
79+
APContext.logError(type, "@AssistFactory targets must have only one abstract method");
80+
}
6981
}
82+
var sb = new StringBuilder(String.format("@AssistFactory targets for type %s must have an abstract method with form '%s <methodName>(", shortName(), shortName()));
83+
var assistNames = new ArrayList<String>();
84+
for (var iterator = assistedElements.iterator(); iterator.hasNext(); ) {
85+
var element = iterator.next();
86+
var typeName = UType.parse(element.asType());
87+
sb.append(String.format("%s %s", typeName.shortWithoutAnnotations(), element.getSimpleName()));
88+
if (iterator.hasNext()) {
89+
sb.append(", ");
90+
}
91+
assistNames.add(String.format("%s %s", typeName.shortWithoutAnnotations(), element.getSimpleName()));
92+
}
93+
var errorMsg = sb.append(")' method.").toString();
94+
95+
Optional.ofNullable(factoryMethod).stream()
96+
.map(ExecutableElement::getParameters)
97+
.findAny()
98+
.ifPresentOrElse(params -> {
99+
var mismatched = params.size() != assistedElements.size();
100+
if (mismatched) {
101+
APContext.logError(t, errorMsg);
102+
return;
103+
}
104+
105+
var paramTypes = params.stream()
106+
.map(v -> String.format("%s %s", UType.parse(v.asType()).shortWithoutAnnotations(), v.getSimpleName()))
107+
.collect(toSet());
108+
109+
var missingParams = assistNames.stream().filter(not(paramTypes::contains)).collect(joining(", "));
110+
if (!missingParams.isBlank()) {
111+
APContext.logError(factoryMethod, "factory method missing required parameters: %s", missingParams);
112+
}
113+
},
114+
() -> APContext.logError(t, errorMsg));
115+
}
116+
117+
private static boolean checkAbstractMethodCount(List<ExecutableElement> methods) {
118+
return methods.stream()
119+
.filter(e -> e.getModifiers().contains(Modifier.ABSTRACT))
120+
.count() != 1;
70121
}
71122

72123
@Override
@@ -90,7 +141,8 @@ void buildRegister(Append writer) {
90141

91142
private Set<String> importTypes() {
92143
importTypes.add("io.avaje.inject.AssistFactory");
93-
importTypes.add(targetType.getQualifiedName().toString());
144+
Optional.ofNullable(targetType).ifPresent(t -> importTypes.add(t.getQualifiedName().toString()));
145+
94146
if (Util.validImportType(type)) {
95147
importTypes.add(type);
96148
}

inject-generator/src/main/java/io/avaje/inject/generator/SimpleAssistWriter.java

Lines changed: 34 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,20 @@
11
package io.avaje.inject.generator;
22

3-
import static io.avaje.inject.generator.APContext.createSourceFile;
4-
import static java.util.function.Predicate.not;
3+
import io.avaje.inject.generator.MethodReader.MethodParam;
54

5+
import javax.lang.model.element.*;
6+
import javax.tools.JavaFileObject;
67
import java.io.IOException;
78
import java.io.Writer;
89
import java.util.List;
910
import java.util.stream.Collectors;
1011

11-
import javax.lang.model.element.*;
12-
import javax.tools.JavaFileObject;
13-
14-
import io.avaje.inject.generator.MethodReader.MethodParam;
12+
import static io.avaje.inject.generator.APContext.createSourceFile;
13+
import static java.util.function.Predicate.not;
1514

16-
/** Write the source code for the bean. */
15+
/**
16+
* Write the source code for the bean.
17+
*/
1718
final class SimpleAssistWriter {
1819

1920
private static final String CODE_COMMENT = "/**\n * Generated source - Factory for %s.\n */";
@@ -25,14 +26,20 @@ final class SimpleAssistWriter {
2526
private final String suffix;
2627
private Append writer;
2728
private final List<Element> assistedElements;
29+
private final boolean hasNoConstructorParams;
2830

29-
SimpleAssistWriter(AssistBeanReader beanReader) {
31+
SimpleAssistWriter(AssistBeanReader beanReader) {
3032
this.beanReader = beanReader;
3133
this.packageName = beanReader.packageName();
3234
this.shortName = beanReader.shortName();
3335
this.suffix = "$AssistFactory";
3436
this.assistedElements = beanReader.assistElements();
3537
this.originName = packageName + "." + shortName;
38+
this.hasNoConstructorParams =
39+
beanReader.constructor().params().stream()
40+
.filter(not(MethodParam::assisted))
41+
.findAny()
42+
.isEmpty();
3643
}
3744

3845
private Writer createFileWriter() throws IOException {
@@ -110,7 +117,9 @@ private void writeInjectFields() {
110117
var type = UType.parse(element.asType());
111118
writer.append(" %s %s$field;", type.shortType(), field.fieldName()).eol().eol();
112119
}
113-
writer.eol();
120+
if (beanReader.injectMethods().isEmpty() && hasNoConstructorParams) {
121+
writer.eol();
122+
}
114123
}
115124

116125
private void writeMethodFields() {
@@ -120,15 +129,13 @@ private void writeMethodFields() {
120129
beanReader.injectMethods().stream()
121130
.flatMap(m -> m.params().stream())
122131
.filter(not(MethodParam::assisted))
123-
.forEach(
124-
p -> {
125-
var element = p.element();
126-
writer
127-
.append(" private %s %s$method;", UType.parse(element.asType()).shortType(), p.simpleName())
128-
.eol()
129-
.eol();
130-
});
131-
writer.eol();
132+
.forEach(p -> {
133+
var element = p.element();
134+
writer.append(" private %s %s$method;", UType.parse(element.asType()).shortType(), p.simpleName()).eol();
135+
});
136+
if (hasNoConstructorParams) {
137+
writer.eol();
138+
}
132139
}
133140

134141
private void writeConstructor() {
@@ -146,7 +153,7 @@ private void writeConstructor() {
146153
if (beanReader.beanType().getNestingKind().isNested()) {
147154
shortName = shortName.replace(".", "$");
148155
}
149-
writer.append(" ").append(shortName).append(suffix).append("(");
156+
writer.eol().append(" ").append(shortName).append(suffix).append("(");
150157

151158
for (var iterator = injectParams.iterator(); iterator.hasNext(); ) {
152159
var p = iterator.next();
@@ -170,7 +177,7 @@ private void writeFieldsForInjected(List<MethodParam> injectParams) {
170177
for (MethodParam p : injectParams) {
171178
var element = p.element();
172179
var type = UType.parse(element.asType()).shortType();
173-
writer.append(" private final %s %s;", type, p.simpleName()).eol().eol();
180+
writer.append(" private final %s %s;", type, p.simpleName()).eol();
174181
}
175182
}
176183

@@ -229,9 +236,13 @@ private void injectFields() {
229236
writer.indent(" ").append("bean.%s = %s;", fieldName, getDependency).eol();
230237
}
231238

232-
for (var field : assistedElements) {
233-
writer.indent(" ").append("bean.%s = %s;", field.getSimpleName(), field.getSimpleName()).eol();
234-
}
239+
assistedElements.stream()
240+
.filter(e -> e.getKind() == ElementKind.FIELD)
241+
.forEach(field ->
242+
writer
243+
.indent(" ")
244+
.append("bean.%s = %s;", field.getSimpleName(), field.getSimpleName())
245+
.eol());
235246
}
236247

237248
private void injectMethods() {

0 commit comments

Comments
 (0)