package io.norberg.automatter.processor;
import com.google.auto.service.AutoService;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import com.squareup.javapoet.AnnotationSpec;
import com.squareup.javapoet.ArrayTypeName;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.CodeBlock;
import com.squareup.javapoet.FieldSpec;
import com.squareup.javapoet.JavaFile;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.ParameterSpec;
import com.squareup.javapoet.ParameterizedTypeName;
import com.squareup.javapoet.TypeName;
import com.squareup.javapoet.TypeSpec;
import com.squareup.javapoet.TypeVariableName;
import com.squareup.javapoet.WildcardTypeName;
import org.modeshape.common.text.Inflector;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.annotation.Generated;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Filer;
import javax.annotation.processing.Messager;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.Processor;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;
import io.norberg.automatter.AutoMatter;
import static com.google.common.base.Preconditions.checkArgument;
import static com.squareup.javapoet.WildcardTypeName.subtypeOf;
import static javax.lang.model.element.Modifier.FINAL;
import static javax.lang.model.element.Modifier.PRIVATE;
import static javax.lang.model.element.Modifier.PUBLIC;
import static javax.lang.model.element.Modifier.STATIC;
import static javax.lang.model.type.TypeKind.ARRAY;
import static javax.lang.model.type.TypeKind.DECLARED;
import static javax.lang.model.type.TypeKind.TYPEVAR;
import static javax.tools.Diagnostic.Kind.ERROR;
/**
* An annotation processor that takes a value type defined as an interface with getter methods and
* materializes it, generating a concrete builder and value class.
*/
@AutoService(Processor.class)
public final class AutoMatterProcessor extends AbstractProcessor {
private static final Inflector INFLECTOR = new Inflector();
private static final Set<String> KEYWORDS = ImmutableSet.of(
"abstract", "continue", "for", "new", "switch", "assert", "default", "if", "package",
"synchronized", "boolean", "do", "goto", "private", "this", "break", "double", "implements",
"protected", "throw", "byte", "else", "import", "public", "throws", "case", "enum",
"instanceof", "return", "transient", "catch", "extends", "int", "short", "try", "char",
"final", "interface", "static", "void", "class", "finally", "long", "strictfp", "volatile",
"const", "float", "native", "super", "while");
private Filer filer;
private Elements elements;
private Messager messager;
private Types types;
@Override
public synchronized void init(final ProcessingEnvironment processingEnv) {
super.init(processingEnv);
filer = processingEnv.getFiler();
elements = processingEnv.getElementUtils();
types = processingEnv.getTypeUtils();
this.messager = processingEnv.getMessager();
}
@Override
public boolean process(final Set<? extends TypeElement> annotations,
final RoundEnvironment env) {
final Set<? extends Element> elements = env.getElementsAnnotatedWith(AutoMatter.class);
for (Element element : elements) {
try {
process(element);
} catch (IOException e) {
messager.printMessage(ERROR, e.getMessage());
} catch (AutoMatterProcessorException e) {
e.print(messager);
}
}
return false;
}
private void process(final Element element) throws IOException, AutoMatterProcessorException {
final Descriptor d = new Descriptor(element, elements, types);
TypeSpec builder = builder(d);
JavaFile javaFile = JavaFile.builder(d.packageName(), builder)
.skipJavaLangImports(true)
.build();
javaFile.writeTo(filer);
}
private TypeSpec builder(final Descriptor d) throws AutoMatterProcessorException {
AnnotationSpec generatedAnnotation = AnnotationSpec.builder(Generated.class)
.addMember("value", "$S", AutoMatterProcessor.class.getName())
.build();
TypeSpec.Builder builder = TypeSpec.classBuilder(d.builderName())
.addTypeVariables(d.typeVariables())
.addModifiers(FINAL)
.addAnnotation(generatedAnnotation);
if (d.isPublic()) {
builder.addModifiers(PUBLIC);
}
for (ExecutableElement field : d.fields()) {
builder.addField(FieldSpec.builder(fieldType(d, field), fieldName(field), PRIVATE).build());
}
builder.addMethod(defaultConstructor(d));
builder.addMethod(copyValueConstructor(d));
builder.addMethod(copyBuilderConstructor(d));
for (MethodSpec accessor : accessors(d)) {
builder.addMethod(accessor);
}
if (d.hasToBuilder()) {
builder.addMethod(toBuilder(d));
}
builder.addMethod(build(d));
builder.addMethod(fromValue(d));
builder.addMethod(fromBuilder(d));
builder.addType(valueClass(d));
return builder.build();
}
private MethodSpec defaultConstructor(final Descriptor d) {
MethodSpec.Builder constructor = MethodSpec.constructorBuilder()
.addModifiers(PUBLIC);
for (ExecutableElement field : d.fields()) {
if (isOptional(field) && shouldEnforceNonNull(field)) {
ClassName type = ClassName.bestGuess(optionalType(field));
constructor.addStatement("this.$N = $T.$L()", fieldName(field), type, optionalEmptyName(field));
}
}
return constructor.build();
}
private MethodSpec copyValueConstructor(final Descriptor d) throws AutoMatterProcessorException {
final MethodSpec.Builder constructor = MethodSpec.constructorBuilder()
.addModifiers(PRIVATE)
.addParameter(upperBoundedValueType(d), "v");
for (ExecutableElement field : d.fields()) {
String fieldName = fieldName(field);
if (isCollection(field) || isMap(field)) {
TypeName fieldType = upperBoundedFieldType(field);
constructor.addStatement("$T _$N = v.$N()", fieldType, fieldName, fieldName);
constructor.addStatement(
"this.$N = (_$N == null) ? null : new $T(_$N)",
fieldName, fieldName, collectionImplType(field), fieldName);
} else {
if (isFieldTypeParameterized(field)) {
TypeName fieldType = fieldType(d, field);
constructor.addStatement("@SuppressWarnings(\"unchecked\") $T _$N = ($T) v.$N()",
fieldType, fieldName, fieldType, fieldName);
constructor.addStatement("this.$N = _$N", fieldName, fieldName);
} else {
constructor.addStatement("this.$N = v.$N()", fieldName, fieldName);
}
}
}
return constructor.build();
}
private boolean isFieldTypeParameterized(final ExecutableElement field) {
final TypeMirror returnType = field.getReturnType();
if (returnType.getKind() != DECLARED) {
return false;
}
final DeclaredType declaredType = (DeclaredType) returnType;
for (final TypeMirror typeArgument : declaredType.getTypeArguments()) {
if (typeArgument.getKind() == TYPEVAR) {
return true;
}
}
return false;
}
private MethodSpec copyBuilderConstructor(final Descriptor d) throws AutoMatterProcessorException {
final MethodSpec.Builder constructor = MethodSpec.constructorBuilder()
.addModifiers(PRIVATE)
.addParameter(upperBoundedBuilderType(d), "v");
for (ExecutableElement field : d.fields()) {
String fieldName = fieldName(field);
if (isCollection(field) || isMap(field)) {
constructor.addStatement(
"this.$N = (v.$N == null) ? null : new $T(v.$N)",
fieldName, fieldName, collectionImplType(field), fieldName);
} else {
if (isFieldTypeParameterized(field)) {
TypeName fieldType = fieldType(d, field);
constructor.addStatement("@SuppressWarnings(\"unchecked\") $T _$N = ($T) v.$N()",
fieldType, fieldName, fieldType, fieldName);
constructor.addStatement("this.$N = _$N", fieldName, fieldName);
} else {
constructor.addStatement("this.$N = v.$N", fieldName, fieldName);
}
}
}
return constructor.build();
}
private Set<MethodSpec> accessors(final Descriptor d) throws AutoMatterProcessorException {
ImmutableSet.Builder<MethodSpec> result = ImmutableSet.builder();
for (ExecutableElement field : d.fields()) {
result.add(getter(d, field));
if (isOptional(field)) {
result.add(optionalRawSetter(d, field));
result.add(optionalSetter(d, field));
} else if (isCollection(field)) {
result.add(collectionSetter(d, field));
result.add(collectionCollectionSetter(d, field));
result.add(collectionIterableSetter(d, field));
result.add(collectionIteratorSetter(d, field));
result.add(collectionVarargSetter(d, field));
MethodSpec adder = collectionAdder(d, field);
if (adder != null) {
result.add(adder);
}
} else if (isMap(field)) {
result.add(mapSetter(d, field));
for (int i = 1; i <= 5; i++) {
result.add(mapSetterPairs(d, field, i));
}
MethodSpec putter = mapPutter(d, field);
if (putter != null) {
result.add(putter);
}
} else {
result.add(setter(d, field));
}
}
return result.build();
}
private MethodSpec getter(final Descriptor d, final ExecutableElement field) throws AutoMatterProcessorException {
String fieldName = fieldName(field);
MethodSpec.Builder getter = MethodSpec.methodBuilder(fieldName)
.addModifiers(PUBLIC)
.returns(fieldType(d, field));
if ((isCollection(field) || isMap(field)) && shouldEnforceNonNull(field)) {
getter.beginControlFlow("if (this.$N == null)", fieldName)
.addStatement("this.$N = new $T()", fieldName, collectionImplType(field))
.endControlFlow();
}
getter.addStatement("return $N", fieldName);
return getter.build();
}
private MethodSpec optionalRawSetter(final Descriptor d, final ExecutableElement field) {
String fieldName = fieldName(field);
ClassName type = ClassName.bestGuess(optionalType(field));
TypeName valueType = genericArgument(field, 0);
return MethodSpec.methodBuilder(fieldName)
.addModifiers(PUBLIC)
.addParameter(valueType, fieldName)
.returns(builderType(d))
.addStatement("return $N($T.$N($N))", fieldName, type, optionalMaybeName(field), fieldName)
.build();
}
private MethodSpec optionalSetter(final Descriptor d, final ExecutableElement field)
throws AutoMatterProcessorException {
String fieldName = fieldName(field);
TypeName valueType = genericArgument(field, 0);
ClassName optionalType = ClassName.bestGuess(optionalType(field));
TypeName parameterType = ParameterizedTypeName.get(optionalType, subtypeOf(valueType));
AnnotationSpec suppressUncheckedAnnotation = AnnotationSpec.builder(SuppressWarnings.class)
.addMember("value", "$S", "unchecked")
.build();
MethodSpec.Builder setter = MethodSpec.methodBuilder(fieldName)
.addAnnotation(suppressUncheckedAnnotation)
.addModifiers(PUBLIC)
.addParameter(parameterType, fieldName)
.returns(builderType(d));
if (shouldEnforceNonNull(field)) {
assertNotNull(setter, fieldName);
}
setter.addStatement("this.$N = ($T)$N", fieldName, fieldType(d, field), fieldName);
return setter.addStatement("return this").build();
}
private MethodSpec collectionSetter(final Descriptor d, final ExecutableElement field) {
String fieldName = fieldName(field);
ClassName collectionType = collectionRawType(field);
TypeName itemType = genericArgument(field, 0);
WildcardTypeName extendedType = subtypeOf(itemType);
return MethodSpec.methodBuilder(fieldName)
.addModifiers(PUBLIC)
.addParameter(ParameterizedTypeName.get(collectionType, extendedType), fieldName)
.returns(builderType(d))
.addStatement("return $N((Collection<$T>) $N)", fieldName, extendedType, fieldName)
.build();
}
private MethodSpec collectionCollectionSetter(final Descriptor d, final ExecutableElement field) {
String fieldName = fieldName(field);
ClassName collectionType = ClassName.get(Collection.class);
TypeName itemType = genericArgument(field, 0);
WildcardTypeName extendedType = subtypeOf(itemType);
MethodSpec.Builder setter = MethodSpec.methodBuilder(fieldName)
.addModifiers(PUBLIC)
.addParameter(ParameterizedTypeName.get(collectionType, extendedType), fieldName)
.returns(builderType(d));
collectionNullGuard(setter, field);
if (shouldEnforceNonNull(field)) {
setter.beginControlFlow("for ($T item : $N)", itemType, fieldName);
assertNotNull(setter, "item", fieldName + ": null item");
setter.endControlFlow();
}
setter.addStatement("this.$N = new $T($N)", fieldName, collectionImplType(field), fieldName);
return setter.addStatement("return this").build();
}
private MethodSpec collectionIterableSetter(final Descriptor d, final ExecutableElement field) {
String fieldName = fieldName(field);
ClassName iterableType = ClassName.get(Iterable.class);
TypeName itemType = genericArgument(field, 0);
WildcardTypeName extendedType = subtypeOf(itemType);
MethodSpec.Builder setter = MethodSpec.methodBuilder(fieldName)
.addModifiers(PUBLIC)
.addParameter(ParameterizedTypeName.get(iterableType, extendedType), fieldName)
.returns(builderType(d));
collectionNullGuard(setter, field);
ClassName collectionType = ClassName.get(Collection.class);
setter.beginControlFlow("if ($N instanceof $T)", fieldName, collectionType)
.addStatement("return $N(($T<$T>) $N)", fieldName, collectionType, extendedType, fieldName)
.endControlFlow();
setter.addStatement("return $N($N.iterator())", fieldName, fieldName);
return setter.build();
}
private MethodSpec collectionIteratorSetter(final Descriptor d, final ExecutableElement field) {
String fieldName = fieldName(field);
ClassName iteratorType = ClassName.get(Iterator.class);
TypeName itemType = genericArgument(field, 0);
WildcardTypeName extendedType = subtypeOf(itemType);
MethodSpec.Builder setter = MethodSpec.methodBuilder(fieldName)
.addModifiers(PUBLIC)
.addParameter(ParameterizedTypeName.get(iteratorType, extendedType), fieldName)
.returns(builderType(d));
collectionNullGuard(setter, field);
setter.addStatement("this.$N = new $T()", fieldName, collectionImplType(field))
.beginControlFlow("while ($N.hasNext())", fieldName)
.addStatement("$T item = $N.next()", itemType, fieldName);
if (shouldEnforceNonNull(field)) {
assertNotNull(setter, "item", fieldName + ": null item");
}
setter.addStatement("this.$N.add(item)", fieldName)
.endControlFlow();
return setter.addStatement("return this").build();
}
private MethodSpec collectionVarargSetter(final Descriptor d, final ExecutableElement field) {
String fieldName = fieldName(field);
TypeName itemType = genericArgument(field, 0);
MethodSpec.Builder setter = MethodSpec.methodBuilder(fieldName)
.addModifiers(PUBLIC)
.addParameter(ArrayTypeName.of(itemType), fieldName)
.varargs()
.returns(builderType(d));
ensureSafeVarargs(setter);
collectionNullGuard(setter, field);
setter.addStatement("return $N($T.asList($N))", fieldName, ClassName.get(Arrays.class), fieldName);
return setter.build();
}
private void ensureSafeVarargs(MethodSpec.Builder setter) {
// TODO: Add SafeVarargs annotation only for non-reifiable types.
AnnotationSpec safeVarargsAnnotation = AnnotationSpec.builder(SafeVarargs.class).build();
setter
.addAnnotation(safeVarargsAnnotation)
.addModifiers(FINAL); // Only because SafeVarargs can be applied to final methods.
}
private MethodSpec collectionAdder(final Descriptor d, final ExecutableElement field) {
final String fieldName = fieldName(field);
final String singular = singular(fieldName);
if (singular == null || singular.isEmpty()) {
return null;
}
final String appendMethodName = "add" + capitalizeFirstLetter(singular);
final TypeName itemType = genericArgument(field, 0);
MethodSpec.Builder adder = MethodSpec.methodBuilder(appendMethodName)
.addModifiers(PUBLIC)
.addParameter(itemType, singular)
.returns(builderType(d));
if (shouldEnforceNonNull(field)) {
assertNotNull(adder, singular);
}
lazyCollectionInitialization(adder, field);
adder.addStatement("$L.add($L)", fieldName, singular);
return adder.addStatement("return this").build();
}
private void collectionNullGuard(final MethodSpec.Builder spec, final ExecutableElement field) {
String fieldName = fieldName(field);
if (shouldEnforceNonNull(field)) {
assertNotNull(spec, fieldName);
} else {
spec.beginControlFlow("if ($N == null)", fieldName)
.addStatement("this.$N = null", fieldName)
.addStatement("return this")
.endControlFlow();
}
}
private void lazyCollectionInitialization(final MethodSpec.Builder spec, final ExecutableElement field) {
final String fieldName = fieldName(field);
spec.beginControlFlow("if (this.$N == null)", fieldName)
.addStatement("this.$N = new $T()", fieldName, collectionImplType(field))
.endControlFlow();
}
private MethodSpec mapSetter(final Descriptor d, final ExecutableElement field) {
final String fieldName = fieldName(field);
final TypeName keyType = subtypeOf(genericArgument(field, 0));
final TypeName valueType = subtypeOf(genericArgument(field, 1));
final TypeName paramType = ParameterizedTypeName.get(ClassName.get(Map.class), keyType, valueType);
MethodSpec.Builder setter = MethodSpec.methodBuilder(fieldName)
.addModifiers(PUBLIC)
.addParameter(paramType, fieldName)
.returns(builderType(d));
if (shouldEnforceNonNull(field)) {
final String entryName = variableName("entry", fieldName);
assertNotNull(setter, fieldName);
setter.beginControlFlow(
"for ($T<$T, $T> $L : $N.entrySet())",
ClassName.get(Map.Entry.class), keyType, valueType, entryName, fieldName);
assertNotNull(setter, entryName + ".getKey()", fieldName + ": null key");
assertNotNull(setter, entryName + ".getValue()", fieldName + ": null value");
setter.endControlFlow();
} else {
setter.beginControlFlow("if ($N == null)", fieldName)
.addStatement("this.$N = null", fieldName)
.addStatement("return this")
.endControlFlow();
}
setter.addStatement("this.$N = new $T($N)", fieldName, collectionImplType(field), fieldName);
return setter.addStatement("return this").build();
}
private MethodSpec mapSetterPairs(final Descriptor d, final ExecutableElement field, int entries) {
checkArgument(entries > 0, "entries");
final String fieldName = fieldName(field);
final TypeName keyType = genericArgument(field, 0);
final TypeName valueType = genericArgument(field, 1);
MethodSpec.Builder setter = MethodSpec.methodBuilder(fieldName)
.addModifiers(PUBLIC)
.returns(builderType(d));
for (int i = 1; i < entries + 1; i++) {
setter.addParameter(keyType, "k" + i);
setter.addParameter(valueType, "v" + i);
}
// Recursion
if (entries > 1) {
final List<String> recursionParameters = Lists.newArrayList();
for (int i = 1; i < entries; i++) {
recursionParameters.add("k" + i);
recursionParameters.add("v" + i);
}
setter.addStatement("$L($L)", fieldName, Joiner.on(", ").join(recursionParameters));
}
// Null checks
final String keyName = "k" + entries;
final String valueName = "v" + entries;
if (shouldEnforceNonNull(field)) {
assertNotNull(setter, keyName, fieldName + ": " + keyName);
assertNotNull(setter, valueName, fieldName + ": " + valueName);
}
// Map instantiation
if (entries == 1) {
setter.addStatement("$N = new $T()", fieldName, collectionImplType(field));
}
// Put
setter.addStatement("$N.put($N, $N)", fieldName, keyName, valueName);
return setter.addStatement("return this").build();
}
private MethodSpec mapPutter(final Descriptor d, final ExecutableElement field) {
final String fieldName = fieldName(field);
final String singular = singular(fieldName);
if (singular == null) {
return null;
}
final String putSingular = "put" + capitalizeFirstLetter(singular);
final TypeName keyType = genericArgument(field, 0);
final TypeName valueType = genericArgument(field, 1);
MethodSpec.Builder setter = MethodSpec.methodBuilder(putSingular)
.addModifiers(PUBLIC)
.addParameter(keyType, "key")
.addParameter(valueType, "value")
.returns(builderType(d));
// Null checks
if (shouldEnforceNonNull(field)) {
assertNotNull(setter, "key", singular + ": key");
assertNotNull(setter, "value", singular + ": value");
}
// Put
lazMapInitialization(setter, field);
setter.addStatement("$N.put(key, value)", fieldName);
return setter.addStatement("return this").build();
}
private void lazMapInitialization(final MethodSpec.Builder spec, final ExecutableElement field) {
final String fieldName = fieldName(field);
spec.beginControlFlow("if (this.$N == null)", fieldName)
.addStatement("this.$N = new $T()", fieldName, collectionImplType(field))
.endControlFlow();
}
private MethodSpec setter(final Descriptor d, final ExecutableElement field) throws AutoMatterProcessorException {
String fieldName = fieldName(field);
ParameterSpec.Builder parameterSpecBuilder =
ParameterSpec.builder(fieldType(d, field), fieldName);
if (!isPrimitive(field)) {
AnnotationMirror nullableAnnotation = nullableAnnotation(field);
if (nullableAnnotation != null) {
parameterSpecBuilder.addAnnotation(AnnotationSpec.get(nullableAnnotation));
}
}
MethodSpec.Builder setter = MethodSpec.methodBuilder(fieldName)
.addModifiers(PUBLIC)
.addParameter(parameterSpecBuilder.build())
.returns(builderType(d));
if (shouldEnforceNonNull(field)) {
assertNotNull(setter, fieldName);
}
setter.addStatement("this.$N = $N", fieldName, fieldName);
return setter.addStatement("return this").build();
}
private MethodSpec toBuilder(final Descriptor d) {
return MethodSpec.methodBuilder("builder")
.addModifiers(PUBLIC)
.returns(builderType(d))
.addStatement("return new $T(this)", builderType(d))
.build();
}
private MethodSpec build(final Descriptor d) throws AutoMatterProcessorException {
MethodSpec.Builder build = MethodSpec.methodBuilder("build")
.addModifiers(PUBLIC)
.returns(valueType(d));
final List<String> parameters = Lists.newArrayList();
for (ExecutableElement field : d.fields()) {
final String fieldName = fieldName(field);
final TypeName fieldType = fieldType(d, field);
final ClassName collections = ClassName.get(Collections.class);
if (isCollection(field)) {
final TypeName itemType = genericArgument(field, 0);
if (shouldEnforceNonNull(field)) {
build.addStatement(
"$T _$L = ($L != null) ? $T.$L(new $T($N)) : $T.<$T>$L()",
fieldType, fieldName, fieldName,
collections, unmodifiableCollection(field), collectionImplType(field), fieldName,
collections, itemType, emptyCollection(field));
} else {
build.addStatement(
"$T _$L = ($L != null) ? $T.$L(new $T($N)) : null",
fieldType, fieldName, fieldName,
collections, unmodifiableCollection(field), collectionImplType(field), fieldName);
}
parameters.add("_" + fieldName);
} else if (isMap(field)) {
final TypeName keyType = genericArgument(field, 0);
final TypeName valueType = genericArgument(field, 1);
if (shouldEnforceNonNull(field)) {
build.addStatement(
"$T _$L = ($L != null) ? $T.unmodifiableMap(new $T($N)) : $T.<$T, $T>emptyMap()",
fieldType, fieldName, fieldName,
collections, collectionImplType(field), fieldName,
collections, keyType, valueType);
} else {
build.addStatement(
"$T _$L = ($L != null) ? $T.unmodifiableMap(new $T($N)) : null",
fieldType, fieldName, fieldName,
collections, collectionImplType(field), fieldName);
}
parameters.add("_" + fieldName);
} else {
parameters.add(fieldName(field));
}
}
return build.addStatement("return new $T($N)", valueImplType(d), Joiner.on(", ").join(parameters)).build();
}
private MethodSpec fromValue(final Descriptor d) {
return MethodSpec.methodBuilder("from")
.addModifiers(PUBLIC, STATIC)
.addTypeVariables(d.typeVariables())
.addParameter(upperBoundedValueType(d), "v")
.returns(builderType(d))
.addStatement("return new $T(v)", builderType(d))
.build();
}
private MethodSpec fromBuilder(final Descriptor d) {
return MethodSpec.methodBuilder("from")
.addModifiers(PUBLIC, STATIC)
.addTypeVariables(d.typeVariables())
.addParameter(upperBoundedBuilderType(d), "v")
.returns(builderType(d))
.addStatement("return new $T(v)", builderType(d))
.build();
}
private TypeSpec valueClass(final Descriptor d) throws AutoMatterProcessorException {
TypeSpec.Builder value = TypeSpec.classBuilder("Value")
.addTypeVariables(d.typeVariables())
.addModifiers(PRIVATE, STATIC, FINAL)
.addSuperinterface(valueType(d));
for (ExecutableElement field : d.fields()) {
value.addField(FieldSpec.builder(fieldType(d, field), fieldName(field), PRIVATE, FINAL).build());
}
value.addMethod(valueConstructor(d));
for (ExecutableElement field : d.fields()) {
value.addMethod(valueGetter(d, field));
}
value.addMethod(valueToBuilder(d));
value.addMethod(valueEquals(d));
value.addMethod(valueHashCode(d));
value.addMethod(valueToString(d));
return value.build();
}
private MethodSpec valueConstructor(final Descriptor d) throws AutoMatterProcessorException {
MethodSpec.Builder constructor = MethodSpec.constructorBuilder()
.addModifiers(PRIVATE);
for (ExecutableElement field : d.fields()) {
if (shouldEnforceNonNull(field) && !isCollection(field) && !isMap(field)) {
assertNotNull(constructor, fieldName(field));
}
}
for (ExecutableElement field : d.fields()) {
String fieldName = fieldName(field);
AnnotationSpec annotation = AnnotationSpec.builder(AutoMatter.Field.class)
.addMember("value", "$S", fieldName)
.build();
ParameterSpec parameter = ParameterSpec.builder(fieldType(d, field), fieldName)
.addAnnotation(annotation)
.build();
constructor.addParameter(parameter);
final ClassName collectionsType = ClassName.get(Collections.class);
if (shouldEnforceNonNull(field) && isCollection(field)) {
final TypeName itemType = genericArgument(field, 0);
constructor.addStatement(
"this.$N = ($N != null) ? $N : $T.<$T>$L()",
fieldName, fieldName, fieldName, collectionsType, itemType, emptyCollection(field));
} else if (shouldEnforceNonNull(field) && isMap(field)) {
final TypeName keyType = genericArgument(field, 0);
final TypeName valueType = genericArgument(field, 1);
constructor.addStatement(
"this.$N = ($N != null) ? $N : $T.<$T, $T>emptyMap()",
fieldName, fieldName, fieldName, collectionsType, keyType, valueType);
} else {
constructor.addStatement("this.$N = $N", fieldName, fieldName);
}
}
return constructor.build();
}
private MethodSpec valueGetter(final Descriptor d, final ExecutableElement field) throws AutoMatterProcessorException {
String fieldName = fieldName(field);
return MethodSpec.methodBuilder(fieldName)
.addAnnotation(AutoMatter.Field.class)
.addAnnotation(Override.class)
.addModifiers(PUBLIC)
.returns(fieldType(d, field))
.addStatement("return $N", fieldName)
.build();
}
private MethodSpec valueToBuilder(final Descriptor d) {
MethodSpec.Builder toBuilder = MethodSpec.methodBuilder("builder")
.addModifiers(PUBLIC)
.returns(builderType(d))
.addStatement("return new $T(this)", builderType(d));
// Always emit toBuilder, but only annotate it with @Override if the target asked for it.
if (d.hasToBuilder()) {
toBuilder.addAnnotation(Override.class);
}
return toBuilder.build();
}
private MethodSpec valueEquals(final Descriptor d) throws AutoMatterProcessorException {
MethodSpec.Builder equals = MethodSpec.methodBuilder("equals")
.addAnnotation(Override.class)
.addModifiers(PUBLIC)
.addParameter(ClassName.get(Object.class), "o")
.returns(TypeName.BOOLEAN);
equals.beginControlFlow("if (this == o)")
.addStatement("return true")
.endControlFlow();
equals.beginControlFlow("if (!(o instanceof $T))", rawValueType(d))
.addStatement("return false")
.endControlFlow();
if (!d.fields().isEmpty()) {
equals.addStatement("final $T that = ($T) o", unboundedValueType(d), unboundedValueType(d));
for (ExecutableElement field : d.fields()) {
equals.addCode(fieldNotEqualCheck(field));
}
}
return equals.addStatement("return true").build();
}
private CodeBlock fieldNotEqualCheck(final ExecutableElement field) throws AutoMatterProcessorException {
final String name = fieldName(field);
final CodeBlock.Builder result = CodeBlock.builder();
final TypeMirror returnType = field.getReturnType();
switch (returnType.getKind()) {
case LONG:
case INT:
case BOOLEAN:
case BYTE:
case SHORT:
case CHAR:
result.beginControlFlow("if ($L != that.$L())", name, name);
break;
case FLOAT:
case DOUBLE:
final TypeName boxed = ClassName.get(returnType).box();
result.beginControlFlow("if ($T.compare($L, that.$L()) != 0)", boxed, name, name);
break;
case ARRAY:
result.beginControlFlow("if (!$T.equals($L, that.$L()))", ClassName.get(Arrays.class), name, name);
break;
case TYPEVAR:
case DECLARED:
result.beginControlFlow(
"if ($L != null ? !$L.equals(that.$L()) : that.$L() != null)",
name, name, name, name);
break;
case ERROR:
throw fail("Cannot resolve type, might be missing import: " + returnType, field);
default:
throw fail("Unsupported type: " + returnType, field);
}
result.addStatement("return false").endControlFlow();
return result.build();
}
private MethodSpec valueHashCode(final Descriptor d) throws AutoMatterProcessorException {
MethodSpec.Builder hashcode = MethodSpec.methodBuilder("hashCode")
.addAnnotation(Override.class)
.addModifiers(PUBLIC)
.returns(TypeName.INT)
.addStatement("int result = 1")
.addStatement("long temp");
for (ExecutableElement field : d.fields()) {
final String name = "this." + fieldName(field);
final TypeMirror type = field.getReturnType();
switch (type.getKind()) {
case LONG:
hashcode.addStatement("result = 31 * result + (int) ($N ^ ($N >>> 32))", name, name);
break;
case INT:
hashcode.addStatement("result = 31 * result + $N", name);
break;
case BOOLEAN:
hashcode.addStatement("result = 31 * result + ($N ? 1231 : 1237)", name);
break;
case BYTE:
case SHORT:
case CHAR:
hashcode.addStatement("result = 31 * result + (int) $N", name);
break;
case FLOAT:
hashcode.addStatement(
"result = 31 * result + ($N != +0.0f ? $T.floatToIntBits($N) : 0)",
name, ClassName.get(Float.class), name);
break;
case DOUBLE:
hashcode.addStatement("temp = $T.doubleToLongBits($N)", ClassName.get(Double.class), name);
hashcode.addStatement("result = 31 * result + (int) (temp ^ (temp >>> 32))");
break;
case ARRAY:
hashcode.addStatement(
"result = 31 * result + ($N != null ? $T.hashCode($N) : 0)",
name, ClassName.get(Arrays.class), name);
break;
case TYPEVAR:
case DECLARED:
hashcode.addStatement("result = 31 * result + ($N != null ? $N.hashCode() : 0)", name, name);
break;
case ERROR:
throw fail("Cannot resolve type, might be missing import: " + type, field);
default:
throw fail("Unsupported type: " + type, field);
}
}
return hashcode.addStatement("return result").build();
}
private MethodSpec valueToString(final Descriptor d) {
MethodSpec.Builder toString = MethodSpec.methodBuilder("toString")
.addAnnotation(Override.class)
.addModifiers(PUBLIC)
.returns(ClassName.get(String.class));
toString.addCode("return \"$L{\" +\n", d.valueTypeName());
for (int i = 0; i < d.fields().size(); i++) {
final ExecutableElement field = d.fields().get(i);
final String comma = (i == 0) ? "" : ", ";
final String name = fieldName(field);
if (field.getReturnType().getKind() == ARRAY) {
toString.addCode("\"$L$L=\" + $T.toString($L) +\n", comma, name, ClassName.get(Arrays.class), name);
} else {
toString.addCode("\"$L$L=\" + $L +\n", comma, name, name);
}
}
toString.addStatement("'}'");
return toString.build();
}
private void assertNotNull(MethodSpec.Builder spec, String name) {
assertNotNull(spec, name, name);
}
private void assertNotNull(MethodSpec.Builder spec, String name, String msg) {
spec.beginControlFlow("if ($N == null)", name)
.addStatement("throw new $T($S)", ClassName.get(NullPointerException.class), msg)
.endControlFlow();
}
private TypeName builderType(final Descriptor d) {
final ClassName raw = rawBuilderType(d);
if (!d.isGeneric()) {
return raw;
}
return ParameterizedTypeName.get(raw, d.typeArguments());
}
private TypeName upperBoundedBuilderType(final Descriptor d) {
final ClassName raw = rawBuilderType(d);
if (!d.isGeneric()) {
return raw;
}
return ParameterizedTypeName.get(raw, upperBounded(d.typeVariables()));
}
private TypeName[] upperBounded(final List<TypeVariableName> typeVariables) {
final TypeName[] typeNames = new TypeName[typeVariables.size()];
for (int i = 0; i < typeVariables.size(); i++) {
typeNames[i] = subtypeOf(typeVariables.get(i));
}
return typeNames;
}
private ClassName rawBuilderType(final Descriptor d) {
return ClassName.get(d.packageName(), d.builderName());
}
private ClassName rawValueType(final Descriptor d) {
return ClassName.get(d.packageName(), d.valueTypeName());
}
private TypeName unboundedValueType(final Descriptor d) {
final ClassName raw = rawValueType(d);
if (!d.isGeneric()) {
return raw;
}
return ParameterizedTypeName.get(raw, wildcards(d.typeVariables().size()));
}
private TypeName[] wildcards(final int size) {
final WildcardTypeName wildcard = subtypeOf(ClassName.get(Object.class));
final TypeName[] wildcards = new TypeName[size];
for (int i = 0; i < size; i++) {
wildcards[i] = wildcard;
}
return wildcards;
}
private TypeName valueType(final Descriptor d) {
final ClassName raw = rawValueType(d);
if (!d.isGeneric()) {
return raw;
}
return ParameterizedTypeName.get(raw, d.typeArguments());
}
private TypeName upperBoundedValueType(final Descriptor d) {
final ClassName raw = rawValueType(d);
if (!d.isGeneric()) {
return raw;
}
return ParameterizedTypeName.get(raw, upperBounded(d.typeVariables()));
}
private TypeName valueImplType(final Descriptor d) {
final ClassName raw = rawValueImplType(d);
if (!d.isGeneric()) {
return raw;
}
return ParameterizedTypeName.get(raw, d.typeArguments());
}
private ClassName rawValueImplType(final Descriptor d) {
return rawBuilderType(d).nestedClass("Value");
}
private TypeName fieldType(final Descriptor d, final ExecutableElement field) throws AutoMatterProcessorException {
final TypeMirror returnType = field.getReturnType();
if (returnType.getKind() == TypeKind.ERROR) {
throw fail("Cannot resolve type, might be missing import: " + returnType, field);
}
final TypeMirror fieldType = d.fieldTypes().get(field);
return TypeName.get(fieldType);
}
private TypeName upperBoundedFieldType(final ExecutableElement field) throws AutoMatterProcessorException {
TypeMirror type = field.getReturnType();
if (type.getKind() == TypeKind.ERROR) {
throw fail("Cannot resolve type, might be missing import: " + type, field);
}
if (type.getKind() != DECLARED) {
return TypeName.get(type);
}
final DeclaredType declaredType = (DeclaredType) type;
if (declaredType.getTypeArguments().isEmpty()) {
return TypeName.get(type);
}
final ClassName raw = rawClassName(declaredType);
if (isOptional(field) || isCollection(field)) {
final TypeName elementType = TypeName.get(declaredType.getTypeArguments().get(0));
return ParameterizedTypeName.get(raw, subtypeOf(elementType));
} else if (isMap(field)) {
final TypeName keyTypeArgument = TypeName.get(declaredType.getTypeArguments().get(0));
final TypeName valueTypeArgument = TypeName.get(declaredType.getTypeArguments().get(1));
return ParameterizedTypeName.get(raw, subtypeOf(keyTypeArgument), subtypeOf(valueTypeArgument));
}
return TypeName.get(type);
}
private ClassName rawClassName(final DeclaredType declaredType) {
final String simpleName = declaredType.asElement().getSimpleName().toString();
final String packageName = packageName(declaredType);
return ClassName.get(packageName, simpleName);
}
private String packageName(final DeclaredType declaredType) {
Element type = declaredType.asElement();
while (type.getKind() != ElementKind.PACKAGE) {
type = type.getEnclosingElement();
}
return type.toString();
}
private TypeName genericArgument(final ExecutableElement field, int index) {
final DeclaredType type = (DeclaredType) field.getReturnType();
checkArgument(type.getTypeArguments().size() >= index);
return TypeName.get(type.getTypeArguments().get(index));
}
private TypeName collectionImplType(final ExecutableElement field) {
switch (collectionType(field)) {
case "List":
return ParameterizedTypeName.get(
ClassName.get(ArrayList.class),
genericArgument(field, 0));
case "Set":
return ParameterizedTypeName.get(
ClassName.get(HashSet.class),
genericArgument(field, 0));
case "Map":
return ParameterizedTypeName.get(
ClassName.get(HashMap.class),
genericArgument(field, 0), genericArgument(field, 1));
default:
throw new IllegalStateException("invalid collection type " + field);
}
}
private ClassName collectionRawType(final ExecutableElement field) {
final DeclaredType type = (DeclaredType) field.getReturnType();
return ClassName.get("java.util", type.asElement().getSimpleName().toString());
}
private static String optionalEmptyName(final ExecutableElement field) {
final String returnType = field.getReturnType().toString();
if (returnType.startsWith("com.google.common.base.Optional<")) {
return "absent";
}
return "empty";
}
private static String optionalMaybeName(final ExecutableElement field) {
final String returnType = field.getReturnType().toString();
if (returnType.startsWith("com.google.common.base.Optional<")) {
return "fromNullable";
}
return "ofNullable";
}
private boolean isCollection(final ExecutableElement field) {
final String returnType = field.getReturnType().toString();
return returnType.startsWith("java.util.List<") ||
returnType.startsWith("java.util.Set<");
}
private String unmodifiableCollection(final ExecutableElement field) {
final String type = collectionType(field);
switch (type) {
case "List":
return "unmodifiableList";
case "Set":
return "unmodifiableSet";
case "Map":
return "unmodifiableMap";
default:
throw new AssertionError();
}
}
private String emptyCollection(final ExecutableElement field) {
final String type = collectionType(field);
switch (type) {
case "List":
return "emptyList";
case "Set":
return "emptySet";
case "Map":
return "emptyMap";
default:
throw new AssertionError();
}
}
private String collectionType(final ExecutableElement field) {
final String returnType = field.getReturnType().toString();
if (returnType.startsWith("java.util.List<")) {
return "List";
} else if (returnType.startsWith("java.util.Set<")) {
return "Set";
} else if (returnType.startsWith("java.util.Map<")) {
return "Map";
} else {
throw new AssertionError();
}
}
private String optionalType(final ExecutableElement field) {
final String returnType = field.getReturnType().toString();
if (returnType.startsWith("java.util.Optional<")) {
return "java.util.Optional";
} else if (returnType.startsWith("com.google.common.base.Optional<")) {
return "com.google.common.base.Optional";
}
return returnType;
}
private boolean isMap(final ExecutableElement field) {
final String returnType = field.getReturnType().toString();
return returnType.startsWith("java.util.Map<");
}
private boolean isPrimitive(final ExecutableElement field) {
return field.getReturnType().getKind().isPrimitive();
}
private boolean isOptional(final ExecutableElement field) {
final String returnType = field.getReturnType().toString();
return returnType.startsWith("java.util.Optional<") ||
returnType.startsWith("com.google.common.base.Optional<");
}
private String singular(final String name) {
final String singular = INFLECTOR.singularize(name);
if (KEYWORDS.contains(singular)) {
return null;
}
if (elements.getTypeElement("java.lang." + singular) != null) {
return null;
}
return name.equals(singular) ? null : singular;
}
private String variableName(final String name, final String... scope) {
return variableName(name, ImmutableSet.copyOf(scope));
}
private String variableName(final String name, final Set<String> scope) {
if (!scope.contains(name)) {
return name;
}
return variableName("_" + name, scope);
}
private String fieldName(final ExecutableElement field) {
return field.getSimpleName().toString();
}
@Override
public Set<String> getSupportedAnnotationTypes() {
return ImmutableSet.of(AutoMatter.class.getName());
}
private boolean shouldEnforceNonNull(final ExecutableElement field) {
return !isPrimitive(field) && !isNullableAnnotated(field);
}
private boolean isNullableAnnotated(final ExecutableElement field) {
return nullableAnnotation(field) != null;
}
private AnnotationMirror nullableAnnotation(final ExecutableElement field) {
for (AnnotationMirror annotation : field.getAnnotationMirrors()) {
if (annotation.getAnnotationType().asElement().getSimpleName().contentEquals("Nullable")) {
return annotation;
}
}
return null;
}
private AutoMatterProcessorException fail(final String msg, final Element element)
throws AutoMatterProcessorException {
throw new AutoMatterProcessorException(msg, element);
}
private static String capitalizeFirstLetter(String s) {
if (s == null) {
throw new NullPointerException("s");
}
if (s.isEmpty()) {
return "";
}
return s.substring(0, 1).toUpperCase() + (s.length() > 1 ? s.substring(1) : "");
}
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
}