From be6f19c77652f984d407e687cbaa46b67a3fbabb Mon Sep 17 00:00:00 2001 From: Belhorma Bendebiche Date: Thu, 1 Jun 2017 13:09:57 -0400 Subject: [PATCH 1/2] Refactor processor to make extension easier This delegates most of the field-specific processing to `FieldProcessor`, which can be implemented to customize handling of different types. Collections are handled by `CollectionProcessor`, optionals by `OptionalProcessor`. Everything else is handled by `DefaultProcessor`. A data-driven approach was taken, with `FieldProcessor` taking the descriptor and field as input and outputting Javapoet specs or statements. However, in this patch `CollectionProcessor` is dependent on `AutoMatterProcessor` for the implementation of `singular`. There is also some duplication and inefficiencies in the processor as a result of this refactor. Since doing away with them would change text order in some places and thus require edits to most of the test files, I prefer leaving that work to a followup commit to reduce review burden. Other notes: * It's not clear where common code should live. For now most of it is in default methods on `FieldProcessor`, but I'm open to suggestions. * Due to the above use of default, source level 1.8 is now required * I wasn't sure how to organize the classes, so I've left them all at top level. It might become clearer once common code is centralized and other processors are added. * Not sure if this is feasible, but it could be interesting to allow users to supply their own processors via some annotation, or to register them somehow with AutoMatter. This would allow extensions to live in separate packages. --- pom.xml | 4 +- .../processor/AutoMatterProcessor.java | 701 +----------------- .../automatter/processor/BuildStatements.java | 11 + .../processor/CollectionProcessor.java | 599 +++++++++++++++ .../processor/DefaultProcessor.java | 142 ++++ .../automatter/processor/FieldProcessor.java | 83 +++ .../processor/OptionalProcessor.java | 103 +++ .../automatter/processor/Statement.java | 11 + 8 files changed, 991 insertions(+), 663 deletions(-) create mode 100644 processor/src/main/java/io/norberg/automatter/processor/BuildStatements.java create mode 100644 processor/src/main/java/io/norberg/automatter/processor/CollectionProcessor.java create mode 100644 processor/src/main/java/io/norberg/automatter/processor/DefaultProcessor.java create mode 100644 processor/src/main/java/io/norberg/automatter/processor/FieldProcessor.java create mode 100644 processor/src/main/java/io/norberg/automatter/processor/OptionalProcessor.java create mode 100644 processor/src/main/java/io/norberg/automatter/processor/Statement.java 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..b6254d2a 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,13 @@ import com.google.auto.service.AutoService; import com.google.common.base.Joiner; +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; @@ -22,13 +21,7 @@ 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 +36,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 +45,12 @@ 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; /** @@ -86,6 +74,8 @@ public final class AutoMatterProcessor extends AbstractProcessor { private Elements elements; private Messager messager; private Types types; + private Map processors; + private DefaultProcessor defaultProcessor = new DefaultProcessor(); @Override @@ -94,7 +84,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(this); + 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 +124,11 @@ private void process(final Element element) throws IOException, AutoMatterProces javaFile.writeTo(filer); } + private FieldProcessor processorForField(Descriptor d, ExecutableElement field) throws AutoMatterProcessorException { + String prefix = fieldType(d, field).toString().split("<")[0]; + 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 +144,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 +168,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 +187,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 +212,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 +234,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 +271,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 +307,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,7 +491,7 @@ private void assertNotNull(MethodSpec.Builder spec, String name, String msg) { .endControlFlow(); } - private TypeName builderType(final Descriptor d) { + public static TypeName builderType(final Descriptor d) { final ClassName raw = rawBuilderType(d); if (!d.isGeneric()) { return raw; @@ -974,7 +515,7 @@ private TypeName[] upperBounded(final List typeVariables) { return typeNames; } - private ClassName rawBuilderType(final Descriptor d) { + private static ClassName rawBuilderType(final Descriptor d) { return ClassName.get(d.packageName(), d.builderName()); } @@ -1036,147 +577,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,13 +592,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) { + public String singular(final String name) { final String singular = INFLECTOR.singularize(name); if (KEYWORDS.contains(singular)) { return null; @@ -1203,17 +603,6 @@ private String singular(final String name) { 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 +634,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..6951e083 --- /dev/null +++ b/processor/src/main/java/io/norberg/automatter/processor/BuildStatements.java @@ -0,0 +1,11 @@ +package io.norberg.automatter.processor; + +public class BuildStatements { + Iterable statements; + String parameter; + + public 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..30e628da --- /dev/null +++ b/processor/src/main/java/io/norberg/automatter/processor/CollectionProcessor.java @@ -0,0 +1,599 @@ +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 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.AutoMatterProcessor.builderType; +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; + +public class CollectionProcessor implements FieldProcessor { + private final AutoMatterProcessor processor; + + public CollectionProcessor(AutoMatterProcessor processor) { + this.processor = processor; + } + + @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 = processor.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 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 = processor.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 + 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/DefaultProcessor.java b/processor/src/main/java/io/norberg/automatter/processor/DefaultProcessor.java new file mode 100644 index 00000000..c53fdfce --- /dev/null +++ b/processor/src/main/java/io/norberg/automatter/processor/DefaultProcessor.java @@ -0,0 +1,142 @@ +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.AutoMatterProcessor.builderType; +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; + +public 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 = 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/FieldProcessor.java b/processor/src/main/java/io/norberg/automatter/processor/FieldProcessor.java new file mode 100644 index 00000000..a743db02 --- /dev/null +++ b/processor/src/main/java/io/norberg/automatter/processor/FieldProcessor.java @@ -0,0 +1,83 @@ +package io.norberg.automatter.processor; + +import com.squareup.javapoet.ClassName; +import com.squareup.javapoet.FieldSpec; +import com.squareup.javapoet.MethodSpec; +import com.squareup.javapoet.TypeName; + +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.Element; +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; + +public 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; + + default 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); + } + + default String fieldName(final ExecutableElement field) { + return field.getSimpleName().toString(); + } + + default boolean isPrimitive(final ExecutableElement field) { + return field.getReturnType().getKind().isPrimitive(); + } + + default AnnotationMirror nullableAnnotation(final ExecutableElement field) { + for (AnnotationMirror annotation : field.getAnnotationMirrors()) { + if (annotation.getAnnotationType().asElement().getSimpleName().contentEquals("Nullable")) { + return annotation; + } + } + return null; + } + + default boolean isNullableAnnotated(final ExecutableElement field) { + return nullableAnnotation(field) != null; + } + + default boolean shouldEnforceNonNull(final ExecutableElement field) { + return !isPrimitive(field) && !isNullableAnnotated(field); + } + + default void assertNotNull(MethodSpec.Builder spec, String name) { + assertNotNull(spec, name, name); + } + + default 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(); + } + + default 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)); + } + + default AutoMatterProcessorException fail(final String msg, final Element element) + throws AutoMatterProcessorException { + throw new AutoMatterProcessorException(msg, element); + } +} 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..13fcb18a --- /dev/null +++ b/processor/src/main/java/io/norberg/automatter/processor/OptionalProcessor.java @@ -0,0 +1,103 @@ +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.AutoMatterProcessor.builderType; +import static javax.lang.model.element.Modifier.PUBLIC; + +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..a88e2f8c --- /dev/null +++ b/processor/src/main/java/io/norberg/automatter/processor/Statement.java @@ -0,0 +1,11 @@ +package io.norberg.automatter.processor; + +public class Statement { + String statement; + Object[] args; + + public Statement(String statement, Object... args) { + this.statement = statement; + this.args = args; + } +} From 8015512f70b45c7ddac3907b518eb060a2c8a56b Mon Sep 17 00:00:00 2001 From: Belhorma Bendebiche Date: Wed, 12 Jul 2017 12:04:47 -0400 Subject: [PATCH 2/2] Move common methods around to avoid circular dependencies --- .travis.yml | 2 - .../processor/AutoMatterProcessor.java | 53 ++++----------- .../automatter/processor/BuildStatements.java | 8 +-- .../processor/CollectionProcessor.java | 21 ++++-- .../norberg/automatter/processor/Common.java | 62 +++++++++++++++++ .../processor/DefaultProcessor.java | 11 +++- .../norberg/automatter/processor/Field.java | 2 +- .../automatter/processor/FieldProcessor.java | 66 +------------------ .../norberg/automatter/processor/Fields.java | 54 +++++++++++++++ .../processor/OptionalProcessor.java | 9 ++- .../automatter/processor/Statement.java | 8 +-- 11 files changed, 168 insertions(+), 128 deletions(-) create mode 100644 processor/src/main/java/io/norberg/automatter/processor/Common.java create mode 100644 processor/src/main/java/io/norberg/automatter/processor/Fields.java 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/processor/src/main/java/io/norberg/automatter/processor/AutoMatterProcessor.java b/processor/src/main/java/io/norberg/automatter/processor/AutoMatterProcessor.java index b6254d2a..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,6 +2,7 @@ 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; @@ -18,8 +19,6 @@ import com.squareup.javapoet.TypeVariableName; import com.squareup.javapoet.WildcardTypeName; -import org.modeshape.common.text.Inflector; - import java.io.IOException; import java.util.Arrays; import java.util.List; @@ -46,6 +45,8 @@ import io.norberg.automatter.AutoMatter; 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; @@ -60,22 +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 DefaultProcessor defaultProcessor = new DefaultProcessor(); + private final DefaultProcessor defaultProcessor = new DefaultProcessor(); @Override @@ -86,7 +79,7 @@ public synchronized void init(final ProcessingEnvironment processingEnv) { types = processingEnv.getTypeUtils(); messager = processingEnv.getMessager(); - CollectionProcessor collectionProcessor = new CollectionProcessor(this); + CollectionProcessor collectionProcessor = new CollectionProcessor(elements); OptionalProcessor optionalProcessor = new OptionalProcessor(); processors = ImmutableMap.of( @@ -125,7 +118,7 @@ private void process(final Element element) throws IOException, AutoMatterProces } private FieldProcessor processorForField(Descriptor d, ExecutableElement field) throws AutoMatterProcessorException { - String prefix = fieldType(d, field).toString().split("<")[0]; + String prefix = FIELD_SPLITTER.split(fieldType(d, field).toString()).iterator().next(); return processors.getOrDefault(prefix, defaultProcessor); } @@ -173,7 +166,7 @@ private MethodSpec defaultConstructor(final Descriptor d) throws AutoMatterProce .addModifiers(PUBLIC); for (ExecutableElement field : d.fields()) { - for (Statement s: processorForField(d, field).defaultConstructor(d, field)) { + for (Statement s : processorForField(d, field).defaultConstructor(d, field)) { constructor.addStatement(s.statement, s.args); } } @@ -187,7 +180,7 @@ private MethodSpec copyValueConstructor(final Descriptor d) throws AutoMatterPro .addParameter(upperBoundedValueType(d), "v"); for (ExecutableElement field : d.fields()) { - for (Statement s: processorForField(d, field).copyValueConstructor(d, field)) { + for (Statement s : processorForField(d, field).copyValueConstructor(d, field)) { constructor.addStatement(s.statement, s.args); } } @@ -201,7 +194,7 @@ private MethodSpec copyBuilderConstructor(final Descriptor d) throws AutoMatterP .addParameter(upperBoundedBuilderType(d), "v"); for (ExecutableElement field : d.fields()) { - for (Statement s: processorForField(d, field).copyBuilderConstructor(d, field)) { + for (Statement s : processorForField(d, field).copyBuilderConstructor(d, field)) { constructor.addStatement(s.statement, s.args); } } @@ -235,7 +228,7 @@ private MethodSpec build(final Descriptor d) throws AutoMatterProcessorException final List parameters = Lists.newArrayList(); for (ExecutableElement field : d.fields()) { BuildStatements buildStatements = processorForField(d, field).build(d, field); - for (Statement s: buildStatements.statements) { + for (Statement s : buildStatements.statements) { build.addStatement(s.statement, s.args); } parameters.add(buildStatements.parameter); @@ -307,7 +300,7 @@ private MethodSpec valueConstructor(final Descriptor d) throws AutoMatterProcess .build(); constructor.addParameter(parameter); - for (Statement s: processorForField(d, field).valueConstructor(d, field)) { + for (Statement s : processorForField(d, field).valueConstructor(d, field)) { constructor.addStatement(s.statement, s.args); } } @@ -491,14 +484,6 @@ private void assertNotNull(MethodSpec.Builder spec, String name, String msg) { .endControlFlow(); } - public static 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()) { @@ -515,9 +500,6 @@ private TypeName[] upperBounded(final List typeVariables) { return typeNames; } - private static 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()); @@ -592,16 +574,7 @@ private boolean isPrimitive(final ExecutableElement field) { return field.getReturnType().getKind().isPrimitive(); } - public 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 fieldName(final ExecutableElement field) { return field.getSimpleName().toString(); diff --git a/processor/src/main/java/io/norberg/automatter/processor/BuildStatements.java b/processor/src/main/java/io/norberg/automatter/processor/BuildStatements.java index 6951e083..65521e97 100644 --- a/processor/src/main/java/io/norberg/automatter/processor/BuildStatements.java +++ b/processor/src/main/java/io/norberg/automatter/processor/BuildStatements.java @@ -1,10 +1,10 @@ package io.norberg.automatter.processor; -public class BuildStatements { - Iterable statements; - String parameter; +class BuildStatements { + final Iterable statements; + final String parameter; - public BuildStatements(Iterable statements, 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 index 30e628da..76123b06 100644 --- a/processor/src/main/java/io/norberg/automatter/processor/CollectionProcessor.java +++ b/processor/src/main/java/io/norberg/automatter/processor/CollectionProcessor.java @@ -18,6 +18,7 @@ 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; @@ -32,17 +33,23 @@ import static com.google.common.base.Preconditions.checkArgument; import static com.squareup.javapoet.WildcardTypeName.subtypeOf; -import static io.norberg.automatter.processor.AutoMatterProcessor.builderType; +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; -public class CollectionProcessor implements FieldProcessor { - private final AutoMatterProcessor processor; +class CollectionProcessor implements FieldProcessor { + private final Elements elements; - public CollectionProcessor(AutoMatterProcessor processor) { - this.processor = processor; + CollectionProcessor(Elements elements) { + this.elements = elements; } @Override @@ -291,7 +298,7 @@ private void ensureSafeVarargs(MethodSpec.Builder setter) { private MethodSpec collectionAdder(final Descriptor d, final ExecutableElement field) { final String fieldName = fieldName(field); - final String singular = processor.singular(fieldName); + final String singular = Common.singular(elements, fieldName); if (singular == null || singular.isEmpty()) { return null; } @@ -523,7 +530,7 @@ private MethodSpec mapSetterPairs(final Descriptor d, final ExecutableElement fi private MethodSpec mapPutter(final Descriptor d, final ExecutableElement field) { final String fieldName = fieldName(field); - final String singular = processor.singular(fieldName); + final String singular = Common.singular(elements, fieldName); if (singular == null) { return null; } 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 index c53fdfce..890756f8 100644 --- a/processor/src/main/java/io/norberg/automatter/processor/DefaultProcessor.java +++ b/processor/src/main/java/io/norberg/automatter/processor/DefaultProcessor.java @@ -14,14 +14,19 @@ import java.util.ArrayList; import java.util.Collections; -import static io.norberg.automatter.processor.AutoMatterProcessor.builderType; +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; -public class DefaultProcessor implements FieldProcessor { +class DefaultProcessor implements FieldProcessor { @Override public FieldSpec builderField(Descriptor d, ExecutableElement field) throws AutoMatterProcessorException { return FieldSpec.builder(fieldType(d, field), fieldName(field), PRIVATE).build(); @@ -109,7 +114,7 @@ private MethodSpec setter(final Descriptor d, final ExecutableElement field) thr ParameterSpec.Builder parameterSpecBuilder = ParameterSpec.builder(fieldType(d, field), fieldName); if (!isPrimitive(field)) { - AnnotationMirror nullableAnnotation = nullableAnnotation(field); + AnnotationMirror nullableAnnotation = Fields.nullableAnnotation(field); if (nullableAnnotation != null) { parameterSpecBuilder.addAnnotation(AnnotationSpec.get(nullableAnnotation)); } 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 index a743db02..39d131d6 100644 --- a/processor/src/main/java/io/norberg/automatter/processor/FieldProcessor.java +++ b/processor/src/main/java/io/norberg/automatter/processor/FieldProcessor.java @@ -1,20 +1,11 @@ package io.norberg.automatter.processor; -import com.squareup.javapoet.ClassName; import com.squareup.javapoet.FieldSpec; import com.squareup.javapoet.MethodSpec; -import com.squareup.javapoet.TypeName; -import javax.lang.model.element.AnnotationMirror; -import javax.lang.model.element.Element; 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; - -public interface FieldProcessor { +interface FieldProcessor { FieldSpec builderField(Descriptor d, ExecutableElement field) throws AutoMatterProcessorException; FieldSpec valueField(Descriptor d, ExecutableElement field) throws AutoMatterProcessorException; @@ -25,59 +16,4 @@ public interface FieldProcessor { Iterable copyBuilderConstructor(Descriptor d, ExecutableElement field) throws AutoMatterProcessorException; BuildStatements build(Descriptor d, ExecutableElement field) throws AutoMatterProcessorException; Iterable valueConstructor(Descriptor d, ExecutableElement field) throws AutoMatterProcessorException; - - default 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); - } - - default String fieldName(final ExecutableElement field) { - return field.getSimpleName().toString(); - } - - default boolean isPrimitive(final ExecutableElement field) { - return field.getReturnType().getKind().isPrimitive(); - } - - default AnnotationMirror nullableAnnotation(final ExecutableElement field) { - for (AnnotationMirror annotation : field.getAnnotationMirrors()) { - if (annotation.getAnnotationType().asElement().getSimpleName().contentEquals("Nullable")) { - return annotation; - } - } - return null; - } - - default boolean isNullableAnnotated(final ExecutableElement field) { - return nullableAnnotation(field) != null; - } - - default boolean shouldEnforceNonNull(final ExecutableElement field) { - return !isPrimitive(field) && !isNullableAnnotated(field); - } - - default void assertNotNull(MethodSpec.Builder spec, String name) { - assertNotNull(spec, name, name); - } - - default 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(); - } - - default 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)); - } - - default AutoMatterProcessorException fail(final String msg, final Element element) - throws AutoMatterProcessorException { - throw new AutoMatterProcessorException(msg, element); - } } 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 index 13fcb18a..c2d840a1 100644 --- a/processor/src/main/java/io/norberg/automatter/processor/OptionalProcessor.java +++ b/processor/src/main/java/io/norberg/automatter/processor/OptionalProcessor.java @@ -13,10 +13,15 @@ import java.util.List; import static com.squareup.javapoet.WildcardTypeName.subtypeOf; -import static io.norberg.automatter.processor.AutoMatterProcessor.builderType; +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; -public class OptionalProcessor extends DefaultProcessor { +class OptionalProcessor extends DefaultProcessor { @Override public Iterable accessors(Descriptor d, ExecutableElement field) throws AutoMatterProcessorException { List methods = Lists.newArrayList(); diff --git a/processor/src/main/java/io/norberg/automatter/processor/Statement.java b/processor/src/main/java/io/norberg/automatter/processor/Statement.java index a88e2f8c..8efdb702 100644 --- a/processor/src/main/java/io/norberg/automatter/processor/Statement.java +++ b/processor/src/main/java/io/norberg/automatter/processor/Statement.java @@ -1,10 +1,10 @@ package io.norberg.automatter.processor; -public class Statement { - String statement; - Object[] args; +class Statement { + final String statement; + final Object[] args; - public Statement(String statement, Object... args) { + Statement(String statement, Object... args) { this.statement = statement; this.args = args; }