Skip to content

Fix Field Assisted Inject #492

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Feb 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 12 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,19 +101,19 @@ module org.example {

## Spring DI comparison

| Spring | Avaje
| Avaje | Spring
| :--- | :--- |
| @Component, @Service, @Repository | [@Singleton](https://avaje.io/inject/#singleton) |
| FactoryBean<T> | [Provider<T>](https://avaje.io/inject/#provider) |
| @Inject, @Autowired | [@Inject](https://avaje.io/inject/#inject) |
| @Autowired(required=false) |[@Inject @Nullable](https://avaje.io/inject/#nullable) or [@Inject Optional<T>](https://avaje.io/inject/#optional) |
| @PostConstruct| [@PostConstruct](https://avaje.io/inject/#post-construct) |
| @PreDestroy| [@PreDestroy](https://avaje.io/inject/#pre-destroy) |
| @Configuration and @Bean | [@Factory and @Bean](https://avaje.io/inject/#factory) |
| @Conditional | [@RequiresBean and @RequiresProperty](https://avaje.io/inject/#conditional) |
| @Primary | [@Primary](https://avaje.io/inject/#primary) |
| @Secondary | [@Secondary](https://avaje.io/inject/#secondary) |
| @Profile | [@Profile](https://avaje.io/inject/#profile) |
| [@Singleton](https://avaje.io/inject/#singleton) | @Component, @Service, @Repository |
| [Provider<T>](https://avaje.io/inject/#provider) | FactoryBean<T> |
| [@Inject](https://avaje.io/inject/#inject) | @Inject, @Autowired |
| [@Inject @Nullable](https://avaje.io/inject/#nullable) or [@Inject Optional<T>](https://avaje.io/inject/#optional) | @Autowired(required=false) |
| [@PostConstruct](https://avaje.io/inject/#post-construct) | @PostConstruct|
| [@PreDestroy](https://avaje.io/inject/#pre-destroy) | @PreDestroy |
| [@Factory and @Bean](https://avaje.io/inject/#factory) | @Configuration and @Bean |
| [@RequiresBean and @RequiresProperty](https://avaje.io/inject/#conditional) | @Conditional |
| [@Primary](https://avaje.io/inject/#primary) | @Primary |
| [@Secondary](https://avaje.io/inject/#secondary) | @Secondary |
| [@AssistFactory](https://avaje.io/inject/#assistInject) | - |

## Generated Code

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package org.example.myapp.assist.droid;

import java.util.List;

import io.avaje.inject.AssistFactory;
import io.avaje.inject.Assisted;
import io.avaje.lang.Nullable;
import jakarta.inject.Inject;
import jakarta.inject.Named;

@Named("tomato")
@AssistFactory(ACarFactory.class)
public class ACar {

final Paint paint;
final Engine engine;
@Assisted List<String> type;
@Inject Wheel wheel;
Radio radio;

public ACar(@Assisted Paint paint, @Nullable Engine engine) {
this.paint = paint;
this.engine = engine;
}

@Inject
void injectMethod(@Assisted int size, Radio radio) {
this.radio = radio;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package org.example.myapp.assist.droid;

import java.util.List;

public abstract class ACarFactory {

public abstract ACar construct(Paint paint, int size, List<String> type);

public void nonFactory() {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package org.example.myapp.assist.droid;

import jakarta.inject.Singleton;

import java.util.List;

@Singleton
class ACarThing {

final ACarFactory factory;

ACarThing(ACarFactory factory) {
this.factory = factory;
}

ACar doIt(Paint paint, int size, List<String> other) {
return factory.construct(paint, size, other);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ public interface DroidFactory {

Droid createDroid(int personality, Model model);

default void defaultMethod() {}

interface Droid {

int personality();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package org.example.myapp.assist.droid;

import jakarta.inject.Singleton;

@Singleton
public class Wheel {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package org.example.myapp.assist.droid;

import io.avaje.inject.BeanScope;
import org.junit.jupiter.api.Test;

import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;

class ACarTest {

@Test
void test() {
try (BeanScope testScope = BeanScope.builder().build()) {
ACarThing carThing = testScope.get(ACarThing.class);
Paint p = new Paint() {};
List<String> stringList = List.of("a");
ACar aCar = carThing.doIt(p, 42, stringList);

assertThat(aCar.paint).isSameAs(p);
assertThat(aCar.type).isSameAs(stringList);
assertThat(aCar.engine).isNotNull();
assertThat(aCar.wheel).isNotNull();
assertThat(aCar.radio).isNotNull();
}
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
package io.avaje.inject.generator;

import io.avaje.inject.generator.MethodReader.MethodParam;

import javax.lang.model.element.*;
import javax.lang.model.util.ElementFilter;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.Set;

import javax.lang.model.element.*;

import io.avaje.inject.generator.MethodReader.MethodParam;
import static java.util.function.Predicate.not;
import static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.toSet;

final class AssistBeanReader {

Expand All @@ -23,7 +28,7 @@ final class AssistBeanReader {
private final TypeReader typeReader;
private final TypeElement targetType;
private final String qualifierName;
private ExecutableElement factoryMethod;
private final ExecutableElement factoryMethod;

AssistBeanReader(TypeElement beanType) {
this.beanType = beanType;
Expand All @@ -38,14 +43,7 @@ final class AssistBeanReader {
this.constructor = typeReader.constructor();

AssistFactoryPrism instanceOn = AssistFactoryPrism.getInstanceOn(beanType);
targetType = APContext.asTypeElement(instanceOn.value());
validateTarget(targetType);

for (Element enclosedElement : targetType.getEnclosedElements()) {
if (enclosedElement.getKind() == ElementKind.METHOD) {
factoryMethod = (ExecutableElement) enclosedElement;
}
}
var factoryType = APContext.asTypeElement(instanceOn.value());

constructor.params().stream()
.filter(MethodParam::assisted)
Expand All @@ -61,12 +59,65 @@ final class AssistBeanReader {
.filter(MethodParam::assisted)
.map(MethodParam::element)
.forEach(assistedElements::add);

factoryMethod =
ElementFilter.methodsIn(factoryType.getEnclosedElements()).stream()
.filter(e -> e.getModifiers().contains(Modifier.ABSTRACT))
.findFirst()
.orElse(null);

validateTarget(factoryType);
this.targetType = factoryType;
}

private void validateTarget(TypeElement t) {
if (t.getKind() != ElementKind.INTERFACE || !t.getModifiers().contains(Modifier.ABSTRACT)) {
APContext.logError(type, "@AssistFactory targets must be abstract");
var methods = ElementFilter.methodsIn(t.getEnclosedElements());
if (!APContext.elements().isFunctionalInterface(t)) {
if (!t.getModifiers().contains(Modifier.ABSTRACT)) {
APContext.logError(type, "@AssistFactory targets must be abstract");
} else if (checkAbstractMethodCount(methods)) {
APContext.logError(type, "@AssistFactory targets must have only one abstract method");
}
}
var sb = new StringBuilder(String.format("@AssistFactory targets for type %s must have an abstract method with form '%s <methodName>(", shortName(), shortName()));
var assistNames = new ArrayList<String>();
for (var iterator = assistedElements.iterator(); iterator.hasNext(); ) {
var element = iterator.next();
var typeName = UType.parse(element.asType());
sb.append(String.format("%s %s", typeName.shortWithoutAnnotations(), element.getSimpleName()));
if (iterator.hasNext()) {
sb.append(", ");
}
assistNames.add(String.format("%s %s", typeName.shortWithoutAnnotations(), element.getSimpleName()));
}
var errorMsg = sb.append(")' method.").toString();

Optional.ofNullable(factoryMethod).stream()
.map(ExecutableElement::getParameters)
.findAny()
.ifPresentOrElse(params -> {
var mismatched = params.size() != assistedElements.size();
if (mismatched) {
APContext.logError(t, errorMsg);
return;
}

var paramTypes = params.stream()
.map(v -> String.format("%s %s", UType.parse(v.asType()).shortWithoutAnnotations(), v.getSimpleName()))
.collect(toSet());

var missingParams = assistNames.stream().filter(not(paramTypes::contains)).collect(joining(", "));
if (!missingParams.isBlank()) {
APContext.logError(factoryMethod, "factory method missing required parameters: %s", missingParams);
}
},
() -> APContext.logError(t, errorMsg));
}

private static boolean checkAbstractMethodCount(List<ExecutableElement> methods) {
return methods.stream()
.filter(e -> e.getModifiers().contains(Modifier.ABSTRACT))
.count() != 1;
}

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

private Set<String> importTypes() {
importTypes.add("io.avaje.inject.AssistFactory");
importTypes.add(targetType.getQualifiedName().toString());
Optional.ofNullable(targetType).ifPresent(t -> importTypes.add(t.getQualifiedName().toString()));

if (Util.validImportType(type)) {
importTypes.add(type);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
package io.avaje.inject.generator;

import static io.avaje.inject.generator.APContext.createSourceFile;
import static java.util.function.Predicate.not;
import io.avaje.inject.generator.MethodReader.MethodParam;

import javax.lang.model.element.*;
import javax.tools.JavaFileObject;
import java.io.IOException;
import java.io.Writer;
import java.util.List;
import java.util.stream.Collectors;

import javax.lang.model.element.*;
import javax.tools.JavaFileObject;

import io.avaje.inject.generator.MethodReader.MethodParam;
import static io.avaje.inject.generator.APContext.createSourceFile;
import static java.util.function.Predicate.not;

/** Write the source code for the bean. */
/**
* Write the source code for the bean.
*/
final class SimpleAssistWriter {

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

SimpleAssistWriter(AssistBeanReader beanReader) {
SimpleAssistWriter(AssistBeanReader beanReader) {
this.beanReader = beanReader;
this.packageName = beanReader.packageName();
this.shortName = beanReader.shortName();
this.suffix = "$AssistFactory";
this.assistedElements = beanReader.assistElements();
this.originName = packageName + "." + shortName;
this.hasNoConstructorParams =
beanReader.constructor().params().stream()
.filter(not(MethodParam::assisted))
.findAny()
.isEmpty();
}

private Writer createFileWriter() throws IOException {
Expand Down Expand Up @@ -110,7 +117,9 @@ private void writeInjectFields() {
var type = UType.parse(element.asType());
writer.append(" %s %s$field;", type.shortType(), field.fieldName()).eol().eol();
}
writer.eol();
if (beanReader.injectMethods().isEmpty() && hasNoConstructorParams) {
writer.eol();
}
}

private void writeMethodFields() {
Expand All @@ -120,15 +129,13 @@ private void writeMethodFields() {
beanReader.injectMethods().stream()
.flatMap(m -> m.params().stream())
.filter(not(MethodParam::assisted))
.forEach(
p -> {
var element = p.element();
writer
.append(" private %s %s$method;", UType.parse(element.asType()).shortType(), p.simpleName())
.eol()
.eol();
});
writer.eol();
.forEach(p -> {
var element = p.element();
writer.append(" private %s %s$method;", UType.parse(element.asType()).shortType(), p.simpleName()).eol();
});
if (hasNoConstructorParams) {
writer.eol();
}
}

private void writeConstructor() {
Expand All @@ -146,7 +153,7 @@ private void writeConstructor() {
if (beanReader.beanType().getNestingKind().isNested()) {
shortName = shortName.replace(".", "$");
}
writer.append(" ").append(shortName).append(suffix).append("(");
writer.eol().append(" ").append(shortName).append(suffix).append("(");

for (var iterator = injectParams.iterator(); iterator.hasNext(); ) {
var p = iterator.next();
Expand All @@ -170,7 +177,7 @@ private void writeFieldsForInjected(List<MethodParam> injectParams) {
for (MethodParam p : injectParams) {
var element = p.element();
var type = UType.parse(element.asType()).shortType();
writer.append(" private final %s %s;", type, p.simpleName()).eol().eol();
writer.append(" private final %s %s;", type, p.simpleName()).eol();
}
}

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

for (var field : assistedElements) {
writer.indent(" ").append("bean.%s = %s;", field.getSimpleName(), field.getSimpleName()).eol();
}
assistedElements.stream()
.filter(e -> e.getKind() == ElementKind.FIELD)
.forEach(field ->
writer
.indent(" ")
.append("bean.%s = %s;", field.getSimpleName(), field.getSimpleName())
.eol());
}

private void injectMethods() {
Expand Down
Loading