/*
* 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;
import static com.google.common.base.MoreObjects.firstNonNull;
import static javax.lang.model.type.TypeKind.DECLARED;
import static org.inferred.freebuilder.processor.BuilderMethods.getter;
import static org.inferred.freebuilder.processor.BuilderMethods.mapper;
import static org.inferred.freebuilder.processor.BuilderMethods.setter;
import static org.inferred.freebuilder.processor.util.ObjectsExcerpts.Nullability.NULLABLE;
import static org.inferred.freebuilder.processor.util.feature.FunctionPackage.FUNCTION_PACKAGE;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Optional;
import com.google.common.collect.ImmutableSet;
import org.inferred.freebuilder.processor.Metadata.Property;
import org.inferred.freebuilder.processor.PropertyCodeGenerator.Config;
import org.inferred.freebuilder.processor.util.Block;
import org.inferred.freebuilder.processor.util.Excerpt;
import org.inferred.freebuilder.processor.util.Excerpts;
import org.inferred.freebuilder.processor.util.ObjectsExcerpts;
import org.inferred.freebuilder.processor.util.ParameterizedType;
import org.inferred.freebuilder.processor.util.PreconditionExcerpts;
import org.inferred.freebuilder.processor.util.SourceBuilder;
import java.util.Set;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.TypeMirror;
/** {@link PropertyCodeGenerator.Factory} providing reference semantics for Nullable properties. */
public class NullablePropertyFactory implements PropertyCodeGenerator.Factory {
@Override
public Optional<CodeGenerator> create(Config config) {
Property property = config.getProperty();
boolean isPrimitive = property.getType().getKind().isPrimitive();
Set<TypeElement> nullableAnnotations = nullablesIn(config.getAnnotations());
if (isPrimitive || nullableAnnotations.isEmpty()) {
return Optional.absent();
}
return Optional.of(new CodeGenerator(config.getMetadata(), property, nullableAnnotations));
}
private static Set<TypeElement> nullablesIn(Iterable<? extends AnnotationMirror> annotations) {
ImmutableSet.Builder<TypeElement> nullableAnnotations = ImmutableSet.builder();
for (AnnotationMirror mirror : annotations) {
if (mirror.getElementValues().isEmpty()) {
TypeElement type = (TypeElement) mirror.getAnnotationType().asElement();
if (type.getSimpleName().contentEquals("Nullable")) {
nullableAnnotations.add(type);
}
}
}
return nullableAnnotations.build();
}
@VisibleForTesting static class CodeGenerator extends PropertyCodeGenerator {
private final Set<TypeElement> nullables;
CodeGenerator(Metadata metadata, Property property, Iterable<TypeElement> nullableAnnotations) {
super(metadata, property);
this.nullables = ImmutableSet.copyOf(nullableAnnotations);
}
@Override
public Type getType() {
return Type.OPTIONAL;
}
@Override
public void addBuilderFieldDeclaration(SourceBuilder code) {
addGetterAnnotations(code);
code.add("private %s %s = null;\n", property.getType(), property.getName());
}
@Override
public void addBuilderFieldAccessors(SourceBuilder code) {
addSetter(code, metadata);
addMapper(code, metadata);
addGetter(code, metadata);
}
private void addSetter(SourceBuilder code, final Metadata metadata) {
code.addLine("")
.addLine("/**")
.addLine(" * Sets the value to be returned by %s.",
metadata.getType().javadocNoArgMethodLink(property.getGetterName()))
.addLine(" *")
.addLine(" * @return this {@code %s} object", metadata.getBuilder().getSimpleName())
.addLine(" */");
addAccessorAnnotations(code);
code.add("public %s %s(", metadata.getBuilder(), setter(property));
addGetterAnnotations(code);
code.add("%s %s) {\n", property.getType(), property.getName())
.addLine(" this.%1$s = %1$s;", property.getName())
.addLine(" return (%s) this;", metadata.getBuilder())
.addLine("}");
}
private void addMapper(SourceBuilder code, final Metadata metadata) {
ParameterizedType unaryOperator = code.feature(FUNCTION_PACKAGE).unaryOperator().orNull();
if (unaryOperator == null) {
return;
}
TypeMirror typeParam = firstNonNull(property.getBoxedType(), property.getType());
code.addLine("")
.addLine("/**")
.addLine(" * If the value to be returned by %s is not",
metadata.getType().javadocNoArgMethodLink(property.getGetterName()))
.addLine(" * null, replaces it by applying {@code mapper} to it and using the result.")
.addLine(" *")
.addLine(" * @return this {@code %s} object", metadata.getBuilder().getSimpleName())
.addLine(" * @throws NullPointerException if {@code mapper} is null")
.addLine(" */")
.addLine("public %s %s(%s mapper) {",
metadata.getBuilder(),
mapper(property),
unaryOperator.withParameters(typeParam))
.add(PreconditionExcerpts.checkNotNull("mapper"))
.addLine(" %s %s = %s();",
property.getType(), property.getName(), getter(property))
.addLine(" if (%s != null) {", property.getName())
.addLine(" %s(mapper.apply(%s));", setter(property), property.getName())
.addLine(" }")
.addLine(" return (%s) this;", metadata.getBuilder())
.addLine("}");
}
private void addGetter(SourceBuilder code, final Metadata metadata) {
code.addLine("")
.addLine("/**")
.addLine(" * Returns the value that will be returned by %s.",
metadata.getType().javadocNoArgMethodLink(property.getGetterName()))
.addLine(" */");
addGetterAnnotations(code);
code.addLine("public %s %s() {", property.getType(), getter(property))
.addLine(" return %s;", property.getName())
.addLine("}");
}
@Override
public void addValueFieldDeclaration(SourceBuilder code, String finalField) {
addGetterAnnotations(code);
code.add("private final %s %s;\n", property.getType(), finalField);
}
@Override
public void addFinalFieldAssignment(SourceBuilder code, String finalField, String builder) {
code.addLine("%s = %s.%s;", finalField, builder, property.getName());
}
@Override
public void addMergeFromValue(Block code, String value) {
Excerpt defaults = Declarations.freshBuilder(code, metadata).orNull();
if (defaults != null) {
code.addLine("if (%s) {", ObjectsExcerpts.notEquals(
Excerpts.add("%s.%s()", value, property.getGetterName()),
Excerpts.add("%s.%s()", defaults, getter(property)),
DECLARED,
NULLABLE));
}
code.addLine(" %s(%s.%s());", setter(property), value, property.getGetterName());
if (defaults != null) {
code.addLine("}");
}
}
@Override
public void addMergeFromBuilder(Block code, String builder) {
Excerpt defaults = Declarations.freshBuilder(code, metadata).orNull();
if (defaults != null) {
code.addLine("if (%s) {", ObjectsExcerpts.notEquals(
Excerpts.add("%s.%s()", builder, getter(property)),
Excerpts.add("%s.%s()", defaults, getter(property)),
DECLARED,
NULLABLE));
}
code.addLine(" %s(%s.%s());", setter(property), builder, getter(property));
if (defaults != null) {
code.addLine("}");
}
}
@Override
public void addGetterAnnotations(SourceBuilder code) {
for (TypeElement nullableAnnotation : nullables) {
code.add("@%s ", nullableAnnotation);
}
}
@Override
public void addSetFromResult(SourceBuilder code, String builder, String variable) {
code.addLine("%s.%s(%s);", builder, setter(property), variable);
}
@Override
public void addClearField(Block code) {
Optional<Excerpt> defaults = Declarations.freshBuilder(code, metadata);
if (defaults.isPresent()) {
code.addLine("%1$s = %2$s.%1$s;", property.getName(), defaults.get());
} else {
code.addLine("%s = null;", property.getName());
}
}
}
}