org.inferred.freebuilder.processor.property.OptionalProperty Maven / Gradle / Ivy
Show all versions of freebuilder Show documentation
/*
* Copyright 2014 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.inferred.freebuilder.processor.property;
import static org.inferred.freebuilder.processor.BuilderMethods.clearMethod;
import static org.inferred.freebuilder.processor.BuilderMethods.getter;
import static org.inferred.freebuilder.processor.BuilderMethods.mapper;
import static org.inferred.freebuilder.processor.BuilderMethods.nullableSetter;
import static org.inferred.freebuilder.processor.BuilderMethods.setter;
import static org.inferred.freebuilder.processor.model.ModelUtils.erasesToAnyOf;
import static org.inferred.freebuilder.processor.model.ModelUtils.maybeDeclared;
import static org.inferred.freebuilder.processor.model.ModelUtils.maybeUnbox;
import static org.inferred.freebuilder.processor.model.ModelUtils.upperBound;
import static org.inferred.freebuilder.processor.property.MergeAction.skippingEmptyOptionals;
import static org.inferred.freebuilder.processor.source.FunctionalType.functionalTypeAcceptedByMethod;
import static org.inferred.freebuilder.processor.source.FunctionalType.unaryOperator;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableSet;
import org.inferred.freebuilder.processor.Datatype;
import org.inferred.freebuilder.processor.Declarations;
import org.inferred.freebuilder.processor.source.Excerpt;
import org.inferred.freebuilder.processor.source.FunctionalType;
import org.inferred.freebuilder.processor.source.QualifiedName;
import org.inferred.freebuilder.processor.source.SourceBuilder;
import org.inferred.freebuilder.processor.source.Variable;
import org.inferred.freebuilder.processor.source.feature.Jsr305;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.UnaryOperator;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.TypeMirror;
/**
* {@link PropertyCodeGenerator} providing a default value (absent/empty) and convenience setter
* methods for Guava and Java 8 Optional properties.
*/
class OptionalProperty extends PropertyCodeGenerator {
@VisibleForTesting
enum OptionalType {
GUAVA(QualifiedName.of(com.google.common.base.Optional.class), "absent", "fromNullable") {
@Override
protected void applyMapper(
SourceBuilder code,
Datatype datatype,
FunctionalType mapperType,
Property property) {
// Guava's transform method throws a NullPointerException if mapper returns null,
// and it has no flatMap-equivalent. We choose to follow the Java 8 convention of
// turning a null into an empty (absent) optional as that is the de facto standard
// now. (If the mapper type *can* return null, of course.)
if (mapperType.canReturnNull()) {
code.addLine("%s.requireNonNull(mapper);", Objects.class)
.addLine(" %s old%s = %s();",
property.getType(), property.getCapitalizedName(), getter(property))
.addLine(" if (old%s.isPresent()) {", property.getCapitalizedName())
.addLine(" %s(mapper.%s(old%s.get()));",
nullableSetter(property),
mapperType.getMethodName(),
property.getCapitalizedName())
.addLine(" }")
.addLine(" return (%s) this;", datatype.getBuilder());
} else {
code.addLine(" return %s(%s().transform(mapper::%s));",
setter(property), getter(property), mapperType.getMethodName());
}
}
@Override
protected void invokeIfPresent(SourceBuilder code, String value, String method) {
code.addLine("if (%s.isPresent()) {", value)
.addLine(" %s(%s.get());", method, value)
.addLine("}");
}
},
JAVA8(QualifiedName.of(Optional.class), "empty", "ofNullable") {
@Override
protected void applyMapper(
SourceBuilder code,
Datatype datatype,
FunctionalType mapperType,
Property property) {
code.add(" return %s(%s().map(mapper", setter(property), getter(property));
if (!mapperType.getFunctionalInterface().getQualifiedName()
.equals(QualifiedName.of(UnaryOperator.class))) {
code.add("::%s", mapperType.getMethodName());
}
code.add("));%n");
}
@Override
protected void invokeIfPresent(SourceBuilder code, String value, String method) {
code.addLine("%s.ifPresent(this::%s);", value, method);
}
};
private final QualifiedName cls;
private final String empty;
private final String ofNullable;
OptionalType(QualifiedName cls, String empty, String ofNullable) {
this.cls = cls;
this.empty = empty;
this.ofNullable = ofNullable;
}
protected abstract void applyMapper(
SourceBuilder code,
Datatype datatype,
FunctionalType mapperType,
Property property);
protected abstract void invokeIfPresent(SourceBuilder code, String value, String method);
}
static class Factory implements PropertyCodeGenerator.Factory {
@Override
public Optional create(Config config) {
Property property = config.getProperty();
DeclaredType type = maybeDeclared(property.getType()).orElse(null);
OptionalType optionalType = maybeOptional(type).orElse(null);
if (optionalType == null) {
return Optional.empty();
}
TypeMirror elementType = upperBound(config.getElements(), type.getTypeArguments().get(0));
Optional unboxedType = maybeUnbox(elementType, config.getTypes());
FunctionalType mapperType = functionalTypeAcceptedByMethod(
config.getBuilder(),
mapper(property),
unaryOperator(elementType),
config.getElements(),
config.getTypes());
return Optional.of(new OptionalProperty(
config.getDatatype(),
property,
optionalType,
elementType,
unboxedType,
mapperType));
}
private static Optional maybeOptional(DeclaredType type) {
for (OptionalType optionalType : OptionalType.values()) {
if (erasesToAnyOf(type, optionalType.cls)) {
return Optional.of(optionalType);
}
}
return Optional.empty();
}
}
private final OptionalType optional;
private final TypeMirror elementType;
private final Optional unboxedType;
private final FunctionalType mapperType;
@VisibleForTesting OptionalProperty(
Datatype datatype,
Property property,
OptionalType optional,
TypeMirror elementType,
Optional unboxedType,
FunctionalType mapperType) {
super(datatype, property);
this.optional = optional;
this.elementType = elementType;
this.unboxedType = unboxedType;
this.mapperType = mapperType;
}
@Override
public Initially initialState() {
return Initially.OPTIONAL;
}
@Override
public void addValueFieldDeclaration(SourceBuilder code) {
code.addLine("// Store a nullable object instead of an Optional. Escape analysis then")
.addLine("// allows the JVM to optimize away the Optional objects created by our")
.addLine("// getter method.")
.addLine("private final %s %s;", elementType, property.getField());
}
@Override
public void addBuilderFieldDeclaration(SourceBuilder code) {
code.addLine("// Store a nullable object instead of an Optional. Escape analysis then")
.addLine("// allows the JVM to optimize away the Optional objects created by and")
.addLine("// passed to our API.")
.addLine("private %s %s = null;", elementType, property.getField());
}
@Override
public void addBuilderFieldAccessors(SourceBuilder code) {
addSetter(code);
addOptionalSetter(code);
addNullableSetter(code);
addMapper(code);
addClear(code);
addGetter(code);
}
private void addSetter(SourceBuilder code) {
code.addLine("")
.addLine("/**")
.addLine(" * Sets the value to be returned by %s.",
datatype.getType().javadocNoArgMethodLink(property.getGetterName()))
.addLine(" *")
.addLine(" * @return this {@code %s} object", datatype.getBuilder().getSimpleName());
if (!unboxedType.isPresent()) {
code.addLine(" * @throws NullPointerException if {@code %s} is null", property.getName());
}
code.addLine(" */")
.addLine("public %s %s(%s %s) {",
datatype.getBuilder(),
setter(property),
unboxedType.orElse(elementType),
property.getName());
if (unboxedType.isPresent()) {
code.addLine(" %s = %s;", property.getField(), property.getName());
} else {
code.addLine(" %s = %s.requireNonNull(%s);",
property.getField(), Objects.class, property.getName());
}
code.addLine(" return (%s) this;", datatype.getBuilder())
.addLine("}");
}
private void addOptionalSetter(SourceBuilder code) {
code.addLine("")
.addLine("/**")
.addLine(" * Sets the value to be returned by %s.",
datatype.getType().javadocNoArgMethodLink(property.getGetterName()))
.addLine(" *")
.addLine(" * @return this {@code %s} object", datatype.getBuilder().getSimpleName())
.addLine(" */");
addAccessorAnnotations(code);
code.addLine("public %s %s(%s extends %s> %s) {",
datatype.getBuilder(),
setter(property),
optional.cls,
elementType,
property.getName())
.addLine(" if (%s.isPresent()) {", property.getName())
.addLine(" return %s(%s.get());", setter(property), property.getName())
.addLine(" } else {")
.addLine(" return %s();", clearMethod(property))
.addLine(" }")
.addLine("}");
}
private void addNullableSetter(SourceBuilder code) {
code.addLine("")
.addLine("/**")
.addLine(" * Sets the value to be returned by %s.",
datatype.getType().javadocNoArgMethodLink(property.getGetterName()))
.addLine(" *")
.addLine(" * @return this {@code %s} object", datatype.getBuilder().getSimpleName())
.addLine(" */")
.addLine("public %s %s(%s %s %s) {",
datatype.getBuilder(),
nullableSetter(property),
Jsr305.nullable(),
elementType,
property.getName())
.addLine(" if (%s != null) {", property.getName())
.addLine(" return %s(%s);", setter(property), property.getName())
.addLine(" } else {")
.addLine(" return %s();", clearMethod(property))
.addLine(" }")
.addLine("}");
}
private void addMapper(SourceBuilder code) {
code.addLine("")
.addLine("/**")
.addLine(" * If the value to be returned by %s is present,",
datatype.getType().javadocNoArgMethodLink(property.getGetterName()))
.addLine(" * replaces it by applying {@code mapper} to it and using the result.");
if (mapperType.canReturnNull()) {
code.addLine(" *")
.addLine(" * If the result is null, clears the value.");
}
code.addLine(" *")
.addLine(" * @return this {@code %s} object", datatype.getBuilder().getSimpleName())
.addLine(" * @throws NullPointerException if {@code mapper} is null")
.addLine(" */")
.addLine("public %s %s(%s mapper) {",
datatype.getBuilder(),
mapper(property),
mapperType.getFunctionalInterface());
optional.applyMapper(code, datatype, mapperType, property);
code.addLine("}");
}
private void addClear(SourceBuilder code) {
code.addLine("")
.addLine("/**")
.addLine(" * Sets the value to be returned by %s",
datatype.getType().javadocNoArgMethodLink(property.getGetterName()))
.addLine(" * to {@link %1$s#%2$s() Optional.%2$s()}.", optional.cls, optional.empty)
.addLine(" *")
.addLine(" * @return this {@code %s} object", datatype.getBuilder().getSimpleName())
.addLine(" */")
.addLine("public %s %s() {", datatype.getBuilder(), clearMethod(property))
.addLine(" %s = null;", property.getField())
.addLine(" return (%s) this;", datatype.getBuilder())
.addLine("}");
}
private void addGetter(SourceBuilder code) {
code.addLine("")
.addLine("/**")
.addLine(" * Returns the value that will be returned by %s.",
datatype.getType().javadocNoArgMethodLink(property.getGetterName()))
.addLine(" */")
.addLine("public %s %s() {", property.getType(), getter(property))
.addLine(" return %s.%s(%s);\n", optional.cls, optional.ofNullable, property.getField())
.addLine("}");
}
@Override
public void addFinalFieldAssignment(SourceBuilder code, Excerpt finalField, String builder) {
code.addLine("%s = %s;", finalField, property.getField().on(builder));
}
@Override
public void addAssignToBuilder(SourceBuilder code, Variable builder) {
code.addLine("%s = %s;", property.getField().on(builder), property.getField());
}
@Override
public void addMergeFromValue(SourceBuilder code, String value) {
String propertyValue = value + "." + property.getGetterName() + "()";
optional.invokeIfPresent(code, propertyValue, setter(property));
}
@Override
public void addMergeFromBuilder(SourceBuilder code, String builder) {
String propertyValue = builder + "." + getter(property) + "()";
optional.invokeIfPresent(code, propertyValue, setter(property));
}
@Override
public Set getMergeActions() {
return ImmutableSet.of(skippingEmptyOptionals());
}
@Override
public void addReadValueFragment(SourceBuilder code, Excerpt finalField) {
code.add("%s.%s(%s)", optional.cls, optional.ofNullable, finalField);
}
@Override
public void addSetFromResult(SourceBuilder code, Excerpt builder, Excerpt variable) {
code.addLine("%s.%s(%s);", builder, setter(property), variable);
}
@Override
public void addClearField(SourceBuilder code) {
Optional defaults = Declarations.freshBuilder(code, datatype);
if (defaults.isPresent()) {
code.addLine("%s = %s;", property.getField(), property.getField().on(defaults.get()));
} else {
code.addLine("%s = null;", property.getField());
}
}
}