Skip to content
This repository was archived by the owner on Oct 16, 2024. It is now read-only.

Commit c85fd5c

Browse files
authored
Merge pull request #225 from google/to.builder
Implement abstract toBuilder method
2 parents 3d832c6 + d35e5b0 commit c85fd5c

32 files changed

+1353
-207
lines changed

README.md

+21-6
Original file line numberDiff line numberDiff line change
@@ -99,15 +99,18 @@ import org.inferred.freebuilder.FreeBuilder;
9999

100100
@FreeBuilder
101101
public interface Person {
102-
/** Returns the person's full (English) name. */
102+
/** Returns this person's full (English) name. */
103103
String name();
104-
/** Returns the person's age in years, rounded down. */
104+
/** Returns this person's age in years, rounded down. */
105105
int age();
106+
/** Returns a new {@link Builder} with the same property values as this person. */
107+
Builder toBuilder();
106108
/** Builder of {@link Person} instances. */
107109
class Builder extends Person_Builder { }
108110
}
109111
```
110112

113+
The `toBuilder()` method here is optional but highly recommended.
111114
If you are writing an abstract class, or using Java 8, you may wish to hide the
112115
builder's constructor and manually provide instead a static `builder()` method
113116
on the value type (though <em>Effective Java</em> does not do this).
@@ -133,6 +136,8 @@ If you write the Person interface shown above, you get:
133136
* `UnsupportedOperationException`-throwing getters for unset fields
134137
* `toString`
135138
* `equals` and `hashCode`
139+
* a special `toBuilder()` method that preserves the 'partial' aspect on newly-minted
140+
Person instances
136141

137142

138143
```java
@@ -180,9 +185,7 @@ For each property `foo`, the builder gets:
180185
The mapper methods are very useful when modifying existing values, e.g.
181186

182187
```java
183-
Person olderPerson = Person.Builder.from(person)
184-
.mapAge(age -> age + 1)
185-
.build();
188+
Person olderPerson = person.toBuilder().mapAge(age -> age + 1).build();
186189
```
187190

