Skip to content

Commit 0e4bf00

Browse files
committed
add builder annotation
1 parent d888cea commit 0e4bf00

File tree

7 files changed

+162
-29
lines changed

7 files changed

+162
-29
lines changed
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
package io.jbock.simple.processor.binding;
2+
3+
import io.jbock.javapoet.ClassName;
4+
import io.jbock.simple.processor.util.ValidationFailure;
5+
6+
import javax.lang.model.element.Element;
7+
import javax.lang.model.element.ExecutableElement;
8+
import javax.lang.model.element.TypeElement;
9+
import javax.lang.model.util.ElementFilter;
10+
import java.util.List;
11+
import java.util.Objects;
12+
import java.util.function.Supplier;
13+
import java.util.stream.Collectors;
14+
15+
import static io.jbock.simple.processor.util.Suppliers.memoize;
16+
import static javax.lang.model.element.Modifier.STATIC;
17+
18+
public class BuilderElement {
19+
20+
private final TypeElement element;
21+
private final ClassName parentClass;
22+
private final KeyFactory keyFactory;
23+
24+
private final Supplier<ExecutableElement> buildMethod = memoize(() -> {
25+
Element componentElement = element().getEnclosingElement();
26+
List<ExecutableElement> methods = ElementFilter.methodsIn(element().getEnclosedElements()).stream()
27+
.filter(m -> !m.getModifiers().contains(STATIC))
28+
.filter(m -> qualifiers().tool().isSameType(m.getReturnType(), componentElement.asType()))
29+
.collect(Collectors.toList());
30+
if (methods.isEmpty()) {
31+
throw new ValidationFailure("Build method not found", element());
32+
}
33+
if (methods.size() != 1) {
34+
throw new ValidationFailure("Only one build method allowed", element());
35+
}
36+
ExecutableElement result = methods.get(0);
37+
if (!result.getParameters().isEmpty()) {
38+
throw new ValidationFailure("The build method may not have any parameters", result);
39+
}
40+
return result;
41+
});
42+
43+
private final Supplier<List<ExecutableElement>> setterMethods = memoize(() -> {
44+
ExecutableElement buildMethod = buildMethod();
45+
List<ExecutableElement> result = ElementFilter.methodsIn(element().getEnclosedElements()).stream()
46+
.filter(m -> !m.getModifiers().contains(STATIC))
47+
.filter(m -> !Objects.equals(m, buildMethod))
48+
.collect(Collectors.toList());
49+
for (ExecutableElement m : result) {
50+
if (!qualifiers().tool().isSameType(m.getReturnType(), element().asType())) {
51+
throw new ValidationFailure("The setter method must return the builder type", m);
52+
}
53+
if (m.getParameters().size() != 1) {
54+
throw new ValidationFailure("The setter method must have exactly one parameter", m);
55+
}
56+
}
57+
return result;
58+
});
59+
60+
BuilderElement(
61+
TypeElement element,
62+
ClassName parentClass,
63+
KeyFactory keyFactory) {
64+
this.element = element;
65+
this.parentClass = parentClass;
66+
this.keyFactory = keyFactory;
67+
}
68+
69+
public TypeElement element() {
70+
return element;
71+
}
72+
73+
public ClassName generatedClass() {
74+
return parentClass.nestedClass(element().getSimpleName() + "_Impl");
75+
}
76+
77+
public ExecutableElement buildMethod() {
78+
return buildMethod.get();
79+
}
80+
81+
List<ParameterBinding> parameterBindings() {
82+
return setterMethods().stream()
83+
.map(p -> ParameterBinding.create(p, qualifiers()))
84+
.collect(Collectors.toList());
85+
}
86+
87+
List<ExecutableElement> setterMethods() {
88+
return setterMethods.get();
89+
}
90+
91+
private KeyFactory qualifiers() {
92+
return keyFactory;
93+
}
94+
}

compiler/src/main/java/io/jbock/simple/processor/binding/ComponentElement.java

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
import javax.lang.model.element.ExecutableElement;
1111
import javax.lang.model.element.Modifier;
1212
import javax.lang.model.element.TypeElement;
13-
import javax.lang.model.element.VariableElement;
1413
import javax.lang.model.type.TypeKind;
1514
import java.util.Collection;
1615
import java.util.LinkedHashMap;
@@ -38,17 +37,33 @@ public final class ComponentElement {
3837
for (Element el : element().getEnclosedElements()) {
3938
boolean hasFactoryAnnotation = el.getAnnotation(Component.Factory.class) != null;
4039
if (!hasFactoryAnnotation) {
41-
return Optional.empty();
40+
continue;
4241
}
4342
TypeElement tel = Visitors.TYPE_ELEMENT_VISITOR.visit(el);
4443
if (tel == null) {
45-
return Optional.empty();
44+
continue;
4645
}
4746
return Optional.of(new FactoryElement(tel, generatedClass(), keyFactory()));
4847
}
4948
return Optional.empty();
5049
});
5150

