/* * 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.collect.Iterables.any; import static com.google.common.collect.Iterables.tryFind; import static javax.lang.model.element.Modifier.PUBLIC; import static javax.lang.model.util.ElementFilter.typesIn; import static org.inferred.freebuilder.processor.BuilderFactory.TypeInference.INFERRED_TYPES; import static org.inferred.freebuilder.processor.BuilderMethods.getBuilderMethod; import static org.inferred.freebuilder.processor.BuilderMethods.mutator; import static org.inferred.freebuilder.processor.BuilderMethods.setter; import static org.inferred.freebuilder.processor.util.ModelUtils.asElement; import static org.inferred.freebuilder.processor.util.ModelUtils.findAnnotationMirror; import static org.inferred.freebuilder.processor.util.ModelUtils.maybeDeclared; 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.base.Preconditions; import com.google.common.base.Predicate; import com.google.common.collect.FluentIterable; 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.ParameterizedType; import org.inferred.freebuilder.processor.util.SourceBuilder; import java.util.List; import javax.lang.model.element.Element; import javax.lang.model.element.ElementKind; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.Modifier; import javax.lang.model.element.TypeElement; import javax.lang.model.type.DeclaredType; import javax.lang.model.type.TypeMirror; import javax.lang.model.util.Types; /** * {@link PropertyCodeGenerator.Factory} for <b>buildable</b> types: that is, types with a Builder * class providing a similar API to proto or @FreeBuilder:<ul> * <li> a public constructor, or static builder()/newBuilder() method; * <li> build(), buildPartial() and clear() methods; and * <li> a mergeWith(Value) method. * </ul> */ public class BuildablePropertyFactory implements PropertyCodeGenerator.Factory { /** How to merge the values from one Builder into another. */ private enum MergeBuilderMethod { MERGE_DIRECTLY, BUILD_PARTIAL_AND_MERGE } /** How to convert a partial value into a Builder. */ private enum PartialToBuilderMethod { MERGE_DIRECTLY, TO_BUILDER_AND_MERGE } @Override public Optional<? extends PropertyCodeGenerator> create(Config config) { DeclaredType type = maybeDeclared(config.getProperty().getType()).orNull(); if (type == null) { return Optional.absent(); } TypeElement element = asElement(type); // Find the builder Optional<TypeElement> builder = tryFind(typesIn(element.getEnclosedElements()), IS_BUILDER_TYPE); if (!builder.isPresent()) { return Optional.absent(); } // Verify the builder can be constructed Optional<BuilderFactory> builderFactory = BuilderFactory.from(builder.get()); if (!builderFactory.isPresent()) { return Optional.absent(); } MergeBuilderMethod mergeFromBuilderMethod; if (findAnnotationMirror(element, "org.inferred.freebuilder.FreeBuilder").isPresent()) { /* * If the element is annotated @FreeBuilder, assume the necessary methods will be added. We * can't check directly as the builder superclass may not have been generated yet. To be * strictly correct, we should delay a round if an error type leaves us unsure about this kind * of API-changing decision, and then we would work with _any_ Builder-generating API. We * would need to drop out part of our own builder superclass, to prevent chains of dependent * buildable types leading to quadratic compilation times (not to mention cycles), and leave a * dangling super-superclass to pick up next round. As an optimization, though, we would * probably skip this for @FreeBuilder-types anyway, to avoid extra types whenever possible, * which leaves a lot of complicated code supporting a currently non-existent edge case. */ mergeFromBuilderMethod = MergeBuilderMethod.MERGE_DIRECTLY; } else { List<ExecutableElement> methods = FluentIterable .from(config.getElements().getAllMembers(builder.get())) .filter(ExecutableElement.class) .filter(new IsCallableMethod()) .toList(); // Check there is a build() method if (!any(methods, new IsBuildMethod("build", type, config.getTypes()))) { return Optional.absent(); } // Check there is a buildPartial() method if (!any(methods, new IsBuildMethod("buildPartial", type, config.getTypes()))) { return Optional.absent(); } // Check there is a clear() method if (!any(methods, new IsClearMethod())) { return Optional.absent(); } // Check there is a mergeFrom(Value) method if (!any(methods, new IsMergeFromMethod(type, config.getTypes()))) { return Optional.absent(); } // Check whether there is a mergeFrom(Builder) method if (any(methods, new IsMergeFromMethod(builder.get().asType(), config.getTypes()))) { mergeFromBuilderMethod = MergeBuilderMethod.MERGE_DIRECTLY; } else { mergeFromBuilderMethod = MergeBuilderMethod.BUILD_PARTIAL_AND_MERGE; } } List<ExecutableElement> valueMethods = FluentIterable .from(config.getElements().getAllMembers(element)) .filter(ExecutableElement.class) .filter(new IsCallableMethod()) .toList(); // Check whether there is a toBuilder() method PartialToBuilderMethod partialToBuilderMethod; if (any(valueMethods, new IsToBuilderMethod(builder.get().asType(), config.getTypes()))) { partialToBuilderMethod = PartialToBuilderMethod.TO_BUILDER_AND_MERGE; } else { partialToBuilderMethod = PartialToBuilderMethod.MERGE_DIRECTLY; } return Optional.of(new CodeGenerator( config.getMetadata(), config.getProperty(), ParameterizedType.from(builder.get()), builderFactory.get(), mergeFromBuilderMethod, partialToBuilderMethod)); } @VisibleForTesting static class CodeGenerator extends PropertyCodeGenerator { private final ParameterizedType builderType; private final BuilderFactory builderFactory; private final MergeBuilderMethod mergeFromBuilderMethod; private final PartialToBuilderMethod partialToBuilderMethod; private CodeGenerator( Metadata metadata, Property property, ParameterizedType builderType, BuilderFactory builderFactory, MergeBuilderMethod mergeFromBuilderMethod, PartialToBuilderMethod partialToBuilderMethod) { super(metadata, property); this.builderType = builderType; this.builderFactory = builderFactory; this.mergeFromBuilderMethod = mergeFromBuilderMethod; this.partialToBuilderMethod = partialToBuilderMethod; } @Override public void addBuilderFieldDeclaration(SourceBuilder code) { code.addLine("private final %s %s = %s;", builderType, property.getName(), builderFactory.newBuilder(builderType, INFERRED_TYPES)); } @Override public void addBuilderFieldAccessors(SourceBuilder code) { addSetter(code, metadata); addSetterTakingBuilder(code, metadata); addMutate(code, metadata); addGetter(code, metadata); } private void addSetter(SourceBuilder code, 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(" * @throws NullPointerException if {@code %s} is null", property.getName()) .addLine(" */"); addAccessorAnnotations(code); code.addLine("public %s %s(%s %s) {", metadata.getBuilder(), setter(property), property.getType(), property.getName()) .addLine(" this.%s.clear();", property.getName()) .addLine(" this.%1$s.mergeFrom(%2$s.checkNotNull(%1$s));", property.getName(), Preconditions.class) .addLine(" return (%s) this;", metadata.getBuilder()) .addLine("}"); } private void addSetterTakingBuilder(SourceBuilder code, 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(" * @throws NullPointerException if {@code builder} is null") .addLine(" */") .addLine("public %s %s(%s builder) {", metadata.getBuilder(), setter(property), builderType) .addLine(" return %s(builder.build());", setter(property)) .addLine("}"); } private void addMutate(SourceBuilder code, Metadata metadata) { ParameterizedType consumer = code.feature(FUNCTION_PACKAGE).consumer().orNull(); if (consumer == null) { return; } code.addLine("") .addLine("/**") .addLine(" * Applies {@code mutator} to the builder for the value that will be") .addLine(" * returned by %s.", metadata.getType().javadocNoArgMethodLink(property.getGetterName())) .addLine(" *") .addLine(" * <p>This method mutates the builder in-place. {@code mutator} is a void") .addLine(" * consumer, so any value returned from a lambda will be ignored.") .addLine(" *") .addLine(" * @return this {@code %s} object", metadata.getBuilder().getSimpleName()) .addLine(" * @throws NullPointerException if {@code mutator} is null") .addLine(" */") .addLine("public %s %s(%s<%s> mutator) {", metadata.getBuilder(), mutator(property), consumer.getQualifiedName(), builderType) .addLine(" mutator.accept(%s);", property.getName()) .addLine(" return (%s) this;", metadata.getBuilder()) .addLine("}"); } private void addGetter(SourceBuilder code, Metadata metadata) { code.addLine("") .addLine("/**") .addLine(" * Returns a builder for the value that will be returned by %s.", metadata.getType().javadocNoArgMethodLink(property.getGetterName())) .addLine(" */") .addLine("public %s %s() {", builderType, getBuilderMethod(property)) .addLine(" return %s;", property.getName()) .addLine("}"); } @Override public void addFinalFieldAssignment(SourceBuilder code, String finalField, String builder) { code.addLine("%s = %s.%s.build();", finalField, builder, property.getName()); } @Override public void addPartialFieldAssignment(SourceBuilder code, String finalField, String builder) { code.addLine("%s = %s.%s.buildPartial();", finalField, builder, property.getName()); } @Override public void addMergeFromValue(Block code, String value) { String propertyName = property.getName(); if (propertyName.equals(value)) { propertyName = "this." + propertyName; // see issue #78 } code.addLine("%s.mergeFrom(%s.%s());", propertyName, value, property.getGetterName()); } @Override public void addMergeFromBuilder(Block code, String builder) { String propertyName = property.getName(); if (propertyName.equals(builder)) { propertyName = "this." + propertyName; // see issue #78 } code.add("%s.mergeFrom(%s.%s()", propertyName, builder, getBuilderMethod(property)); if (mergeFromBuilderMethod == MergeBuilderMethod.BUILD_PARTIAL_AND_MERGE) { code.add(".buildPartial()"); } code.add(");\n"); } @Override public void addSetBuilderFromPartial(Block code, String builder) { if (partialToBuilderMethod == PartialToBuilderMethod.TO_BUILDER_AND_MERGE) { code.add("%s.%s().mergeFrom(%s.toBuilder());", builder, getBuilderMethod(property), setter(property), property.getName()); } else { code.add("%s.%s().mergeFrom(%s);", builder, getBuilderMethod(property), setter(property), property.getName()); } } @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) { code.addLine("%s.clear();", property.getName()); } } private static final class IsCallableMethod implements Predicate<ExecutableElement> { @Override public boolean apply(ExecutableElement element) { boolean isMethod = (element.getKind() == ElementKind.METHOD); boolean isPublic = element.getModifiers().contains(Modifier.PUBLIC); boolean isNotStatic = !element.getModifiers().contains(Modifier.STATIC); boolean declaresNoExceptions = element.getThrownTypes().isEmpty(); return isMethod && isPublic && isNotStatic && declaresNoExceptions; } } private static final class IsBuildMethod implements Predicate<ExecutableElement> { final String methodName; final TypeMirror builtType; final Types types; IsBuildMethod(String methodName, TypeMirror builtType, Types types) { this.methodName = methodName; this.builtType = builtType; this.types = types; } @Override public boolean apply(ExecutableElement element) { if (!element.getParameters().isEmpty()) { return false; } if (!element.getSimpleName().contentEquals(methodName)) { return false; } if (!types.isSubtype(element.getReturnType(), builtType)) { return false; } return true; } } private static final class IsClearMethod implements Predicate<ExecutableElement> { @Override public boolean apply(ExecutableElement element) { if (!element.getParameters().isEmpty()) { return false; } if (!element.getSimpleName().contentEquals("clear")) { return false; } return true; } } private static final class IsMergeFromMethod implements Predicate<ExecutableElement> { final TypeMirror builderType; final Types types; IsMergeFromMethod(TypeMirror sourceType, Types types) { this.builderType = sourceType; this.types = types; } @Override public boolean apply(ExecutableElement element) { if (element.getParameters().size() != 1) { return false; } if (!element.getSimpleName().contentEquals("mergeFrom")) { return false; } if (!types.isSubtype(builderType, element.getParameters().get(0).asType())) { return false; } return true; } } private static final class IsToBuilderMethod implements Predicate<ExecutableElement> { final TypeMirror builderType; final Types types; IsToBuilderMethod(TypeMirror sourceType, Types types) { this.builderType = sourceType; this.types = types; } @Override public boolean apply(ExecutableElement element) { if (element.getParameters().size() != 0) { return false; } if (!element.getSimpleName().contentEquals("toBuilder")) { return false; } if (!types.isSubtype(element.getReturnType(), builderType)) { return false; } return true; } } private static final Predicate<Element> IS_BUILDER_TYPE = new Predicate<Element>() { @Override public boolean apply(Element element) { return element.getSimpleName().contentEquals("Builder") && element.getModifiers().contains(PUBLIC); } }; }