188191
[UnaryOperator]: https://docs.oracle.com/javase/8/docs/api/java/util/function/UnaryOperator.html
@@ -489,7 +492,7 @@ it may violate a cross-field constraint.
489492
Person person = new Person.Builder()
490493
.name("Phil")
491494
.buildPartial(); // build() would throw an IllegalStateException here
492-
System.out.println(person); // prints: partial Person{name="Phil"}
495+
System.out.println(person); // prints: partial Person{name=Phil}
493496
person.age(); // throws UnsupportedOperationException
494497
```
495498

@@ -501,6 +504,18 @@ restrictions of the value type, partials can reduce the fragility of your test
501504
suite, allowing you to add new required fields or other constraints to an
502505
existing value type without breaking swathes of test code.
503506

507+
To allow robust tests of modify-rebuild code, the `toBuilder()` method on
508+
partials returns a subclass of Builder that returns partials from the `build()`
509+
method.
510+
511+
```java
512+
Person anotherPerson = person.toBuilder().name("Bob").build();
513+
System.out.println(anotherPerson); // prints: partial Person{name=Bob}
514+
```
515+
516+
(Note the `from` and `mergeFrom` methods do not behave this way; instead, they
517+
will throw an UnsupportedOperationException if given a partial.)
518+
504519

505520
### Jackson
506521

src/main/java/org/inferred/freebuilder/processor/Analyser.java

+42-4
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
import org.inferred.freebuilder.processor.Metadata.UnderrideLevel;
4949
import org.inferred.freebuilder.processor.PropertyCodeGenerator.Config;
5050
import org.inferred.freebuilder.processor.naming.NamingConvention;
51+
import org.inferred.freebuilder.processor.util.ModelUtils;
5152
import org.inferred.freebuilder.processor.util.ParameterizedType;
5253
import org.inferred.freebuilder.processor.util.QualifiedName;
5354

@@ -138,17 +139,18 @@ Metadata analyse(TypeElement type) throws CannotGenerateCodeException {
138139
QualifiedName generatedBuilder = QualifiedName.of(
139140
pkg.getQualifiedName().toString(), generatedBuilderSimpleName(type));
140141
Optional<TypeElement> builder = tryFindBuilder(generatedBuilder, type);
142+
Optional<BuilderFactory> builderFactory = builderFactory(builder);
141143
QualifiedName valueType = generatedBuilder.nestedType("Value");
142144
QualifiedName partialType = generatedBuilder.nestedType("Partial");
143145
QualifiedName propertyType = generatedBuilder.nestedType("Property");
144146
List<? extends TypeParameterElement> typeParameters = type.getTypeParameters();
145147
Map<ExecutableElement, Property> properties =
146-
findProperties(type, removeUnderriddenAndConcreteMethods(methods));
148+
findProperties(type, removeNonGetterMethods(builder, methods));
147149
Metadata.Builder metadataBuilder = new Metadata.Builder()
148150
.setType(QualifiedName.of(type).withParameters(typeParameters))
149151
.setInterfaceType(type.getKind().isInterface())
150152
.setBuilder(parameterized(builder, typeParameters))
151-
.setBuilderFactory(builderFactory(builder))
153+
.setBuilderFactory(builderFactory)
152154
.setGeneratedBuilder(generatedBuilder.withParameters(typeParameters))
153155
.setValueType(valueType.withParameters(typeParameters))
154156
.setPartialType(partialType.withParameters(typeParameters))
@@ -158,6 +160,7 @@ Metadata analyse(TypeElement type) throws CannotGenerateCodeException {
158160
.addVisibleNestedTypes(propertyType)
159161
.addAllVisibleNestedTypes(visibleTypesIn(type)) // Because we inherit from type
160162
.putAllStandardMethodUnderrides(findUnderriddenMethods(methods))
163+
.setHasToBuilderMethod(hasToBuilderMethod(builder, builderFactory, methods))
161164
.setBuilderSerializable(shouldBuilderBeSerializable(builder))
162165
.addAllProperties(properties.values());
163166
Metadata baseMetadata = metadataBuilder.build();
@@ -306,13 +309,48 @@ private Map<StandardMethod, UnderrideLevel> findUnderriddenMethods(
306309
return result.build();
307310
}
308311

309-
private static Set<ExecutableElement> removeUnderriddenAndConcreteMethods(
312+
/** Find a toBuilder method, if the user has provided one.
313+
* @param builderFactory */
314+
private boolean hasToBuilderMethod(
315+
Optional<TypeElement> builder,
316+
Optional<BuilderFactory> builderFactory,
310317
Iterable<ExecutableElement> methods) {
318+
if (!builder.isPresent()) {
319+
return false;
320+
}
321+
for (ExecutableElement method : methods) {
322+
if (isToBuilderMethod(builder.get(), method)) {
323+
if (!builderFactory.isPresent()) {
324+
messager.printMessage(ERROR,
325+
"No accessible no-args Builder constructor available to implement toBuilder",
326+
method);
327+
}
328+
return true;
329+
}
330+
}
331+
return false;
332+
}
333+
334+
private static boolean isToBuilderMethod(TypeElement builder, ExecutableElement method) {
335+
if (method.getSimpleName().contentEquals("toBuilder")
336+
&& method.getModifiers().contains(Modifier.ABSTRACT)
337+
&& method.getParameters().isEmpty()) {
338+
Optional<TypeElement> returnType = ModelUtils.maybeAsTypeElement(method.getReturnType());
339+
if (returnType.isPresent() && returnType.get().equals(builder)) {
340+
return true;
341+
}
342+
}
343+
return false;
344+
}
345+
346+
private static Set<ExecutableElement> removeNonGetterMethods(
347+
Optional<TypeElement> builder, Iterable<ExecutableElement> methods) {
311348
ImmutableSet.Builder<ExecutableElement> nonUnderriddenMethods = ImmutableSet.builder();
312349
for (ExecutableElement method : methods) {
313350
boolean isAbstract = method.getModifiers().contains(Modifier.ABSTRACT);
314351
boolean isStandardMethod = maybeStandardMethod(method).isPresent();
315-
if (isAbstract && !isStandardMethod) {
352+
boolean isToBuilderMethod = builder.isPresent() && isToBuilderMethod(builder.get(), method);
353+
if (isAbstract && !isStandardMethod && !isToBuilderMethod) {
316354
nonUnderriddenMethods.add(method);
317355
}
318356
}

src/main/java/org/inferred/freebuilder/processor/BuildablePropertyFactory.java

+63-6
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,11 @@ private enum MergeBuilderMethod {
6666
MERGE_DIRECTLY, BUILD_PARTIAL_AND_MERGE
6767
}
6868

69+
/** How to convert a partial value into a Builder. */
70+
private enum PartialToBuilderMethod {
71+
MERGE_DIRECTLY, TO_BUILDER_AND_MERGE
72+
}
73+
6974
@Override
7075
public Optional<? extends PropertyCodeGenerator> create(Config config) {
7176
DeclaredType type = maybeDeclared(config.getProperty().getType()).orNull();
@@ -136,30 +141,48 @@ public Optional<? extends PropertyCodeGenerator> create(Config config) {
136141
}
137142
}
138143

144+
List<ExecutableElement> valueMethods = FluentIterable
145+
.from(config.getElements().getAllMembers(element))
146+
.filter(ExecutableElement.class)
147+
.filter(new IsCallableMethod())
148+
.toList();
149+
150+
// Check whether there is a toBuilder() method
151+
PartialToBuilderMethod partialToBuilderMethod;
152+
if (any(valueMethods, new IsToBuilderMethod(builder.get().asType(), config.getTypes()))) {
153+
partialToBuilderMethod = PartialToBuilderMethod.TO_BUILDER_AND_MERGE;
154+
} else {
155+
partialToBuilderMethod = PartialToBuilderMethod.MERGE_DIRECTLY;
156+
}
157+
139158
return Optional.of(new CodeGenerator(
140159
config.getMetadata(),
141160
config.getProperty(),
142161
ParameterizedType.from(builder.get()),
143162
builderFactory.get(),
144-
mergeFromBuilderMethod));
163+
mergeFromBuilderMethod,
164+
partialToBuilderMethod));
145165
}
146166

147167
@VisibleForTesting static class CodeGenerator extends PropertyCodeGenerator {
148168

149-
final ParameterizedType builderType;
150-
final BuilderFactory builderFactory;
151-
final MergeBuilderMethod mergeFromBuilderMethod;
169+
private final ParameterizedType builderType;
170+
private final BuilderFactory builderFactory;
171+
private final MergeBuilderMethod mergeFromBuilderMethod;
172+
private final PartialToBuilderMethod partialToBuilderMethod;
152173

153-
CodeGenerator(
174+
private CodeGenerator(
154175
Metadata metadata,
155176
Property property,
156177
ParameterizedType builderType,
157178
BuilderFactory builderFactory,
158-
MergeBuilderMethod mergeFromBuilderMethod) {
179+
MergeBuilderMethod mergeFromBuilderMethod,
180+
PartialToBuilderMethod partialToBuilderMethod) {
159181
super(metadata, property);
160182
this.builderType = builderType;
161183
this.builderFactory = builderFactory;
162184
this.mergeFromBuilderMethod = mergeFromBuilderMethod;
185+
this.partialToBuilderMethod = partialToBuilderMethod;
163186
}
164187

165188
@Override
@@ -285,6 +308,17 @@ public void addMergeFromBuilder(Block code, String builder) {
285308
code.add(");\n");
286309
}
287310

311+
@Override
312+
public void addSetBuilderFromPartial(Block code, String builder) {
313+
if (partialToBuilderMethod == PartialToBuilderMethod.TO_BUILDER_AND_MERGE) {
314+
code.add("%s.%s().mergeFrom(%s.toBuilder());",
315+
builder, getBuilderMethod(property), setter(property), property.getName());
316+
} else {
317+
code.add("%s.%s().mergeFrom(%s);",
318+
builder, getBuilderMethod(property), setter(property), property.getName());
319+
}
320+
}
321+
288322
@Override
289323
public void addSetFromResult(SourceBuilder code, String builder, String variable) {
290324
code.addLine("%s.%s(%s);", builder, setter(property), variable);
@@ -367,6 +401,29 @@ private static final class IsMergeFromMethod implements Predicate<ExecutableElem
367401
}
368402
}
369403

404+
private static final class IsToBuilderMethod implements Predicate<ExecutableElement> {
405+
final TypeMirror builderType;
406+
final Types types;
407+
408+
IsToBuilderMethod(TypeMirror sourceType, Types types) {
409+
this.builderType = sourceType;
410+
this.types = types;
411+
}
412+
413+
@Override public boolean apply(ExecutableElement element) {
414+
if (element.getParameters().size() != 0) {
415+
return false;
416+
}
417+
if (!element.getSimpleName().contentEquals("toBuilder")) {
418+
return false;
419+
}
420+
if (!types.isSubtype(element.getReturnType(), builderType)) {
421+
return false;
422+
}
423+
return true;
424+
}
425+
}
426+
370427
private static final Predicate<Element> IS_BUILDER_TYPE = new Predicate<Element>() {
371428
@Override public boolean apply(Element element) {
372429
return element.getSimpleName().contentEquals("Builder")

src/main/java/org/inferred/freebuilder/processor/CodeGenerator.java

+61-4
Original file line numberDiff line numberDiff line change
@@ -231,16 +231,26 @@ private static void addBuildPartialMethod(SourceBuilder code, Metadata metadata)
231231
code.addLine("")
232232
.addLine("/**")
233233
.addLine(" * Returns a newly-created partial %s", metadata.getType().javadocLink())
234-
.addLine(" * based on the contents of the {@code %s}.",
235-
metadata.getBuilder().getSimpleName())
236-
.addLine(" * State checking will not be performed.");
234+
.addLine(" * for use in unit tests. State checking will not be performed.");
237235
if (any(metadata.getProperties(), IS_REQUIRED)) {
238236
code.addLine(" * Unset properties will throw an {@link %s}",
239237
UnsupportedOperationException.class)
240238
.addLine(" * when accessed via the partial object.");
241239
}
240+
if (metadata.getHasToBuilderMethod()
241+
&& metadata.getBuilderFactory() == Optional.of(BuilderFactory.NO_ARGS_CONSTRUCTOR)) {
242+
code.addLine(" *")
243+
.addLine(" * <p>The builder returned by a partial's {@link %s#toBuilder() toBuilder}",
244+
metadata.getType())
245+
.addLine(" * method overrides {@link %s#build() build()} to return another partial.",
246+
metadata.getBuilder())
247+
.addLine(" * This allows for robust tests of modify-rebuild code.");
248+
}
242249
code.addLine(" *")
243-
.addLine(" * <p>Partials should only ever be used in tests.")
250+
.addLine(" * <p>Partials should only ever be used in tests. They permit writing robust")
251+
.addLine(" * test cases that won't fail if this type gains more application-level")
252+
.addLine(" * constraints (e.g. new required fields) in future. If you require partially")
253+
.addLine(" * complete values in production code, consider using a Builder.")
244254
.addLine(" */");
245255
if (code.feature(GUAVA).isAvailable()) {
246256
code.addLine("@%s()", VisibleForTesting.class);
@@ -308,6 +318,20 @@ private static void addValueType(SourceBuilder code, Metadata metadata) {
308318
code.add(";\n");
309319
code.addLine(" }");
310320
}
321+
// toBuilder
322+
if (metadata.getHasToBuilderMethod()) {
323+
code.addLine("")
324+
.addLine(" @%s", Override.class)
325+
.addLine(" public %s toBuilder() {", metadata.getBuilder());
326+
BuilderFactory builderFactory = metadata.getBuilderFactory().orNull();
327+
if (builderFactory != null) {
328+
code.addLine(" return %s.mergeFrom(this);",
329+
builderFactory.newBuilder(metadata.getBuilder(), EXPLICIT_TYPES));
330+
} else {
331+
code.addLine(" throw new %s();", UnsupportedOperationException.class);
332+
}
333+
code.addLine(" }");
334+
}
311335
// Equals
312336
switch (metadata.standardMethodUnderride(StandardMethod.EQUALS)) {
313337
case ABSENT:
@@ -510,6 +534,7 @@ private static void addPartialType(SourceBuilder code, Metadata metadata) {
510534
code.add(";\n");
511535
code.addLine(" }");
512536
}
537+
addPartialToBuilderMethod(code, metadata);
513538
// Equals
514539
if (metadata.standardMethodUnderride(StandardMethod.EQUALS) != FINAL) {
515540
code.addLine("")
@@ -603,6 +628,38 @@ private static void addPartialType(SourceBuilder code, Metadata metadata) {
603628
code.addLine("}");
604629
}
605630

631+
private static void addPartialToBuilderMethod(SourceBuilder code, Metadata metadata) {
632+
if (!metadata.getHasToBuilderMethod()) {
633+
return;
634+
}
635+
BuilderFactory builderFactory = metadata.getBuilderFactory().orNull();
636+
if (builderFactory == BuilderFactory.NO_ARGS_CONSTRUCTOR) {
637+
code.addLine("")
638+
.addLine(" private static class PartialBuilder%s extends %s {",
639+
metadata.getBuilder().declarationParameters(), metadata.getBuilder())
640+
.addLine(" @Override public %s build() {", metadata.getType())
641+
.addLine(" return buildPartial();")
642+
.addLine(" }")
643+
.addLine(" }");
644+
}
645+
code.addLine("")
646+
.addLine(" @%s", Override.class)
647+
.addLine(" public %s toBuilder() {", metadata.getBuilder());
648+
if (builderFactory == BuilderFactory.NO_ARGS_CONSTRUCTOR) {
649+
code.addLine(" %s builder = new PartialBuilder%s();",
650+
metadata.getBuilder(), metadata.getBuilder().typeParametersOrDiamondOperator());
651+
Block block = new Block(code);
652+
for (Property property : metadata.getProperties()) {
653+
property.getCodeGenerator().addSetBuilderFromPartial(block, "builder");
654+
}
655+
code.add(block)
656+
.addLine(" return builder;");
657+
} else {
658+
code.addLine(" throw new %s();", UnsupportedOperationException.class);
659+
}
660+
code.addLine(" }");
661+
}
662+
606663
private static void writeToStringWithBuilder(
607664
SourceBuilder code, Metadata metadata, boolean isPartial) {
608665
code.addLine("%1$s result = new %1$s(\"%2$s%3$s{\");",

src/main/java/org/inferred/freebuilder/processor/DefaultPropertyFactory.java

+12
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,18 @@ public void addMergeFromBuilder(Block code, String builder) {
223223
}
224224
}
225225

226+
@Override
227+
public void addSetBuilderFromPartial(Block code, String builder) {
228+
if (!hasDefault) {
229+
code.add("if (!_unsetProperties.contains(%s.%s)) {",
230+
metadata.getPropertyEnum(), property.getAllCapsName());
231+
}
232+
code.addLine(" %s.%s(%s);", builder, setter(property), property.getName());
233+
if (!hasDefault) {
234+
code.addLine("}");
235+
}
236+
}
237+
226238
@Override
227239
public void addSetFromResult(SourceBuilder code, String builder, String variable) {
228240
code.addLine("%s.%s(%s);", builder, setter(property), variable);

0 commit comments

Comments
 (0)