51+
52+
private final Supplier<Optional<BuilderElement>> builderElement = memoize(() -> {
53+
for (Element el : element().getEnclosedElements()) {
54+
boolean hasBuilderAnnotation = el.getAnnotation(Component.Builder.class) != null;
55+
if (!hasBuilderAnnotation) {
56+
continue;
57+
}
58+
TypeElement tel = Visitors.TYPE_ELEMENT_VISITOR.visit(el);
59+
if (tel == null) {
60+
continue;
61+
}
62+
return Optional.of(new BuilderElement(tel, generatedClass(), keyFactory()));
63+
}
64+
return Optional.empty();
65+
});
66+
5267
private final Supplier<Map<Key, InjectBinding>> providesBindings = memoize(() -> {
5368
List<ExecutableElement> methods = methodsIn(element().getEnclosedElements());
5469
Map<Key, InjectBinding> result = new LinkedHashMap<>();
@@ -91,14 +106,15 @@ public final class ComponentElement {
91106
private final Supplier<Map<Key, ParameterBinding>> parameterBindings = memoize(() -> {
92107
List<ParameterBinding> pBindings = factoryElement()
93108
.map(FactoryElement::parameterBindings)
109+
.or(() -> builderElement().map(BuilderElement::parameterBindings))
94110
.orElse(List.of());
95111
Map<Key, ParameterBinding> result = new LinkedHashMap<>();
96112
for (ParameterBinding b : pBindings) {
97113
ParameterBinding previousBinding = result.put(b.key(), b);
98114
if (previousBinding != null) {
99-
VariableElement p = previousBinding.parameter();
115+
Element p = previousBinding.element();
100116
throw new ValidationFailure("The binding is in conflict with another parameter: " +
101-
p.asType() + ' ' + p.getSimpleName(), b.parameter());
117+
p.asType() + ' ' + p.getSimpleName(), b.element());
102118
}
103119
}
104120
return result;
@@ -125,6 +141,10 @@ public Optional<FactoryElement> factoryElement() {
125141
return factoryElement.get();
126142
}
127143

144+
public Optional<BuilderElement> builderElement() {
145+
return builderElement.get();
146+
}
147+
128148
public boolean isComponentRequest(Binding binding) {
129149
return requests.get().containsKey(binding.key());
130150
}

compiler/src/main/java/io/jbock/simple/processor/binding/KeyFactory.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,10 @@ private boolean hasQualifierAnnotation(AnnotationMirror mirror) {
7878
return tool.hasQualifierAnnotation(element);
7979
}
8080

81+
TypeTool tool() {
82+
return tool;
83+
}
84+
8185
@Override
8286
public void clearCache() {
8387
keyCache.clear();

compiler/src/main/java/io/jbock/simple/processor/binding/ParameterBinding.java

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,28 +4,30 @@
44
import io.jbock.javapoet.ParameterSpec;
55

66
import javax.lang.model.element.Element;
7+
import javax.lang.model.element.ExecutableElement;
78
import javax.lang.model.element.VariableElement;
89
import java.util.List;
910
import java.util.function.Function;
1011

1112
public final class ParameterBinding extends Binding {
1213

13-
private final VariableElement parameter;
14+
private final Element element; // VariableElement or ExecutableElement (setter)
1415

1516
private ParameterBinding(
1617
Key key,
17-
VariableElement parameter) {
18+
Element element) {
1819
super(key);
19-
this.parameter = parameter;
20+
this.element = element;
2021
}
2122

2223
public static ParameterBinding create(VariableElement parameter, KeyFactory keyFactory) {
2324
Key key = keyFactory.getKey(parameter);
2425
return new ParameterBinding(key, parameter);
2526
}
2627

27-
public VariableElement parameter() {
28-
return parameter;
28+
public static ParameterBinding create(ExecutableElement setter, KeyFactory keyFactory) {
29+
Key key = keyFactory.getKey(setter.getParameters().get(0));
30+
return new ParameterBinding(key, setter);
2931
}
3032

3133
@Override
@@ -40,7 +42,7 @@ public String toString() {
4042

4143
@Override
4244
public Element element() {
43-
return parameter;
45+
return element;
4446
}
4547

4648
@Override
@@ -50,6 +52,6 @@ public List<DependencyRequest> requests() {
5052

5153
@Override
5254
public String suggestedVariableName() {
53-
return parameter.getSimpleName().toString();
55+
return element.getSimpleName().toString();
5456
}
5557
}

compiler/src/main/java/io/jbock/simple/processor/step/ComponentFactoryStep.java

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,9 @@ public ComponentFactoryStep(
3636

3737
@Override
3838
public Set<String> annotations() {
39-
return Set.of(Component.Factory.class.getCanonicalName());
39+
return Set.of(
40+
Component.Factory.class.getCanonicalName(),
41+
Component.Builder.class.getCanonicalName());
4042
}
4143

4244
@Override
@@ -52,11 +54,12 @@ public Set<? extends Element> process(Map<String, Set<Element>> elementsByAnnota
5254
if (enclosing.getAnnotation(Component.class) == null) {
5355
throw new ValidationFailure("The @Factory must be nested inside a @Component", typeElement);
5456
}
55-
if (enclosing.getEnclosedElements().stream()
56-
.filter(enclosed -> enclosed.getKind().isInterface())
57-
.filter(enclosed -> enclosed.getAnnotation(Component.Factory.class) != null)
57+
List<? extends Element> siblings = enclosing.getEnclosedElements();
58+
if (siblings.stream()
59+
.filter(sibling -> sibling.getAnnotation(Component.Factory.class) != null
60+
|| sibling.getAnnotation(Component.Builder.class) != null)
5861
.count() >= 2) {
59-
throw new ValidationFailure("Found more than one @Factory", enclosing);
62+
throw new ValidationFailure("Only one @Factory or @Builder allowed", enclosing);
6063
}
6164
for (ExecutableElement m : ElementFilter.methodsIn(typeElement.getEnclosedElements())) {
6265
if (m.getModifiers().contains(Modifier.ABSTRACT)) {

compiler/src/main/java/io/jbock/simple/processor/util/TypeTool.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@ public boolean isSameType(TypeMirror mirror, String canonicalName) {
4646
return types.isSameType(mirror, typeElement.asType());
4747
}
4848

49+
public boolean isSameType(TypeMirror mirror, TypeMirror other) {
50+
return types.isSameType(mirror, other);
51+
}
52+
4953
public boolean hasInjectAnnotation(Element m) {
5054
if (m.getKind() != ElementKind.CONSTRUCTOR && m.getKind() != ElementKind.METHOD) {
5155
return false;

simple-component/src/main/java/io/jbock/simple/Component.java

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,13 @@
1212
* have the name of the type annotated, appended with {@code _Impl}. For
1313
* example, {@code @Component interface MyComponent {...}} will produce an implementation named
1414
* {@code MyComponent_Impl}.
15-
*
15+
*
1616
* <h2>Component methods</h2>
1717
*
1818
* <p>Every type annotated with {@code @Component} must contain at least one abstract component
1919
* method. Component methods may have any name, but must have no parameters and return a bound type.
2020
* A bound type is one of the following:
21-
*
21+
*
2222
* <ul>
2323
* <li>an {@link Inject injected} type
2424
* <li>a {@link Provides provided} type
@@ -34,20 +34,26 @@
3434
* A factory for a component. Components <em>may</em> have a single nested {@code interface}
3535
* annotated with {@code @Component.Factory}.
3636
*
37-
* <p>A factory is a type with a single method that returns a new component instance each time it
38-
* is called. The parameters of that method allow the caller to provide the bound instances
37+
* <p>A factory is an interface with a single method that returns a new component instance each time it
38+
* is called. The parameters of that method provide the bound instances
3939
* required by the component.
40-
*
41-
* <p>Components may have a single nested {@code interface}
42-
* annotated with {@code @Component.Factory}. Factory types must follow some rules:
43-
*
44-
* <ul>
45-
* <li>There must be exactly one abstract method, which must return the component type.
46-
* <li>The method parameters bind the instance passed for that parameter within the component.
47-
* </ul>
4840
*/
4941
@Target(TYPE)
5042
@Retention(SOURCE)
5143
@interface Factory {
5244
}
45+
46+
/**
47+
* A builder for a component. Components <em>may</em> have a single nested {@code interface}
48+
* annotated with {@code @Component.Builder}.
49+
*
50+
* <p>The builder is an interface with zero or more setter methods that return the builder type.
51+
* Additionally, there must be exactly one abstract no-argument method that returns the component
52+
* type, called the "build method". The setter methods provide the bound instances
53+
* required by the component.
54+
*/
55+
@Retention(SOURCE)
56+
@Target(TYPE)
57+
@interface Builder {
58+
}
5359
}

0 commit comments

Comments
 (0)