/*
* 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 org.inferred.freebuilder.processor.BuilderMethods.clearMethod;
import static org.inferred.freebuilder.processor.BuilderMethods.getter;
import static org.inferred.freebuilder.processor.BuilderMethods.mutator;
import static org.inferred.freebuilder.processor.BuilderMethods.putAllMethod;
import static org.inferred.freebuilder.processor.BuilderMethods.putMethod;
import static org.inferred.freebuilder.processor.BuilderMethods.removeMethod;
import static org.inferred.freebuilder.processor.Util.erasesToAnyOf;
import static org.inferred.freebuilder.processor.Util.upperBound;
import static org.inferred.freebuilder.processor.util.ModelUtils.maybeDeclared;
import static org.inferred.freebuilder.processor.util.ModelUtils.maybeUnbox;
import static org.inferred.freebuilder.processor.util.ModelUtils.overrides;
import static org.inferred.freebuilder.processor.util.StaticExcerpt.Type.METHOD;
import static org.inferred.freebuilder.processor.util.feature.FunctionPackage.FUNCTION_PACKAGE;
import static org.inferred.freebuilder.processor.util.feature.GuavaLibrary.GUAVA;
import static org.inferred.freebuilder.processor.util.feature.SourceLevel.diamondOperator;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Optional;
import com.google.common.collect.ImmutableMap;
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.excerpt.CheckedMap;
import org.inferred.freebuilder.processor.util.Block;
import org.inferred.freebuilder.processor.util.Excerpts;
import org.inferred.freebuilder.processor.util.ParameterizedType;
import org.inferred.freebuilder.processor.util.PreconditionExcerpts;
import org.inferred.freebuilder.processor.util.QualifiedName;
import org.inferred.freebuilder.processor.util.SourceBuilder;
import org.inferred.freebuilder.processor.util.StaticExcerpt;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.TypeMirror;
/**
* {@link PropertyCodeGenerator.Factory} providing append-only semantics for {@link Map}
* properties.
*/
public class MapPropertyFactory implements PropertyCodeGenerator.Factory {
@Override
public Optional<? extends PropertyCodeGenerator> create(Config config) {
DeclaredType type = maybeDeclared(config.getProperty().getType()).orNull();
if (type == null || !erasesToAnyOf(type, Map.class, ImmutableMap.class)) {
return Optional.absent();
}
TypeMirror keyType = upperBound(config.getElements(), type.getTypeArguments().get(0));
TypeMirror valueType = upperBound(config.getElements(), type.getTypeArguments().get(1));
Optional<TypeMirror> unboxedKeyType = maybeUnbox(keyType, config.getTypes());
Optional<TypeMirror> unboxedValueType = maybeUnbox(valueType, config.getTypes());
boolean overridesPutMethod = hasPutMethodOverride(
config, unboxedKeyType.or(keyType), unboxedValueType.or(valueType));
return Optional.of(new CodeGenerator(
config.getMetadata(),
config.getProperty(),
overridesPutMethod,
keyType,
unboxedKeyType,
valueType,
unboxedValueType));
}
private static boolean hasPutMethodOverride(
Config config, TypeMirror keyType, TypeMirror valueType) {
return overrides(
config.getBuilder(),
config.getTypes(),
putMethod(config.getProperty()),
keyType,
valueType);
}
@VisibleForTesting
static class CodeGenerator extends PropertyCodeGenerator {
private static final ParameterizedType COLLECTION =
QualifiedName.of(Collection.class).withParameters("E");
private final boolean overridesPutMethod;
private final TypeMirror keyType;
private final Optional<TypeMirror> unboxedKeyType;
private final TypeMirror valueType;
private final Optional<TypeMirror> unboxedValueType;
CodeGenerator(
Metadata metadata,
Property property,
boolean overridesPutMethod,
TypeMirror keyType,
Optional<TypeMirror> unboxedKeyType,
TypeMirror valueType,
Optional<TypeMirror> unboxedValueType) {
super(metadata, property);
this.overridesPutMethod = overridesPutMethod;
this.keyType = keyType;
this.unboxedKeyType = unboxedKeyType;
this.valueType = valueType;
this.unboxedValueType = unboxedValueType;
}
@Override
public void addBuilderFieldDeclaration(SourceBuilder code) {
code.addLine("private final %1$s<%2$s, %3$s> %4$s = new %1$s%5$s();",
LinkedHashMap.class,
keyType,
valueType,
property.getName(),
diamondOperator(Excerpts.add("%s, %s", keyType, valueType)));
}
@Override
public void addBuilderFieldAccessors(SourceBuilder code) {
addPut(code, metadata);
addPutAll(code, metadata);
addRemove(code, metadata);
addMutate(code, metadata);
addClear(code, metadata);
addGetter(code, metadata);
}
private void addPut(SourceBuilder code, Metadata metadata) {
code.addLine("")
.addLine("/**")
.addLine(" * Associates {@code key} with {@code value} in the map to be returned from")
.addLine(" * %s.", metadata.getType().javadocNoArgMethodLink(property.getGetterName()))
.addLine(" * If the map previously contained a mapping for the key,")
.addLine(" * the old value is replaced by the specified value.")
.addLine(" *")
.addLine(" * @return this {@code %s} object", metadata.getBuilder().getSimpleName());
if (!unboxedKeyType.isPresent() || !unboxedValueType.isPresent()) {
code.add(" * @throws NullPointerException if ");
if (unboxedKeyType.isPresent()) {
code.add("{@code value} is");
} else if (unboxedValueType.isPresent()) {
code.add("{@code key} is");
} else {
code.add("either {@code key} or {@code value} are");
}
code.add(" null\n");
}
code.addLine(" */")
.addLine("public %s %s(%s key, %s value) {",
metadata.getBuilder(),
putMethod(property),
unboxedKeyType.or(keyType),
unboxedValueType.or(valueType));
if (!unboxedKeyType.isPresent()) {
code.add(PreconditionExcerpts.checkNotNull("key"));
}
if (!unboxedValueType.isPresent()) {
code.add(PreconditionExcerpts.checkNotNull("value"));
}
code.addLine(" %s.put(key, value);", property.getName())
.addLine(" return (%s) this;", metadata.getBuilder())
.addLine("}");
}
private void addPutAll(SourceBuilder code, Metadata metadata) {
code.addLine("")
.addLine("/**")
.addLine(" * Copies all of the mappings from {@code map} to the map to be returned from")
.addLine(" * %s.", metadata.getType().javadocNoArgMethodLink(property.getGetterName()))
.addLine(" *")
.addLine(" * @return this {@code %s} object", metadata.getBuilder().getSimpleName())
.addLine(" * @throws NullPointerException if {@code map} is null or contains a")
.addLine(" * null key or value")
.addLine(" */");
addAccessorAnnotations(code);
code.addLine("public %s %s(%s<? extends %s, ? extends %s> map) {",
metadata.getBuilder(),
putAllMethod(property),
Map.class,
keyType,
valueType)
.addLine(" for (%s<? extends %s, ? extends %s> entry : map.entrySet()) {",
Map.Entry.class, keyType, valueType)
.addLine(" %s(entry.getKey(), entry.getValue());", putMethod(property))
.addLine(" }")
.addLine(" return (%s) this;", metadata.getBuilder())
.addLine("}");
}
private void addRemove(SourceBuilder code, Metadata metadata) {
code.addLine("")
.addLine("/**")
.addLine(" * Removes the mapping for {@code key} from the map to be returned from")
.addLine(" * %s, if one is present.",
metadata.getType().javadocNoArgMethodLink(property.getGetterName()))
.addLine(" *")
.addLine(" * @return this {@code %s} object", metadata.getBuilder().getSimpleName());
if (!unboxedKeyType.isPresent()) {
code.addLine(" * @throws NullPointerException if {@code key} is null");
}
code.addLine(" */")
.addLine("public %s %s(%s key) {",
metadata.getBuilder(),
removeMethod(property),
unboxedKeyType.or(keyType));
if (!unboxedKeyType.isPresent()) {
code.add(PreconditionExcerpts.checkNotNull("key"));
}
code.addLine(" %s.remove(key);", property.getName())
.addLine(" return (%s) this;", metadata.getBuilder())
.addLine("}");
}
private void addMutate(SourceBuilder code, Metadata metadata) {
ParameterizedType consumer = code.feature(FUNCTION_PACKAGE).consumer().orNull();
if (consumer == null) {
return;
}
code.addLine("")
.addLine("/**")
.addLine(" * Invokes {@code mutator} with the map to be returned from")
.addLine(" * %s.", metadata.getType().javadocNoArgMethodLink(property.getGetterName()))
.addLine(" *")
.addLine(" * <p>This method mutates the map in-place. {@code mutator} is a void")
.addLine(" * consumer, so any value returned from a lambda will be ignored. Take care")
.addLine(" * not to call pure functions, like %s.",
COLLECTION.javadocNoArgMethodLink("stream"))
.addLine(" *")
.addLine(" * @return this {@code Builder} object")
.addLine(" * @throws NullPointerException if {@code mutator} is null")
.addLine(" */")
.addLine("public %s %s(%s<? super %s<%s, %s>> mutator) {",
metadata.getBuilder(),
mutator(property),
consumer.getQualifiedName(),
Map.class,
keyType,
valueType);
if (overridesPutMethod) {
code.addLine(" mutator.accept(new CheckedMap<>(%s, this::%s));",
property.getName(), putMethod(property));
} else {
code.addLine(" // If %s is overridden, this method will be updated to delegate to it",
putMethod(property))
.addLine(" mutator.accept(%s);", property.getName());
}
code.addLine(" return (%s) this;", metadata.getBuilder())
.addLine("}");
}
private void addClear(SourceBuilder code, Metadata metadata) {
code.addLine("")
.addLine("/**")
.addLine(" * Removes all of the mappings from the map to be returned from ")
.addLine(" * %s.", metadata.getType().javadocNoArgMethodLink(property.getGetterName()))
.addLine(" *")
.addLine(" * @return this {@code %s} object", metadata.getBuilder().getSimpleName())
.addLine(" */")
.addLine("public %s %s() {", metadata.getBuilder(), clearMethod(property))
.addLine(" %s.clear();", property.getName())
.addLine(" return (%s) this;", metadata.getBuilder())
.addLine("}");
}
private void addGetter(SourceBuilder code, Metadata metadata) {
code.addLine("")
.addLine("/**")
.addLine(" * Returns an unmodifiable view of the map that will be returned by")
.addLine(" * %s.", metadata.getType().javadocNoArgMethodLink(property.getGetterName()))
.addLine(" * Changes to this builder will be reflected in the view.")
.addLine(" */")
.addLine("public %s<%s, %s> %s() {", Map.class, keyType, valueType, getter(property))
.addLine(" return %s.unmodifiableMap(%s);", Collections.class, property.getName())
.addLine("}");
}
@Override
public void addFinalFieldAssignment(SourceBuilder code, String finalField, String builder) {
code.add("%s = ", finalField);
if (code.feature(GUAVA).isAvailable()) {
code.add("%s.copyOf", ImmutableMap.class);
} else {
code.add("immutableMap");
}
code.add("(%s.%s);\n", builder, property.getName());
}
@Override
public void addMergeFromValue(Block code, String value) {
code.addLine("%s(%s.%s());", putAllMethod(property), value, property.getGetterName());
}
@Override
public void addMergeFromBuilder(Block code, String builder) {
code.addLine("%s(((%s) %s).%s);",
putAllMethod(property),
metadata.getGeneratedBuilder(),
builder,
property.getName());
}
@Override
public void addSetFromResult(SourceBuilder code, String builder, String variable) {
code.addLine("%s.%s(%s);", builder, putAllMethod(property), variable);
}
@Override
public void addClearField(Block code) {
code.addLine("%s.clear();", property.getName());
}
@Override
public Set<StaticExcerpt> getStaticExcerpts() {
ImmutableSet.Builder<StaticExcerpt> result = ImmutableSet.builder();
result.add(IMMUTABLE_MAP);
if (overridesPutMethod) {
result.addAll(CheckedMap.excerpts());
}
return result.build();
}
}
private static final StaticExcerpt IMMUTABLE_MAP = new StaticExcerpt(METHOD, "immutableMap") {
@Override
public void addTo(SourceBuilder code) {
if (!code.feature(GUAVA).isAvailable()) {
code.addLine("")
.addLine("private static <K, V> %1$s<K, V> immutableMap(%1$s<K, V> entries) {",
Map.class)
.addLine(" switch (entries.size()) {")
.addLine(" case 0:")
.addLine(" return %s.emptyMap();", Collections.class)
.addLine(" case 1:")
.addLine(" %s<K, V> entry = entries.entrySet().iterator().next();", Map.Entry.class)
.addLine(" return %s.singletonMap(entry.getKey(), entry.getValue());",
Collections.class)
.addLine(" default:")
.addLine(" return %s.unmodifiableMap(new %s%s(entries));",
Collections.class, LinkedHashMap.class, diamondOperator("K, V"))
.addLine(" }")
.addLine("}");
}
}
};
}