diff --git a/avaje-record-builder-core/src/main/java/io/avaje/recordbuilder/internal/ClassBodyBuilder.java b/avaje-record-builder-core/src/main/java/io/avaje/recordbuilder/internal/ClassBodyBuilder.java new file mode 100644 index 0000000..334bdbc --- /dev/null +++ b/avaje-record-builder-core/src/main/java/io/avaje/recordbuilder/internal/ClassBodyBuilder.java @@ -0,0 +1,76 @@ +package io.avaje.recordbuilder.internal; + +import static io.avaje.recordbuilder.internal.APContext.elements; +import static io.avaje.recordbuilder.internal.Templates.classTemplate; +import static java.util.stream.Collectors.joining; + +import java.text.MessageFormat; +import java.util.List; + +import javax.lang.model.element.RecordComponentElement; +import javax.lang.model.element.TypeElement; + +// TODO better name? +public class ClassBodyBuilder { + + static String createClassStart(TypeElement type, boolean isImported) { + + final var components = type.getRecordComponents(); + final var packageName = + elements().getPackageOf(type).getQualifiedName().toString() + + (isImported ? ".builder" : ""); + final var shortName = type.getSimpleName().toString(); + if (type.getEnclosingElement() instanceof TypeElement) { + isImported = true; + } + final RecordModel rm = new RecordModel(type, isImported, components); + rm.initialImports(); + final String fieldString = rm.fields(); + final var imports = rm.importsFormat(); + final var numberOfComponents = components.size(); + + // String fieldString = fields(components); + final String constructorParams = constructorParams(components, numberOfComponents > 5); + final String constructorBody = constructorBody(components); + final String builderFrom = + builderFrom(components).transform(s -> numberOfComponents > 5 ? "\n " + s : s); + final String build = + build(components).transform(s -> numberOfComponents > 6 ? "\n " + s : s); + return classTemplate( + packageName, + imports, + shortName, + fieldString, + constructorParams, + constructorBody, + builderFrom, + build); + } + + private static String constructorParams( + List components, boolean verticalArgs) { + + return components.stream() + .map(r -> UType.parse(r.asType()).shortType() + " " + r.getSimpleName()) + .collect(joining(verticalArgs ? ",\n " : ", ")) + .transform(s -> verticalArgs ? "\n " + s : s); + } + + private static String constructorBody(List components) { + return components.stream() + .map(RecordComponentElement::getSimpleName) + .map(s -> MessageFormat.format("this.{0} = {0};", s)) + .collect(joining("\n ")); + } + + private static String builderFrom(List components) { + return components.stream() + .map(RecordComponentElement::getSimpleName) + .map("from.%s()"::formatted) + .collect(joining(", ")); + } + + private static String build(List components) { + return components.stream().map(RecordComponentElement::getSimpleName).collect(joining(", ")); + } +} diff --git a/avaje-record-builder-core/src/main/java/io/avaje/recordbuilder/internal/InitMap.java b/avaje-record-builder-core/src/main/java/io/avaje/recordbuilder/internal/InitMap.java new file mode 100644 index 0000000..9ce63ae --- /dev/null +++ b/avaje-record-builder-core/src/main/java/io/avaje/recordbuilder/internal/InitMap.java @@ -0,0 +1,60 @@ +package io.avaje.recordbuilder.internal; + +import java.io.ObjectOutputStream.PutField; +import java.util.HashMap; +import java.util.Map; + +public class InitMap { + private static final Map defaultsMap = new HashMap<>(); + + static { + // TODO add the rest of the collections + final var util = "java.util."; + final var initDiamond = "new java.util.%s<>()"; + // optional + put(util + "Optional", util + "Optional.empty()"); + put(util + "OptionalInt", util + "OptionalInt.empty()"); + put(util + "OptionalDouble", util + "OptionalDouble.empty()"); + put(util + "OptionalLong", util + "OptionalLong.empty()"); + + put(util + "Collection", initDiamond.formatted("ArrayList")); + put(util + "SequencedCollection", initDiamond.formatted("ArrayList")); + // list + put(util + "List", initDiamond.formatted("ArrayList")); + put(util + "ArrayList", initDiamond.formatted("ArrayList")); + put(util + "LinkedList", initDiamond.formatted("LinkedList")); + // set + put(util + "Set", initDiamond.formatted("HashSet")); + put(util + "SequencedSet", initDiamond.formatted("LinkedHashSet")); + put(util + "HashSet", initDiamond.formatted("HashSet")); + put(util + "TreeSet", initDiamond.formatted("TreeSet")); + put(util + "SortedSet", initDiamond.formatted("TreeSet")); + put(util + "NavigableSet", initDiamond.formatted("TreeSet")); + put(util + "LinkedHashSet", initDiamond.formatted("LinkedHashSet")); + // map + put(util + "Map", initDiamond.formatted("HashMap")); + put(util + "SequencedMap", initDiamond.formatted("LinkedHashMap")); + put(util + "HashMap", initDiamond.formatted("HashMap")); + put(util + "LinkedHashMap", initDiamond.formatted("LinkedHashMap")); + put(util + "TreeMap", initDiamond.formatted("TreeMap")); + put(util + "SortedMap", initDiamond.formatted("TreeMap")); + put(util + "NavigableMap", initDiamond.formatted("TreeMap")); + + // queue + + // deque + } + + static void put(String key, String value) { + + defaultsMap.put(key, value); + } + + static String get(String key) { + return defaultsMap.get(key); + } + + public static void putAll(Map map) { + defaultsMap.putAll(map); + } +} diff --git a/avaje-record-builder-core/src/main/java/io/avaje/recordbuilder/internal/RecordModel.java b/avaje-record-builder-core/src/main/java/io/avaje/recordbuilder/internal/RecordModel.java index ca0b985..470248e 100644 --- a/avaje-record-builder-core/src/main/java/io/avaje/recordbuilder/internal/RecordModel.java +++ b/avaje-record-builder-core/src/main/java/io/avaje/recordbuilder/internal/RecordModel.java @@ -3,11 +3,13 @@ import static java.util.function.Predicate.not; import static java.util.stream.Collectors.joining; +import java.lang.invoke.VarHandle; import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeSet; +import java.util.regex.Pattern; import java.util.stream.Collectors; import javax.lang.model.element.RecordComponentElement; @@ -23,7 +25,12 @@ final class RecordModel { private final Set importTypes = new TreeSet<>(); - RecordModel(TypeElement type, boolean isImported, List components) { + // Create a Pattern object + Pattern JAVA_UTIL = Pattern.compile("new\\s+(java\\.util\\.[A-Za-z]+)"); + Pattern OPTIONAL = Pattern.compile("(java\\.util\\.[A-Za-z]+)"); + + RecordModel( + TypeElement type, boolean isImported, List components) { this.type = type; this.isImported = isImported; this.components = components; @@ -47,7 +54,7 @@ void initialImports() { importTypes.addAll(types); } - String fields(Map defaultsMap) { + String fields() { final var builder = new StringBuilder(); for (final var element : components) { final var uType = UType.parse(element.asType()); @@ -57,12 +64,21 @@ String fields(Map defaultsMap) { if (initPrism != null) { defaultVal = " = " + initPrism.value(); } else { - final String dt = defaultsMap.get(uType.mainType()); + final String dt = InitMap.get(uType.mainType()); if (dt != null) { - var javaUtil = dt.startsWith("java.util"); - if (javaUtil) { - importTypes.add(dt); - defaultVal = " = new " + ProcessorUtils.shortType(dt) + "<>()"; + var javaUtil = dt.startsWith("new java.util"); + var optional = dt.startsWith("java.util.Optional"); + if (javaUtil || optional) { + if (optional) { + var matcher = OPTIONAL.matcher(dt); + matcher.find(); + importTypes.add(matcher.group(0)); + } else { + var matcher = JAVA_UTIL.matcher(dt); + matcher.find(); + importTypes.add(matcher.group(1)); + } + defaultVal = " = " + dt.replace("java.util.", ""); } else { defaultVal = " = " + dt; } @@ -70,8 +86,7 @@ String fields(Map defaultsMap) { } builder.append( - " private %s %s%s;\n" - .formatted(uType.shortType(), element.getSimpleName(), defaultVal)); + " private %s %s%s;\n".formatted(uType.shortType(), element.getSimpleName(), defaultVal)); } return builder.toString(); diff --git a/avaje-record-builder-core/src/main/java/io/avaje/recordbuilder/internal/RecordProcessor.java b/avaje-record-builder-core/src/main/java/io/avaje/recordbuilder/internal/RecordProcessor.java index 12d23e0..32caf39 100644 --- a/avaje-record-builder-core/src/main/java/io/avaje/recordbuilder/internal/RecordProcessor.java +++ b/avaje-record-builder-core/src/main/java/io/avaje/recordbuilder/internal/RecordProcessor.java @@ -1,7 +1,23 @@ package io.avaje.recordbuilder.internal; -import io.avaje.prism.GenerateAPContext; -import io.avaje.prism.GenerateUtils; +import static io.avaje.recordbuilder.internal.Templates.*; +import static io.avaje.recordbuilder.internal.APContext.asTypeElement; +import static io.avaje.recordbuilder.internal.APContext.createSourceFile; +import static io.avaje.recordbuilder.internal.APContext.elements; +import static io.avaje.recordbuilder.internal.APContext.getModuleInfoReader; +import static io.avaje.recordbuilder.internal.APContext.logError; +import static io.avaje.recordbuilder.internal.APContext.typeElement; +import static java.util.stream.Collectors.joining; +import static java.util.stream.Collectors.toMap; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.text.MessageFormat; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; import javax.annotation.processing.AbstractProcessor; import javax.annotation.processing.ProcessingEnvironment; @@ -12,57 +28,15 @@ import javax.lang.model.element.RecordComponentElement; import javax.lang.model.element.TypeElement; import javax.lang.model.util.ElementFilter; -import java.io.IOException; -import java.io.UncheckedIOException; -import java.text.MessageFormat; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; -import static io.avaje.recordbuilder.internal.APContext.*; -import static java.util.stream.Collectors.joining; -import static java.util.stream.Collectors.toMap; +import io.avaje.prism.GenerateAPContext; +import io.avaje.prism.GenerateUtils; -// TODO break up this God class @GenerateUtils @GenerateAPContext @SupportedAnnotationTypes({RecordBuilderPrism.PRISM_TYPE, ImportPrism.PRISM_TYPE}) public final class RecordProcessor extends AbstractProcessor { - private static final Map defaultsMap = new HashMap<>(); - - static { - // TODO add the rest of the collections - final var util = "java.util."; - defaultsMap.put(util + "Collection", util + "ArrayList"); - defaultsMap.put(util + "SequencedCollection", util + "ArrayList"); - // list - defaultsMap.put(util + "List", util + "ArrayList"); - defaultsMap.put(util + "ArrayList", util + "ArrayList"); - defaultsMap.put(util + "LinkedList", util + "LinkedList"); - // set - defaultsMap.put(util + "Set", util + "HashSet"); - defaultsMap.put(util + "SequencedSet", util + "LinkedHashSet"); - defaultsMap.put(util + "HashSet", util + "HashSet"); - defaultsMap.put(util + "TreeSet", util + "TreeSet"); - defaultsMap.put(util + "SortedSet", util + "TreeSet"); - defaultsMap.put(util + "NavigableSet", util + "TreeSet"); - defaultsMap.put(util + "LinkedHashSet", util + "LinkedHashSet"); - // map - defaultsMap.put(util + "Map", util + "HashMap"); - defaultsMap.put(util + "SequencedMap", util + "LinkedHashMap"); - defaultsMap.put(util + "HashMap", util + "HashMap"); - defaultsMap.put(util + "LinkedHashMap", util + "LinkedHashMap"); - defaultsMap.put(util + "TreeMap", util + "TreeMap"); - defaultsMap.put(util + "SortedMap", util + "TreeMap"); - defaultsMap.put(util + "NavigableMap", util + "TreeMap"); - - // queue - - // deque - } - @Override public SourceVersion getSupportedSourceVersion() { return SourceVersion.latestSupported(); @@ -78,15 +52,15 @@ public synchronized void init(ProcessingEnvironment env) { public boolean process(Set tes, RoundEnvironment roundEnv) { final var globalTypeInitializers = - roundEnv.getElementsAnnotatedWith(typeElement(GlobalPrism.PRISM_TYPE)).stream() - .map(GlobalPrism::getInstanceOn) - .collect(toMap(s -> s.type().toString(), GlobalPrism::value)); + roundEnv.getElementsAnnotatedWith(typeElement(GlobalPrism.PRISM_TYPE)).stream() + .map(GlobalPrism::getInstanceOn) + .collect(toMap(s -> s.type().toString(), GlobalPrism::value)); - defaultsMap.putAll(globalTypeInitializers); + InitMap.putAll(globalTypeInitializers); APContext.setProjectModuleElement(tes, roundEnv); for (final TypeElement type : - ElementFilter.typesIn( - roundEnv.getElementsAnnotatedWith(typeElement(RecordBuilderPrism.PRISM_TYPE)))) { + ElementFilter.typesIn( + roundEnv.getElementsAnnotatedWith(typeElement(RecordBuilderPrism.PRISM_TYPE)))) { if (type.getRecordComponents().isEmpty()) { logError(type, "Builders can only be generated for record classes"); continue; @@ -95,11 +69,11 @@ public boolean process(Set tes, RoundEnvironment roundEnv } roundEnv.getElementsAnnotatedWith(typeElement(ImportPrism.PRISM_TYPE)).stream() - .map(ImportPrism::getInstanceOn) - .map(ImportPrism::value) - .flatMap(List::stream) - .map(APContext::asTypeElement) - .forEach(this::readElement); + .map(ImportPrism::getInstanceOn) + .map(ImportPrism::value) + .flatMap(List::stream) + .map(APContext::asTypeElement) + .forEach(this::readElement); if (roundEnv.processingOver()) { try (var reader = getModuleInfoReader()) { @@ -120,39 +94,14 @@ private void readElement(TypeElement type, boolean isImported) { final var components = type.getRecordComponents(); final var packageName = - elements().getPackageOf(type).getQualifiedName().toString() - + (isImported ? ".builder" : ""); + elements().getPackageOf(type).getQualifiedName().toString() + + (isImported ? ".builder" : ""); final var shortName = type.getSimpleName().toString(); - if (type.getEnclosingElement() instanceof TypeElement) { - isImported = true; - } - final RecordModel rm = new RecordModel(type, isImported, components); - rm.initialImports(); - final String fieldString = rm.fields(defaultsMap); - final var imports = rm.importsFormat(); - final var numberOfComponents = components.size(); - - // String fieldString = fields(components); - final String constructorParams = constructorParams(components, numberOfComponents > 5); - final String constructorBody = constructorBody(components); - final String builderFrom = - builderFrom(components).transform(s -> numberOfComponents > 5 ? "\n " + s : s); - final String build = - build(components).transform(s -> numberOfComponents > 6 ? "\n " + s : s); try (var writer = - new Append(createSourceFile(packageName + "." + shortName + "Builder").openWriter())) { - final var temp = - template( - packageName, - imports, - shortName, - fieldString, - constructorParams, - constructorBody, - builderFrom, - build); - writer.append(temp); + new Append(createSourceFile(packageName + "." + shortName + "Builder").openWriter())) { + + writer.append(ClassBodyBuilder.createClassStart(type, isImported)); final var writeGetters = RecordBuilderPrism.getInstanceOn(type).getters(); methods(writer, shortName, components, writeGetters); } catch (final IOException e) { @@ -160,38 +109,11 @@ private void readElement(TypeElement type, boolean isImported) { } } - private static String constructorParams( - List components, boolean verticalArgs) { - - return components.stream() - .map(r -> UType.parse(r.asType()).shortType() + " " + r.getSimpleName()) - .collect(joining(verticalArgs ? ",\n " : ", ")) - .transform(s -> verticalArgs ? "\n " + s : s); - } - - private static String constructorBody(List components) { - return components.stream() - .map(RecordComponentElement::getSimpleName) - .map(s -> MessageFormat.format("this.{0} = {0};", s)) - .collect(joining("\n ")); - } - - private static String builderFrom(List components) { - return components.stream() - .map(RecordComponentElement::getSimpleName) - .map("from.%s()"::formatted) - .collect(joining(", ")); - } - - private static String build(List components) { - return components.stream().map(RecordComponentElement::getSimpleName).collect(joining(", ")); - } - private void methods( - Append writer, - String shortName, - List components, - Boolean writeGetters) { + Append writer, + String shortName, + List components, + Boolean writeGetters) { boolean getters = Boolean.TRUE.equals(writeGetters); @@ -206,99 +128,24 @@ private void methods( String param0 = type.param0(); String param0ShortType = UType.parse(param0).shortType(); Name simpleName = element.getSimpleName(); - writer.append(methodAdd(simpleName.toString(), type.shortType(), shortName, param0ShortType)); + writer.append( + methodAdd(simpleName.toString(), type.shortType(), shortName, param0ShortType)); + } + if (APContext.isAssignable(typeElement, "java.util.Map")) { + String param0 = type.param0(); + String param0ShortType = UType.parse(param0).shortType(); + String param1 = type.param1(); + String param1ShortType = UType.parse(param1).shortType(); + Name simpleName = element.getSimpleName(); + writer.append( + methodPut( + simpleName.toString(), + type.shortType(), + shortName, + param0ShortType, + param1ShortType)); } } writer.append("}"); } - - private String methodSetter(CharSequence componentName, String type, String shortName) { - return MessageFormat.format( - """ - - /** Set a new value for '{'@code {0}'}'. */ - public {2}Builder {0}({1} {0}) '{' - this.{0} = {0}; - return this; - '}' - """, - componentName, type, shortName.replace(".", "$")); - } - - private String methodGetter(CharSequence componentName, String type, String shortName) { - return MessageFormat.format( - """ - - /** Return the current value for '{'@code {0}'}'. */ - public {1} {0}() '{' - return {0}; - '}' - """, - componentName, type, shortName.replace(".", "$")); - } - - private String methodAdd(String componentName, String type, String shortName, String param0) { - String upperCamal = Character.toUpperCase(componentName.charAt(0)) + componentName.substring(1); - return MessageFormat.format( - """ - - /** Add to the '{'@code {0}'}'. */ - public {2}Builder add{3}({4} element) '{' - this.{0}.add(element); - return this; - '}' - """, - componentName, type, shortName.replace(".", "$"), upperCamal, param0); - } - - private String template( - String packageName, - String imports, - String shortName, - String fields, - String constructor, - String constructorBody, - String builderFrom, - String build) { - - return MessageFormat.format( - """ - package {0}; - - {1} - - /** Builder class for '{'@link {2}'}' */ - @Generated("avaje-record-builder") - public class {2}Builder '{' - {3} - private {2}Builder() '{' - '}' - - private {2}Builder({4}) '{' - {5} - '}' - - /** - * Return a new builder with all fields set to default Java values - */ - public static {2}Builder builder() '{' - return new {2}Builder(); - '}' - - /** - * Return a new builder with all fields set to the values taken from the given record instance - */ - public static {2}Builder builder({2} from) '{' - return new {2}Builder({6}); - '}' - - /** - * Return a new {2} instance with all fields set to the current values in this builder - */ - public {2} build() '{' - return new {2}({7}); - '}' - """, - packageName, imports, shortName, fields, constructor, constructorBody, builderFrom, build); - } } diff --git a/avaje-record-builder-core/src/main/java/io/avaje/recordbuilder/internal/Templates.java b/avaje-record-builder-core/src/main/java/io/avaje/recordbuilder/internal/Templates.java new file mode 100644 index 0000000..6302001 --- /dev/null +++ b/avaje-record-builder-core/src/main/java/io/avaje/recordbuilder/internal/Templates.java @@ -0,0 +1,111 @@ +package io.avaje.recordbuilder.internal; + +import java.text.MessageFormat; + +public class Templates { + private Templates() {} + + static String classTemplate( + String packageName, + String imports, + String shortName, + String fields, + String constructor, + String constructorBody, + String builderFrom, + String build) { + + return MessageFormat.format( + """ + package {0}; + + {1} + + /** Builder class for '{'@link {2}'}' */ + @Generated("avaje-record-builder") + public class {2}Builder '{' + {3} + private {2}Builder() '{' + '}' + + private {2}Builder({4}) '{' + {5} + '}' + + /** + * Return a new builder with all fields set to default Java values + */ + public static {2}Builder builder() '{' + return new {2}Builder(); + '}' + + /** + * Return a new builder with all fields set to the values taken from the given record instance + */ + public static {2}Builder builder({2} from) '{' + return new {2}Builder({6}); + '}' + + /** + * Return a new {2} instance with all fields set to the current values in this builder + */ + public {2} build() '{' + return new {2}({7}); + '}' + + """, + packageName, imports, shortName, fields, constructor, constructorBody, builderFrom, build); + } + + static String methodSetter(CharSequence componentName, String type, String shortName) { + return MessageFormat.format( + """ + /** Set a new value for '{'@code {0}'}'. */ + public {2}Builder {0}({1} {0}) '{' + this.{0} = {0}; + return this; + '}' + """, + componentName, type, shortName.replace(".", "$")); + } + + static String methodGetter(CharSequence componentName, String type, String shortName) { + return MessageFormat.format( + """ + /** Return the current value for '{'@code {0}'}'. */ + public {1} {0}() '{' + return {0}; + '}' + """, + componentName, type, shortName.replace(".", "$")); + } + + static String methodAdd(String componentName, String type, String shortName, String param0) { + String upperCamel = Character.toUpperCase(componentName.charAt(0)) + componentName.substring(1); + return MessageFormat.format( + """ + + /** Add new element to the '{'@code {0}'}' collection. */ + public {2}Builder add{3}({4} element) '{' + this.{0}.add(element); + return this; + '}' + """, + componentName, type, shortName.replace(".", "$"), upperCamel, param0); + } + + static String methodPut( + String componentName, String type, String shortName, String param0, String param1) { + String upperCamel = Character.toUpperCase(componentName.charAt(0)) + componentName.substring(1); + return MessageFormat.format( + """ + + /** Add new key/value pair to the '{'@code {0}'}' map. */ + public {2}Builder put{3}({4} key, {5} value) '{' + this.{0}.add(element); + return this; + '}' + """, + componentName, type, shortName.replace(".", "$"), upperCamel, param0, param1); + } +} diff --git a/avaje-record-builder-core/src/main/java/io/avaje/recordbuilder/internal/UType.java b/avaje-record-builder-core/src/main/java/io/avaje/recordbuilder/internal/UType.java index d379816..3022d95 100644 --- a/avaje-record-builder-core/src/main/java/io/avaje/recordbuilder/internal/UType.java +++ b/avaje-record-builder-core/src/main/java/io/avaje/recordbuilder/internal/UType.java @@ -92,6 +92,11 @@ public String shortType() { public String mainType() { return rawType; } + + @Override + public String toString() { + return rawType; + } } /** Generic type. */ @@ -123,6 +128,11 @@ public String full() { return rawType; } + @Override + public String toString() { + return rawType; + } + @Override public Set importTypes() { final Set set = new LinkedHashSet<>(); diff --git a/avaje-record-builder/src/main/java/io/avaje/recordbuilder/Generated.java b/avaje-record-builder/src/main/java/io/avaje/recordbuilder/Generated.java index 5c7fa94..36ba6fa 100644 --- a/avaje-record-builder/src/main/java/io/avaje/recordbuilder/Generated.java +++ b/avaje-record-builder/src/main/java/io/avaje/recordbuilder/Generated.java @@ -9,7 +9,7 @@ * Marks source code that has been generated. */ @Target(ElementType.TYPE) -@Retention(RetentionPolicy.RUNTIME) +@Retention(RetentionPolicy.SOURCE) public @interface Generated { /** diff --git a/blackbox-test-records/src/main/java/io/avaje/recordbuilder/test/Optionals.java b/blackbox-test-records/src/main/java/io/avaje/recordbuilder/test/Optionals.java new file mode 100644 index 0000000..ad7aab3 --- /dev/null +++ b/blackbox-test-records/src/main/java/io/avaje/recordbuilder/test/Optionals.java @@ -0,0 +1,10 @@ +package io.avaje.recordbuilder.test; + +import java.util.*; +import java.util.OptionalInt; + +import io.avaje.recordbuilder.RecordBuilder; + +@RecordBuilder +public record Optionals( + Optional op, OptionalInt opInt, OptionalDouble opDouble, OptionalLong opLong) {}