diff --git a/.travis.yml b/.travis.yml index f0845000..110ecffd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,6 @@ language: java jdk: - oraclejdk8 - - oraclejdk7 - - openjdk7 script: - mvn test - "./jackson-it.sh" diff --git a/pom.xml b/pom.xml index efdc7f19..29b34e90 100644 --- a/pom.xml +++ b/pom.xml @@ -60,8 +60,8 @@ 3.3 false - 1.7 - 1.7 + 1.8 + 1.8 -Xlint:all diff --git a/processor/src/main/java/io/norberg/automatter/processor/AutoMatterProcessor.java b/processor/src/main/java/io/norberg/automatter/processor/AutoMatterProcessor.java index cd49bde7..f261fc69 100644 --- a/processor/src/main/java/io/norberg/automatter/processor/AutoMatterProcessor.java +++ b/processor/src/main/java/io/norberg/automatter/processor/AutoMatterProcessor.java @@ -2,14 +2,14 @@ import com.google.auto.service.AutoService; import com.google.common.base.Joiner; +import com.google.common.base.Splitter; +import com.google.common.collect.ImmutableMap; 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; @@ -19,16 +19,8 @@ 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; @@ -43,10 +35,8 @@ 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; @@ -54,15 +44,14 @@ import io.norberg.automatter.AutoMatter; -import static com.google.common.base.Preconditions.checkArgument; import static com.squareup.javapoet.WildcardTypeName.subtypeOf; +import static io.norberg.automatter.processor.Common.builderType; +import static io.norberg.automatter.processor.Common.rawBuilderType; 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; /** @@ -72,20 +61,14 @@ @AutoService(Processor.class) public final class AutoMatterProcessor extends AbstractProcessor { - private static final Inflector INFLECTOR = new Inflector(); - - private static final Set 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 static final Splitter FIELD_SPLITTER = Splitter.on("<"); private Filer filer; private Elements elements; private Messager messager; private Types types; + private Map processors; + private final DefaultProcessor defaultProcessor = new DefaultProcessor(); @Override @@ -94,7 +77,18 @@ public synchronized void init(final ProcessingEnvironment processingEnv) { filer = processingEnv.getFiler(); elements = processingEnv.getElementUtils(); types = processingEnv.getTypeUtils(); - this.messager = processingEnv.getMessager(); + messager = processingEnv.getMessager(); + + CollectionProcessor collectionProcessor = new CollectionProcessor(elements); + OptionalProcessor optionalProcessor = new OptionalProcessor(); + + processors = ImmutableMap.of( + "java.util.Map", collectionProcessor, + "java.util.List", collectionProcessor, + "java.util.Set", collectionProcessor, + "java.util.Optional", optionalProcessor, + "com.google.common.base.Optional", optionalProcessor + ); } @Override @@ -123,6 +117,11 @@ private void process(final Element element) throws IOException, AutoMatterProces javaFile.writeTo(filer); } + private FieldProcessor processorForField(Descriptor d, ExecutableElement field) throws AutoMatterProcessorException { + String prefix = FIELD_SPLITTER.split(fieldType(d, field).toString()).iterator().next(); + return processors.getOrDefault(prefix, defaultProcessor); + } + private TypeSpec builder(final Descriptor d) throws AutoMatterProcessorException { AnnotationSpec generatedAnnotation = AnnotationSpec.builder(Generated.class) .addMember("value", "$S", AutoMatterProcessor.class.getName()) @@ -138,7 +137,7 @@ private TypeSpec builder(final Descriptor d) throws AutoMatterProcessorException } for (ExecutableElement field : d.fields()) { - builder.addField(FieldSpec.builder(fieldType(d, field), fieldName(field), PRIVATE).build()); + builder.addField(processorForField(d, field).builderField(d, field)); } builder.addMethod(defaultConstructor(d)); @@ -162,14 +161,13 @@ private TypeSpec builder(final Descriptor d) throws AutoMatterProcessorException return builder.build(); } - private MethodSpec defaultConstructor(final Descriptor d) { + private MethodSpec defaultConstructor(final Descriptor d) throws AutoMatterProcessorException { 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)); + for (Statement s : processorForField(d, field).defaultConstructor(d, field)) { + constructor.addStatement(s.statement, s.args); } } @@ -182,64 +180,22 @@ private MethodSpec copyValueConstructor(final Descriptor d) throws AutoMatterPro .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); - } + for (Statement s : processorForField(d, field).copyValueConstructor(d, field)) { + constructor.addStatement(s.statement, s.args); } } 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); - } + for (Statement s : processorForField(d, field).copyBuilderConstructor(d, field)) { + constructor.addStatement(s.statement, s.args); } } @@ -249,386 +205,13 @@ private MethodSpec copyBuilderConstructor(final Descriptor d) throws AutoMatterP private Set accessors(final Descriptor d) throws AutoMatterProcessorException { ImmutableSet.Builder 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)); + for (MethodSpec method: processorForField(d, field).accessors(d, field)) { + result.add(method); } } 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 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) @@ -644,48 +227,11 @@ private MethodSpec build(final Descriptor d) throws AutoMatterProcessorException final List 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)); + BuildStatements buildStatements = processorForField(d, field).build(d, field); + for (Statement s : buildStatements.statements) { + build.addStatement(s.statement, s.args); } + parameters.add(buildStatements.parameter); } return build.addStatement("return new $T($N)", valueImplType(d), Joiner.on(", ").join(parameters)).build(); @@ -718,7 +264,7 @@ private TypeSpec valueClass(final Descriptor d) throws AutoMatterProcessorExcept .addSuperinterface(valueType(d)); for (ExecutableElement field : d.fields()) { - value.addField(FieldSpec.builder(fieldType(d, field), fieldName(field), PRIVATE, FINAL).build()); + value.addField(processorForField(d, field).valueField(d, field)); } value.addMethod(valueConstructor(d)); @@ -754,20 +300,8 @@ private MethodSpec valueConstructor(final Descriptor d) throws AutoMatterProcess .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); + for (Statement s : processorForField(d, field).valueConstructor(d, field)) { + constructor.addStatement(s.statement, s.args); } } @@ -950,14 +484,6 @@ private void assertNotNull(MethodSpec.Builder spec, String name, String 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()) { @@ -974,9 +500,6 @@ private TypeName[] upperBounded(final List typeVariables) { 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()); @@ -1036,147 +559,12 @@ private TypeName fieldType(final Descriptor d, final ExecutableElement field) th 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<"); @@ -1186,33 +574,7 @@ 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 scope) { - if (!scope.contains(name)) { - return name; - } - return variableName("_" + name, scope); - } private String fieldName(final ExecutableElement field) { return field.getSimpleName().toString(); @@ -1245,16 +607,6 @@ private AutoMatterProcessorException fail(final String msg, final Element elemen 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(); diff --git a/processor/src/main/java/io/norberg/automatter/processor/BuildStatements.java b/processor/src/main/java/io/norberg/automatter/processor/BuildStatements.java new file mode 100644 index 00000000..65521e97 --- /dev/null +++ b/processor/src/main/java/io/norberg/automatter/processor/BuildStatements.java @@ -0,0 +1,11 @@ +package io.norberg.automatter.processor; + +class BuildStatements { + final Iterable statements; + final String parameter; + + BuildStatements(Iterable statements, String parameter) { + this.statements = statements; + this.parameter = parameter; + } +} diff --git a/processor/src/main/java/io/norberg/automatter/processor/CollectionProcessor.java b/processor/src/main/java/io/norberg/automatter/processor/CollectionProcessor.java new file mode 100644 index 00000000..76123b06 --- /dev/null +++ b/processor/src/main/java/io/norberg/automatter/processor/CollectionProcessor.java @@ -0,0 +1,606 @@ +package io.norberg.automatter.processor; + +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.FieldSpec; +import com.squareup.javapoet.MethodSpec; +import com.squareup.javapoet.ParameterizedTypeName; +import com.squareup.javapoet.TypeName; +import com.squareup.javapoet.WildcardTypeName; + +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.ExecutableElement; +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 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 static com.google.common.base.Preconditions.checkArgument; +import static com.squareup.javapoet.WildcardTypeName.subtypeOf; +import static io.norberg.automatter.processor.Common.assertNotNull; +import static io.norberg.automatter.processor.Common.builderType; +import static io.norberg.automatter.processor.Common.fail; +import static io.norberg.automatter.processor.Fields.fieldName; +import static io.norberg.automatter.processor.Fields.fieldType; +import static io.norberg.automatter.processor.Fields.genericArgument; +import static io.norberg.automatter.processor.Fields.shouldEnforceNonNull; +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.type.TypeKind.DECLARED; + +class CollectionProcessor implements FieldProcessor { + private final Elements elements; + + CollectionProcessor(Elements elements) { + this.elements = elements; + } + + @Override + public FieldSpec builderField(Descriptor d, ExecutableElement field) throws AutoMatterProcessorException { + return FieldSpec.builder(fieldType(d, field), fieldName(field), PRIVATE).build(); + } + + @Override + public FieldSpec valueField(Descriptor d, ExecutableElement field) throws AutoMatterProcessorException { + return FieldSpec.builder(fieldType(d, field), fieldName(field), PRIVATE, FINAL).build(); + } + + @Override + public Iterable accessors(Descriptor d, ExecutableElement field) throws AutoMatterProcessorException { + ArrayList result = new ArrayList<>(); + + result.add(getter(d, field)); + 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 { + 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); + } + } + return result; + } + + @Override + public Iterable defaultConstructor(Descriptor d, ExecutableElement field) { + return Collections.emptyList(); + } + + @Override + public Iterable copyValueConstructor(Descriptor d, ExecutableElement field) + throws AutoMatterProcessorException { + ArrayList result = new ArrayList<>(); + String fieldName = fieldName(field); + TypeName fieldType = upperBoundedFieldType(field); + result.add(new Statement("$T _$N = v.$N()", fieldType, fieldName, fieldName)); + result.add(new Statement("this.$N = (_$N == null) ? null : new $T(_$N)", + fieldName, fieldName, collectionImplType(field), fieldName)); + return result; + } + + @Override + public Iterable copyBuilderConstructor(Descriptor d, ExecutableElement field) + throws AutoMatterProcessorException { + String fieldName = fieldName(field); + return Collections.singletonList(new Statement( + "this.$N = (v.$N == null) ? null : new $T(v.$N)", + fieldName, fieldName, collectionImplType(field), fieldName)); + } + + @Override + public BuildStatements build(Descriptor d, ExecutableElement field) throws AutoMatterProcessorException { + final List statements = Lists.newArrayList(); + 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)) { + statements.add(new Statement( + "$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 { + statements.add(new Statement( + "$T _$L = ($L != null) ? $T.$L(new $T($N)) : null", + fieldType, fieldName, fieldName, + collections, unmodifiableCollection(field), collectionImplType(field), fieldName)); + } + } else if (isMap(field)) { + final TypeName keyType = genericArgument(field, 0); + final TypeName valueType = genericArgument(field, 1); + + if (shouldEnforceNonNull(field)) { + statements.add(new Statement( + "$T _$L = ($L != null) ? $T.unmodifiableMap(new $T($N)) : $T.<$T, $T>emptyMap()", + fieldType, fieldName, fieldName, + collections, collectionImplType(field), fieldName, + collections, keyType, valueType)); + } else { + statements.add(new Statement( + "$T _$L = ($L != null) ? $T.unmodifiableMap(new $T($N)) : null", + fieldType, fieldName, fieldName, + collections, collectionImplType(field), fieldName)); + } + } + String parameter = "_" + fieldName; + return new BuildStatements(statements, parameter); + } + + @Override + public Iterable valueConstructor(Descriptor d, ExecutableElement field) + throws AutoMatterProcessorException { + final List statements = Lists.newArrayList(); + final String fieldName = fieldName(field); + final ClassName collectionsType = ClassName.get(Collections.class); + if (shouldEnforceNonNull(field) && isCollection(field)) { + final TypeName itemType = genericArgument(field, 0); + statements.add(new Statement( + "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); + statements.add(new Statement( + "this.$N = ($N != null) ? $N : $T.<$T, $T>emptyMap()", + fieldName, fieldName, fieldName, collectionsType, keyType, valueType)); + } else { + statements.add(new Statement("this.$N = $N", fieldName, fieldName)); + } + return statements; + } + + 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 = Common.singular(elements, 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 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 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 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 (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 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 boolean isMap(final ExecutableElement field) { + final String returnType = field.getReturnType().toString(); + return returnType.startsWith("java.util.Map<"); + } + + 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 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 = Common.singular(elements, 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 + lazyMapInitialization(setter, field); + setter.addStatement("$N.put(key, value)", fieldName); + + return setter.addStatement("return this").build(); + } + + private void lazyMapInitialization(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 String variableName(final String name, final String... scope) { + return variableName(name, ImmutableSet.copyOf(scope)); + } + + private String variableName(final String name, final Set scope) { + if (!scope.contains(name)) { + return name; + } + return variableName("_" + name, scope); + } + + 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 (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 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) : ""); + } + +} diff --git a/processor/src/main/java/io/norberg/automatter/processor/Common.java b/processor/src/main/java/io/norberg/automatter/processor/Common.java new file mode 100644 index 00000000..5331963c --- /dev/null +++ b/processor/src/main/java/io/norberg/automatter/processor/Common.java @@ -0,0 +1,62 @@ +package io.norberg.automatter.processor; + +import com.google.common.collect.ImmutableSet; +import com.squareup.javapoet.ClassName; +import com.squareup.javapoet.MethodSpec; +import com.squareup.javapoet.ParameterizedTypeName; +import com.squareup.javapoet.TypeName; +import org.modeshape.common.text.Inflector; + +import javax.lang.model.element.Element; +import javax.lang.model.util.Elements; +import java.util.Set; + +class Common { + private static final Inflector INFLECTOR = new Inflector(); + + private static final Set 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"); + + static void assertNotNull(MethodSpec.Builder spec, String name) { + assertNotNull(spec, name, name); + } + + static 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(); + } + + static AutoMatterProcessorException fail(final String msg, final Element element) + throws AutoMatterProcessorException { + throw new AutoMatterProcessorException(msg, element); + } + + static String singular(final Elements elements, 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; + } + + static ClassName rawBuilderType(final Descriptor d) { + return ClassName.get(d.packageName(), d.builderName()); + } + + static TypeName builderType(final Descriptor d) { + final ClassName raw = rawBuilderType(d); + if (!d.isGeneric()) { + return raw; + } + return ParameterizedTypeName.get(raw, d.typeArguments()); + } +} diff --git a/processor/src/main/java/io/norberg/automatter/processor/DefaultProcessor.java b/processor/src/main/java/io/norberg/automatter/processor/DefaultProcessor.java new file mode 100644 index 00000000..890756f8 --- /dev/null +++ b/processor/src/main/java/io/norberg/automatter/processor/DefaultProcessor.java @@ -0,0 +1,147 @@ +package io.norberg.automatter.processor; + +import com.squareup.javapoet.AnnotationSpec; +import com.squareup.javapoet.FieldSpec; +import com.squareup.javapoet.MethodSpec; +import com.squareup.javapoet.ParameterSpec; +import com.squareup.javapoet.TypeName; + +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.TypeMirror; + +import java.util.ArrayList; +import java.util.Collections; + +import static io.norberg.automatter.processor.Common.assertNotNull; +import static io.norberg.automatter.processor.Common.builderType; +import static io.norberg.automatter.processor.Fields.fieldName; +import static io.norberg.automatter.processor.Fields.fieldType; +import static io.norberg.automatter.processor.Fields.isPrimitive; +import static io.norberg.automatter.processor.Fields.shouldEnforceNonNull; +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.type.TypeKind.DECLARED; +import static javax.lang.model.type.TypeKind.TYPEVAR; + +class DefaultProcessor implements FieldProcessor { + @Override + public FieldSpec builderField(Descriptor d, ExecutableElement field) throws AutoMatterProcessorException { + return FieldSpec.builder(fieldType(d, field), fieldName(field), PRIVATE).build(); + } + + @Override + public FieldSpec valueField(Descriptor d, ExecutableElement field) throws AutoMatterProcessorException { + return FieldSpec.builder(fieldType(d, field), fieldName(field), PRIVATE, FINAL).build(); + } + + @Override + public Iterable accessors(Descriptor d, ExecutableElement field) throws AutoMatterProcessorException { + ArrayList methods = new ArrayList<>(); + methods.add(getter(d, field)); + methods.add(setter(d, field)); + return methods; + } + + @Override + public Iterable defaultConstructor(Descriptor d, ExecutableElement field) { + return Collections.emptyList(); + } + + @Override + public Iterable copyValueConstructor(Descriptor d, ExecutableElement field) + throws AutoMatterProcessorException { + ArrayList statements = new ArrayList<>(); + String fieldName = fieldName(field); + TypeName fieldType = fieldType(d, field); + if (isFieldTypeParameterized(field)) { + statements.add(new Statement("@SuppressWarnings(\"unchecked\") $T _$N = ($T) v.$N()", + fieldType, fieldName, fieldType, fieldName)); + statements.add(new Statement("this.$N = _$N", fieldName, fieldName)); + } else { + statements.add(new Statement("this.$N = v.$N()", fieldName, fieldName)); + } + return statements; + } + + @Override + public Iterable copyBuilderConstructor(Descriptor d, ExecutableElement field) + throws AutoMatterProcessorException { + ArrayList statements = new ArrayList<>(); + String fieldName = fieldName(field); + TypeName fieldType = fieldType(d, field); + if (isFieldTypeParameterized(field)) { + statements.add(new Statement("@SuppressWarnings(\"unchecked\") $T _$N = ($T) v.$N()", + fieldType, fieldName, fieldType, fieldName)); + statements.add(new Statement("this.$N = _$N", fieldName, fieldName)); + } else { + statements.add(new Statement("this.$N = v.$N", fieldName, fieldName)); + } + return statements; + } + + @Override + public BuildStatements build(Descriptor d, ExecutableElement field) throws AutoMatterProcessorException { + return new BuildStatements(Collections.emptyList(), fieldName(field)); + } + + @Override + public Iterable valueConstructor(Descriptor d, ExecutableElement field) + throws AutoMatterProcessorException { + String fieldName = fieldName(field); + return Collections.singletonList(new Statement("this.$N = $N", fieldName, fieldName)); + } + + 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 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 = Fields.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(); + } + + protected 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)); + + getter.addStatement("return $N", fieldName); + + return getter.build(); + } + +} diff --git a/processor/src/main/java/io/norberg/automatter/processor/Field.java b/processor/src/main/java/io/norberg/automatter/processor/Field.java index db61ebca..654fcfea 100644 --- a/processor/src/main/java/io/norberg/automatter/processor/Field.java +++ b/processor/src/main/java/io/norberg/automatter/processor/Field.java @@ -7,7 +7,7 @@ class Field { final ExecutableElement method; final TypeMirror type; - public Field(final ExecutableElement method, final TypeMirror type) { + Field(final ExecutableElement method, final TypeMirror type) { this.method = method; this.type = type; } diff --git a/processor/src/main/java/io/norberg/automatter/processor/FieldProcessor.java b/processor/src/main/java/io/norberg/automatter/processor/FieldProcessor.java new file mode 100644 index 00000000..39d131d6 --- /dev/null +++ b/processor/src/main/java/io/norberg/automatter/processor/FieldProcessor.java @@ -0,0 +1,19 @@ +package io.norberg.automatter.processor; + +import com.squareup.javapoet.FieldSpec; +import com.squareup.javapoet.MethodSpec; + +import javax.lang.model.element.ExecutableElement; + +interface FieldProcessor { + FieldSpec builderField(Descriptor d, ExecutableElement field) throws AutoMatterProcessorException; + FieldSpec valueField(Descriptor d, ExecutableElement field) throws AutoMatterProcessorException; + + Iterable accessors(Descriptor d, ExecutableElement field) throws AutoMatterProcessorException; + + Iterable defaultConstructor(Descriptor d, ExecutableElement field); + Iterable copyValueConstructor(Descriptor d, ExecutableElement field) throws AutoMatterProcessorException; + Iterable copyBuilderConstructor(Descriptor d, ExecutableElement field) throws AutoMatterProcessorException; + BuildStatements build(Descriptor d, ExecutableElement field) throws AutoMatterProcessorException; + Iterable valueConstructor(Descriptor d, ExecutableElement field) throws AutoMatterProcessorException; +} diff --git a/processor/src/main/java/io/norberg/automatter/processor/Fields.java b/processor/src/main/java/io/norberg/automatter/processor/Fields.java new file mode 100644 index 00000000..bf7f6c16 --- /dev/null +++ b/processor/src/main/java/io/norberg/automatter/processor/Fields.java @@ -0,0 +1,54 @@ +package io.norberg.automatter.processor; + +import com.squareup.javapoet.TypeName; + +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; + +import static com.google.common.base.Preconditions.checkArgument; +import static io.norberg.automatter.processor.Common.fail; + +class Fields { + static 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); + } + + static String fieldName(final ExecutableElement field) { + return field.getSimpleName().toString(); + } + + static boolean isPrimitive(final ExecutableElement field) { + return field.getReturnType().getKind().isPrimitive(); + } + + static AnnotationMirror nullableAnnotation(final ExecutableElement field) { + for (AnnotationMirror annotation : field.getAnnotationMirrors()) { + if (annotation.getAnnotationType().asElement().getSimpleName().contentEquals("Nullable")) { + return annotation; + } + } + return null; + } + + static boolean isNullableAnnotated(final ExecutableElement field) { + return nullableAnnotation(field) != null; + } + + static boolean shouldEnforceNonNull(final ExecutableElement field) { + return !isPrimitive(field) && !isNullableAnnotated(field); + } + + static 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)); + } +} diff --git a/processor/src/main/java/io/norberg/automatter/processor/OptionalProcessor.java b/processor/src/main/java/io/norberg/automatter/processor/OptionalProcessor.java new file mode 100644 index 00000000..c2d840a1 --- /dev/null +++ b/processor/src/main/java/io/norberg/automatter/processor/OptionalProcessor.java @@ -0,0 +1,108 @@ +package io.norberg.automatter.processor; + +import com.google.common.collect.Lists; +import com.squareup.javapoet.AnnotationSpec; +import com.squareup.javapoet.ClassName; +import com.squareup.javapoet.MethodSpec; +import com.squareup.javapoet.ParameterizedTypeName; +import com.squareup.javapoet.TypeName; + +import javax.lang.model.element.ExecutableElement; + +import java.util.Collections; +import java.util.List; + +import static com.squareup.javapoet.WildcardTypeName.subtypeOf; +import static io.norberg.automatter.processor.Common.assertNotNull; +import static io.norberg.automatter.processor.Common.builderType; +import static io.norberg.automatter.processor.Fields.fieldName; +import static io.norberg.automatter.processor.Fields.fieldType; +import static io.norberg.automatter.processor.Fields.genericArgument; +import static io.norberg.automatter.processor.Fields.shouldEnforceNonNull; +import static javax.lang.model.element.Modifier.PUBLIC; + +class OptionalProcessor extends DefaultProcessor { + @Override + public Iterable accessors(Descriptor d, ExecutableElement field) throws AutoMatterProcessorException { + List methods = Lists.newArrayList(); + methods.add(getter(d, field)); + methods.add(optionalRawSetter(d, field)); + methods.add(optionalSetter(d, field)); + return methods; + } + + @Override + public Iterable defaultConstructor(Descriptor d, ExecutableElement field) { + if (shouldEnforceNonNull(field)) { + ClassName type = ClassName.bestGuess(optionalType(field)); + return Collections.singletonList( + new Statement("this.$N = $T.$L()", fieldName(field), type, optionalEmptyName(field))); + } + return Collections.emptyList(); + } + + 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 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 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"; + } +} diff --git a/processor/src/main/java/io/norberg/automatter/processor/Statement.java b/processor/src/main/java/io/norberg/automatter/processor/Statement.java new file mode 100644 index 00000000..8efdb702 --- /dev/null +++ b/processor/src/main/java/io/norberg/automatter/processor/Statement.java @@ -0,0 +1,11 @@ +package io.norberg.automatter.processor; + +class Statement { + final String statement; + final Object[] args; + + Statement(String statement, Object... args) { + this.statement = statement; + this.args = args; + } +}