diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..168e83e --- /dev/null +++ b/.gitignore @@ -0,0 +1,21 @@ +*.class + +target +*target/* + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.ear + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* +TODO + +# IntelliJ +/out/ +*.iml +.idea diff --git a/README.md b/README.md new file mode 100644 index 0000000..58524e4 --- /dev/null +++ b/README.md @@ -0,0 +1,68 @@ +# Bob +Builder generator for Java + +## Getting Started + +Annotate the class you which to build with `@Buildable` + + package foo.bar + + @Buildable + public class Car { + private Brand brand; + private String color; + private BigDecimal price; + + ... + } + +A `CarBuilder` class will be generated for you in the same package as the source class with *builder* as suffix. +For the car example this will be `foo.bar.builder` + +The location of the builder can be changed: + + @Buildable(package = "custom.package") + public class Car { + ... + +The generated builder can be used now: + + Car redCar = new CarBuilder().color("red").build(); + +Bob will try to be smart about creating a builder for you. +* If there are *standard* Java Bean setters available they will be used. (`setField`) +* If you do not have any setters reflection will be used. +* If the fields are accessible directly (public or protected fields) they will be set directly. + +Fields can be excluded: + + @Buildable(excludes = {"brand", "color"}) + public class Car { + ... + +By default Bob will generated setter methods consisting out of *new style setters* (`name(String name)` instead of `setName(String name)` or the default builder pattern setter style `withName(String name)`) +If you want to change the prefix of those setter methods you can: + + @Buildable(prefix = "with") + public class Car { + ... + +Bob is not afraid of generics + + @Buildable + public class Cup { + private T contents; + private R topping; + + // usage + + Cup string = new CupBuilder().topping("String").contents(BigDecimal.ZERO).build(); + +Bob can handle final fields + + @Buildable + public class Car { + private final String color; + + public Car(String color) { + .... diff --git a/bob/pom.xml b/bob/pom.xml new file mode 100644 index 0000000..753dca5 --- /dev/null +++ b/bob/pom.xml @@ -0,0 +1,58 @@ + + + 4.0.0 + + io.jonasg + bob-project + 0.1.0-SNAPSHOT + + + bob + pom + + + 21 + 21 + UTF-8 + + + + + io.jonasg + bob-compiler + ${project.version} + + + io.jonasg + bob-core + ${project.version} + + + + + + + maven-assembly-plugin + ${maven-aseembly-plugin.version} + + + jar-with-dependencies + + bob + + + + make-assembly + package + + single + + + + + + + + diff --git a/compiler/pom.xml b/compiler/pom.xml new file mode 100644 index 0000000..04f67f6 --- /dev/null +++ b/compiler/pom.xml @@ -0,0 +1,39 @@ + + + 4.0.0 + + + bob-project + io.jonasg + 0.1.0-SNAPSHOT + + jar + + bob-compiler + + + + ${project.groupId} + bob-core + ${project.version} + + + com.squareup + javapoet + + + org.mockito + mockito-core + + + org.junit.jupiter + junit-jupiter + test + + + org.assertj + assertj-core + + + + diff --git a/compiler/src/main/java/io/jonasg/bob/Buildable.java b/compiler/src/main/java/io/jonasg/bob/Buildable.java new file mode 100644 index 0000000..f901af7 --- /dev/null +++ b/compiler/src/main/java/io/jonasg/bob/Buildable.java @@ -0,0 +1,14 @@ +package io.jonasg.bob; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.SOURCE) +@Target(ElementType.TYPE) +public @interface Buildable { + String[] excludes() default {}; + String prefix() default ""; + String packageName() default ""; +} diff --git a/compiler/src/main/java/io/jonasg/bob/TypeDefinitionFactory.java b/compiler/src/main/java/io/jonasg/bob/TypeDefinitionFactory.java new file mode 100644 index 0000000..c63bd4a --- /dev/null +++ b/compiler/src/main/java/io/jonasg/bob/TypeDefinitionFactory.java @@ -0,0 +1,141 @@ +package io.jonasg.bob; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.TypeElement; +import javax.lang.model.element.TypeParameterElement; +import javax.lang.model.element.VariableElement; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.util.ElementFilter; +import javax.lang.model.util.Elements; + +import io.jonasg.bob.definitions.ConstructorDefinition; +import io.jonasg.bob.definitions.FieldDefinition; +import io.jonasg.bob.definitions.GenericParameterDefinition; +import io.jonasg.bob.definitions.MethodDefinition; +import io.jonasg.bob.definitions.ParameterDefinition; +import io.jonasg.bob.definitions.SimpleTypeDefinition; +import io.jonasg.bob.definitions.TypeDefinition; + +public class TypeDefinitionFactory { + + protected final Elements elementUtils; + + private Element element; + + public TypeDefinitionFactory(Elements elementUtils) { + this.elementUtils = elementUtils; + } + + /** + * Create a TypeDefinition for the given element + * + * @param element the element to create a TypeDefinition for + * @return a TypeDefinition for the given element + */ + public TypeDefinition typeDefinitionForElement(Element element) { + this.element = element; + return TypeDefinition.newBuilder() + .typeName(typeName()) + .genericParameters(generics(element)) + .packageName(packageName()) + .methods(methods()) + .enclosedIn(outerFullTypeName()) + .fields(fields()) + .constructors(constructors(element)) + .build(); + } + + private List generics(Element element) { + List parameters = new ArrayList<>(); + if (ElementKind.CLASS.equals(element.getKind())) + for (TypeParameterElement param : ((TypeElement) element).getTypeParameters()) + parameters.add(new GenericParameterDefinition(param.getSimpleName().toString(), toTypeDefinitions(param.getBounds()))); + return parameters; + } + + private List toTypeDefinitions(List mirrors) { + List definitions = new ArrayList<>(); + for (TypeMirror mirror : mirrors) { + if (!"java.lang.Object".equals(mirror.toString())) { + List parts = new ArrayList<>(Arrays.asList(mirror.toString().split("\\."))); + Collections.reverse(parts); + String name = parts.get(0); + parts.remove(0); + Collections.reverse(parts); + String packageName = join(parts.toArray(new String[parts.size()]), "."); + definitions.add(new SimpleTypeDefinition(name, packageName)); + } + } + return definitions; + } + + private static String join(String[] aArr, String sSep) { + StringBuilder sbStr = new StringBuilder(); + for (int i = 0, il = aArr.length; i < il; i++) { + if (i > 0) + sbStr.append(sSep); + sbStr.append(aArr[i]); + } + return sbStr.toString(); + } + + private List fields(List fields) { + List definitions = new ArrayList<>(); + for (VariableElement field : fields) + definitions.add(new FieldDefinition(field.getSimpleName().toString(), field.getModifiers(), field.asType())); + return definitions; + } + + private List constructors(Element element) { + List definitions = new ArrayList<>(); + for (ExecutableElement constructor : ElementFilter.constructorsIn(element.getEnclosedElements())) { + List constructorParams = new ArrayList<>(); + for (VariableElement param : constructor.getParameters()) + constructorParams.add(new ParameterDefinition(param.getSimpleName().toString())); + definitions.add(new ConstructorDefinition(constructorParams, constructor.getModifiers())); + } + return definitions; + } + + private String outerType(Element enclosingElement) { + String enclosedIn = null; + while (!enclosingElement.getKind().equals(ElementKind.PACKAGE)) { + if (enclosedIn == null) + enclosedIn = enclosingElement.getSimpleName().toString(); + else + enclosedIn += String.format(".%s", enclosingElement.getSimpleName()); + enclosingElement = enclosingElement.getEnclosingElement(); + } + return enclosedIn; + } + + private String typeName() { + return element.getSimpleName().toString(); + } + + private String packageName() { + return elementUtils.getPackageOf(element).getQualifiedName().toString(); + } + + private String outerFullTypeName() { + Element enclosingElement = element.getEnclosingElement(); + return outerType(enclosingElement); + } + + private List fields() { + return fields(ElementFilter.fieldsIn(element.getEnclosedElements())); + } + + private List methods() { + return ElementFilter.methodsIn(element.getEnclosedElements()).stream() + .map(e -> new MethodDefinition(e.getSimpleName().toString())) + .toList(); + } +} diff --git a/compiler/src/main/java/io/jonasg/bob/TypeWriter.java b/compiler/src/main/java/io/jonasg/bob/TypeWriter.java new file mode 100644 index 0000000..1a408e6 --- /dev/null +++ b/compiler/src/main/java/io/jonasg/bob/TypeWriter.java @@ -0,0 +1,19 @@ +package io.jonasg.bob; + +import com.squareup.javapoet.JavaFile; +import com.squareup.javapoet.TypeSpec; + +import javax.annotation.processing.Filer; +import java.io.IOException; + +public class TypeWriter { + public static void write(Filer filer, String packageName, TypeSpec spec) { + JavaFile javaFile = JavaFile.builder(packageName, spec) + .build(); + try { + javaFile.writeTo(filer); + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/compiler/src/main/java/io/jonasg/bob/definitions/ConstructorDefinition.java b/compiler/src/main/java/io/jonasg/bob/definitions/ConstructorDefinition.java new file mode 100644 index 0000000..2d0e173 --- /dev/null +++ b/compiler/src/main/java/io/jonasg/bob/definitions/ConstructorDefinition.java @@ -0,0 +1,24 @@ +package io.jonasg.bob.definitions; + +import javax.lang.model.element.Modifier; +import java.util.List; +import java.util.Set; + +public class ConstructorDefinition { + + private List parameters; + private Set modifiers; + + public ConstructorDefinition(List parameters, Set modifiers) { + this.parameters = parameters; + this.modifiers = modifiers; + } + + public boolean isPrivate() { + return modifiers.contains(Modifier.PRIVATE); + } + + public List parameters() { + return parameters; + } +} diff --git a/compiler/src/main/java/io/jonasg/bob/definitions/FieldDefinition.java b/compiler/src/main/java/io/jonasg/bob/definitions/FieldDefinition.java new file mode 100644 index 0000000..f763802 --- /dev/null +++ b/compiler/src/main/java/io/jonasg/bob/definitions/FieldDefinition.java @@ -0,0 +1,46 @@ +package io.jonasg.bob.definitions; + +import javax.lang.model.element.Modifier; +import javax.lang.model.type.TypeMirror; +import java.util.Set; + +public class FieldDefinition { + + private final String name; + private final Set modifiers; + private final TypeMirror type; + + public FieldDefinition(String name, Set modifiers, TypeMirror type) { + this.name = name; + this.modifiers = modifiers; + this.type = type; + } + + public String name() { + return name; + } + + public TypeMirror type() { + return type; + } + + public boolean isPrivate() { + return modifiers.contains(Modifier.PRIVATE); + } + + public boolean isProtected() { + return modifiers.contains(Modifier.PROTECTED); + } + + public boolean isFinal() { + return modifiers.contains(Modifier.FINAL); + } + + public boolean isPublic() { + return modifiers.contains(Modifier.PUBLIC); + } + + public boolean isPackageLocal() { + return modifiers.isEmpty(); + } +} diff --git a/compiler/src/main/java/io/jonasg/bob/definitions/GenericParameterDefinition.java b/compiler/src/main/java/io/jonasg/bob/definitions/GenericParameterDefinition.java new file mode 100644 index 0000000..97d2822 --- /dev/null +++ b/compiler/src/main/java/io/jonasg/bob/definitions/GenericParameterDefinition.java @@ -0,0 +1,18 @@ +package io.jonasg.bob.definitions; + +import java.util.List; + +public class GenericParameterDefinition extends ParameterDefinition { + + private final List bounds; + + public GenericParameterDefinition(String name, List bounds) { + super(name); + this.bounds = bounds; + } + + public List bounds() { + return bounds; + } + +} diff --git a/compiler/src/main/java/io/jonasg/bob/definitions/MethodDefinition.java b/compiler/src/main/java/io/jonasg/bob/definitions/MethodDefinition.java new file mode 100644 index 0000000..3a7a5f0 --- /dev/null +++ b/compiler/src/main/java/io/jonasg/bob/definitions/MethodDefinition.java @@ -0,0 +1,13 @@ +package io.jonasg.bob.definitions; + +public class MethodDefinition { + private final String name; + + public MethodDefinition(String name) { + this.name = name; + } + + public String name() { + return name; + } +} diff --git a/compiler/src/main/java/io/jonasg/bob/definitions/ParameterDefinition.java b/compiler/src/main/java/io/jonasg/bob/definitions/ParameterDefinition.java new file mode 100644 index 0000000..3e66ce0 --- /dev/null +++ b/compiler/src/main/java/io/jonasg/bob/definitions/ParameterDefinition.java @@ -0,0 +1,52 @@ +package io.jonasg.bob.definitions; + + +public class ParameterDefinition { + + private String name; + + public ParameterDefinition(String name) { + this.name = name; + } + + public String name() { + return name; + } + + private ParameterDefinition() { + } + + public static Builder newBuilder() { + return new Builder(); + } + + public static class Builder { + private ParameterDefinition instance = new ParameterDefinition(); + + public Builder name(String name) { + instance.name = name; + return this; + } + + public ParameterDefinition build() { + ParameterDefinition result = instance; + instance = null; + return result; + } + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + ParameterDefinition that = (ParameterDefinition) o; + + return name != null ? name.equals(that.name) : that.name == null; + } + + @Override + public int hashCode() { + return name != null ? name.hashCode() : 0; + } +} diff --git a/compiler/src/main/java/io/jonasg/bob/definitions/SimpleTypeDefinition.java b/compiler/src/main/java/io/jonasg/bob/definitions/SimpleTypeDefinition.java new file mode 100644 index 0000000..77c583e --- /dev/null +++ b/compiler/src/main/java/io/jonasg/bob/definitions/SimpleTypeDefinition.java @@ -0,0 +1,30 @@ +package io.jonasg.bob.definitions; + +public class SimpleTypeDefinition { + + protected String typeName; + + protected String packageName; + + protected String parent; + + public SimpleTypeDefinition(String typeName, String packageName) { + this.typeName = typeName; + this.packageName = packageName; + } + + SimpleTypeDefinition() {} + + public String typeName() { + return typeName; + } + + public String packageName() { + return packageName; + } + + public String fullTypeName() { + return (parent != null ? parent + "." : "") + typeName; + } + +} diff --git a/compiler/src/main/java/io/jonasg/bob/definitions/TypeDefinition.java b/compiler/src/main/java/io/jonasg/bob/definitions/TypeDefinition.java new file mode 100644 index 0000000..9e14e89 --- /dev/null +++ b/compiler/src/main/java/io/jonasg/bob/definitions/TypeDefinition.java @@ -0,0 +1,114 @@ +package io.jonasg.bob.definitions; + + + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +@SuppressWarnings("unused") +public class TypeDefinition extends SimpleTypeDefinition { + + private List fields = new ArrayList<>(); + private List constructors = new ArrayList<>(); + private List genericParameters = new ArrayList<>(); + private List methods; + + private TypeDefinition(String typeName, String packageName, String enclosedIn, List fields, List constructors) { + super(typeName, packageName); + this.parent = enclosedIn; + this.fields = fields; + this.constructors = constructors; + } + + public TypeDefinition() { + super(); + } + + public List fields() { + return fields; + } + + public List fields(Predicate predicate) { + return fields().stream() + .filter(predicate) + .collect(Collectors.toList()); + } + + public List methods(Predicate predicate) { + return methods.stream() + .filter(predicate) + .toList(); + } + + public String nestedIn() { + return parent; + } + + public List genericParameters() { + return new ArrayList<>(genericParameters); + } + + public List constructors() { + return new ArrayList<>(constructors); + } + + public String fullTypeName() { + return (parent != null ? parent + "." : "") + typeName; + } + + public boolean isNested() { + return parent != null; + } + + public static Builder newBuilder() { + return new Builder(); + } + + public static class Builder { + private TypeDefinition instance = new TypeDefinition(); + + public Builder typeName(String typeName) { + instance.typeName = typeName; + return this; + } + + public Builder packageName(String packageName) { + instance.packageName = packageName; + return this; + } + + public Builder methods(List methods) { + instance.methods = methods; + return this; + } + + public Builder enclosedIn(String enclosedIn) { + instance.parent = enclosedIn; + return this; + } + + public Builder fields(List fields) { + instance.fields = fields; + return this; + } + + public Builder constructors(List constructors) { + instance.constructors = constructors; + return this; + } + + public Builder genericParameters(List generics) { + instance.genericParameters = generics; + return this; + } + + public TypeDefinition build() { + TypeDefinition result = instance; + instance = null; + return result; + } + } +} diff --git a/compiler/src/main/java/io/jonasg/bob/processor/BuildableProcessor.java b/compiler/src/main/java/io/jonasg/bob/processor/BuildableProcessor.java new file mode 100644 index 0000000..72cb574 --- /dev/null +++ b/compiler/src/main/java/io/jonasg/bob/processor/BuildableProcessor.java @@ -0,0 +1,38 @@ +package io.jonasg.bob.processor; + + +import java.util.Set; + +import javax.annotation.processing.AbstractProcessor; +import javax.annotation.processing.RoundEnvironment; +import javax.annotation.processing.SupportedAnnotationTypes; +import javax.annotation.processing.SupportedSourceVersion; +import javax.lang.model.SourceVersion; +import javax.lang.model.element.TypeElement; +import javax.lang.model.util.Elements; + +import io.jonasg.bob.Buildable; +import io.jonasg.bob.definitions.TypeDefinition; +import io.jonasg.bob.TypeDefinitionFactory; + +@SupportedAnnotationTypes("io.jonasg.bob.Buildable") +@SupportedSourceVersion(SourceVersion.RELEASE_8) +public final class BuildableProcessor extends AbstractProcessor { + + @Override + public boolean process(Set annotations, RoundEnvironment roundEnv) { + Elements elementUtils = processingEnv.getElementUtils(); + + BuilderGenerator builderGenerator = new BuilderGenerator(processingEnv.getFiler()); + TypeDefinitionFactory typeDefinitionFactory = new TypeDefinitionFactory(elementUtils); + + roundEnv.getElementsAnnotatedWith(Buildable.class).forEach(elem -> { + Buildable buildable = elem.getAnnotation(Buildable.class); + TypeDefinition sourceDefinition = typeDefinitionFactory.typeDefinitionForElement(elem); + builderGenerator.generate(sourceDefinition, buildable); + }); + return true; + } + + +} diff --git a/compiler/src/main/java/io/jonasg/bob/processor/BuilderGenerator.java b/compiler/src/main/java/io/jonasg/bob/processor/BuilderGenerator.java new file mode 100644 index 0000000..13a9581 --- /dev/null +++ b/compiler/src/main/java/io/jonasg/bob/processor/BuilderGenerator.java @@ -0,0 +1,25 @@ +package io.jonasg.bob.processor; + +import io.jonasg.bob.Buildable; +import io.jonasg.bob.TypeWriter; +import io.jonasg.bob.definitions.TypeDefinition; +import io.jonasg.bob.specs.InstanceInsideBuilderTypeSpecFactory; +import io.jonasg.bob.specs.TypeSpecFactory; +import com.squareup.javapoet.TypeSpec; + +import javax.annotation.processing.Filer; + +public class BuilderGenerator { + + private final Filer filer; + + public BuilderGenerator(Filer filer) { + this.filer = filer; + } + + public void generate(TypeDefinition source, Buildable buildable) { + TypeSpec typeSpec = TypeSpecFactory.produce(source, buildable); + TypeWriter.write(filer, InstanceInsideBuilderTypeSpecFactory.builderPackage(source, buildable), typeSpec); + } + +} diff --git a/compiler/src/main/java/io/jonasg/bob/specs/BaseTypeSpecFactory.java b/compiler/src/main/java/io/jonasg/bob/specs/BaseTypeSpecFactory.java new file mode 100644 index 0000000..15160e9 --- /dev/null +++ b/compiler/src/main/java/io/jonasg/bob/specs/BaseTypeSpecFactory.java @@ -0,0 +1,177 @@ +package io.jonasg.bob.specs; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import javax.lang.model.element.Modifier; + +import com.squareup.javapoet.ClassName; +import com.squareup.javapoet.CodeBlock; +import com.squareup.javapoet.FieldSpec; +import com.squareup.javapoet.MethodSpec; +import com.squareup.javapoet.ParameterizedTypeName; +import com.squareup.javapoet.TypeName; +import com.squareup.javapoet.TypeSpec; +import com.squareup.javapoet.TypeVariableName; +import io.jonasg.bob.AbstractBuilder; +import io.jonasg.bob.Buildable; +import io.jonasg.bob.definitions.ConstructorDefinition; +import io.jonasg.bob.definitions.FieldDefinition; +import io.jonasg.bob.definitions.GenericParameterDefinition; +import io.jonasg.bob.definitions.ParameterDefinition; +import io.jonasg.bob.definitions.SimpleTypeDefinition; +import io.jonasg.bob.definitions.TypeDefinition; +import io.jonasg.bob.utils.Formatter; + +abstract class BaseTypeSpecFactory { + + protected TypeDefinition source; + protected Buildable buildable; + + protected BaseTypeSpecFactory(TypeDefinition source, Buildable buildable) { + this.source = source; + this.buildable = buildable; + } + + protected String builderTypeName(TypeDefinition source) { + return Formatter.format("$typeName$suffix", source.typeName(), "Builder"); + } + + protected abstract MethodSpec newInstance(); + + /** + * The setters that the builder will have + * @return List of {@link MethodSpec} + */ + protected abstract List setters(); + + /** + * The fields that the builder will have + * @return List of {@link FieldSpec} + */ + protected abstract List fields(); + + + /** + * The build method that the builder will have + * @return {@link MethodSpec} + */ + protected abstract MethodSpec buildMethod(); + + protected TypeSpec typeSpec() { + TypeSpec.Builder builder = TypeSpec.classBuilder(builderTypeName(source)) + .superclass(ParameterizedTypeName.get(ClassName.get(AbstractBuilder.class), className(source))) + .addModifiers(Modifier.PUBLIC, Modifier.FINAL); + if (!source.genericParameters().isEmpty()) + builder.addTypeVariables(toTypeVariableNames(source)); + builder.addMethod(newInstance()); + builder.addMethods(setters()); + builder.addFields(fields()); + builder.addMethod(buildMethod()); + builder.addMethod(constructor()); + if (!source.genericParameters().isEmpty()) + builder.addMethod(of()); + return builder.build(); + } + + private MethodSpec of() { + CodeBlock.Builder body = CodeBlock.builder(); + body.addStatement("return new $L<>()", builderTypeName(source)); + MethodSpec.Builder of = MethodSpec.methodBuilder("of") + .addModifiers(Modifier.PUBLIC, Modifier.STATIC) + .addTypeVariables(builderTypeGenerics()) + .returns(builderType()); + for (ParameterDefinition parameter : source.genericParameters()) + of.addParameter(ParameterizedTypeName.get(ClassName.get("java.lang", "Class"), TypeVariableName.get(parameter.name())), String.format("%stype", parameter.name())); + of.addCode(body.build()); + return of.build(); + } + + protected TypeName builderType() { + if (source.genericParameters().isEmpty()) + return ClassName.get(builderPackage(source, buildable), builderTypeName(source)); + List typeVariableNames = toTypeVariableNames(source); + return ParameterizedTypeName.get(ClassName.get(builderPackage(source, buildable), builderTypeName(source)), typeVariableNames.toArray(new TypeName[typeVariableNames.size()])); + } + + public static String builderPackage(TypeDefinition source, Buildable buildable) { + if (!buildable.packageName().isEmpty()) + return buildable.packageName(); + else + return String.format("%s.builder", source.packageName()); + } + + private List builderTypeGenerics() { + List typeVariableNames = new ArrayList<>(); + for (GenericParameterDefinition param : source.genericParameters()) { + List bounds = new ArrayList<>(); + for (SimpleTypeDefinition definition : param.bounds()) { + bounds.add(TypeVariableName.get(definition.typeName())); + } + typeVariableNames.add(TypeVariableName.get(param.name(), bounds.toArray(new TypeName[bounds.size()]))); + } + return typeVariableNames; + } + + protected TypeName className(TypeDefinition definition) { + if (definition.genericParameters().isEmpty()) { + if (definition.isNested()) + return ClassName.get(definition.packageName(), definition.nestedIn()).nestedClass(definition.typeName()); + else + return ClassName.get(definition.packageName(), definition.fullTypeName()); + } else { + List genericParameters = toTypeVariableNames(definition); + return ParameterizedTypeName.get(ClassName.get(definition.packageName(), definition.fullTypeName()), + genericParameters.toArray(new TypeName[genericParameters.size()])); + } + } + + protected List toTypeVariableNames(TypeDefinition definition) { + List genericParameters = new ArrayList<>(); + for (GenericParameterDefinition parameterDefinition : definition.genericParameters()) + genericParameters.add(TypeVariableName.get(parameterDefinition.name(), simpleClassNames(parameterDefinition.bounds()).toArray(new TypeName[parameterDefinition.bounds().size()]))); + return genericParameters; + } + + private List simpleClassNames(List definitions) { + List typeNames = new ArrayList<>(); + for (SimpleTypeDefinition definition : definitions) + typeNames.add(ClassName.get(definition.packageName(), definition.fullTypeName())); + return typeNames; + } + + private TypeName classNameWithoutGenerics(TypeDefinition definition) { + return ClassName.get(definition.packageName(), definition.fullTypeName()); + } + + private boolean defaultConstructorPresent() { + if (source.constructors().isEmpty()) + return true; + else + for (ConstructorDefinition constructor : source.constructors()) + if (constructor.parameters().isEmpty()) + return true; + return false; + } + + protected String fieldName(String name) { + if(buildable.prefix().isEmpty()) + return name; + return Formatter.format("$prefix$name", buildable.prefix(), name.substring(0,1).toUpperCase() + name.substring(1)); + } + + protected boolean notExcluded(FieldDefinition field) { + return !Arrays.asList(buildable.excludes()).contains(field.name()); + } + + protected boolean notWithinTheSamePackage() { + return !source.packageName().equals(buildable.packageName()); + } + + protected MethodSpec constructor() { + return MethodSpec.constructorBuilder() + .addModifiers(Modifier.PUBLIC) + .build(); + } +} diff --git a/compiler/src/main/java/io/jonasg/bob/specs/FieldsInsideBuilderTypeSpecFactory.java b/compiler/src/main/java/io/jonasg/bob/specs/FieldsInsideBuilderTypeSpecFactory.java new file mode 100644 index 0000000..c9f476f --- /dev/null +++ b/compiler/src/main/java/io/jonasg/bob/specs/FieldsInsideBuilderTypeSpecFactory.java @@ -0,0 +1,136 @@ +package io.jonasg.bob.specs; + +import io.jonasg.bob.Buildable; +import io.jonasg.bob.definitions.ConstructorDefinition; +import io.jonasg.bob.definitions.FieldDefinition; +import io.jonasg.bob.definitions.ParameterDefinition; +import io.jonasg.bob.definitions.TypeDefinition; +import com.squareup.javapoet.*; + +import javax.lang.model.element.Modifier; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static io.jonasg.bob.utils.Formatter.format; + +public class FieldsInsideBuilderTypeSpecFactory extends BaseTypeSpecFactory { + + protected FieldsInsideBuilderTypeSpecFactory(TypeDefinition source, Buildable buildable) { + super(source, buildable); + } + + @Override + protected MethodSpec newInstance() { + Optional constructor = constructorMatchingFinalFields(); + if (!constructor.isPresent()) + throw new IllegalStateException("Missing constructor for final fields"); + return MethodSpec.methodBuilder("newInstance") + .addModifiers(Modifier.PROTECTED) + .returns(className(source)) + .addStatement("return new $L(" + constructorArguments(constructor.get().parameters()) + ")", args(constructor.get().parameters())) + .build(); + } + + private Object[] args(List constructorParams) { + final List finalFields = finalFields(); + List parameters = constructorParams.stream() + .map(p -> finalFields.contains(p.name()) ? p.name() : "null") + .toList(); + Object[] args = new Object[constructorParams.size() + 1]; + args[0] = className(source); + for (int i = 1; i <= parameters.size(); i++) { + args[i] = parameters.get(i - 1); + } + return args; + } + + private List finalFields() { + return source.fields(FieldDefinition::isFinal).stream() + .map(FieldDefinition::name) + .toList(); + } + + private String constructorArguments(List params) { + return params.stream() + .map(parameterDefinition -> "$L") + .collect(Collectors.joining(",")); + } + + private Optional constructorMatchingFinalFields() { + List finalFields = source.fields(new Predicate() { + @Override + public boolean test(FieldDefinition fieldDefinition) { + return fieldDefinition.isFinal(); + } + }); + for (ConstructorDefinition constructor : source.constructors()) { + for (FieldDefinition finalField : finalFields) { + if (!constructor.parameters().contains(finalField.name())) + break; + } + return Optional.of(constructor); + } + return Optional.empty(); + } + + @Override + protected List setters() { + List setters = new ArrayList<>(); + for (FieldDefinition field : source.fields()) { + if (notExcluded(field)) { + MethodSpec.Builder setter = MethodSpec.methodBuilder(fieldName(field.name())) + .addModifiers(Modifier.PUBLIC) + .returns(builderType()) + .addParameter(TypeName.get(field.type()), field.name()); + setter + .addStatement("this.$L = $L", field.name(), field.name()) + .build(); + setters.add(setter + .addStatement("return this") + .build()); + } + } + return setters; + } + + @Override + protected List fields() { + return source.fields().stream() + .map(field -> FieldSpec.builder(TypeName.get(field.type()), field.name(), Modifier.PRIVATE) + .build()) + .toList(); + } + + @Override + protected MethodSpec buildMethod() { + final MethodSpec.Builder builder = MethodSpec.methodBuilder("build") + .addModifiers(Modifier.PUBLIC) + .returns(className(source)) + .addStatement(format("$type result = newInstance()", className(source).toString())); + source.fields().stream() + .filter(field -> !field.isFinal()) + .forEach(field -> { + if (field.isPublic()) { + builder.addStatement(format("result.$fieldName = $fieldName", field.name(), field.name())); + } else if (field.isPrivate()) { + builder.addStatement(format("setField(result, \"$fieldName\", $fieldName)", field.name(), field.name())); + } else if (field.isProtected() && notWithinTheSamePackage()) { + builder.addStatement(format("setField(result, \"$fieldName\", $fieldName)", field.name(), field.name())); + } else { + builder.addStatement(format("result.$fieldName = $fieldName", field.name(), field.name())); + } + }); + builder.addStatement("return result", className(source).toString()); + return builder.build(); + } + + public static TypeSpec produce(TypeDefinition source, Buildable buildable) { + return new FieldsInsideBuilderTypeSpecFactory(source, buildable).typeSpec(); + } +} diff --git a/compiler/src/main/java/io/jonasg/bob/specs/InstanceInsideBuilderTypeSpecFactory.java b/compiler/src/main/java/io/jonasg/bob/specs/InstanceInsideBuilderTypeSpecFactory.java new file mode 100644 index 0000000..4efee72 --- /dev/null +++ b/compiler/src/main/java/io/jonasg/bob/specs/InstanceInsideBuilderTypeSpecFactory.java @@ -0,0 +1,149 @@ +package io.jonasg.bob.specs; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.function.Predicate; + +import javax.lang.model.element.Modifier; + +import com.squareup.javapoet.ClassName; +import com.squareup.javapoet.FieldSpec; +import com.squareup.javapoet.MethodSpec; +import com.squareup.javapoet.TypeName; +import com.squareup.javapoet.TypeSpec; +import io.jonasg.bob.Buildable; +import io.jonasg.bob.definitions.ConstructorDefinition; +import io.jonasg.bob.definitions.FieldDefinition; +import io.jonasg.bob.definitions.MethodDefinition; +import io.jonasg.bob.definitions.TypeDefinition; +import io.jonasg.bob.utils.Formatter; + +public class InstanceInsideBuilderTypeSpecFactory extends BaseTypeSpecFactory { + + private InstanceInsideBuilderTypeSpecFactory(TypeDefinition source, Buildable buildable) { + super(source, buildable); + } + + public static TypeSpec produce(TypeDefinition source, Buildable buildable) { + return new InstanceInsideBuilderTypeSpecFactory(source, buildable).typeSpec(); + } + + private TypeName classNameWithoutGenerics(TypeDefinition definition) { + return ClassName.get(definition.packageName(), definition.fullTypeName()); + } + + @Override + protected List setters() { + List setters = new ArrayList<>(); + for (FieldDefinition field : source.fields()) { + if (!field.isFinal() && notExcluded(field)) { + MethodSpec.Builder builder = MethodSpec.methodBuilder(fieldName(field.name())) + .addModifiers(Modifier.PUBLIC) + .returns(builderType()) + .addParameter(TypeName.get(field.type()), field.name()); + Optional setter = setter(field); + if (setter.isPresent()) { + builder + .addStatement("instance." + setter.get().name() + "($L)", field.name()); + } else if (field.isPrivate() || field.isProtected() && notWithinTheSamePackage()) { + builder + .addStatement("setField($S, $L)", field.name(), field.name()) + .build(); + } else if (field.isPackageLocal()) { + if(!notWithinTheSamePackage()) + builder + .addStatement("instance.$L = $L", field.name(), field.name()) + .build(); + else + builder + .addStatement("setField($S, $L)", field.name(), field.name()) + .build(); + } else { + builder + .addStatement("instance.$L = $L", field.name(), field.name()) + .build(); + } + setters.add(builder + .addStatement("return this") + .build()); + } + } + return setters; + } + + private Optional setter(FieldDefinition field) { + List methods = source.methods(setterForField(field)); + if(methods.isEmpty()) return Optional.empty(); + return Optional.of(methods.get(0)); + } + + private Predicate setterForField(final FieldDefinition field) { + return new Predicate() { + @Override + public boolean test(MethodDefinition methodDefinition) { + String defaultSetter = Formatter.format("set$name", + field.name().substring(0, 1).toUpperCase() + field.name().substring(1)); + if(defaultSetter.equals(methodDefinition.name())) + return true; + return false; + } + }; + } + + @Override + protected List fields() { + return Collections.emptyList(); + } + + @Override + protected MethodSpec buildMethod() { + return MethodSpec.methodBuilder("build") + .addModifiers(Modifier.PUBLIC) + .returns(className(source)) + .addStatement(Formatter.format("$type result = instance;\n" + + "instance = newInstance();\n" + + "return result", className(source).toString())) + .build(); + } + + @Override + protected MethodSpec newInstance() { + MethodSpec.Builder newInstance = MethodSpec.methodBuilder("newInstance") + .addModifiers(Modifier.PROTECTED) + .returns(className(source)); + Optional defaultConstructor = defaultConstructor(); + String instantiationCode;; + if (defaultConstructor.isPresent() && !defaultConstructor.get().isPrivate()) + newInstance.addStatement("return new $L()", className(source)); + else { + if(defaultConstructor.isPresent() && defaultConstructor.get().isPrivate()) { + instantiationCode = "\tjava.lang.reflect.Constructor<$L> constructor = $L.class.getDeclaredConstructor(new Class[0]);\n" + + "constructor.setAccessible(true);\n" + + "instance = constructor.newInstance();\n" + + "} catch (NoSuchMethodException e) {\n" + + "\tthrow new RuntimeException();\n"; + } else { + instantiationCode = "\tinstance = ($L) $L.class.getDeclaredConstructors()[0].newInstance((Object[])new java.lang.reflect.Array[]{null});\n"; + } + newInstance.addStatement(" $L instance;try {\n" + + instantiationCode + + "} catch (InstantiationException e) {\n" + + "\tthrow new RuntimeException();\n" + + "} catch (IllegalAccessException e) {\n" + + "\tthrow new RuntimeException();\n" + + "} catch (java.lang.reflect.InvocationTargetException e) {\n" + + "\tthrow new RuntimeException();\n" + + "}return instance", className(source), className(source), classNameWithoutGenerics(source)); + } + return newInstance.build(); + } + + private Optional defaultConstructor() { + for (ConstructorDefinition constructor : source.constructors()) + if (constructor.parameters().isEmpty()) + return Optional.of(constructor); + return Optional.empty(); + } +} diff --git a/compiler/src/main/java/io/jonasg/bob/specs/TypeSpecFactory.java b/compiler/src/main/java/io/jonasg/bob/specs/TypeSpecFactory.java new file mode 100644 index 0000000..546edc6 --- /dev/null +++ b/compiler/src/main/java/io/jonasg/bob/specs/TypeSpecFactory.java @@ -0,0 +1,22 @@ +package io.jonasg.bob.specs; + +import io.jonasg.bob.Buildable; +import io.jonasg.bob.definitions.FieldDefinition; +import io.jonasg.bob.definitions.TypeDefinition; +import com.squareup.javapoet.TypeSpec; + +public abstract class TypeSpecFactory { + public static TypeSpec produce(TypeDefinition source, Buildable buildable) { + if (hasFinalFields(source)) + return FieldsInsideBuilderTypeSpecFactory.produce(source, buildable); + else + return InstanceInsideBuilderTypeSpecFactory.produce(source, buildable); + } + + private static boolean hasFinalFields(TypeDefinition source) { + for (FieldDefinition field : source.fields()) + if (field.isFinal()) + return true; + return false; + } +} diff --git a/compiler/src/main/java/io/jonasg/bob/utils/Formatter.java b/compiler/src/main/java/io/jonasg/bob/utils/Formatter.java new file mode 100644 index 0000000..101a84e --- /dev/null +++ b/compiler/src/main/java/io/jonasg/bob/utils/Formatter.java @@ -0,0 +1,8 @@ +package io.jonasg.bob.utils; + +public class Formatter { + + public static String format(String source, Object ... args) { + return String.format(source.replaceAll("\\$\\w+", "%s"), args); + } +} diff --git a/compiler/src/main/resources/META-INF/services/javax.annotation.processing.Processor b/compiler/src/main/resources/META-INF/services/javax.annotation.processing.Processor new file mode 100644 index 0000000..013d14c --- /dev/null +++ b/compiler/src/main/resources/META-INF/services/javax.annotation.processing.Processor @@ -0,0 +1 @@ +io.jonasg.bob.processor.BuildableProcessor diff --git a/compiler/src/test/java/io/jonasg/bob/specs/InstanceInsideBuilderTypeSpecFactoryTest.java b/compiler/src/test/java/io/jonasg/bob/specs/InstanceInsideBuilderTypeSpecFactoryTest.java new file mode 100644 index 0000000..bdc2efc --- /dev/null +++ b/compiler/src/test/java/io/jonasg/bob/specs/InstanceInsideBuilderTypeSpecFactoryTest.java @@ -0,0 +1,311 @@ +package io.jonasg.bob.specs; + +import static io.jonasg.bob.utils.Formatter.format; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Set; + +import javax.lang.model.element.Modifier; +import javax.lang.model.type.PrimitiveType; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.type.TypeVisitor; + +import org.junit.jupiter.api.Test; + +import com.squareup.javapoet.MethodSpec; +import com.squareup.javapoet.ParameterizedTypeName; +import com.squareup.javapoet.TypeName; +import com.squareup.javapoet.TypeSpec; +import io.jonasg.bob.Buildable; +import io.jonasg.bob.definitions.ConstructorDefinition; +import io.jonasg.bob.definitions.FieldDefinition; +import io.jonasg.bob.definitions.ParameterDefinition; +import io.jonasg.bob.definitions.TypeDefinition; + + +public class InstanceInsideBuilderTypeSpecFactoryTest { + + private static final String NO_INNER_CLASS = null; + + @Test + public void produceTest_normalPackagedBasedClass() { + List fields = new ArrayList<>(); + PrimitiveType primitiveType = primitiveType(); + fields.add(new FieldDefinition("count", Set.of(Modifier.PRIVATE), primitiveType)); + + TypeDefinition source = builderDefinition("com.wine.bar", "Cheese", NO_INNER_CLASS, fields, Collections.emptyList()); + Buildable buildable = newBuildable().get(); + TypeSpec typeSpec = InstanceInsideBuilderTypeSpecFactory.produce(source, buildable); + + assertThat(((ParameterizedTypeName) typeSpec.superclass).typeArguments.get(0).toString()) + .isEqualTo("com.wine.bar.Cheese"); + assertThat(typeSpec.name) + .isEqualTo("CheeseBuilder"); + } + + + @Test + public void produceTest_nestedClass() { + List fields = new ArrayList<>(); + PrimitiveType primitiveType = primitiveType(); + fields.add(new FieldDefinition("count", Set.of(Modifier.PRIVATE), primitiveType)); + + List defaultConstructor = Collections.singletonList(new ConstructorDefinition(Collections.emptyList(), Collections.singleton(Modifier.PUBLIC))); + TypeDefinition source = builderDefinition("com.wine.bar", "Cheese", "Cave.Cellar", fields, defaultConstructor); + Buildable buildable = newBuildable().get(); + TypeSpec typeSpec = InstanceInsideBuilderTypeSpecFactory.produce(source, buildable); + + String expectedClass = "com.wine.bar.Cave.Cellar.Cheese"; + assertThat(((ParameterizedTypeName) typeSpec.superclass).typeArguments.get(0).toString()) + .isEqualTo(expectedClass); + assertThat(typeSpec.name) + .isEqualTo("CheeseBuilder"); + assertThat(newInstance(typeSpec).returnType.toString()) + .isEqualTo(expectedClass); + assertThat(newInstance(typeSpec).code.toString()) + .isEqualTo("return new " + expectedClass + "();\n"); + } + + @Test + public void produceTest_newInstance_privateDefaultConstructor() { + List fields = new ArrayList<>(); + + List constructors = Collections.singletonList(new ConstructorDefinition(Collections.emptyList(), Collections.singleton(Modifier.PRIVATE))); + TypeDefinition source = builderDefinition("com.wine.bar", "Cheese", "Cave.Cellar", fields, constructors); + + Buildable buildable = newBuildable().get(); + TypeSpec typeSpec = InstanceInsideBuilderTypeSpecFactory.produce(source, buildable); + + String expectedClass = "com.wine.bar.Cave.Cellar.Cheese"; + assertThat(((ParameterizedTypeName) typeSpec.superclass).typeArguments.get(0).toString()) + .isEqualTo(expectedClass); + assertThat(typeSpec.name) + .isEqualTo("CheeseBuilder"); + assertThat(newInstance(typeSpec).returnType.toString()) + .isEqualTo(expectedClass); + assertThat(newInstance(typeSpec).code.toString()) + .isEqualTo(" " + expectedClass + " instance;try {\n" + + " \tjava.lang.reflect.Constructor constructor = " + + "com.wine.bar.Cave.Cellar.Cheese.class.getDeclaredConstructor(new Class[0]);\n" + + " constructor.setAccessible(true);\n" + + " instance = constructor.newInstance();\n" + + " } catch (NoSuchMethodException e) {\n" + + " \tthrow new RuntimeException();\n" + + " } catch (InstantiationException e) {\n" + + " \tthrow new RuntimeException();\n" + + " } catch (IllegalAccessException e) {\n" + + " \tthrow new RuntimeException();\n" + + " } catch (java.lang.reflect.InvocationTargetException e) {\n" + + " \tthrow new RuntimeException();\n" + + " }return instance;\n"); + } + + @Test + public void produceTest_newInstance_missingDefaultConstructor() { + List fields = new ArrayList<>(); + + List constructors = Collections.singletonList(new ConstructorDefinition(Collections.singletonList(new ParameterDefinition("name")), Collections.singleton(Modifier.PRIVATE))); + TypeDefinition source = builderDefinition("com.wine.bar", "Cheese", "Cave.Cellar", fields, constructors); + + Buildable buildable = newBuildable().get(); + TypeSpec typeSpec = InstanceInsideBuilderTypeSpecFactory.produce(source, buildable); + + String expectedClass = "com.wine.bar.Cave.Cellar.Cheese"; + assertThat(((ParameterizedTypeName) typeSpec.superclass).typeArguments.get(0).toString()) + .isEqualTo(expectedClass); + assertThat(typeSpec.name) + .isEqualTo("CheeseBuilder"); + assertThat(newInstance(typeSpec).returnType.toString()) + .isEqualTo(expectedClass); + assertThat(newInstance(typeSpec).code.toString()) + .isEqualTo(" " + expectedClass + " instance;try {\n" + + " \tinstance = (" + expectedClass + ") " + expectedClass + ".class.getDeclaredConstructors()[0].newInstance((Object[])new java.lang.reflect.Array[]{null});\n" + + " } catch (InstantiationException e) {\n" + + " \tthrow new RuntimeException();\n" + + " } catch (IllegalAccessException e) {\n" + + " \tthrow new RuntimeException();\n" + + " } catch (java.lang.reflect.InvocationTargetException e) {\n" + + " \tthrow new RuntimeException();\n" + + " }return instance;\n"); + } + + + @Test + public void produceTest_protectedFields_areSetUsingReflectionWhenNotInSamePackage() { + List fields = new ArrayList<>(); + TypeMirror type = mock(TypeMirror.class); + when(type.accept(any(TypeVisitor.class), any())).thenReturn(TypeName.get(String.class)); + fields.add(new FieldDefinition("age", Collections.singleton(Modifier.PROTECTED), type)); + + List constructors = Collections.singletonList(new ConstructorDefinition(Collections.singletonList(new ParameterDefinition("name")), Collections.emptySet())); + TypeDefinition source = builderDefinition("com.wine.bar", "Cheese", "Cave.Cellar", fields, constructors); + Buildable buildable = newBuildable().get(); + TypeSpec typeSpec = InstanceInsideBuilderTypeSpecFactory.produce(source, buildable); + + MethodSpec age = filter(typeSpec.methodSpecs, "age"); + assertThat(age.code.toString()).isEqualTo("setField(\"age\", age);\nreturn this;\n"); + } + + @Test + public void produceTest_protectedFieldsThatAreInTheSamePackage_areSetDirectly() { + List fields = new ArrayList<>(); + TypeMirror type = mock(TypeMirror.class); + when(type.accept(any(TypeVisitor.class), any())).thenReturn(TypeName.get(String.class)); + fields.add(new FieldDefinition("age", Collections.singleton(Modifier.PROTECTED), type)); + + List constructors = Collections.singletonList(new ConstructorDefinition(Collections.singletonList(new ParameterDefinition("name")), Collections.emptySet())); + TypeDefinition source = builderDefinition("com.wine.bar", "Cheese", null, fields, constructors); + Buildable buildable = newBuildable() + .withPackageName("com.wine.bar") + .get(); + TypeSpec typeSpec = InstanceInsideBuilderTypeSpecFactory.produce(source, buildable); + + MethodSpec age = filter(typeSpec.methodSpecs, "age"); + assertThat(age.code.toString()).isEqualTo("instance.age = age;\nreturn this;\n"); + } + + @SuppressWarnings("all") + private MethodSpec filter(List specs, String name) { + Objects.requireNonNull(name); + for(com.squareup.javapoet.MethodSpec spec: specs) + if(name.equals(spec.name)) + return spec; + throw new IllegalStateException(format("Unable to find method named %name", name)); + } + + + @Test + public void produceTest_prefix_appliedCorrectly() { + List fields = new ArrayList<>(); + TypeMirror type = mock(TypeMirror.class); + when(type.accept(any(TypeVisitor.class), any())).thenReturn(TypeName.get(String.class)); + fields.add(new FieldDefinition("age", Collections.singleton(Modifier.PUBLIC), type)); + + List constructors = Collections.singletonList(new ConstructorDefinition(Collections.singletonList(new ParameterDefinition("name")), Collections.emptySet())); + TypeDefinition source = builderDefinition("com.wine.bar", "Cheese", "Cave.Cellar", fields, constructors); + Buildable buildable = newBuildable() + .withPrefix("with") + .get(); + TypeSpec typeSpec = InstanceInsideBuilderTypeSpecFactory.produce(source, buildable); + + boolean found = false; + for(MethodSpec method: typeSpec.methodSpecs) { + if(Objects.equals(method.name, "withAge")) { + found = true; + } + } + assertThat(found).isTrue(); + } + + @Test + public void produceTest_excludes_satisfied() { + List fields = new ArrayList<>(); + TypeMirror type = mock(TypeMirror.class); + when(type.accept(any(TypeVisitor.class), any())).thenReturn(TypeName.get(String.class)); + fields.add(new FieldDefinition("age", Collections.singleton(Modifier.PUBLIC), type)); + fields.add(new FieldDefinition("taste", Collections.singleton(Modifier.PRIVATE), type)); + fields.add(new FieldDefinition("location", Collections.singleton(Modifier.PUBLIC), type)); + + List constructors = Collections.singletonList(new ConstructorDefinition(Collections.singletonList(new ParameterDefinition("name")), Collections.emptySet())); + TypeDefinition source = builderDefinition("com.wine.bar", "Cheese", "Cave.Cellar", fields, constructors); + Buildable buildable = newBuildable() + .withPrefix("with") + .withExcludes("age", "taste") + .get(); + TypeSpec typeSpec = InstanceInsideBuilderTypeSpecFactory.produce(source, buildable); + + boolean ageFound = false; + boolean locationFound = false; + boolean withTaste = false; + for(MethodSpec method: typeSpec.methodSpecs) { + if(Objects.equals(method.name, "withAge")) { + ageFound = true; + } + if(Objects.equals(method.name, "withLocation")) { + locationFound = true; + } + if(Objects.equals(method.name, "withTaste")) { + withTaste = true; + } + } + assertThat(ageFound).isFalse(); + assertThat(withTaste).isFalse(); + assertThat(locationFound).isTrue(); + } + + private MethodSpec newInstance(TypeSpec typeSpec) { + for (MethodSpec spec : typeSpec.methodSpecs) + if ("newInstance".equals(spec.name)) + return spec; + throw new IllegalStateException("Method newInstance not found"); + } + + @SuppressWarnings("all") + private TypeDefinition builderDefinition(String packageName, String typeName, String enclosedIn, List fields, List constructors) { + TypeDefinition definition = mock(TypeDefinition.class); + when(definition.typeName()).thenReturn(typeName); + when(definition.typeName()).thenReturn(typeName); + when(definition.packageName()).thenReturn(packageName); + when(definition.nestedIn()).thenReturn(enclosedIn); + when(definition.fullTypeName()).thenReturn(enclosedIn == null ? typeName : enclosedIn + "." + typeName); + when(definition.constructors()).thenReturn(constructors); + when(definition.fields()).thenReturn(fields); + + return definition; + } + + @SuppressWarnings("unchecked") + private PrimitiveType primitiveType() { + PrimitiveType primitiveType = mock(PrimitiveType.class); + when(primitiveType.accept(any(TypeVisitor.class), any())).thenReturn(mock(TypeName.class)); + return primitiveType; + } + + private static BuildableBuilder newBuildable() { + return new BuildableBuilder(); + } + + private static class BuildableBuilder { + private Buildable buildable; + private String prefix = ""; + private String packageName = ""; + private List excludes = new ArrayList<>(); + + BuildableBuilder() { + buildable = mock(Buildable.class); + } + + @SuppressWarnings("all") + BuildableBuilder withPrefix(String prefix) { + this.prefix = prefix; + return this; + } + + BuildableBuilder withExcludes(String ... excludes) { + this.excludes.addAll(Arrays.asList(excludes)); + return this; + } + + @SuppressWarnings("all") + BuildableBuilder withPackageName(String name) { + this.packageName = name; + return this; + } + + Buildable get() { + when(buildable.prefix()).thenReturn(prefix); + when(buildable.packageName()).thenReturn(packageName); + when(buildable.excludes()).thenReturn(excludes.toArray(new String[excludes.size()])); + return buildable; + } + } + +} diff --git a/core/pom.xml b/core/pom.xml new file mode 100644 index 0000000..48e4a70 --- /dev/null +++ b/core/pom.xml @@ -0,0 +1,11 @@ + + + + bob-project + io.jonasg + 0.1.0-SNAPSHOT + + jar + 4.0.0 + bob-core + diff --git a/core/src/main/java/io/jonasg/bob/AbstractBuilder.java b/core/src/main/java/io/jonasg/bob/AbstractBuilder.java new file mode 100644 index 0000000..3272025 --- /dev/null +++ b/core/src/main/java/io/jonasg/bob/AbstractBuilder.java @@ -0,0 +1,42 @@ +package io.jonasg.bob; + +import java.lang.reflect.Field; + +@SuppressWarnings("unused") +public abstract class AbstractBuilder implements Builder { + + protected T instance; + + public AbstractBuilder() { + instance = newInstance(); + } + + abstract protected T newInstance(); + abstract public T build(); + + protected void setField(String name, Object value) { + try { + Field field = instance.getClass(). + getDeclaredField(name); + field.setAccessible(true); + field.set(instance, value); + field.setAccessible(false); + } catch (NoSuchFieldException | IllegalAccessException e) { + throw new RuntimeException(e); + } + } + + protected void setField(Object instance, String name, Object value) { + try { + Field field = instance.getClass(). + getDeclaredField(name); + field.setAccessible(true); + field.set(instance, value); + field.setAccessible(false); + } catch (NoSuchFieldException | IllegalAccessException e) { + throw new RuntimeException(e); + } + } + + +} diff --git a/core/src/main/java/io/jonasg/bob/Builder.java b/core/src/main/java/io/jonasg/bob/Builder.java new file mode 100644 index 0000000..553a6e1 --- /dev/null +++ b/core/src/main/java/io/jonasg/bob/Builder.java @@ -0,0 +1,8 @@ +package io.jonasg.bob; + +@SuppressWarnings("unused") +public interface Builder { + + T build(); + +} diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..e7a561d --- /dev/null +++ b/pom.xml @@ -0,0 +1,131 @@ + + + 4.0.0 + + bob + Builder generator for Java + https://github.com/jonas-grgt/bob + io.jonasg + + + + Jonas Geiregat + jonas.grgt@gmail.com + + + + + + Apache-2.0 + http://www.apache.org/licenses/LICENSE-2.0 + + + + bob-project + pom + 0.1.0-SNAPSHOT + + core + compiler + bob + + + + UTF-8 + UTF-8 + + 3.12.1 + 3.6.0 + 3.3.0 + + 17 + 17 + + 1.9.0 + 5.10.1 + 3.23.1 + 5.11.0 + + + + https://github.com/jonasg-grgt/bob + scm:git:https://github.com/jonasg-grgt/bob + HEAD + + + + + + com.squareup + javapoet + 1.13.0 + + + org.junit + junit-bom + ${junit-bom.version} + pom + import + + + org.assertj + assertj-core + ${assertj-core.version} + test + + + org.mockito + mockito-core + ${mockito-core.version} + test + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + ${maven-compiler-plugin.version} + + ${maven.compiler.source} + ${maven.compiler.target} + -Xlint:all + true + true + + -proc:none + + + + org.apache.maven.plugins + maven-source-plugin + ${maven-source-plugin.version} + + + attach-sources + + jar + + + true + + + + + + + + + + snapshots + https://oss.sonatype.org/content/repositories/snapshots + + + release + https://oss.sonatype.org/service/local/staging/deploy/maven2/ + + + +