diff --git a/README.md b/README.md index 1b7fbabd..00d55078 100644 --- a/README.md +++ b/README.md @@ -364,7 +364,7 @@ This project was made possible thanks to the following: ### Tools and Libraries -- **[JavaPoet](https://github.com/palantir/javapoet)** - An excellent library for generating Java source code. Originally created by Square, now maintained by Palantir. JavaPoet made it straightforward to generate clean, readable builder code. +- **[Roaster](https://github.com/forge/roaster)** - A fluent Java source generation and formatting library from the JBoss Forge ecosystem. Roaster is used to generate and format the builder source code. - **[Google Compile Testing](https://github.com/google/compile-testing)** - Essential for testing annotation processors with comprehensive compilation diagnostics. ### Learning Resources @@ -372,7 +372,7 @@ This project was made possible thanks to the following: The following resources were invaluable for understanding annotation processing: - **[Baeldung: Java Annotation Processing and Creating a Builder](https://www.baeldung.com/java-annotation-processing-builder)** - Comprehensive guide to annotation processing fundamentals -- **[SkyRo Tech: Code Generation with JavaPoet in Practice](https://medium.com/skyro-tech/code-generation-with-javapoet-on-practice-bfbe8ca56a61)** - Practical examples of using JavaPoet +- **[Roaster GitHub Repository](https://github.com/forge/roaster)** - Reference for Java source generation and formatting with Roaster - **[Annotation Processing Demo](https://github.com/ledungcobra/annotation-processing-demo)** by Le Dung - Hands-on examples of annotation processor implementation Thank you to all contributors and the Java community for making this project possible! diff --git a/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/BookDtoBuilder.java b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/BookDtoBuilder.java index 3b58cd81..94e880ca 100644 --- a/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/BookDtoBuilder.java +++ b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/BookDtoBuilder.java @@ -3,7 +3,6 @@ import static org.javahelpers.simple.builders.core.util.TrackedValue.changedValue; import static org.javahelpers.simple.builders.core.util.TrackedValue.initialValue; import static org.javahelpers.simple.builders.core.util.TrackedValue.unsetValue; - import java.math.BigDecimal; import java.time.LocalDate; import java.time.LocalDateTime; @@ -11,6 +10,8 @@ import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Supplier; import org.apache.commons.lang3.builder.ToStringBuilder; import org.javahelpers.simple.builders.core.util.BuilderToStringStyle; import org.javahelpers.simple.builders.core.util.TrackedValue; @@ -19,101 +20,83 @@ * Builder for {@code org.javahelpers.simple.builders.example.BookDto}. *

* This builder provides a fluent API for creating instances of org.javahelpers.simple.builders.example.BookDto with - * method chaining and validation. Use the static {@code create()} method - * to obtain a new builder instance, configure the desired properties using - * the setter methods, and then call {@code build()} to create the final DTO. + * method chaining and validation. Use the static {@code create()} method to obtain a new builder instance, configure + * the desired properties using the setter methods, and then call {@code build()} to create the final DTO. */ public class BookDtoBuilder { + /** * Tracked value for author: the book author to set. */ private TrackedValue author = unsetValue(); - /** * Tracked value for available: true if available, false otherwise. */ private TrackedValue available = unsetValue(); - /** * Tracked value for category: the category code to set. */ private TrackedValue category = unsetValue(); - /** * Tracked value for discount: the discount percentage to set. */ private TrackedValue discount = unsetValue(); - /** * Tracked value for edition: the edition number to set. */ private TrackedValue edition = unsetValue(); - /** * Tracked value for exactPrice: the exact book price to set. */ private TrackedValue exactPrice = unsetValue(); - /** * Tracked value for genres: the set of genres to set. */ private TrackedValue> genres = unsetValue(); - /** * Tracked value for isbn: the ISBN to set. */ private TrackedValue isbn = unsetValue(); - /** * Tracked value for lastUpdated: the last update timestamp to set. */ private TrackedValue lastUpdated = unsetValue(); - /** * Tracked value for metadata: the metadata map to set. */ private TrackedValue> metadata = unsetValue(); - /** * Tracked value for pages: the page count to set. */ private TrackedValue pages = unsetValue(); - /** * Tracked value for price: the book price to set. */ private TrackedValue price = unsetValue(); - /** * Tracked value for publishDate: the publication date to set. */ private TrackedValue publishDate = unsetValue(); - /** * Tracked value for publisher: the publisher to set. */ private TrackedValue publisher = unsetValue(); - /** * Tracked value for rating: the book rating to set. */ private TrackedValue rating = unsetValue(); - /** * Tracked value for salesCount: the sales count to set. */ private TrackedValue salesCount = unsetValue(); - /** * Tracked value for subtitle: an Optional containing the subtitle to set. */ private TrackedValue> subtitle = unsetValue(); - /** * Tracked value for tags: the list of tags to set. */ private TrackedValue> tags = unsetValue(); - /** * Tracked value for title: the book title to set. */ @@ -127,26 +110,30 @@ public BookDtoBuilder() { /** * Initialisation of builder for {@code org.javahelpers.simple.builders.example.BookDto} by a instance. - * + * * @param instance object instance for initialisiation */ public BookDtoBuilder(BookDto instance) { this.author = initialValue(instance.getAuthor()); this.available = initialValue(instance.isAvailable()); if (this.available.value() == null) { - throw new IllegalArgumentException("Cannot initialize builder from instance: field 'available' is marked as non-null but source object has null value"); + throw new IllegalArgumentException( + "Cannot initialize builder from instance: field 'available' is marked as non-null but source object has null value"); } this.category = initialValue(instance.getCategory()); if (this.category.value() == null) { - throw new IllegalArgumentException("Cannot initialize builder from instance: field 'category' is marked as non-null but source object has null value"); + throw new IllegalArgumentException( + "Cannot initialize builder from instance: field 'category' is marked as non-null but source object has null value"); } this.discount = initialValue(instance.getDiscount()); if (this.discount.value() == null) { - throw new IllegalArgumentException("Cannot initialize builder from instance: field 'discount' is marked as non-null but source object has null value"); + throw new IllegalArgumentException( + "Cannot initialize builder from instance: field 'discount' is marked as non-null but source object has null value"); } this.edition = initialValue(instance.getEdition()); if (this.edition.value() == null) { - throw new IllegalArgumentException("Cannot initialize builder from instance: field 'edition' is marked as non-null but source object has null value"); + throw new IllegalArgumentException( + "Cannot initialize builder from instance: field 'edition' is marked as non-null but source object has null value"); } this.exactPrice = initialValue(instance.getExactPrice()); this.genres = initialValue(instance.getGenres()); @@ -155,21 +142,25 @@ public BookDtoBuilder(BookDto instance) { this.metadata = initialValue(instance.getMetadata()); this.pages = initialValue(instance.getPages()); if (this.pages.value() == null) { - throw new IllegalArgumentException("Cannot initialize builder from instance: field 'pages' is marked as non-null but source object has null value"); + throw new IllegalArgumentException( + "Cannot initialize builder from instance: field 'pages' is marked as non-null but source object has null value"); } this.price = initialValue(instance.getPrice()); if (this.price.value() == null) { - throw new IllegalArgumentException("Cannot initialize builder from instance: field 'price' is marked as non-null but source object has null value"); + throw new IllegalArgumentException( + "Cannot initialize builder from instance: field 'price' is marked as non-null but source object has null value"); } this.publishDate = initialValue(instance.getPublishDate()); this.publisher = initialValue(instance.getPublisher()); this.rating = initialValue(instance.getRating()); if (this.rating.value() == null) { - throw new IllegalArgumentException("Cannot initialize builder from instance: field 'rating' is marked as non-null but source object has null value"); + throw new IllegalArgumentException( + "Cannot initialize builder from instance: field 'rating' is marked as non-null but source object has null value"); } this.salesCount = initialValue(instance.getSalesCount()); if (this.salesCount.value() == null) { - throw new IllegalArgumentException("Cannot initialize builder from instance: field 'salesCount' is marked as non-null but source object has null value"); + throw new IllegalArgumentException( + "Cannot initialize builder from instance: field 'salesCount' is marked as non-null but source object has null value"); } this.subtitle = initialValue(instance.getSubtitle()); this.tags = initialValue(instance.getTags()); @@ -178,7 +169,7 @@ public BookDtoBuilder(BookDto instance) { /** * Creating a new builder for {@code org.javahelpers.simple.builders.example.BookDto}. - * + * * @return builder for {@code org.javahelpers.simple.builders.example.BookDto} */ public static BookDtoBuilder create() { @@ -187,7 +178,7 @@ public static BookDtoBuilder create() { /** * Sets the value for author. - * + * * @param author the book author to set * @return current instance of builder */ @@ -198,7 +189,7 @@ public BookDtoBuilder author(String author) { /** * Sets the value for available. - * + * * @param available true if available, false otherwise * @return current instance of builder */ @@ -209,7 +200,7 @@ public BookDtoBuilder available(boolean available) { /** * Sets the value for category. - * + * * @param category the category code to set * @return current instance of builder */ @@ -220,7 +211,7 @@ public BookDtoBuilder category(char category) { /** * Sets the value for discount. - * + * * @param discount the discount percentage to set * @return current instance of builder */ @@ -231,7 +222,7 @@ public BookDtoBuilder discount(float discount) { /** * Sets the value for edition. - * + * * @param edition the edition number to set * @return current instance of builder */ @@ -242,7 +233,7 @@ public BookDtoBuilder edition(short edition) { /** * Sets the value for exactPrice. - * + * * @param exactPrice the exact book price to set * @return current instance of builder */ @@ -253,7 +244,7 @@ public BookDtoBuilder exactPrice(BigDecimal exactPrice) { /** * Sets the value for genres. - * + * * @param genres the set of genres to set * @return current instance of builder */ @@ -264,7 +255,7 @@ public BookDtoBuilder genres(Set genres) { /** * Sets the value for isbn. - * + * * @param isbn the ISBN to set * @return current instance of builder */ @@ -275,7 +266,7 @@ public BookDtoBuilder isbn(String isbn) { /** * Sets the value for lastUpdated. - * + * * @param lastUpdated the last update timestamp to set * @return current instance of builder */ @@ -286,7 +277,7 @@ public BookDtoBuilder lastUpdated(LocalDateTime lastUpdated) { /** * Sets the value for metadata. - * + * * @param metadata the metadata map to set * @return current instance of builder */ @@ -297,7 +288,7 @@ public BookDtoBuilder metadata(Map metadata) { /** * Sets the value for pages. - * + * * @param pages the page count to set * @return current instance of builder */ @@ -308,7 +299,7 @@ public BookDtoBuilder pages(int pages) { /** * Sets the value for price. - * + * * @param price the book price to set * @return current instance of builder */ @@ -319,7 +310,7 @@ public BookDtoBuilder price(double price) { /** * Sets the value for publishDate. - * + * * @param publishDate the publication date to set * @return current instance of builder */ @@ -330,7 +321,7 @@ public BookDtoBuilder publishDate(LocalDate publishDate) { /** * Sets the value for publisher. - * + * * @param publisher the publisher to set * @return current instance of builder */ @@ -341,7 +332,7 @@ public BookDtoBuilder publisher(PersonDto publisher) { /** * Sets the value for rating. - * + * * @param rating the book rating to set * @return current instance of builder */ @@ -352,7 +343,7 @@ public BookDtoBuilder rating(byte rating) { /** * Sets the value for salesCount. - * + * * @param salesCount the sales count to set * @return current instance of builder */ @@ -363,7 +354,7 @@ public BookDtoBuilder salesCount(long salesCount) { /** * Sets the value for subtitle. - * + * * @param subtitle an Optional containing the subtitle to set * @return current instance of builder */ @@ -374,7 +365,7 @@ public BookDtoBuilder subtitle(Optional subtitle) { /** * Sets the value for tags. - * + * * @param tags the list of tags to set * @return current instance of builder */ @@ -385,7 +376,7 @@ public BookDtoBuilder tags(List tags) { /** * Sets the value for title. - * + * * @param title the book title to set * @return current instance of builder */ @@ -396,39 +387,39 @@ public BookDtoBuilder title(String title) { /** * Validates that the author field is not null or empty. - * + * * @return this builder instance for chaining * @throws IllegalArgumentException if author is null or empty */ BookDtoBuilder validateAuthor() { if (!author.isSet() || author.value().trim().isEmpty()) { - throw new IllegalArgumentException("Author cannot be null or empty"); + throw new IllegalArgumentException("Author cannot be null or empty"); } return this; } /** * Validates that the isbn field is not null or empty. - * + * * @return this builder instance for chaining * @throws IllegalArgumentException if isbn is null or empty */ BookDtoBuilder validateIsbn() { if (!isbn.isSet() || isbn.value().trim().isEmpty()) { - throw new IllegalArgumentException("Isbn cannot be null or empty"); + throw new IllegalArgumentException("Isbn cannot be null or empty"); } return this; } /** * Validates that the title field is not null or empty. - * + * * @return this builder instance for chaining * @throws IllegalArgumentException if title is null or empty */ BookDtoBuilder validateTitle() { if (!title.isSet() || title.value().trim().isEmpty()) { - throw new IllegalArgumentException("Title cannot be null or empty"); + throw new IllegalArgumentException("Title cannot be null or empty"); } return this; } @@ -486,31 +477,30 @@ public BookDto build() { /** * Returns a string representation of this builder, including only fields that have been set. - * + * * @return string representation of the builder */ @Override public String toString() { - return new ToStringBuilder(this, BuilderToStringStyle.INSTANCE) - .append("author", this.author) - .append("available", this.available) - .append("category", this.category) - .append("discount", this.discount) - .append("edition", this.edition) - .append("exactPrice", this.exactPrice) - .append("genres", this.genres) - .append("isbn", this.isbn) - .append("lastUpdated", this.lastUpdated) - .append("metadata", this.metadata) - .append("pages", this.pages) - .append("price", this.price) - .append("publishDate", this.publishDate) - .append("publisher", this.publisher) - .append("rating", this.rating) - .append("salesCount", this.salesCount) - .append("subtitle", this.subtitle) - .append("tags", this.tags) - .append("title", this.title) - .toString(); + return new ToStringBuilder(this, BuilderToStringStyle.INSTANCE).append("author", this.author) + .append("available", this.available) + .append("category", this.category) + .append("discount", this.discount) + .append("edition", this.edition) + .append("exactPrice", this.exactPrice) + .append("genres", this.genres) + .append("isbn", this.isbn) + .append("lastUpdated", this.lastUpdated) + .append("metadata", this.metadata) + .append("pages", this.pages) + .append("price", this.price) + .append("publishDate", this.publishDate) + .append("publisher", this.publisher) + .append("rating", this.rating) + .append("salesCount", this.salesCount) + .append("subtitle", this.subtitle) + .append("tags", this.tags) + .append("title", this.title) + .toString(); } -} +} \ No newline at end of file diff --git a/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/JacksonIntegrationDtoBuilder.java b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/JacksonIntegrationDtoBuilder.java index 8e99d43f..f430b042 100644 --- a/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/JacksonIntegrationDtoBuilder.java +++ b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/JacksonIntegrationDtoBuilder.java @@ -3,7 +3,6 @@ import static org.javahelpers.simple.builders.core.util.TrackedValue.changedValue; import static org.javahelpers.simple.builders.core.util.TrackedValue.initialValue; import static org.javahelpers.simple.builders.core.util.TrackedValue.unsetValue; - import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; import java.util.function.BooleanSupplier; import java.util.function.Consumer; @@ -18,24 +17,20 @@ /** * Builder for {@code org.javahelpers.simple.builders.example.JacksonIntegrationDto}. *

- * This builder provides a fluent API for creating instances of org.javahelpers.simple.builders.example.JacksonIntegrationDto with - * method chaining and validation. Use the static {@code create()} method - * to obtain a new builder instance, configure the desired properties using - * the setter methods, and then call {@code build()} to create the final DTO. + * This builder provides a fluent API for creating instances of + * org.javahelpers.simple.builders.example.JacksonIntegrationDto with method chaining and validation. Use the static + * {@code create()} method to obtain a new builder instance, configure the desired properties using the setter methods, + * and then call {@code build()} to create the final DTO. */ @Generated("Generated by org.javahelpers.simple.builders.processor.BuilderProcessor") -@BuilderImplementation( - forClass = JacksonIntegrationDto.class -) -@JsonPOJOBuilder( - withPrefix = "" -) +@BuilderImplementation(forClass = JacksonIntegrationDto.class) +@JsonPOJOBuilder(withPrefix = "") public class JacksonIntegrationDtoBuilder implements IBuilderBase { + /** * Tracked value for name: name. */ private TrackedValue name = unsetValue(); - /** * Tracked value for age: age. */ @@ -49,20 +44,21 @@ public JacksonIntegrationDtoBuilder() { /** * Initialisation of builder for {@code org.javahelpers.simple.builders.example.JacksonIntegrationDto} by a instance. - * + * * @param instance object instance for initialisiation */ public JacksonIntegrationDtoBuilder(JacksonIntegrationDto instance) { this.name = initialValue(instance.name()); this.age = initialValue(instance.age()); if (this.age.value() == null) { - throw new IllegalArgumentException("Cannot initialize builder from instance: field 'age' is marked as non-null but source object has null value"); + throw new IllegalArgumentException( + "Cannot initialize builder from instance: field 'age' is marked as non-null but source object has null value"); } } /** * Creating a new builder for {@code org.javahelpers.simple.builders.example.JacksonIntegrationDto}. - * + * * @return builder for {@code org.javahelpers.simple.builders.example.JacksonIntegrationDto} */ public static JacksonIntegrationDtoBuilder create() { @@ -71,7 +67,7 @@ public static JacksonIntegrationDtoBuilder create() { /** * Sets the value for age. - * + * * @param age age * @return current instance of builder */ @@ -82,7 +78,7 @@ public JacksonIntegrationDtoBuilder age(int age) { /** * Sets the value for age by invoking the provided supplier. - * + * * @param ageSupplier supplier for age * @return current instance of builder */ @@ -93,7 +89,7 @@ public JacksonIntegrationDtoBuilder age(Supplier ageSupplier) { /** * Sets the value for name. - * + * * @param name name * @return current instance of builder */ @@ -104,7 +100,7 @@ public JacksonIntegrationDtoBuilder name(String name) { /** * Sets the value for name by executing the provided consumer. - * + * * @param nameStringBuilderConsumer consumer providing an instance of name * @return current instance of builder */ @@ -117,7 +113,7 @@ public JacksonIntegrationDtoBuilder name(Consumer nameStringBuild /** * Sets the value for name by invoking the provided supplier. - * + * * @param nameSupplier supplier for name * @return current instance of builder */ @@ -127,9 +123,9 @@ public JacksonIntegrationDtoBuilder name(Supplier nameSupplier) { } /** - * Sets the String value for name by using String.format(format, args). - * See {@link String#format(String, Object...)} for details. - * + * Sets the String value for name by using String.format(format, args). See + * {@link String#format(String, Object...)} for details. + * * @param format A format string * @param args Arguments referenced by the format specifiers in the format string. * @return current instance of builder @@ -141,20 +137,20 @@ public JacksonIntegrationDtoBuilder name(String format, Object... args) { /** * Validates that the name field is not null or empty. - * + * * @return this builder instance for chaining * @throws IllegalArgumentException if name is null or empty */ JacksonIntegrationDtoBuilder validateName() { if (!name.isSet() || name.value().trim().isEmpty()) { - throw new IllegalArgumentException("Name cannot be null or empty"); + throw new IllegalArgumentException("Name cannot be null or empty"); } return this; } /** * Conditionally applies builder modifications if the condition is true. - * + * * @param condition the condition to evaluate * @param yesCondition the consumer to apply if condition is true * @return this builder instance @@ -166,19 +162,18 @@ public JacksonIntegrationDtoBuilder conditional(BooleanSupplier condition, /** * Conditionally applies builder modifications based on a condition evaluation. - * + * * @param condition the condition to evaluate * @param trueCase the consumer to apply if condition is true * @param falseCase the consumer to apply if condition is false (can be null) * @return this builder instance */ public JacksonIntegrationDtoBuilder conditional(BooleanSupplier condition, - Consumer trueCase, - Consumer falseCase) { + Consumer trueCase, Consumer falseCase) { if (condition.getAsBoolean()) { - trueCase.accept(this); + trueCase.accept(this); } else if (falseCase != null) { - falseCase.accept(this); + falseCase.accept(this); } return this; } @@ -200,15 +195,14 @@ public JacksonIntegrationDto build() { /** * Returns a string representation of this builder, including only fields that have been set. - * + * * @return string representation of the builder */ @Override public String toString() { - return new ToStringBuilder(this, BuilderToStringStyle.INSTANCE) - .append("name", this.name) - .append("age", this.age) - .toString(); + return new ToStringBuilder(this, BuilderToStringStyle.INSTANCE).append("name", this.name) + .append("age", this.age) + .toString(); } /** @@ -216,8 +210,9 @@ public String toString() { */ public interface With { /** - * Initializes a builder from an instance of this class, using methods of this builder to change values and returns the new built object. - * + * Initializes a builder from an instance of this class, using methods of this builder to change values and returns + * the new built object. + * * @param b the consumer to apply modifications * @return the modified instance */ @@ -226,7 +221,9 @@ default JacksonIntegrationDto with(Consumer b) { try { builder = new JacksonIntegrationDtoBuilder(JacksonIntegrationDto.class.cast(this)); } catch (ClassCastException ex) { - throw new IllegalArgumentException("The interface 'JacksonIntegrationDtoBuilder.With' should only be implemented by classes, which could be casted to 'JacksonIntegrationDto'", ex); + throw new IllegalArgumentException( + "The interface 'JacksonIntegrationDtoBuilder.With' should only be implemented by classes, which could be casted to 'JacksonIntegrationDto'", + ex); } b.accept(builder); return builder.build(); @@ -234,15 +231,17 @@ default JacksonIntegrationDto with(Consumer b) { /** * Creates a builder initialized from this instance. - * + * * @return a builder initialized with this instance's values */ default JacksonIntegrationDtoBuilder with() { try { return new JacksonIntegrationDtoBuilder(JacksonIntegrationDto.class.cast(this)); } catch (ClassCastException ex) { - throw new IllegalArgumentException("The interface 'JacksonIntegrationDtoBuilder.With' should only be implemented by classes, which could be casted to 'JacksonIntegrationDto'", ex); + throw new IllegalArgumentException( + "The interface 'JacksonIntegrationDtoBuilder.With' should only be implemented by classes, which could be casted to 'JacksonIntegrationDto'", + ex); } } } -} +} \ No newline at end of file diff --git a/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/MannschaftDtoBuilder.java b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/MannschaftDtoBuilder.java index 99357b79..1ad4b485 100644 --- a/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/MannschaftDtoBuilder.java +++ b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/MannschaftDtoBuilder.java @@ -3,7 +3,6 @@ import static org.javahelpers.simple.builders.core.util.TrackedValue.changedValue; import static org.javahelpers.simple.builders.core.util.TrackedValue.initialValue; import static org.javahelpers.simple.builders.core.util.TrackedValue.unsetValue; - import java.util.HashSet; import java.util.Set; import java.util.function.BooleanSupplier; @@ -20,21 +19,18 @@ /** * Builder for {@code org.javahelpers.simple.builders.example.MannschaftDto}. *

- * This builder provides a fluent API for creating instances of org.javahelpers.simple.builders.example.MannschaftDto with - * method chaining and validation. Use the static {@code create()} method - * to obtain a new builder instance, configure the desired properties using - * the setter methods, and then call {@code build()} to create the final DTO. + * This builder provides a fluent API for creating instances of org.javahelpers.simple.builders.example.MannschaftDto + * with method chaining and validation. Use the static {@code create()} method to obtain a new builder instance, + * configure the desired properties using the setter methods, and then call {@code build()} to create the final DTO. */ @Generated("Generated by org.javahelpers.simple.builders.processor.BuilderProcessor") -@BuilderImplementation( - forClass = MannschaftDto.class -) +@BuilderImplementation(forClass = MannschaftDto.class) public class MannschaftDtoBuilder implements IBuilderBase { + /** * Tracked value for name: name. */ private TrackedValue name = unsetValue(); - /** * Tracked value for sponsoren: sponsoren. */ @@ -48,7 +44,7 @@ public MannschaftDtoBuilder() { /** * Initialisation of builder for {@code org.javahelpers.simple.builders.example.MannschaftDto} by a instance. - * + * * @param instance object instance for initialisiation */ public MannschaftDtoBuilder(MannschaftDto instance) { @@ -58,7 +54,7 @@ public MannschaftDtoBuilder(MannschaftDto instance) { /** * Creating a new builder for {@code org.javahelpers.simple.builders.example.MannschaftDto}. - * + * * @return builder for {@code org.javahelpers.simple.builders.example.MannschaftDto} */ public static MannschaftDtoBuilder create() { @@ -67,7 +63,7 @@ public static MannschaftDtoBuilder create() { /** * Adds a single element to sponsoren. - * + * * @param element the element to add * @return current instance of builder */ @@ -85,7 +81,7 @@ public MannschaftDtoBuilder add2Sponsoren(SponsorDto element) { /** * Sets the value for name. - * + * * @param name name * @return current instance of builder */ @@ -96,7 +92,7 @@ public MannschaftDtoBuilder name(String name) { /** * Sets the value for name by executing the provided consumer. - * + * * @param nameStringBuilderConsumer consumer providing an instance of name * @return current instance of builder */ @@ -109,7 +105,7 @@ public MannschaftDtoBuilder name(Consumer nameStringBuilderConsum /** * Sets the value for name by invoking the provided supplier. - * + * * @param nameSupplier supplier for name * @return current instance of builder */ @@ -119,9 +115,9 @@ public MannschaftDtoBuilder name(Supplier nameSupplier) { } /** - * Sets the String value for name by using String.format(format, args). - * See {@link String#format(String, Object...)} for details. - * + * Sets the String value for name by using String.format(format, args). See + * {@link String#format(String, Object...)} for details. + * * @param format A format string * @param args Arguments referenced by the format specifiers in the format string. * @return current instance of builder @@ -133,7 +129,7 @@ public MannschaftDtoBuilder name(String format, Object... args) { /** * Sets the value for sponsoren. - * + * * @param sponsoren sponsoren * @return current instance of builder */ @@ -144,7 +140,7 @@ public MannschaftDtoBuilder sponsoren(SponsorDto... sponsoren) { /** * Sets the value for sponsoren. - * + * * @param sponsoren sponsoren * @return current instance of builder */ @@ -155,13 +151,16 @@ public MannschaftDtoBuilder sponsoren(Set sponsoren) { /** * Sets the value for sponsoren using a builder consumer that produces the value. - * + * * @param sponsorenBuilderConsumer consumer providing an instance of a builder for sponsoren * @return current instance of builder */ public MannschaftDtoBuilder sponsoren( Consumer> sponsorenBuilderConsumer) { - HashSetBuilderWithElementBuilders builder = this.sponsoren.isSet() ? new HashSetBuilderWithElementBuilders(this.sponsoren.value(), SponsorDtoBuilder::create) : new HashSetBuilderWithElementBuilders(SponsorDtoBuilder::create); + HashSetBuilderWithElementBuilders builder = this.sponsoren.isSet() + ? new HashSetBuilderWithElementBuilders(this.sponsoren.value(), + SponsorDtoBuilder::create) + : new HashSetBuilderWithElementBuilders(SponsorDtoBuilder::create); sponsorenBuilderConsumer.accept(builder); this.sponsoren = changedValue(builder.build()); return this; @@ -169,7 +168,7 @@ public MannschaftDtoBuilder sponsoren( /** * Sets the value for sponsoren by invoking the provided supplier. - * + * * @param sponsorenSupplier supplier for sponsoren * @return current instance of builder */ @@ -180,43 +179,42 @@ public MannschaftDtoBuilder sponsoren(Supplier> sponsorenSupplie /** * Validates that the name field is not null or empty. - * + * * @return this builder instance for chaining * @throws IllegalArgumentException if name is null or empty */ MannschaftDtoBuilder validateName() { if (!name.isSet() || name.value().trim().isEmpty()) { - throw new IllegalArgumentException("Name cannot be null or empty"); + throw new IllegalArgumentException("Name cannot be null or empty"); } return this; } /** * Conditionally applies builder modifications if the condition is true. - * + * * @param condition the condition to evaluate * @param yesCondition the consumer to apply if condition is true * @return this builder instance */ - public MannschaftDtoBuilder conditional(BooleanSupplier condition, - Consumer yesCondition) { + public MannschaftDtoBuilder conditional(BooleanSupplier condition, Consumer yesCondition) { return conditional(condition, yesCondition, null); } /** * Conditionally applies builder modifications based on a condition evaluation. - * + * * @param condition the condition to evaluate * @param trueCase the consumer to apply if condition is true * @param falseCase the consumer to apply if condition is false (can be null) * @return this builder instance */ - public MannschaftDtoBuilder conditional(BooleanSupplier condition, - Consumer trueCase, Consumer falseCase) { + public MannschaftDtoBuilder conditional(BooleanSupplier condition, Consumer trueCase, + Consumer falseCase) { if (condition.getAsBoolean()) { - trueCase.accept(this); + trueCase.accept(this); } else if (falseCase != null) { - falseCase.accept(this); + falseCase.accept(this); } return this; } @@ -234,15 +232,14 @@ public MannschaftDto build() { /** * Returns a string representation of this builder, including only fields that have been set. - * + * * @return string representation of the builder */ @Override public String toString() { - return new ToStringBuilder(this, BuilderToStringStyle.INSTANCE) - .append("name", this.name) - .append("sponsoren", this.sponsoren) - .toString(); + return new ToStringBuilder(this, BuilderToStringStyle.INSTANCE).append("name", this.name) + .append("sponsoren", this.sponsoren) + .toString(); } /** @@ -250,8 +247,9 @@ public String toString() { */ public interface With { /** - * Initializes a builder from an instance of this class, using methods of this builder to change values and returns the new built object. - * + * Initializes a builder from an instance of this class, using methods of this builder to change values and returns + * the new built object. + * * @param b the consumer to apply modifications * @return the modified instance */ @@ -260,7 +258,9 @@ default MannschaftDto with(Consumer b) { try { builder = new MannschaftDtoBuilder(MannschaftDto.class.cast(this)); } catch (ClassCastException ex) { - throw new IllegalArgumentException("The interface 'MannschaftDtoBuilder.With' should only be implemented by classes, which could be casted to 'MannschaftDto'", ex); + throw new IllegalArgumentException( + "The interface 'MannschaftDtoBuilder.With' should only be implemented by classes, which could be casted to 'MannschaftDto'", + ex); } b.accept(builder); return builder.build(); @@ -268,15 +268,17 @@ default MannschaftDto with(Consumer b) { /** * Creates a builder initialized from this instance. - * + * * @return a builder initialized with this instance's values */ default MannschaftDtoBuilder with() { try { return new MannschaftDtoBuilder(MannschaftDto.class.cast(this)); } catch (ClassCastException ex) { - throw new IllegalArgumentException("The interface 'MannschaftDtoBuilder.With' should only be implemented by classes, which could be casted to 'MannschaftDto'", ex); + throw new IllegalArgumentException( + "The interface 'MannschaftDtoBuilder.With' should only be implemented by classes, which could be casted to 'MannschaftDto'", + ex); } } } -} +} \ No newline at end of file diff --git a/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/PersonDtoBuilder.java b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/PersonDtoBuilder.java index 767ab9bc..c3f6e7a4 100644 --- a/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/PersonDtoBuilder.java +++ b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/PersonDtoBuilder.java @@ -3,7 +3,6 @@ import static org.javahelpers.simple.builders.core.util.TrackedValue.changedValue; import static org.javahelpers.simple.builders.core.util.TrackedValue.initialValue; import static org.javahelpers.simple.builders.core.util.TrackedValue.unsetValue; - import java.time.LocalDate; import java.util.ArrayList; import java.util.List; @@ -22,35 +21,29 @@ * Builder for {@code org.javahelpers.simple.builders.example.PersonDto}. *

* This builder provides a fluent API for creating instances of org.javahelpers.simple.builders.example.PersonDto with - * method chaining and validation. Use the static {@code create()} method - * to obtain a new builder instance, configure the desired properties using - * the setter methods, and then call {@code build()} to create the final DTO. + * method chaining and validation. Use the static {@code create()} method to obtain a new builder instance, configure + * the desired properties using the setter methods, and then call {@code build()} to create the final DTO. */ @Generated("Generated by org.javahelpers.simple.builders.processor.BuilderProcessor") -@BuilderImplementation( - forClass = PersonDto.class -) +@BuilderImplementation(forClass = PersonDto.class) public class PersonDtoBuilder implements IBuilderBase { + /** * Tracked value for name: name. */ private TrackedValue name = unsetValue(); - /** * Tracked value for birthdate: birthdate. */ private TrackedValue birthdate = unsetValue(); - /** * Tracked value for mannschaft: mannschaft. */ private TrackedValue mannschaft = unsetValue(); - /** * Tracked value for nickNames: nickNames. */ private TrackedValue> nickNames = unsetValue(); - /** * Tracked value for nickNames2: nickNames2. */ @@ -64,7 +57,7 @@ public PersonDtoBuilder() { /** * Initialisation of builder for {@code org.javahelpers.simple.builders.example.PersonDto} by a instance. - * + * * @param instance object instance for initialisiation */ public PersonDtoBuilder(PersonDto instance) { @@ -76,7 +69,7 @@ public PersonDtoBuilder(PersonDto instance) { /** * Creating a new builder for {@code org.javahelpers.simple.builders.example.PersonDto}. - * + * * @return builder for {@code org.javahelpers.simple.builders.example.PersonDto} */ public static PersonDtoBuilder create() { @@ -85,7 +78,7 @@ public static PersonDtoBuilder create() { /** * Adds a single element to nickNames. - * + * * @param element the element to add * @return current instance of builder */ @@ -103,7 +96,7 @@ public PersonDtoBuilder add2NickNames(String element) { /** * Sets the value for birthdate. - * + * * @param birthdate birthdate * @return current instance of builder */ @@ -114,7 +107,7 @@ public PersonDtoBuilder birthdate(LocalDate birthdate) { /** * Sets the value for birthdate by invoking the provided supplier. - * + * * @param birthdateSupplier supplier for birthdate * @return current instance of builder */ @@ -125,7 +118,7 @@ public PersonDtoBuilder birthdate(Supplier birthdateSupplier) { /** * Sets the value for mannschaft. - * + * * @param mannschaft mannschaft * @return current instance of builder */ @@ -136,12 +129,14 @@ public PersonDtoBuilder mannschaft(MannschaftDto mannschaft) { /** * Sets the value for mannschaft using a builder consumer that produces the value. - * + * * @param mannschaftBuilderConsumer consumer providing an instance of a builder for mannschaft * @return current instance of builder */ public PersonDtoBuilder mannschaft(Consumer mannschaftBuilderConsumer) { - MannschaftDtoBuilder builder = this.mannschaft.isSet() ? new MannschaftDtoBuilder(this.mannschaft.value()) : new MannschaftDtoBuilder(); + MannschaftDtoBuilder builder = this.mannschaft.isSet() + ? new MannschaftDtoBuilder(this.mannschaft.value()) + : new MannschaftDtoBuilder(); mannschaftBuilderConsumer.accept(builder); this.mannschaft = changedValue(builder.build()); return this; @@ -149,7 +144,7 @@ public PersonDtoBuilder mannschaft(Consumer mannschaftBuil /** * Sets the value for mannschaft by invoking the provided supplier. - * + * * @param mannschaftSupplier supplier for mannschaft * @return current instance of builder */ @@ -160,7 +155,7 @@ public PersonDtoBuilder mannschaft(Supplier mannschaftSupplier) { /** * Sets the value for name. - * + * * @param name name * @return current instance of builder */ @@ -171,7 +166,7 @@ public PersonDtoBuilder name(String name) { /** * Sets the value for name by executing the provided consumer. - * + * * @param nameStringBuilderConsumer consumer providing an instance of name * @return current instance of builder */ @@ -184,7 +179,7 @@ public PersonDtoBuilder name(Consumer nameStringBuilderConsumer) /** * Sets the value for name by invoking the provided supplier. - * + * * @param nameSupplier supplier for name * @return current instance of builder */ @@ -194,9 +189,9 @@ public PersonDtoBuilder name(Supplier nameSupplier) { } /** - * Sets the String value for name by using String.format(format, args). - * See {@link String#format(String, Object...)} for details. - * + * Sets the String value for name by using String.format(format, args). See + * {@link String#format(String, Object...)} for details. + * * @param format A format string * @param args Arguments referenced by the format specifiers in the format string. * @return current instance of builder @@ -208,7 +203,7 @@ public PersonDtoBuilder name(String format, Object... args) { /** * Sets the value for nickNames. - * + * * @param nickNames nickNames * @return current instance of builder */ @@ -219,7 +214,7 @@ public PersonDtoBuilder nickNames(String... nickNames) { /** * Sets the value for nickNames. - * + * * @param nickNames nickNames * @return current instance of builder */ @@ -230,12 +225,14 @@ public PersonDtoBuilder nickNames(List nickNames) { /** * Sets the value for nickNames using a builder consumer that produces the value. - * + * * @param nickNamesBuilderConsumer consumer providing an instance of a builder for nickNames * @return current instance of builder */ public PersonDtoBuilder nickNames(Consumer> nickNamesBuilderConsumer) { - ArrayListBuilder builder = this.nickNames.isSet() ? new ArrayListBuilder(this.nickNames.value()) : new ArrayListBuilder(); + ArrayListBuilder builder = this.nickNames.isSet() + ? new ArrayListBuilder(this.nickNames.value()) + : new ArrayListBuilder(); nickNamesBuilderConsumer.accept(builder); this.nickNames = changedValue(builder.build()); return this; @@ -243,7 +240,7 @@ public PersonDtoBuilder nickNames(Consumer> nickNamesBu /** * Sets the value for nickNames by invoking the provided supplier. - * + * * @param nickNamesSupplier supplier for nickNames * @return current instance of builder */ @@ -254,7 +251,7 @@ public PersonDtoBuilder nickNames(Supplier> nickNamesSupplier) { /** * Sets the value for nickNames2. - * + * * @param nickNames2 nickNames2 * @return current instance of builder */ @@ -265,7 +262,7 @@ public PersonDtoBuilder nickNames2(String... nickNames2) { /** * Sets the value for nickNames2. - * + * * @param nickNames2 nickNames2 * @return current instance of builder */ @@ -276,12 +273,14 @@ public PersonDtoBuilder nickNames2(List nickNames2) { /** * Sets the value for nickNames2 using the fluent builder consumer. - * + * * @param nickNames2BuilderConsumer consumer for nickNames2 * @return current instance of builder */ public PersonDtoBuilder nickNames2(Consumer> nickNames2BuilderConsumer) { - ArrayListBuilder builder = this.nickNames2.isSet() ? new ArrayListBuilder(java.util.List.of(this.nickNames2.value())) : new ArrayListBuilder(); + ArrayListBuilder builder = this.nickNames2.isSet() + ? new ArrayListBuilder(java.util.List.of(this.nickNames2.value())) + : new ArrayListBuilder(); nickNames2BuilderConsumer.accept(builder); this.nickNames2 = changedValue(builder.build().toArray(new String[0])); return this; @@ -289,7 +288,7 @@ public PersonDtoBuilder nickNames2(Consumer> nickNames2 /** * Sets the value for nickNames2 by invoking the provided supplier. - * + * * @param nickNames2Supplier supplier for nickNames2 * @return current instance of builder */ @@ -300,43 +299,42 @@ public PersonDtoBuilder nickNames2(Supplier nickNames2Supplier) { /** * Validates that the name field is not null or empty. - * + * * @return this builder instance for chaining * @throws IllegalArgumentException if name is null or empty */ PersonDtoBuilder validateName() { if (!name.isSet() || name.value().trim().isEmpty()) { - throw new IllegalArgumentException("Name cannot be null or empty"); + throw new IllegalArgumentException("Name cannot be null or empty"); } return this; } /** * Conditionally applies builder modifications if the condition is true. - * + * * @param condition the condition to evaluate * @param yesCondition the consumer to apply if condition is true * @return this builder instance */ - public PersonDtoBuilder conditional(BooleanSupplier condition, - Consumer yesCondition) { + public PersonDtoBuilder conditional(BooleanSupplier condition, Consumer yesCondition) { return conditional(condition, yesCondition, null); } /** * Conditionally applies builder modifications based on a condition evaluation. - * + * * @param condition the condition to evaluate * @param trueCase the consumer to apply if condition is true * @param falseCase the consumer to apply if condition is false (can be null) * @return this builder instance */ - public PersonDtoBuilder conditional(BooleanSupplier condition, - Consumer trueCase, Consumer falseCase) { + public PersonDtoBuilder conditional(BooleanSupplier condition, Consumer trueCase, + Consumer falseCase) { if (condition.getAsBoolean()) { - trueCase.accept(this); + trueCase.accept(this); } else if (falseCase != null) { - falseCase.accept(this); + falseCase.accept(this); } return this; } @@ -356,18 +354,17 @@ public PersonDto build() { /** * Returns a string representation of this builder, including only fields that have been set. - * + * * @return string representation of the builder */ @Override public String toString() { - return new ToStringBuilder(this, BuilderToStringStyle.INSTANCE) - .append("name", this.name) - .append("birthdate", this.birthdate) - .append("mannschaft", this.mannschaft) - .append("nickNames", this.nickNames) - .append("nickNames2", this.nickNames2) - .toString(); + return new ToStringBuilder(this, BuilderToStringStyle.INSTANCE).append("name", this.name) + .append("birthdate", this.birthdate) + .append("mannschaft", this.mannschaft) + .append("nickNames", this.nickNames) + .append("nickNames2", this.nickNames2) + .toString(); } /** @@ -375,8 +372,9 @@ public String toString() { */ public interface With { /** - * Initializes a builder from an instance of this class, using methods of this builder to change values and returns the new built object. - * + * Initializes a builder from an instance of this class, using methods of this builder to change values and returns + * the new built object. + * * @param b the consumer to apply modifications * @return the modified instance */ @@ -385,7 +383,9 @@ default PersonDto with(Consumer b) { try { builder = new PersonDtoBuilder(PersonDto.class.cast(this)); } catch (ClassCastException ex) { - throw new IllegalArgumentException("The interface 'PersonDtoBuilder.With' should only be implemented by classes, which could be casted to 'PersonDto'", ex); + throw new IllegalArgumentException( + "The interface 'PersonDtoBuilder.With' should only be implemented by classes, which could be casted to 'PersonDto'", + ex); } b.accept(builder); return builder.build(); @@ -393,15 +393,17 @@ default PersonDto with(Consumer b) { /** * Creates a builder initialized from this instance. - * + * * @return a builder initialized with this instance's values */ default PersonDtoBuilder with() { try { return new PersonDtoBuilder(PersonDto.class.cast(this)); } catch (ClassCastException ex) { - throw new IllegalArgumentException("The interface 'PersonDtoBuilder.With' should only be implemented by classes, which could be casted to 'PersonDto'", ex); + throw new IllegalArgumentException( + "The interface 'PersonDtoBuilder.With' should only be implemented by classes, which could be casted to 'PersonDto'", + ex); } } } -} +} \ No newline at end of file diff --git a/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/ProductRecordBuilder.java b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/ProductRecordBuilder.java index a34f67ec..fc7d430a 100644 --- a/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/ProductRecordBuilder.java +++ b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/ProductRecordBuilder.java @@ -3,7 +3,6 @@ import static org.javahelpers.simple.builders.core.util.TrackedValue.changedValue; import static org.javahelpers.simple.builders.core.util.TrackedValue.initialValue; import static org.javahelpers.simple.builders.core.util.TrackedValue.unsetValue; - import java.util.function.BooleanSupplier; import java.util.function.Consumer; import java.util.function.Supplier; @@ -17,26 +16,22 @@ /** * Builder for {@code org.javahelpers.simple.builders.example.ProductRecord}. *

- * This builder provides a fluent API for creating instances of org.javahelpers.simple.builders.example.ProductRecord with - * method chaining and validation. Use the static {@code create()} method - * to obtain a new builder instance, configure the desired properties using - * the setter methods, and then call {@code build()} to create the final DTO. + * This builder provides a fluent API for creating instances of org.javahelpers.simple.builders.example.ProductRecord + * with method chaining and validation. Use the static {@code create()} method to obtain a new builder instance, + * configure the desired properties using the setter methods, and then call {@code build()} to create the final DTO. */ @Generated("Generated by org.javahelpers.simple.builders.processor.BuilderProcessor") -@BuilderImplementation( - forClass = ProductRecord.class -) +@BuilderImplementation(forClass = ProductRecord.class) public class ProductRecordBuilder implements IBuilderBase { + /** * Tracked value for name: name. */ private TrackedValue name = unsetValue(); - /** * Tracked value for price: price. */ private TrackedValue price = unsetValue(); - /** * Tracked value for category: category. */ @@ -50,21 +45,22 @@ public ProductRecordBuilder() { /** * Initialisation of builder for {@code org.javahelpers.simple.builders.example.ProductRecord} by a instance. - * + * * @param instance object instance for initialisiation */ public ProductRecordBuilder(ProductRecord instance) { this.name = initialValue(instance.name()); this.price = initialValue(instance.price()); if (this.price.value() == null) { - throw new IllegalArgumentException("Cannot initialize builder from instance: field 'price' is marked as non-null but source object has null value"); + throw new IllegalArgumentException( + "Cannot initialize builder from instance: field 'price' is marked as non-null but source object has null value"); } this.category = initialValue(instance.category()); } /** * Creating a new builder for {@code org.javahelpers.simple.builders.example.ProductRecord}. - * + * * @return builder for {@code org.javahelpers.simple.builders.example.ProductRecord} */ public static ProductRecordBuilder create() { @@ -73,7 +69,7 @@ public static ProductRecordBuilder create() { /** * Sets the value for category. - * + * * @param category category * @return current instance of builder */ @@ -84,7 +80,7 @@ public ProductRecordBuilder category(String category) { /** * Sets the value for category by executing the provided consumer. - * + * * @param categoryStringBuilderConsumer consumer providing an instance of category * @return current instance of builder */ @@ -97,7 +93,7 @@ public ProductRecordBuilder category(Consumer categoryStringBuild /** * Sets the value for category by invoking the provided supplier. - * + * * @param categorySupplier supplier for category * @return current instance of builder */ @@ -107,9 +103,9 @@ public ProductRecordBuilder category(Supplier categorySupplier) { } /** - * Sets the String value for category by using String.format(format, args). - * See {@link String#format(String, Object...)} for details. - * + * Sets the String value for category by using String.format(format, args). See + * {@link String#format(String, Object...)} for details. + * * @param format A format string * @param args Arguments referenced by the format specifiers in the format string. * @return current instance of builder @@ -121,7 +117,7 @@ public ProductRecordBuilder category(String format, Object... args) { /** * Sets the value for name. - * + * * @param name name * @return current instance of builder */ @@ -132,7 +128,7 @@ public ProductRecordBuilder name(String name) { /** * Sets the value for name by executing the provided consumer. - * + * * @param nameStringBuilderConsumer consumer providing an instance of name * @return current instance of builder */ @@ -145,7 +141,7 @@ public ProductRecordBuilder name(Consumer nameStringBuilderConsum /** * Sets the value for name by invoking the provided supplier. - * + * * @param nameSupplier supplier for name * @return current instance of builder */ @@ -155,9 +151,9 @@ public ProductRecordBuilder name(Supplier nameSupplier) { } /** - * Sets the String value for name by using String.format(format, args). - * See {@link String#format(String, Object...)} for details. - * + * Sets the String value for name by using String.format(format, args). See + * {@link String#format(String, Object...)} for details. + * * @param format A format string * @param args Arguments referenced by the format specifiers in the format string. * @return current instance of builder @@ -169,7 +165,7 @@ public ProductRecordBuilder name(String format, Object... args) { /** * Sets the value for price. - * + * * @param price price * @return current instance of builder */ @@ -180,7 +176,7 @@ public ProductRecordBuilder price(double price) { /** * Sets the value for price by invoking the provided supplier. - * + * * @param priceSupplier supplier for price * @return current instance of builder */ @@ -191,56 +187,55 @@ public ProductRecordBuilder price(Supplier priceSupplier) { /** * Validates that the category field is not null or empty. - * + * * @return this builder instance for chaining * @throws IllegalArgumentException if category is null or empty */ ProductRecordBuilder validateCategory() { if (!category.isSet() || category.value().trim().isEmpty()) { - throw new IllegalArgumentException("Category cannot be null or empty"); + throw new IllegalArgumentException("Category cannot be null or empty"); } return this; } /** * Validates that the name field is not null or empty. - * + * * @return this builder instance for chaining * @throws IllegalArgumentException if name is null or empty */ ProductRecordBuilder validateName() { if (!name.isSet() || name.value().trim().isEmpty()) { - throw new IllegalArgumentException("Name cannot be null or empty"); + throw new IllegalArgumentException("Name cannot be null or empty"); } return this; } /** * Conditionally applies builder modifications if the condition is true. - * + * * @param condition the condition to evaluate * @param yesCondition the consumer to apply if condition is true * @return this builder instance */ - public ProductRecordBuilder conditional(BooleanSupplier condition, - Consumer yesCondition) { + public ProductRecordBuilder conditional(BooleanSupplier condition, Consumer yesCondition) { return conditional(condition, yesCondition, null); } /** * Conditionally applies builder modifications based on a condition evaluation. - * + * * @param condition the condition to evaluate * @param trueCase the consumer to apply if condition is true * @param falseCase the consumer to apply if condition is false (can be null) * @return this builder instance */ - public ProductRecordBuilder conditional(BooleanSupplier condition, - Consumer trueCase, Consumer falseCase) { + public ProductRecordBuilder conditional(BooleanSupplier condition, Consumer trueCase, + Consumer falseCase) { if (condition.getAsBoolean()) { - trueCase.accept(this); + trueCase.accept(this); } else if (falseCase != null) { - falseCase.accept(this); + falseCase.accept(this); } return this; } @@ -262,16 +257,15 @@ public ProductRecord build() { /** * Returns a string representation of this builder, including only fields that have been set. - * + * * @return string representation of the builder */ @Override public String toString() { - return new ToStringBuilder(this, BuilderToStringStyle.INSTANCE) - .append("name", this.name) - .append("price", this.price) - .append("category", this.category) - .toString(); + return new ToStringBuilder(this, BuilderToStringStyle.INSTANCE).append("name", this.name) + .append("price", this.price) + .append("category", this.category) + .toString(); } /** @@ -279,8 +273,9 @@ public String toString() { */ public interface With { /** - * Initializes a builder from an instance of this class, using methods of this builder to change values and returns the new built object. - * + * Initializes a builder from an instance of this class, using methods of this builder to change values and returns + * the new built object. + * * @param b the consumer to apply modifications * @return the modified instance */ @@ -289,7 +284,9 @@ default ProductRecord with(Consumer b) { try { builder = new ProductRecordBuilder(ProductRecord.class.cast(this)); } catch (ClassCastException ex) { - throw new IllegalArgumentException("The interface 'ProductRecordBuilder.With' should only be implemented by classes, which could be casted to 'ProductRecord'", ex); + throw new IllegalArgumentException( + "The interface 'ProductRecordBuilder.With' should only be implemented by classes, which could be casted to 'ProductRecord'", + ex); } b.accept(builder); return builder.build(); @@ -297,15 +294,17 @@ default ProductRecord with(Consumer b) { /** * Creates a builder initialized from this instance. - * + * * @return a builder initialized with this instance's values */ default ProductRecordBuilder with() { try { return new ProductRecordBuilder(ProductRecord.class.cast(this)); } catch (ClassCastException ex) { - throw new IllegalArgumentException("The interface 'ProductRecordBuilder.With' should only be implemented by classes, which could be casted to 'ProductRecord'", ex); + throw new IllegalArgumentException( + "The interface 'ProductRecordBuilder.With' should only be implemented by classes, which could be casted to 'ProductRecord'", + ex); } } } -} +} \ No newline at end of file diff --git a/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/SimpleBuildersJacksonModule.java b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/SimpleBuildersJacksonModule.java index 5766b3ae..6e406c1a 100644 --- a/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/SimpleBuildersJacksonModule.java +++ b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/SimpleBuildersJacksonModule.java @@ -4,13 +4,12 @@ import com.fasterxml.jackson.databind.module.SimpleModule; public class SimpleBuildersJacksonModule extends SimpleModule { - public SimpleBuildersJacksonModule() { - setMixInAnnotation(JacksonIntegrationDto.class, JacksonIntegrationDtoMixin.class); - } - @JsonDeserialize( - builder = JacksonIntegrationDtoBuilder.class - ) + @JsonDeserialize(builder = JacksonIntegrationDtoBuilder.class) private interface JacksonIntegrationDtoMixin { } -} + + public SimpleBuildersJacksonModule() { + setMixInAnnotation(JacksonIntegrationDto.class, JacksonIntegrationDtoMixin.class); + } +} \ No newline at end of file diff --git a/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/SponsorDtoBuilder.java b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/SponsorDtoBuilder.java index 861c1167..295bcb70 100644 --- a/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/SponsorDtoBuilder.java +++ b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/SponsorDtoBuilder.java @@ -3,7 +3,6 @@ import static org.javahelpers.simple.builders.core.util.TrackedValue.changedValue; import static org.javahelpers.simple.builders.core.util.TrackedValue.initialValue; import static org.javahelpers.simple.builders.core.util.TrackedValue.unsetValue; - import java.util.function.BooleanSupplier; import java.util.function.Consumer; import java.util.function.Supplier; @@ -18,15 +17,13 @@ * Builder for {@code org.javahelpers.simple.builders.example.SponsorDto}. *

* This builder provides a fluent API for creating instances of org.javahelpers.simple.builders.example.SponsorDto with - * method chaining and validation. Use the static {@code create()} method - * to obtain a new builder instance, configure the desired properties using - * the setter methods, and then call {@code build()} to create the final DTO. + * method chaining and validation. Use the static {@code create()} method to obtain a new builder instance, configure + * the desired properties using the setter methods, and then call {@code build()} to create the final DTO. */ @Generated("Generated by org.javahelpers.simple.builders.processor.BuilderProcessor") -@BuilderImplementation( - forClass = SponsorDto.class -) +@BuilderImplementation(forClass = SponsorDto.class) public class SponsorDtoBuilder implements IBuilderBase { + /** * Tracked value for name: name. */ @@ -40,7 +37,7 @@ public SponsorDtoBuilder() { /** * Initialisation of builder for {@code org.javahelpers.simple.builders.example.SponsorDto} by a instance. - * + * * @param instance object instance for initialisiation */ public SponsorDtoBuilder(SponsorDto instance) { @@ -49,7 +46,7 @@ public SponsorDtoBuilder(SponsorDto instance) { /** * Creating a new builder for {@code org.javahelpers.simple.builders.example.SponsorDto}. - * + * * @return builder for {@code org.javahelpers.simple.builders.example.SponsorDto} */ public static SponsorDtoBuilder create() { @@ -58,7 +55,7 @@ public static SponsorDtoBuilder create() { /** * Sets the value for name. - * + * * @param name name * @return current instance of builder */ @@ -69,7 +66,7 @@ public SponsorDtoBuilder name(String name) { /** * Sets the value for name by executing the provided consumer. - * + * * @param nameStringBuilderConsumer consumer providing an instance of name * @return current instance of builder */ @@ -82,7 +79,7 @@ public SponsorDtoBuilder name(Consumer nameStringBuilderConsumer) /** * Sets the value for name by invoking the provided supplier. - * + * * @param nameSupplier supplier for name * @return current instance of builder */ @@ -92,9 +89,9 @@ public SponsorDtoBuilder name(Supplier nameSupplier) { } /** - * Sets the String value for name by using String.format(format, args). - * See {@link String#format(String, Object...)} for details. - * + * Sets the String value for name by using String.format(format, args). See + * {@link String#format(String, Object...)} for details. + * * @param format A format string * @param args Arguments referenced by the format specifiers in the format string. * @return current instance of builder @@ -106,43 +103,42 @@ public SponsorDtoBuilder name(String format, Object... args) { /** * Validates that the name field is not null or empty. - * + * * @return this builder instance for chaining * @throws IllegalArgumentException if name is null or empty */ SponsorDtoBuilder validateName() { if (!name.isSet() || name.value().trim().isEmpty()) { - throw new IllegalArgumentException("Name cannot be null or empty"); + throw new IllegalArgumentException("Name cannot be null or empty"); } return this; } /** * Conditionally applies builder modifications if the condition is true. - * + * * @param condition the condition to evaluate * @param yesCondition the consumer to apply if condition is true * @return this builder instance */ - public SponsorDtoBuilder conditional(BooleanSupplier condition, - Consumer yesCondition) { + public SponsorDtoBuilder conditional(BooleanSupplier condition, Consumer yesCondition) { return conditional(condition, yesCondition, null); } /** * Conditionally applies builder modifications based on a condition evaluation. - * + * * @param condition the condition to evaluate * @param trueCase the consumer to apply if condition is true * @param falseCase the consumer to apply if condition is false (can be null) * @return this builder instance */ - public SponsorDtoBuilder conditional(BooleanSupplier condition, - Consumer trueCase, Consumer falseCase) { + public SponsorDtoBuilder conditional(BooleanSupplier condition, Consumer trueCase, + Consumer falseCase) { if (condition.getAsBoolean()) { - trueCase.accept(this); + trueCase.accept(this); } else if (falseCase != null) { - falseCase.accept(this); + falseCase.accept(this); } return this; } @@ -159,14 +155,12 @@ public SponsorDto build() { /** * Returns a string representation of this builder, including only fields that have been set. - * + * * @return string representation of the builder */ @Override public String toString() { - return new ToStringBuilder(this, BuilderToStringStyle.INSTANCE) - .append("name", this.name) - .toString(); + return new ToStringBuilder(this, BuilderToStringStyle.INSTANCE).append("name", this.name).toString(); } /** @@ -174,8 +168,9 @@ public String toString() { */ public interface With { /** - * Initializes a builder from an instance of this class, using methods of this builder to change values and returns the new built object. - * + * Initializes a builder from an instance of this class, using methods of this builder to change values and returns + * the new built object. + * * @param b the consumer to apply modifications * @return the modified instance */ @@ -184,7 +179,9 @@ default SponsorDto with(Consumer b) { try { builder = new SponsorDtoBuilder(SponsorDto.class.cast(this)); } catch (ClassCastException ex) { - throw new IllegalArgumentException("The interface 'SponsorDtoBuilder.With' should only be implemented by classes, which could be casted to 'SponsorDto'", ex); + throw new IllegalArgumentException( + "The interface 'SponsorDtoBuilder.With' should only be implemented by classes, which could be casted to 'SponsorDto'", + ex); } b.accept(builder); return builder.build(); @@ -192,15 +189,17 @@ default SponsorDto with(Consumer b) { /** * Creates a builder initialized from this instance. - * + * * @return a builder initialized with this instance's values */ default SponsorDtoBuilder with() { try { return new SponsorDtoBuilder(SponsorDto.class.cast(this)); } catch (ClassCastException ex) { - throw new IllegalArgumentException("The interface 'SponsorDtoBuilder.With' should only be implemented by classes, which could be casted to 'SponsorDto'", ex); + throw new IllegalArgumentException( + "The interface 'SponsorDtoBuilder.With' should only be implemented by classes, which could be casted to 'SponsorDto'", + ex); } } } -} +} \ No newline at end of file diff --git a/processor/pom.xml b/processor/pom.xml index 7bec0bf3..38788390 100644 --- a/processor/pom.xml +++ b/processor/pom.xml @@ -54,7 +54,7 @@ 1.1.1 - 0.12.0 + 2.31.0.Final 3.20.0 4.5.0 6.0.3 @@ -111,9 +111,15 @@ - com.palantir.javapoet - javapoet - ${javapoet.version} + org.jboss.forge.roaster + roaster-api + ${roaster.version} + + + org.jboss.forge.roaster + roaster-jdt + ${roaster.version} + runtime diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java index f578e00a..5ab749ff 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/BuilderProcessor.java @@ -42,7 +42,7 @@ import javax.lang.model.element.TypeElement; import org.javahelpers.simple.builders.core.annotations.SimpleBuilder; import org.javahelpers.simple.builders.core.annotations.SimpleBuilder.Template; -import org.javahelpers.simple.builders.processor.classgen.javapoet.JavaCodeGenerator; +import org.javahelpers.simple.builders.processor.classgen.roaster.RoasterCodeGenerator; import org.javahelpers.simple.builders.processor.exceptions.BuilderException; import org.javahelpers.simple.builders.processor.generators.integration.JacksonModuleGenerator; import org.javahelpers.simple.builders.processor.model.core.BuilderConfiguration; @@ -62,7 +62,7 @@ @SupportedAnnotationTypes("*") public class BuilderProcessor extends AbstractProcessor { private ProcessingContext context; - private JavaCodeGenerator codeGenerator; + private RoasterCodeGenerator codeGenerator; private JacksonModuleGenerator jacksonModuleGenerator; private boolean supportedJdk = true; @@ -78,7 +78,7 @@ public synchronized void init(ProcessingEnvironment processingEnv) { logger.debug("Loaded global configuration from compiler arguments: %s", globalConfig); this.context = new ProcessingContext(logger, globalConfig, processingEnv); - this.codeGenerator = new JavaCodeGenerator(processingEnv, logger); + this.codeGenerator = new RoasterCodeGenerator(processingEnv, logger); this.jacksonModuleGenerator = new JacksonModuleGenerator(processingEnv, logger); // Initialize GeneratorRegistry once during processor initialization diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/javapoet/JavaCodeGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/javapoet/JavaCodeGenerator.java deleted file mode 100644 index 5499f44c..00000000 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/javapoet/JavaCodeGenerator.java +++ /dev/null @@ -1,684 +0,0 @@ -/* - * MIT License - * - * Copyright (c) 2026 Andreas Igel - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -package org.javahelpers.simple.builders.processor.classgen.javapoet; - -import static javax.lang.model.element.Modifier.PUBLIC; -import static org.javahelpers.simple.builders.processor.classgen.javapoet.JavapoetMapper.*; - -import com.palantir.javapoet.AnnotationSpec; -import com.palantir.javapoet.ClassName; -import com.palantir.javapoet.CodeBlock; -import com.palantir.javapoet.FieldSpec; -import com.palantir.javapoet.JavaFile; -import com.palantir.javapoet.MethodSpec; -import com.palantir.javapoet.ParameterSpec; -import com.palantir.javapoet.ParameterizedTypeName; -import com.palantir.javapoet.TypeSpec; -import java.io.IOException; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import javax.annotation.processing.ProcessingEnvironment; -import javax.lang.model.element.Modifier; -import javax.lang.model.element.TypeElement; -import org.apache.commons.collections4.CollectionUtils; -import org.apache.commons.lang3.StringUtils; -import org.javahelpers.simple.builders.core.util.TrackedValue; -import org.javahelpers.simple.builders.processor.analysis.JavaLangMapper; -import org.javahelpers.simple.builders.processor.exceptions.BuilderException; -import org.javahelpers.simple.builders.processor.model.annotation.AnnotationDto; -import org.javahelpers.simple.builders.processor.model.annotation.InterfaceName; -import org.javahelpers.simple.builders.processor.model.core.BuilderDefinitionDto; -import org.javahelpers.simple.builders.processor.model.core.FieldDto; -import org.javahelpers.simple.builders.processor.model.integration.JacksonModuleDefinitionDto; -import org.javahelpers.simple.builders.processor.model.integration.JacksonModuleEntryDto; -import org.javahelpers.simple.builders.processor.model.method.MethodDto; -import org.javahelpers.simple.builders.processor.model.method.MethodParameterDto; -import org.javahelpers.simple.builders.processor.model.type.NestedTypeDto; -import org.javahelpers.simple.builders.processor.model.type.TypeNameArray; -import org.javahelpers.simple.builders.processor.processing.ProcessingLogger; - -/** JavaCodeGenerator generates with BuilderDefinitionDto JavaCode for the builder. */ -public class JavaCodeGenerator { - /** Processing environment for accessing filer and element utilities. */ - private final ProcessingEnvironment processingEnv; - - /** Logger for debug output during code generation. */ - private final ProcessingLogger logger; - - /** - * Constructor for JavaCodeGenerator. - * - * @param processingEnv Processing environment for accessing filer and element utilities - * @param logger Logger for debug output - */ - public JavaCodeGenerator(ProcessingEnvironment processingEnv, ProcessingLogger logger) { - this.processingEnv = processingEnv; - this.logger = logger; - } - - /** - * Generates a builder class from the given builder definition. - * - * @param builderDef dto of all information to create the builder - * @throws BuilderException if there is an error in source code generation - */ - public void generateBuilder(BuilderDefinitionDto builderDef) throws BuilderException { - logger.debugStartOperation( - "Code generation for builder: %s", builderDef.getBuilderTypeName().getClassName()); - TypeSpec.Builder classBuilder = createClassBuilder(builderDef); - addClassMetadata(classBuilder, builderDef); - addFieldsToBuilder(classBuilder, builderDef); - addMethodsToBuilder(classBuilder, builderDef); - addConstructorsToBuilder(classBuilder, builderDef); - addNestedTypesToBuilder(classBuilder, builderDef); - addAnnotationsToBuilder(classBuilder, builderDef); - - writeBuilderClassToFile(classBuilder.build(), builderDef); - logger.debugEndOperation( - "Successfully generated builder: %s", builderDef.getBuilderTypeName().getClassName()); - } - - private TypeSpec.Builder createClassBuilder(BuilderDefinitionDto builderDef) { - ClassName builderBaseClass = map2ClassName(builderDef.getBuilderTypeName()); - if (CollectionUtils.isNotEmpty(builderDef.getGenerics())) { - logger.debug("Builder has %d generic type parameter(s)", builderDef.getGenerics().size()); - } - - TypeSpec.Builder result = - TypeSpec.classBuilder(builderBaseClass) - .addTypeVariables(map2TypeVariables(builderDef.getGenerics())); - logger.debug("Class builder created"); - return result; - } - - private void addClassMetadata(TypeSpec.Builder classBuilder, BuilderDefinitionDto builderDef) { - // Add class JavaDoc if provided by enhancer - if (builderDef.getClassJavadoc() != null) { - classBuilder.addJavadoc(builderDef.getClassJavadoc()); - } - - // Set builder class access level - Modifier builderAccessModifier = - JavaLangMapper.mapAccessModifier(builderDef.getConfiguration().getBuilderAccess()); - if (builderAccessModifier != null) { - classBuilder.addModifiers(builderAccessModifier); - } - - // Adding interfaces from enhancers - for (InterfaceName interfaceName : builderDef.getInterfaces()) { - com.palantir.javapoet.TypeName interfaceType = - JavapoetMapper.mapInterfaceToTypeName(interfaceName); - classBuilder.addSuperinterface(interfaceType); - } - - logger.debug("Class metadata added"); - } - - private void addFieldsToBuilder(TypeSpec.Builder classBuilder, BuilderDefinitionDto builderDef) { - logger.debugStartOperation( - "Generating %d constructor fields and %d setter fields", - builderDef.getConstructorFieldsForBuilder().size(), - builderDef.getSetterFieldsForBuilder().size()); - - // Generate backing fields for each DTO field (constructor and setter fields) - // Note: Builder field name conflicts are now resolved in BuilderDefinitionCreator - for (FieldDto fieldDto : builderDef.getConstructorFieldsForBuilder()) { - FieldSpec fieldSpec = createFieldMember(fieldDto); - classBuilder.addField(fieldSpec); - } - for (FieldDto fieldDto : builderDef.getSetterFieldsForBuilder()) { - FieldSpec fieldSpec = createFieldMember(fieldDto); - classBuilder.addField(fieldSpec); - } - logger.debugEndOperation("Fields added: %d fields", builderDef.getAllFieldsForBuilder().size()); - } - - private void addMethodsToBuilder(TypeSpec.Builder classBuilder, BuilderDefinitionDto builderDef) { - // Collect all methods from all fields, setting javadoc and tracking field relationship - Map allMethods = collectAllMethods(builderDef); - logger.debugStartOperation("Adding Methods for %d candidates", allMethods.size()); - - // Resolve conflicts and sort by ordering - List resolvedMethods = resolveMethodConflicts(allMethods); - logger.debug("Resolved %d methods after conflict resolution", resolvedMethods.size()); - - // Generate all methods in order - int generatedCnt = 0; - for (MethodDto methodDto : resolvedMethods) { - MethodSpec methodSpec = createMethod(methodDto); - classBuilder.addMethod(methodSpec); - generatedCnt++; - } - logger.debugEndOperation("%d Methods added", generatedCnt); - } - - private Map collectAllMethods(BuilderDefinitionDto builderDef) { - Map allMethods = new HashMap<>(); - - for (FieldDto fieldDto : builderDef.getConstructorFieldsForBuilder()) { - for (MethodDto method : fieldDto.getMethods()) { - allMethods.put(method, fieldDto); - } - } - for (FieldDto fieldDto : builderDef.getSetterFieldsForBuilder()) { - for (MethodDto method : fieldDto.getMethods()) { - allMethods.put(method, fieldDto); - } - } - - // Add core methods to the collection - for (MethodDto coreMethod : builderDef.getCoreMethods()) { - allMethods.put(coreMethod, null); // Core methods don't have associated fields - } - - return allMethods; - } - - private void addConstructorsToBuilder( - TypeSpec.Builder classBuilder, BuilderDefinitionDto builderDef) { - generateConstructors(classBuilder, builderDef); - logger.debug("Constructors added"); - } - - private void addNestedTypesToBuilder( - TypeSpec.Builder classBuilder, BuilderDefinitionDto builderDef) { - if (CollectionUtils.isEmpty(builderDef.getNestedTypes())) { - return; - } - // Adding nested types (e.g., With interface) - logger.debugStartOperation("Generating %d nested type(s)", builderDef.getNestedTypes().size()); - for (NestedTypeDto nestedType : builderDef.getNestedTypes()) { - TypeSpec nestedTypeSpec = createNestedType(nestedType); - classBuilder.addType(nestedTypeSpec); - logger.debug("Generated nested type: %s", nestedType.getTypeName()); - } - logger.debugEndOperation("Nested types added"); - } - - private void addAnnotationsToBuilder( - TypeSpec.Builder classBuilder, BuilderDefinitionDto builderDef) { - if (CollectionUtils.isEmpty(builderDef.getClassAnnotations())) { - return; - } - // Adding annotations from enhancers - for (AnnotationDto annotation : builderDef.getClassAnnotations()) { - AnnotationSpec annotationSpec = map2AnnotationSpec(annotation); - classBuilder.addAnnotation(annotationSpec); - } - - logger.debug("Class-level annotations added"); - } - - private void writeBuilderClassToFile(TypeSpec typeSpec, BuilderDefinitionDto builderDef) - throws BuilderException { - logger.debug( - "Writing builder class to file: %s.%s", - builderDef.getBuilderTypeName().getPackageName(), - builderDef.getBuilderTypeName().getClassName()); - - // Extract qualified name from the builder definition - String qualifiedName = builderDef.getBuilderTypeName().getFullQualifiedName(); - - // Check if builder class already exists before attempting to write - if (builderClassAlreadyExists(qualifiedName)) { - throw new BuilderException( - null, - """ - Builder class '%s' already exists. This may be a manually written builder or a previously generated builder. - To resolve this: - 1. If you have a manual builder, consider renaming it or removing @SimpleBuilder from the DTO - 2. If this is from a previous compilation, clean and rebuild the project - 3. Check that you're not trying to generate multiple builders for the same DTO - """ - .formatted(qualifiedName)); - } - - try { - JavaFile.builder(builderDef.getBuilderTypeName().getPackageName(), typeSpec) - .skipJavaLangImports(true) - .addStaticImport(TrackedValue.class, "initialValue") - .addStaticImport(TrackedValue.class, "changedValue") - .addStaticImport(TrackedValue.class, "unsetValue") - .build() - .writeTo(processingEnv.getFiler()); - } catch (IOException ex) { - // Handle file system errors during file write - String message = ex.getMessage(); - String errorMessage = - """ - Unable to create builder class '%s': %s. - Check the build environment and ensure all necessary directories are accessible. - """ - .formatted( - qualifiedName, StringUtils.isNotBlank(message) ? message : "Unknown error"); - throw new BuilderException(null, errorMessage); - } - } - - /** - * Checks if a builder class already exists by attempting to find the type element. - * - * @param qualifiedName the fully qualified name of the class to check - * @return true if the class already exists, false otherwise - */ - private boolean builderClassAlreadyExists(String qualifiedName) { - try { - TypeElement existingType = processingEnv.getElementUtils().getTypeElement(qualifiedName); - return existingType != null; - } catch (Exception e) { - // Log the exception at debug level - this should rarely happen but is useful for - // troubleshooting - logger.debug( - "Error checking if builder class '%s' already exists: %s", - qualifiedName, StringUtils.isNotBlank(e.getMessage()) ? e.getMessage() : "No message"); - // If there's any error checking, assume the class doesn't exist - return false; - } - } - - private void writeSimpleClassToFile(String packageName, TypeSpec typeSpec) - throws BuilderException { - try { - JavaFile.builder(packageName, typeSpec) - .skipJavaLangImports(true) - .build() - .writeTo(processingEnv.getFiler()); - } catch (IOException ex) { - // Handle file system issues for Jackson modules and other simple classes - String message = ex.getMessage(); - String errorMessage = - """ - Unable to create class: %s. - Check the build environment and ensure all necessary directories are accessible. - """ - .formatted(StringUtils.isNotBlank(message) ? message : "Unknown error"); - throw new BuilderException(null, errorMessage); - } - } - - /** - * Resolves method conflicts by keeping only the highest priority method for each signature and - * sorts all methods by ordering then name. This prevents compilation errors when methods from - * different fields have the same signature and ensures proper method generation order. - * - * @param methodToField mapping from method to its source field (null for core methods) - * @return list of all methods with conflicts resolved, sorted by ordering for proper generation - */ - private List resolveMethodConflicts(Map methodToField) { - MethodDto.MethodComparator comparator = new MethodDto.MethodComparator(); - - // Sort entries using MethodComparator for deterministic conflict resolution - // This ensures consistent behavior when multiple methods have the same signature - List> sortedEntries = - methodToField.entrySet().stream() - .sorted((e1, e2) -> comparator.compare(e1.getKey(), e2.getKey())) - .toList(); - - // Use LinkedHashMap to preserve insertion order from sorted entries - Map signatureToMethod = new java.util.LinkedHashMap<>(); - - // Process all methods and resolve conflicts - for (Map.Entry entry : sortedEntries) { - MethodDto method = entry.getKey(); - FieldDto field = entry.getValue(); - String signature = method.getSignatureKey(); - - MethodDto existing = signatureToMethod.get(signature); - if (existing == null) { - // No conflict, add the method - signatureToMethod.put(signature, method); - } else { - // Conflict detected: keep the higher priority method - String existingSource = getSourceDescription(existing, methodToField.get(existing)); - String newSource = getSourceDescription(method, field); - - if (method.getPriority() > existing.getPriority()) { - // New method wins - signatureToMethod.put(signature, method); - logger.warning( - " Method conflict: '%s' from %s (priority %d) dropped in favor of %s (priority %d)", - signature, existingSource, existing.getPriority(), newSource, method.getPriority()); - } else if (method.getPriority() < existing.getPriority()) { - // Existing method wins - logger.warning( - " Method conflict: '%s' from %s (priority %d) dropped in favor of %s (priority %d)", - signature, newSource, method.getPriority(), existingSource, existing.getPriority()); - } else { - // Equal priority - keep first - logger.warning( - " Method conflict: '%s' from %s (priority %d) dropped in favor of %s (priority %d) - equal priority, keeping first", - signature, newSource, method.getPriority(), existingSource, existing.getPriority()); - } - } - } - - // Return methods in insertion order (already sorted from conflict resolution) - return new java.util.ArrayList<>(signatureToMethod.values()); - } - - /** - * Gets a description of the method source for logging purposes. - * - * @param method the method - * @param field the associated field (null for core methods) - * @return description of the method source - */ - private String getSourceDescription(MethodDto method, FieldDto field) { - if (field == null) { - return "core method '" + method.getMethodName() + "'"; - } else { - return "field '" + field.getFieldNameInBuilder() + "'"; - } - } - - /** - * Generates constructors for the builder class. - * - * @param classBuilder the TypeSpec.Builder to add constructors to - * @param builderDef the builder definition containing field information - */ - private void generateConstructors( - TypeSpec.Builder classBuilder, BuilderDefinitionDto builderDef) { - // Get access modifiers from configuration - Modifier constructorAccessModifier = - JavaLangMapper.mapAccessModifier( - builderDef.getConfiguration().getBuilderConstructorAccess()); - ClassName dtoBaseClass = map2ClassName(builderDef.getBuildingTargetTypeName()); - - // Generate empty constructor - MethodSpec emptyConstructor = createEmptyConstructor(dtoBaseClass, constructorAccessModifier); - classBuilder.addMethod(emptyConstructor); - - // Generate constructor with instance - com.palantir.javapoet.TypeName dtoTypeName = - map2ParameterType(builderDef.getBuildingTargetTypeName()); - MethodSpec instanceConstructor = - createConstructorWithInstance( - dtoBaseClass, - dtoTypeName, - builderDef.getAllFieldsForBuilder(), - constructorAccessModifier); - classBuilder.addMethod(instanceConstructor); - } - - private MethodSpec createEmptyConstructor(ClassName dtoClass, Modifier accessModifier) { - MethodSpec.Builder constructorBuilder = - MethodSpec.constructorBuilder() - .addModifiers(accessModifier) - .addJavadoc( - """ - Empty constructor of builder for {@code $1N.$2T}. - """, - dtoClass.packageName(), - dtoClass); - return constructorBuilder.build(); - } - - private MethodSpec createConstructorWithInstance( - ClassName dtoBaseClass, - com.palantir.javapoet.TypeName dtoType, - List fields, - Modifier accessModifier) { - MethodSpec.Builder cb = - MethodSpec.constructorBuilder() - .addModifiers(accessModifier) - .addParameter(dtoType, "instance") - .addJavadoc( - """ - Initialisation of builder for {@code $1N.$2T} by a instance. - - @param instance object instance for initialisiation - """, - dtoBaseClass.packageName(), - dtoBaseClass); - - for (FieldDto f : fields) { - f.getGetterName().ifPresent(getter -> addFieldInitializationWithValidation(cb, f, getter)); - } - return cb.build(); - } - - private void addFieldInitializationWithValidation( - MethodSpec.Builder cb, FieldDto field, String getter) { - // Initialize field from source instance - cb.addStatement( - "this.$N = $T.initialValue(instance.$N())", - field.getFieldNameInBuilder(), - ClassName.get(TrackedValue.class), - getter); - - // Validate non-nullable fields immediately - fail fast if source object is invalid - if (field.isNonNullable()) { - cb.beginControlFlow("if (this.$N.value() == null)", field.getFieldNameInBuilder()) - .addStatement( - "throw new $T($S)", - IllegalArgumentException.class, - "Cannot initialize builder from instance: field '" - + field.getFieldNameInBuilder() - + "' is marked as non-null but source object has null value") - .endControlFlow(); - } - } - - private FieldSpec createFieldMember(FieldDto fieldDto) { - com.palantir.javapoet.TypeName fieldType = map2ParameterType(fieldDto.getFieldType()); - if (fieldType.isPrimitive()) { - fieldType = fieldType.box(); - } - // Wrap all fields in TrackedValue - ClassName builderFieldWrapper = ClassName.get(TrackedValue.class); - ParameterizedTypeName wrappedFieldType = - ParameterizedTypeName.get(builderFieldWrapper, fieldType); - - return FieldSpec.builder(wrappedFieldType, fieldDto.getFieldNameInBuilder(), Modifier.PRIVATE) - .addJavadoc( - "Tracked value for $L: $L.\n", - fieldDto.getFieldNameInBuilder(), - fieldDto.getJavaDoc()) - .initializer("$T.unsetValue()", builderFieldWrapper) - .build(); - } - - private MethodSpec createMethod(MethodDto methodDto) { - com.palantir.javapoet.TypeName returnType = map2ParameterType(methodDto.getReturnType()); - MethodSpec.Builder methodBuilder = - MethodSpec.methodBuilder(methodDto.getMethodName()).returns(returnType); - - // Use modifier from MethodDto if present - methodDto.getModifier().ifPresent(methodBuilder::addModifiers); - - // Add static modifier if method is static - if (methodDto.isStatic()) { - methodBuilder.addModifiers(javax.lang.model.element.Modifier.STATIC); - } - - // Add generic type parameters if any - if (CollectionUtils.isNotEmpty(methodDto.getGenericParameters())) { - List typeVariables = - methodDto.getGenericParameters().stream() - .map(param -> com.palantir.javapoet.TypeVariableName.get(param.getName())) - .toList(); - methodBuilder.addTypeVariables(typeVariables); - } - - // Use javadoc from MethodDto if available - if (StringUtils.isNoneBlank(methodDto.getJavadoc())) { - methodBuilder.addJavadoc(methodDto.getJavadoc()); - } - - // Add annotations from MethodDto - if (!methodDto.getAnnotations().isEmpty()) { - methodBuilder.addAnnotations(map2AnnotationSpecs(methodDto.getAnnotations())); - } - - // Add parameters - for (MethodParameterDto paramDto : methodDto.getParameters()) { - methodBuilder.addParameter(createParameter(paramDto)); - if (paramDto.getParameterType() instanceof TypeNameArray) { - methodBuilder.varargs(); // Arrays should be mapped to be generics - } - } - - CodeBlock codeBlock = map2CodeBlock(methodDto.getMethodCodeDto()); - methodBuilder.addCode(codeBlock); - return methodBuilder.build(); - } - - private ParameterSpec createParameter(MethodParameterDto paramDto) { - com.palantir.javapoet.TypeName parameterType = map2ParameterType(paramDto.getParameterType()); - ParameterSpec.Builder paramBuilder = - ParameterSpec.builder(parameterType, paramDto.getParameterName()); - if (!paramDto.getAnnotations().isEmpty()) { - paramBuilder.addAnnotations(map2AnnotationSpecs(paramDto.getAnnotations())); - } - return paramBuilder.build(); - } - - private TypeSpec createNestedType(NestedTypeDto nestedType) { - TypeSpec.Builder typeBuilder; - boolean isInterface = nestedType.getKind() == NestedTypeDto.NestedTypeKind.INTERFACE; - if (isInterface) { - typeBuilder = TypeSpec.interfaceBuilder(nestedType.getTypeName()); - } else { - typeBuilder = TypeSpec.classBuilder(nestedType.getTypeName()); - } - - if (nestedType.isPublic()) { - typeBuilder.addModifiers(PUBLIC); - } - - if (nestedType.getJavadoc() != null) { - typeBuilder.addJavadoc(nestedType.getJavadoc()); - } - - for (MethodDto methodDto : nestedType.getMethods()) { - MethodSpec methodSpec = createNestedTypeMethod(methodDto, isInterface); - typeBuilder.addMethod(methodSpec); - } - - return typeBuilder.build(); - } - - /** - * Creates a method specification from a MethodDto for nested types (e.g., With interface - * methods). - * - * @param methodDto the method definition - * @param isInterface whether the containing type is an interface - * @return the generated MethodSpec - */ - private MethodSpec createNestedTypeMethod(MethodDto methodDto, boolean isInterface) { - MethodSpec.Builder methodBuilder = - MethodSpec.methodBuilder(methodDto.getMethodName()).addModifiers(PUBLIC); - - // Set return type using mapper - methodBuilder.returns(JavapoetMapper.map2ParameterType(methodDto.getReturnType())); - - // Add parameters using mapper - for (MethodParameterDto paramDto : methodDto.getParameters()) { - methodBuilder.addParameter(createParameter(paramDto)); - } - - if (methodDto.getJavadoc() != null) { - methodBuilder.addJavadoc(methodDto.getJavadoc()); - } - - // Add code only if method has implementation (even for interfaces with default methods) - if (methodDto.getMethodCodeDto() != null) { - if (isInterface) { - methodBuilder.addModifiers(javax.lang.model.element.Modifier.DEFAULT); - } - - methodBuilder.addCode(map2CodeBlock(methodDto.getMethodCodeDto())); - } - - return methodBuilder.build(); - } - - /** - * Generates a Jackson SimpleModule based on the provided definition. - * - * @param moduleDef the definition of the Jackson module to generate - */ - public void generateJacksonModule(JacksonModuleDefinitionDto moduleDef) { - String packageName = moduleDef.getTargetPackage(); - String moduleClassName = "SimpleBuildersJacksonModule"; - - logger.info("Generating Jackson Module '%s' in package '%s'", moduleClassName, packageName); - - ClassName simpleModuleClass = - ClassName.get("com.fasterxml.jackson.databind.module", "SimpleModule"); - ClassName jsonDeserializeClass = - ClassName.get("com.fasterxml.jackson.databind.annotation", "JsonDeserialize"); - - // Create the constructor - MethodSpec.Builder constructorBuilder = - MethodSpec.constructorBuilder().addModifiers(Modifier.PUBLIC); - - // Create the class - TypeSpec.Builder classBuilder = - TypeSpec.classBuilder(moduleClassName) - .addModifiers(Modifier.PUBLIC) - .superclass(simpleModuleClass); - - for (JacksonModuleEntryDto entry : moduleDef.getEntries()) { - ClassName dtoClass = - ClassName.get(entry.dtoType().getPackageName(), entry.dtoType().getClassName()); - ClassName builderClass = - ClassName.get(entry.builderType().getPackageName(), entry.builderType().getClassName()); - - // Create MixIn interface name: DtoNameMixin - String mixinName = entry.dtoType().getClassName() + "Mixin"; - - // Create MixIn interface with @JsonDeserialize(builder = Builder.class) - TypeSpec mixinInterface = - TypeSpec.interfaceBuilder(mixinName) - .addModifiers(Modifier.PRIVATE) - .addAnnotation( - AnnotationSpec.builder(jsonDeserializeClass) - .addMember("builder", "$T.class", builderClass) - .build()) - .build(); - - classBuilder.addType(mixinInterface); - - // Add registration to constructor: setMixInAnnotation(Dto.class, Mixin.class) - constructorBuilder.addStatement( - "setMixInAnnotation($T.class, $N.class)", dtoClass, mixinName); - } - - classBuilder.addMethod(constructorBuilder.build()); - - // Write file - try { - writeSimpleClassToFile(packageName, classBuilder.build()); - } catch (BuilderException e) { - logger.warning( - "simple-builders: Error generating Jackson module for package %s: %s\n%s", - packageName, e.getMessage(), java.util.Arrays.toString(e.getStackTrace())); - } - } -} diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/javapoet/JavapoetMapper.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/javapoet/JavapoetMapper.java deleted file mode 100644 index c8cbb2f0..00000000 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/javapoet/JavapoetMapper.java +++ /dev/null @@ -1,272 +0,0 @@ -/* - * MIT License - * - * Copyright (c) 2026 Andreas Igel - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -package org.javahelpers.simple.builders.processor.classgen.javapoet; - -import com.palantir.javapoet.AnnotationSpec; -import com.palantir.javapoet.ArrayTypeName; -import com.palantir.javapoet.ClassName; -import com.palantir.javapoet.CodeBlock; -import com.palantir.javapoet.ParameterizedTypeName; -import com.palantir.javapoet.TypeName; -import com.palantir.javapoet.TypeVariableName; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; -import org.apache.commons.collections4.CollectionUtils; -import org.apache.commons.lang3.StringUtils; -import org.javahelpers.simple.builders.processor.classgen.javapoet.exceptions.JavapoetMapperException; -import org.javahelpers.simple.builders.processor.model.annotation.AnnotationDto; -import org.javahelpers.simple.builders.processor.model.annotation.InterfaceName; -import org.javahelpers.simple.builders.processor.model.method.MethodCodePlaceholder; -import org.javahelpers.simple.builders.processor.model.method.MethodCodeStringPlaceholder; -import org.javahelpers.simple.builders.processor.model.method.MethodCodeTypePlaceholder; -import org.javahelpers.simple.builders.processor.model.type.GenericParameterDto; -import org.javahelpers.simple.builders.processor.model.type.TypeNameArray; -import org.javahelpers.simple.builders.processor.model.type.TypeNameGeneric; -import org.javahelpers.simple.builders.processor.model.type.TypeNamePrimitive; - -/** Helper functions to create JavaPoet types from DTOs of simple builder. */ -public final class JavapoetMapper { - - private JavapoetMapper() {} - - /** - * Maps a list of simple-builder DTO type names to an array of JavaPoet {@code TypeName}s. - * Primitives are boxed because JavaPoet requires reference types for type arguments. - * - * @param typeArguments the list of type arguments to map - * @return array of JavaPoet TypeName instances - */ - public static TypeName[] map2TypeArgumentsArray( - List typeArguments) { - java.util.List args = new java.util.ArrayList<>(typeArguments.size()); - for (org.javahelpers.simple.builders.processor.model.type.TypeName tn : typeArguments) { - TypeName mapped = map2ParameterType(tn); - if (mapped.isPrimitive()) { - mapped = mapped.box(); - } - args.add(mapped); - } - return args.toArray(new TypeName[0]); - } - - /** - * Mapper for parameterType. Maps into javapoet classes. - * - * @param parameterType simple-builder dto to be mapped - * @return javapoet TypeName - */ - public static TypeName map2ParameterType( - org.javahelpers.simple.builders.processor.model.type.TypeName parameterType) { - TypeName typeName; - if (parameterType - instanceof - org.javahelpers.simple.builders.processor.model.type.TypeNameVariable typeVariable) { - typeName = TypeVariableName.get(typeVariable.getClassName()); - } else if (parameterType instanceof TypeNamePrimitive parameterTypePrim) { - typeName = mapPrimitive(parameterTypePrim); - } else if (parameterType instanceof TypeNameArray parameterTypeArray) { - typeName = ArrayTypeName.of(map2ParameterType(parameterTypeArray.getTypeOfArray())); - } else if (parameterType instanceof TypeNameGeneric parameterTypeGeneric) { - typeName = mapGeneric(parameterTypeGeneric); - } else { - typeName = ClassName.get(parameterType.getPackageName(), parameterType.getClassName()); - } - - if (typeName != null && CollectionUtils.isNotEmpty(parameterType.getAnnotations())) { - typeName = typeName.annotated(map2AnnotationSpecs(parameterType.getAnnotations())); - } - return typeName; - } - - private static TypeName mapPrimitive(TypeNamePrimitive parameterTypePrim) { - return switch (parameterTypePrim.getType()) { - case BOOLEAN -> TypeName.BOOLEAN; - case BYTE -> TypeName.BYTE; - case CHAR -> TypeName.CHAR; - case DOUBLE -> TypeName.DOUBLE; - case FLOAT -> TypeName.FLOAT; - case INT -> TypeName.INT; - case LONG -> TypeName.LONG; - case SHORT -> TypeName.SHORT; - default -> null; - }; - } - - private static TypeName mapGeneric(TypeNameGeneric param) { - ClassName classNameParameter = ClassName.get(param.getPackageName(), param.getClassName()); - if (param.getInnerTypeArguments().isEmpty()) { - return classNameParameter; - } - TypeName[] typeArgs = map2TypeArgumentsArray(param.getInnerTypeArguments()); - return ParameterizedTypeName.get(classNameParameter, typeArgs); - } - - /** - * Mapper for typename. Maps into javapoet classes. - * - * @param typeName simple-builder dto to be mapped - * @return javapoet TypeName - */ - public static ClassName map2ClassName( - org.javahelpers.simple.builders.processor.model.type.TypeName typeName) { - if (StringUtils.isNoneEmpty(typeName.getPackageName())) { - return ClassName.get(typeName.getPackageName(), typeName.getClassName()); - } else { - return ClassName.bestGuess(typeName.getClassName()); - } - } - - /** - * Maps a base type and generic parameters to a JavaPoet ParameterizedTypeName. - * - * @param baseType the base type to parameterize - * @param builderGenerics the list of generic parameters - * @return a ParameterizedTypeName with the given type parameters - */ - public static ParameterizedTypeName map2ParameterizedTypeName( - org.javahelpers.simple.builders.processor.model.type.TypeName baseType, - List builderGenerics) { - ClassName baseTypeClassName = map2ClassName(baseType); - return ParameterizedTypeName.get( - baseTypeClassName, map2TypeVariables(builderGenerics).toArray(new TypeVariableName[0])); - } - - /** - * Maps a list of GenericParameterDto to JavaPoet TypeVariableName instances. - * - * @param builderGenerics the list of generic parameters to map - * @return list of TypeVariableName representing the generic parameters - */ - public static List map2TypeVariables( - List builderGenerics) { - List javapoetGenerics = new ArrayList<>(); - for (GenericParameterDto g : builderGenerics) { - List bounds = new ArrayList<>(); - for (org.javahelpers.simple.builders.processor.model.type.TypeName b : g.getUpperBounds()) { - bounds.add(map2ParameterType(b)); - } - TypeVariableName tv = - bounds.isEmpty() - ? TypeVariableName.get(g.getName()) - : TypeVariableName.get(g.getName(), bounds.toArray(new TypeName[0])); - javapoetGenerics.add(tv); - } - return javapoetGenerics; - } - - /** - * CodeBlock creating by definition in {@code MethodCodeDto}. - * - * @param codeDto code definition - * @return {@code CodeBlock} of javapoet - */ - public static CodeBlock map2CodeBlock( - org.javahelpers.simple.builders.processor.model.method.MethodCodeDto codeDto) { - Map arguments = - codeDto.getCodeArguments().stream() - .collect( - Collectors.toMap( - MethodCodePlaceholder::getLabel, JavapoetMapper::toCodeblockValue)); - return CodeBlock.builder().addNamed(codeDto.getCodeFormat(), arguments).build(); - } - - private static Object toCodeblockValue(MethodCodePlaceholder placeHolderValue) { - if (placeHolderValue instanceof MethodCodeStringPlaceholder stringPlaceholder) { - return stringPlaceholder.getValue(); - } else if (placeHolderValue instanceof MethodCodeTypePlaceholder typePlaceholder) { - return map2ParameterType(typePlaceholder.getValue()); - } else { - throw new UnsupportedOperationException( - "Unsupported placeholder type: " + placeHolderValue.getClass()); - } - } - - /** - * Maps an AnnotationDto to a JavaPoet AnnotationSpec. - * - * @param annotationDto the annotation DTO to map - * @return the mapped JavaPoet AnnotationSpec, always not null - * @throws JavapoetMapperException if mapping fails - */ - public static AnnotationSpec map2AnnotationSpec(AnnotationDto annotationDto) { - try { - ClassName annotationType = map2ClassName(annotationDto.getAnnotationType()); - AnnotationSpec.Builder builder = AnnotationSpec.builder(annotationType); - - for (Map.Entry member : annotationDto.getMembers().entrySet()) { - builder.addMember(member.getKey(), "$L", member.getValue()); - } - - return builder.build(); - } catch (Exception e) { - throw new JavapoetMapperException( - e, - "Failed to map annotation %s: %s", - annotationDto.getAnnotationType().getClassName(), - e.getMessage()); - } - } - - /** - * Maps a list of AnnotationDto to JavaPoet AnnotationSpec instances. - * - * @param annotations the list of annotations to map - * @return list of AnnotationSpec for all annotations - * @throws JavapoetMapperException if any annotation mapping fails - */ - public static List map2AnnotationSpecs(List annotations) { - return annotations.stream().map(JavapoetMapper::map2AnnotationSpec).toList(); - } - - /** - * Maps an InterfaceName to a JavaPoet TypeName. - * - * @param interfaceName the interface name to map - * @return the mapped JavaPoet TypeName, always not null - * @throws JavapoetMapperException if mapping fails - */ - public static TypeName mapInterfaceToTypeName(InterfaceName interfaceName) { - try { - TypeName interfaceType = - ClassName.get(interfaceName.getPackageName(), interfaceName.getSimpleName()); - - // Add type parameters if present - if (interfaceName.hasTypeParameters()) { - TypeName[] typeArgs = - interfaceName.getTypeParameters().stream() - .map(JavapoetMapper::map2ParameterType) - .toArray(TypeName[]::new); - interfaceType = ParameterizedTypeName.get((ClassName) interfaceType, typeArgs); - } - - return interfaceType; - } catch (Exception e) { - throw new JavapoetMapperException( - e, "Failed to map interface %s: %s", interfaceName.toString(), e.getMessage()); - } - } -} diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/javapoet/exceptions/package-info.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/javapoet/exceptions/package-info.java deleted file mode 100644 index 76bcbff7..00000000 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/javapoet/exceptions/package-info.java +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Package org.javahelpers.simple.builders.processor.classgen.javapoet.exceptions - * - *

JavaPoet-specific exception classes. - * - *

This package contains exceptions specific to JavaPoet code generation, providing error - * handling for mapping and code generation issues within the JavaPoet layer. - */ -package org.javahelpers.simple.builders.processor.classgen.javapoet.exceptions; diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/javapoet/package-info.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/javapoet/package-info.java deleted file mode 100644 index 0574a8c5..00000000 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/javapoet/package-info.java +++ /dev/null @@ -1,26 +0,0 @@ -/** - * Package org.javahelpers.simple.builders.processor.classgen.javapoet - * - *

JavaPoet-based code generation for simple-builders processor. - * - *

This package contains the core code generation components: - * - *

- * - *

Exception classes are organized in separate packages: - * - *

- * - *

This package isolates JavaPoet-specific code, making it easier to replace the code generation - * implementation if needed. - */ -package org.javahelpers.simple.builders.processor.classgen.javapoet; diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterCodeGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterCodeGenerator.java new file mode 100644 index 00000000..bd86c0f0 --- /dev/null +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterCodeGenerator.java @@ -0,0 +1,965 @@ +/* + * MIT License + * + * Copyright (c) 2026 Andreas Igel + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.javahelpers.simple.builders.processor.classgen.roaster; + +import static org.javahelpers.simple.builders.processor.classgen.roaster.RoasterMapper.mapBoxedType; +import static org.javahelpers.simple.builders.processor.classgen.roaster.RoasterMapper.mapType; +import static org.javahelpers.simple.builders.processor.classgen.roaster.RoasterMapper.packageNameOf; +import static org.javahelpers.simple.builders.processor.classgen.roaster.RoasterMapper.resolveCodeTemplate; + +import java.io.IOException; +import java.io.InputStream; +import java.io.Writer; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Supplier; +import javax.annotation.processing.ProcessingEnvironment; +import javax.lang.model.element.Modifier; +import javax.lang.model.element.TypeElement; +import javax.tools.JavaFileObject; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.javahelpers.simple.builders.core.util.TrackedValue; +import org.javahelpers.simple.builders.processor.analysis.JavaLangMapper; +import org.javahelpers.simple.builders.processor.exceptions.BuilderException; +import org.javahelpers.simple.builders.processor.model.annotation.AnnotationDto; +import org.javahelpers.simple.builders.processor.model.annotation.InterfaceName; +import org.javahelpers.simple.builders.processor.model.core.BuilderDefinitionDto; +import org.javahelpers.simple.builders.processor.model.core.FieldDto; +import org.javahelpers.simple.builders.processor.model.integration.JacksonModuleDefinitionDto; +import org.javahelpers.simple.builders.processor.model.integration.JacksonModuleEntryDto; +import org.javahelpers.simple.builders.processor.model.method.MethodCodePlaceholder; +import org.javahelpers.simple.builders.processor.model.method.MethodCodeTypePlaceholder; +import org.javahelpers.simple.builders.processor.model.method.MethodDto; +import org.javahelpers.simple.builders.processor.model.method.MethodParameterDto; +import org.javahelpers.simple.builders.processor.model.type.NestedTypeDto; +import org.javahelpers.simple.builders.processor.model.type.TypeName; +import org.javahelpers.simple.builders.processor.model.type.TypeNameArray; +import org.javahelpers.simple.builders.processor.model.type.TypeNameGeneric; +import org.javahelpers.simple.builders.processor.model.type.TypeNamePrimitive; +import org.javahelpers.simple.builders.processor.model.type.TypeNameVariable; +import org.javahelpers.simple.builders.processor.processing.ProcessingLogger; +import org.jboss.forge.roaster.Roaster; +import org.jboss.forge.roaster.model.source.AnnotationSource; +import org.jboss.forge.roaster.model.source.FieldSource; +import org.jboss.forge.roaster.model.source.JavaClassSource; +import org.jboss.forge.roaster.model.source.JavaDocSource; +import org.jboss.forge.roaster.model.source.JavaInterfaceSource; +import org.jboss.forge.roaster.model.source.JavaSource; +import org.jboss.forge.roaster.model.source.MethodSource; +import org.jboss.forge.roaster.model.source.ParameterSource; +import org.jboss.forge.roaster.model.source.TypeVariableSource; +import org.jboss.forge.roaster.model.util.FormatterProfileReader; + +/** Roaster-based code generator for builder source files. */ +public class RoasterCodeGenerator { + private static final String FORMATTER_PROFILE_RESOURCE = "eclipse-java-format.xml"; + + /** Processing environment for accessing filer and element utilities. */ + private final ProcessingEnvironment processingEnv; + + /** Logger for debug output during code generation. */ + private final ProcessingLogger logger; + + private final Properties formatterProperties; + + /** + * Constructor for RoasterCodeGenerator. + * + * @param processingEnv Processing environment for accessing filer and element utilities + * @param logger Logger for debug output + */ + public RoasterCodeGenerator(ProcessingEnvironment processingEnv, ProcessingLogger logger) { + this.processingEnv = processingEnv; + this.logger = logger; + this.formatterProperties = loadFormatterProperties(); + } + + /** + * Generates a builder class from the given builder definition. + * + * @param builderDef dto of all information to create the builder + * @throws BuilderException if there is an error in source code generation + */ + public void generateBuilder(BuilderDefinitionDto builderDef) throws BuilderException { + logger.debugStartOperation( + "Code generation for builder: %s", builderDef.getBuilderTypeName().getClassName()); + + String sourceCode = createBuilderSource(builderDef); + writeBuilderClassToFile(sourceCode, builderDef); + + logger.debugEndOperation( + "Successfully generated builder: %s", builderDef.getBuilderTypeName().getClassName()); + } + + private String createBuilderSource(BuilderDefinitionDto builderDef) { + JavaClassSource source = createClassSource(builderDef); + addClassMetadata(source, builderDef); + appendFields(source, builderDef); + appendConstructors(source, builderDef); + appendMethods(source, builderDef); + appendNestedTypes(source, builderDef); + applyClassAnnotations(source, builderDef); + return renderClassSource(source); + } + + private void applyClassAnnotations(JavaClassSource source, BuilderDefinitionDto builderDef) { + if (CollectionUtils.isEmpty(builderDef.getClassAnnotations())) { + return; + } + // Adding annotations from enhancers + applyAnnotations(source, builderDef.getClassAnnotations()); + logger.debug("Class-level annotations added"); + } + + private JavaClassSource createClassSource(BuilderDefinitionDto builderDef) { + if (CollectionUtils.isNotEmpty(builderDef.getGenerics())) { + logger.debug("Builder has %d generic type parameter(s)", builderDef.getGenerics().size()); + } + + JavaClassSource source = Roaster.create(JavaClassSource.class); + String packageName = builderDef.getBuilderTypeName().getPackageName(); + if (StringUtils.isNotBlank(packageName)) { + source.setPackage(packageName); + } else { + source.setDefaultPackage(); + } + source.setName(builderDef.getBuilderTypeName().getClassName()); + addGenericDeclarations(source, builderDef.getGenerics()); + addTrackedValueStaticImports(source); + collectImports(builderDef).stream().sorted().forEach(source::addImport); + logger.debug("Class builder created"); + return source; + } + + private void addClassMetadata(JavaClassSource source, BuilderDefinitionDto builderDef) { + // Add class JavaDoc if provided by enhancer + applyJavadoc(source, builderDef.getClassJavadoc()); + + // Set builder class access level + applyVisibility( + source, JavaLangMapper.mapAccessModifier(builderDef.getConfiguration().getBuilderAccess())); + + // Adding interfaces from enhancers + for (InterfaceName interfaceName : builderDef.getInterfaces()) { + source.addInterface(RoasterMapper.mapInterfaceToTypeName(interfaceName)); + } + + logger.debug("Class metadata added"); + } + + private String renderClassSource(JavaClassSource source) { + String rendered = source.toUnformattedString(); + return formatSource(rendered); + } + + private void appendFields(JavaClassSource source, BuilderDefinitionDto builderDef) { + logger.debugStartOperation( + "Generating %d constructor fields and %d setter fields", + builderDef.getConstructorFieldsForBuilder().size(), + builderDef.getSetterFieldsForBuilder().size()); + + for (FieldDto fieldDto : builderDef.getConstructorFieldsForBuilder()) { + appendField(source, fieldDto); + } + for (FieldDto fieldDto : builderDef.getSetterFieldsForBuilder()) { + appendField(source, fieldDto); + } + + logger.debugEndOperation("Fields added: %d fields", builderDef.getAllFieldsForBuilder().size()); + } + + private void appendField(JavaClassSource source, FieldDto fieldDto) { + String boxedFieldType = mapBoxedType(fieldDto.getFieldType()); + FieldSource field = source.addField(); + field.setName(fieldDto.getFieldNameInBuilder()); + field.setType(TrackedValue.class.getSimpleName() + "<" + boxedFieldType + ">"); + field.setPrivate(); + field.setLiteralInitializer("unsetValue()"); + applyJavaDocToField(field.getJavaDoc(), fieldDto); + } + + private void applyJavaDocToField(JavaDocSource javaDoc, FieldDto fieldDto) { + javaDoc.setText( + "Tracked value for %s: %s." + .formatted( + fieldDto.getFieldNameInBuilder(), + StringUtils.defaultString(fieldDto.getJavaDoc()))); + } + + private void appendConstructors(JavaClassSource source, BuilderDefinitionDto builderDef) { + Modifier constructorAccessModifier = + JavaLangMapper.mapAccessModifier( + builderDef.getConfiguration().getBuilderConstructorAccess()); + TypeName dtoBaseClass = builderDef.getBuildingTargetTypeName(); + + appendEmptyConstructor(source, dtoBaseClass, constructorAccessModifier); + appendConstructorWithInstance( + source, dtoBaseClass, builderDef.getAllFieldsForBuilder(), constructorAccessModifier); + logger.debug("Constructors added"); + } + + private void appendEmptyConstructor( + JavaClassSource source, TypeName dtoClass, Modifier accessModifier) { + MethodSource constructor = source.addMethod(); + constructor.setConstructor(true); + applyVisibility(constructor, accessModifier); + constructor.setBody(""); + applyJavadoc( + constructor, + "Empty constructor of builder for {@code %s}.".formatted(dtoClass.getFullQualifiedName())); + } + + private void appendConstructorWithInstance( + JavaClassSource source, + TypeName dtoBaseClass, + List fields, + Modifier accessModifier) { + MethodSource constructor = source.addMethod(); + constructor.setConstructor(true); + applyVisibility(constructor, accessModifier); + constructor.addParameter(mapType(dtoBaseClass), "instance"); + constructor.setBody(buildConstructorBody(fields)); + applyJavadoc( + constructor, + """ + Initialisation of builder for {@code %s} by a instance. + + @param instance object instance for initialisiation + """ + .formatted(dtoBaseClass.getFullQualifiedName())); + } + + private String buildConstructorBody(List fields) { + StringBuilder body = new StringBuilder(); + for (FieldDto field : fields) { + field + .getGetterName() + .ifPresent(getter -> addFieldInitializationWithValidation(body, field, getter)); + } + return body.toString(); + } + + private void addFieldInitializationWithValidation( + StringBuilder body, FieldDto field, String getter) { + String fieldInBuilder = field.getFieldNameInBuilder(); + + String initialisationCode = + """ + this.%s = initialValue(instance.%s()); + """ + .formatted(fieldInBuilder, getter); + + String validationCode = ""; + if (field.isNonNullable()) { + validationCode = + """ + + if (this.%s.value() == null) { + throw new IllegalArgumentException("Cannot initialize builder from instance: field '%s' is marked as non-null but source object has null value"); + } + """ + .formatted(fieldInBuilder, fieldInBuilder); + } + + body.append(initialisationCode).append(validationCode); + } + + private void appendMethods(JavaClassSource source, BuilderDefinitionDto builderDef) { + Map allMethods = collectAllMethods(builderDef); + logger.debugStartOperation("Adding Methods for %d candidates", allMethods.size()); + + List resolvedMethods = resolveMethodConflicts(allMethods); + logger.debug("Resolved %d methods after conflict resolution", resolvedMethods.size()); + + int generatedCnt = 0; + for (MethodDto methodDto : resolvedMethods) { + appendMethod(source, methodDto, false, false); + generatedCnt++; + } + logger.debugEndOperation("%d Methods added", generatedCnt); + } + + private Map collectAllMethods(BuilderDefinitionDto builderDef) { + Map allMethods = new HashMap<>(); + + for (FieldDto fieldDto : builderDef.getConstructorFieldsForBuilder()) { + for (MethodDto method : fieldDto.getMethods()) { + allMethods.put(method, fieldDto); + } + } + for (FieldDto fieldDto : builderDef.getSetterFieldsForBuilder()) { + for (MethodDto method : fieldDto.getMethods()) { + allMethods.put(method, fieldDto); + } + } + for (MethodDto coreMethod : builderDef.getCoreMethods()) { + allMethods.put(coreMethod, null); + } + + return allMethods; + } + + private List resolveMethodConflicts(Map methodToField) { + MethodDto.MethodComparator comparator = new MethodDto.MethodComparator(); + + List> sortedEntries = + methodToField.entrySet().stream() + .sorted((e1, e2) -> comparator.compare(e1.getKey(), e2.getKey())) + .toList(); + + Map signatureToMethod = new java.util.LinkedHashMap<>(); + + for (Map.Entry entry : sortedEntries) { + MethodDto method = entry.getKey(); + FieldDto field = entry.getValue(); + String signature = method.getSignatureKey(); + + MethodDto existing = signatureToMethod.get(signature); + if (existing == null) { + signatureToMethod.put(signature, method); + } else { + String existingSource = + createSourceDescriptionForLogging(existing, methodToField.get(existing)); + String newSource = createSourceDescriptionForLogging(method, field); + + if (method.getPriority() > existing.getPriority()) { + signatureToMethod.put(signature, method); + logger.warning( + " Method conflict: '%s' from %s (priority %d) dropped in favor of %s (priority %d)", + signature, existingSource, existing.getPriority(), newSource, method.getPriority()); + } else if (method.getPriority() < existing.getPriority()) { + logger.warning( + " Method conflict: '%s' from %s (priority %d) dropped in favor of %s (priority %d)", + signature, newSource, method.getPriority(), existingSource, existing.getPriority()); + } else { + logger.warning( + " Method conflict: '%s' from %s (priority %d) dropped in favor of %s (priority %d) - equal priority, keeping first", + signature, newSource, method.getPriority(), existingSource, existing.getPriority()); + } + } + } + + return new java.util.ArrayList<>(signatureToMethod.values()); + } + + /** + * Creates a description string for method source identification. + * + * @param method method to describe + * @param field associated field (may be null) + * @return description string for logging/debugging + */ + public static String createSourceDescriptionForLogging(MethodDto method, FieldDto field) { + if (field == null) { + return "core method '" + method.getMethodName() + "'"; + } + return "field '" + field.getFieldNameInBuilder() + "'"; + } + + private void appendMethod( + JavaClassSource source, + MethodDto methodDto, + boolean nestedTypeMethod, + boolean interfaceMethod) { + MethodSource method = source.addMethod(); + configureMethod(method, methodDto, nestedTypeMethod, interfaceMethod); + String body = + methodDto.getMethodCodeDto() != null + && StringUtils.isNotBlank(methodDto.getMethodCodeDto().getCodeFormat()) + ? resolveCodeTemplate(methodDto.getMethodCodeDto()) + : ""; + method.setBody(body); + } + + private void appendNestedTypes(JavaClassSource source, BuilderDefinitionDto builderDef) { + if (CollectionUtils.isEmpty(builderDef.getNestedTypes())) { + return; + } + logger.debugStartOperation("Generating %d nested type(s)", builderDef.getNestedTypes().size()); + for (NestedTypeDto nestedType : builderDef.getNestedTypes()) { + appendNestedType(source, nestedType); + logger.debug("Generated nested type: %s", nestedType.getTypeName()); + } + logger.debugEndOperation("Nested types added"); + } + + private void appendNestedType(JavaClassSource source, NestedTypeDto nestedType) { + JavaSource nestedSource = + nestedType.getKind() == NestedTypeDto.NestedTypeKind.INTERFACE + ? source.addNestedType(JavaInterfaceSource.class) + : source.addNestedType(JavaClassSource.class); + nestedSource.setName(nestedType.getTypeName()); + if (nestedType.isPublic()) { + nestedSource.setPublic(); + } else { + nestedSource.setPackagePrivate(); + } + applyJavadoc(nestedSource, nestedType.getJavadoc()); + boolean isInterface = nestedType.getKind() == NestedTypeDto.NestedTypeKind.INTERFACE; + for (MethodDto methodDto : nestedType.getMethods()) { + appendNestedMethod(nestedSource, methodDto, isInterface); + } + } + + private void appendNestedMethod(JavaSource source, MethodDto methodDto, boolean isInterface) { + org.jboss.forge.roaster.model.source.MethodHolderSource methodHolder = + (org.jboss.forge.roaster.model.source.MethodHolderSource) source; + MethodSource method = methodHolder.addMethod(); + configureMethod(method, methodDto, true, isInterface); + if (methodDto.getMethodCodeDto() != null + && StringUtils.isNotBlank(methodDto.getMethodCodeDto().getCodeFormat())) { + method.setBody(resolveCodeTemplate(methodDto.getMethodCodeDto())); + } else { + method.setAbstract(true); + method.setBody(""); + } + } + + private void applyVisibility( + org.jboss.forge.roaster.model.source.VisibilityScopedSource source, Modifier modifier) { + if (modifier == null) { + source.setPackagePrivate(); + return; + } + switch (modifier) { + case PUBLIC -> source.setPublic(); + case PROTECTED -> source.setProtected(); + case PRIVATE -> source.setPrivate(); + default -> source.setPackagePrivate(); + } + } + + private void addGenericDeclarations( + JavaClassSource source, + List generics) { + if (CollectionUtils.isEmpty(generics)) { + return; + } + for (org.javahelpers.simple.builders.processor.model.type.GenericParameterDto generic : + generics) { + TypeVariableSource typeVariable = source.addTypeVariable(generic.getName()); + if (CollectionUtils.isNotEmpty(generic.getUpperBounds())) { + typeVariable.setBounds( + generic.getUpperBounds().stream().map(RoasterMapper::mapType).toArray(String[]::new)); + } + } + } + + private void addGenericDeclarations( + MethodSource source, + List generics) { + if (CollectionUtils.isEmpty(generics)) { + return; + } + for (org.javahelpers.simple.builders.processor.model.type.GenericParameterDto generic : + generics) { + TypeVariableSource typeVariable = source.addTypeVariable(generic.getName()); + if (CollectionUtils.isNotEmpty(generic.getUpperBounds())) { + typeVariable.setBounds( + generic.getUpperBounds().stream().map(RoasterMapper::mapType).toArray(String[]::new)); + } + } + } + + private void applyJavadoc( + org.jboss.forge.roaster.model.source.JavaDocCapableSource source, String javadoc) { + if (StringUtils.isBlank(javadoc)) { + return; + } + // Normalize line endings without regex to avoid ReDoS vulnerability + String normalized = javadoc.replace("\r\n", "\n").replace("\r", "\n"); + // Remove trailing newlines using StringUtils + normalized = StringUtils.stripEnd(normalized, "\n"); + String[] lines = normalized.split("\n", -1); + StringBuilder text = new StringBuilder(); + source.getJavaDoc().removeAllTags(); + boolean inTags = false; + for (String line : lines) { + if (!inTags && line.startsWith("@")) { + inTags = true; + } + if (inTags && line.startsWith("@")) { + processJavadocTag(source, line); + } else { + if (!text.isEmpty()) { + text.append('\n'); + } + text.append(line); + } + } + source.getJavaDoc().setText(text.toString()); + } + + private void processJavadocTag( + org.jboss.forge.roaster.model.source.JavaDocCapableSource source, String line) { + int firstSpace = line.indexOf(' '); + if (firstSpace > 1) { + source + .getJavaDoc() + .addTagValue(line.substring(0, firstSpace), line.substring(firstSpace + 1)); + } else if (line.length() > 1) { + source.getJavaDoc().addTagValue(line, ""); + } + } + + private void applyAnnotations( + org.jboss.forge.roaster.model.source.AnnotationTargetSource source, + java.util.Collection annotations) { + if (CollectionUtils.isEmpty(annotations)) { + return; + } + for (AnnotationDto annotationDto : annotations) { + AnnotationSource annotation = + source.addAnnotation(annotationDto.getAnnotationType().getFullQualifiedName()); + for (Map.Entry member : annotationDto.getMembers().entrySet()) { + if ("value".equals(member.getKey())) { + annotation.setLiteralValue(member.getValue()); + } else { + annotation.setLiteralValue(member.getKey(), member.getValue()); + } + } + } + } + + private void configureMethod( + MethodSource method, + MethodDto methodDto, + boolean nestedTypeMethod, + boolean interfaceMethod) { + method.setName(methodDto.getMethodName()); + if (methodDto.getReturnType() == null) { + method.setReturnTypeVoid(); + } else { + method.setReturnType(mapType(methodDto.getReturnType())); + } + + if (nestedTypeMethod) { + boolean hasBody = + interfaceMethod + && methodDto.getMethodCodeDto() != null + && StringUtils.isNotBlank(methodDto.getMethodCodeDto().getCodeFormat()); + if (hasBody) { + method.setDefault(true); + } else { + method.setPublic(); + } + } else { + applyVisibility(method, methodDto.getModifier().orElse(null)); + method.setStatic(methodDto.isStatic()); + } + + addGenericDeclarations(method, methodDto.getGenericParameters()); + applyJavadoc(method, methodDto.getJavadoc()); + applyAnnotations(method, methodDto.getAnnotations()); + + for (int i = 0; i < methodDto.getParameters().size(); i++) { + MethodParameterDto paramDto = methodDto.getParameters().get(i); + boolean lastParameter = i == methodDto.getParameters().size() - 1; + addParameter(method, paramDto, lastParameter); + } + } + + private void addParameter( + MethodSource method, MethodParameterDto paramDto, boolean lastParameter) { + String parameterType = + lastParameter && paramDto.getParameterType() instanceof TypeNameArray arrayType + ? mapType(arrayType.getTypeOfArray()) + : mapType(paramDto.getParameterType()); + ParameterSource parameter = method.addParameter(parameterType, paramDto.getParameterName()); + if (lastParameter && paramDto.getParameterType() instanceof TypeNameArray) { + parameter.setVarArgs(true); + } + applyAnnotations(parameter, paramDto.getAnnotations()); + } + + private void addTrackedValueStaticImports(JavaClassSource source) { + source.addImport(TrackedValue.class.getName() + ".changedValue").setStatic(true); + source.addImport(TrackedValue.class.getName() + ".initialValue").setStatic(true); + source.addImport(TrackedValue.class.getName() + ".unsetValue").setStatic(true); + } + + private Set collectImports(BuilderDefinitionDto builderDef) { + Set imports = new LinkedHashSet<>(); + String currentPackage = builderDef.getBuilderTypeName().getPackageName(); + + addImportIfNeeded(imports, currentPackage, TrackedValue.class.getName()); + addImportIfNeeded(imports, currentPackage, Consumer.class.getName()); + addImportIfNeeded(imports, currentPackage, Supplier.class.getName()); + addTypeImports(imports, currentPackage, builderDef.getBuildingTargetTypeName()); + addTypeImports(imports, currentPackage, builderDef.getBuilderTypeName()); + builderDef + .getGenerics() + .forEach( + generic -> + generic + .getUpperBounds() + .forEach(type -> addTypeImports(imports, currentPackage, type))); + builderDef + .getInterfaces() + .forEach(interfaceName -> addInterfaceImports(imports, currentPackage, interfaceName)); + builderDef + .getClassAnnotations() + .forEach(annotation -> addAnnotationImports(imports, currentPackage, annotation)); + builderDef + .getAllFieldsForBuilder() + .forEach(field -> addFieldImports(imports, currentPackage, field)); + builderDef + .getCoreMethods() + .forEach(method -> addMethodImports(imports, currentPackage, method)); + builderDef + .getNestedTypes() + .forEach( + nestedType -> + nestedType + .getMethods() + .forEach(method -> addMethodImports(imports, currentPackage, method))); + + return imports; + } + + private void addFieldImports(Set imports, String currentPackage, FieldDto field) { + addTypeImports(imports, currentPackage, field.getFieldType()); + field + .getParameterAnnotations() + .forEach(annotation -> addAnnotationImports(imports, currentPackage, annotation)); + field.getMethods().forEach(method -> addMethodImports(imports, currentPackage, method)); + } + + private void addMethodImports(Set imports, String currentPackage, MethodDto method) { + if (method.getReturnType() != null) { + addTypeImports(imports, currentPackage, method.getReturnType()); + } + method + .getGenericParameters() + .forEach( + generic -> + generic + .getUpperBounds() + .forEach(type -> addTypeImports(imports, currentPackage, type))); + method + .getAnnotations() + .forEach(annotation -> addAnnotationImports(imports, currentPackage, annotation)); + method + .getParameters() + .forEach(parameter -> addParameterImports(imports, currentPackage, parameter)); + addBodyImports(imports, currentPackage, method); + } + + private void addParameterImports( + Set imports, String currentPackage, MethodParameterDto parameter) { + addTypeImports(imports, currentPackage, parameter.getParameterType()); + parameter + .getAnnotations() + .forEach(annotation -> addAnnotationImports(imports, currentPackage, annotation)); + } + + private void addInterfaceImports( + Set imports, + String currentPackage, + org.javahelpers.simple.builders.processor.model.annotation.InterfaceName interfaceName) { + if (StringUtils.isNotBlank(interfaceName.getPackageName())) { + addImportIfNeeded( + imports, + currentPackage, + interfaceName.getPackageName() + "." + interfaceName.getSimpleName()); + } + interfaceName + .getAnnotations() + .forEach(annotation -> addAnnotationImports(imports, currentPackage, annotation)); + interfaceName + .getTypeParameters() + .forEach(type -> addTypeImports(imports, currentPackage, type)); + } + + private void addAnnotationImports( + Set imports, String currentPackage, AnnotationDto annotation) { + if (annotation.getAnnotationType() != null) { + addTypeImports(imports, currentPackage, annotation.getAnnotationType()); + } + } + + private void addTypeImports(Set imports, String currentPackage, TypeName type) { + if (type == null || type instanceof TypeNamePrimitive || type instanceof TypeNameVariable) { + return; + } + + type.getAnnotations() + .forEach(annotation -> addAnnotationImports(imports, currentPackage, annotation)); + + if (type instanceof TypeNameArray arrayType) { + addTypeImports(imports, currentPackage, arrayType.getTypeOfArray()); + return; + } + + if (type instanceof TypeNameGeneric genericType) { + addImportIfNeeded( + imports, currentPackage, genericType.getFullQualifiedName().replaceAll("<.*$", "")); + genericType + .getInnerTypeArguments() + .forEach(inner -> addTypeImports(imports, currentPackage, inner)); + return; + } + + addImportIfNeeded(imports, currentPackage, type.getFullQualifiedName()); + } + + private void addImportIfNeeded(Set imports, String currentPackage, String fqn) { + if (StringUtils.isBlank(fqn) + || !fqn.contains(".") + || fqn.startsWith("java.lang.") + || java.util.Objects.equals(packageNameOf(fqn), currentPackage)) { + return; + } + imports.add(fqn); + } + + private void addBodyImports(Set imports, String currentPackage, MethodDto method) { + if (method.getMethodCodeDto() == null + || StringUtils.isBlank(method.getMethodCodeDto().getCodeFormat())) { + return; + } + for (MethodCodePlaceholder argument : method.getMethodCodeDto().getCodeArguments()) { + if (argument instanceof MethodCodeTypePlaceholder typePlaceholder) { + addTypeImports(imports, currentPackage, typePlaceholder.getValue()); + } + } + String code = method.getMethodCodeDto().getCodeFormat(); + if (code.contains("List.of(")) { + imports.add(List.class.getName()); + } + if (code.contains("Optional.of(") + || code.contains("Optional.empty(") + || code.contains("Optional.ofNullable(")) { + imports.add(java.util.Optional.class.getName()); + } + } + + private String formatSource(String rawSource) { + if (formatterProperties.isEmpty()) { + return rawSource; + } + try { + return Roaster.format(formatterProperties, rawSource); + } catch (Exception ex) { + logger.warning( + "simple-builders: Failed to format generated source with bundled Eclipse formatter profile: %s", + StringUtils.defaultIfBlank(ex.getMessage(), ex.getClass().getSimpleName())); + return rawSource; + } + } + + private Properties loadFormatterProperties() { + try (InputStream inputStream = + RoasterCodeGenerator.class + .getClassLoader() + .getResourceAsStream(FORMATTER_PROFILE_RESOURCE)) { + if (inputStream == null) { + logger.warning( + "simple-builders: Bundled Eclipse formatter profile '%s' was not found on the processor classpath.", + FORMATTER_PROFILE_RESOURCE); + return new Properties(); + } + FormatterProfileReader profileReader = FormatterProfileReader.fromEclipseXml(inputStream); + return profileReader.getDefaultProperties(); + } catch (IOException ex) { + logger.warning( + "simple-builders: Failed to load bundled Eclipse formatter profile '%s': %s", + FORMATTER_PROFILE_RESOURCE, + StringUtils.defaultIfBlank(ex.getMessage(), ex.getClass().getSimpleName())); + return new Properties(); + } + } + + private void writeBuilderClassToFile(String sourceCode, BuilderDefinitionDto builderDef) + throws BuilderException { + logger.debug( + "Writing builder class to file: %s.%s", + builderDef.getBuilderTypeName().getPackageName(), + builderDef.getBuilderTypeName().getClassName()); + + String qualifiedName = builderDef.getBuilderTypeName().getFullQualifiedName(); + if (builderClassAlreadyExists(qualifiedName)) { + throw new BuilderException( + null, + """ + Builder class '%s' already exists. This may be a manually written builder or a previously generated builder. + To resolve this: + 1. If you have a manual builder, consider renaming it or removing @SimpleBuilder from the DTO + 2. If this is from a previous compilation, clean and rebuild the project + 3. Check that you're not trying to generate multiple builders for the same DTO + """ + .formatted(qualifiedName)); + } + + try { + JavaFileObject file = processingEnv.getFiler().createSourceFile(qualifiedName); + try (Writer writer = file.openWriter()) { + writer.write(sourceCode); + } + } catch (IOException ex) { + String message = ex.getMessage(); + String errorMessage = + """ + Unable to create builder class '%s': %s. + Check the build environment and ensure all necessary directories are accessible. + """ + .formatted( + qualifiedName, StringUtils.isNotBlank(message) ? message : "Unknown error"); + throw new BuilderException(null, errorMessage); + } + } + + /** + * Checks if a builder class already exists by attempting to find the type element. + * + * @param qualifiedName the fully qualified name of the class to check + * @return true if the class already exists, false otherwise + */ + private boolean builderClassAlreadyExists(String qualifiedName) { + try { + TypeElement existingType = processingEnv.getElementUtils().getTypeElement(qualifiedName); + return existingType != null; + } catch (Exception e) { + logger.debug( + "Error checking if builder class '%s' already exists: %s", + qualifiedName, StringUtils.isNotBlank(e.getMessage()) ? e.getMessage() : "No message"); + return false; + } + } + + /** + * Generates a Jackson SimpleModule based on the provided definition. + * + * @param moduleDef the definition of the Jackson module to generate + */ + public void generateJacksonModule(JacksonModuleDefinitionDto moduleDef) { + String packageName = moduleDef.getTargetPackage(); + String moduleClassName = "SimpleBuildersJacksonModule"; + + logger.info("Generating Jackson Module '%s' in package '%s'", moduleClassName, packageName); + + try { + // Create the class using Roaster + JavaClassSource moduleClass = Roaster.create(JavaClassSource.class); + moduleClass.setPackage(packageName); + moduleClass.setName(moduleClassName); + moduleClass.setSuperType("SimpleModule"); + + // Collect imports in a list, sort them, then add to Roaster + Set imports = new LinkedHashSet<>(); + imports.add("com.fasterxml.jackson.databind.annotation.JsonDeserialize"); + imports.add("com.fasterxml.jackson.databind.module.SimpleModule"); + + // Adding imports for DTO types + moduleDef.getEntries().stream() + .filter(e -> shouldAddImport(packageName, e.dtoType().getFullQualifiedName())) + .forEach(e -> imports.add(e.dtoType().getFullQualifiedName())); + + // Adding imports for builder types + moduleDef.getEntries().stream() + .filter(e -> shouldAddImport(packageName, e.builderType().getFullQualifiedName())) + .forEach(e -> imports.add(e.builderType().getFullQualifiedName())); + + // Sort imports and add to Roaster + imports.stream().sorted().forEach(moduleClass::addImport); + + // Add mixin interfaces as nested interfaces + for (JacksonModuleEntryDto entry : moduleDef.getEntries()) { + String mixinName = entry.dtoType().getClassName() + "Mixin"; + + // Create the nested interface + JavaInterfaceSource mixinInterface = Roaster.create(JavaInterfaceSource.class); + mixinInterface.setName(mixinName); + mixinInterface.setPrivate(); + + // Add the JsonDeserialize annotation + AnnotationSource annotation = + mixinInterface.addAnnotation("JsonDeserialize"); + annotation.setLiteralValue("builder", entry.builderType().getClassName() + ".class"); + + // Add as nested type to the module class + moduleClass.addNestedType(mixinInterface); + } + + // Add constructor + MethodSource constructor = moduleClass.addMethod(); + constructor.setConstructor(true); + constructor.setPublic(); + + // Build constructor body + StringBuilder constructorBody = new StringBuilder(); + for (JacksonModuleEntryDto entry : moduleDef.getEntries()) { + String mixinName = entry.dtoType().getClassName() + "Mixin"; + constructorBody.append( + "setMixInAnnotation(%s.class, %s.class);%n" + .formatted(entry.dtoType().getClassName(), mixinName)); + } + constructor.setBody(constructorBody.toString()); + + // Write the generated class + String source = formatSource(moduleClass.toString()); + writeSimpleClassToFile(packageName, moduleClassName, source); + + } catch (BuilderException e) { + logger.warning( + "simple-builders: Error generating Jackson module for package %s: %s\n%s", + packageName, e.getMessage(), java.util.Arrays.toString(e.getStackTrace())); + } + } + + private boolean shouldAddImport(String currentPackage, String fqn) { + return StringUtils.isNotBlank(fqn) + && fqn.contains(".") + && !fqn.startsWith("java.lang.") + && !currentPackage.equals(packageNameOf(fqn)); + } + + private void writeSimpleClassToFile(String packageName, String className, String sourceCode) + throws BuilderException { + try { + String qualifiedName = + StringUtils.isBlank(packageName) ? className : packageName + "." + className; + JavaFileObject file = processingEnv.getFiler().createSourceFile(qualifiedName); + try (Writer writer = file.openWriter()) { + writer.write(sourceCode); + } + } catch (IOException ex) { + String message = ex.getMessage(); + String errorMessage = + """ + Unable to create class: %s. + Check the build environment and ensure all necessary directories are accessible. + """ + .formatted(StringUtils.isNotBlank(message) ? message : "Unknown error"); + throw new BuilderException(null, errorMessage); + } + } +} diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterMapper.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterMapper.java new file mode 100644 index 00000000..8769949a --- /dev/null +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/RoasterMapper.java @@ -0,0 +1,233 @@ +/* + * MIT License + * + * Copyright (c) 2026 Andreas Igel + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.javahelpers.simple.builders.processor.classgen.roaster; + +import java.util.List; +import java.util.Map; +import org.apache.commons.collections4.CollectionUtils; +import org.javahelpers.simple.builders.processor.classgen.roaster.exceptions.RoasterMapperException; +import org.javahelpers.simple.builders.processor.model.annotation.AnnotationDto; +import org.javahelpers.simple.builders.processor.model.annotation.InterfaceName; +import org.javahelpers.simple.builders.processor.model.method.MethodCodeDto; +import org.javahelpers.simple.builders.processor.model.method.MethodCodePlaceholder; +import org.javahelpers.simple.builders.processor.model.method.MethodCodeStringPlaceholder; +import org.javahelpers.simple.builders.processor.model.method.MethodCodeTypePlaceholder; +import org.javahelpers.simple.builders.processor.model.type.TypeName; +import org.javahelpers.simple.builders.processor.model.type.TypeNameArray; +import org.javahelpers.simple.builders.processor.model.type.TypeNameGeneric; +import org.javahelpers.simple.builders.processor.model.type.TypeNamePrimitive; +import org.javahelpers.simple.builders.processor.model.type.TypeNameVariable; + +/** Helper functions to create Roaster-compatible source code strings from DTOs. */ +public final class RoasterMapper { + + private RoasterMapper() {} + + /** + * Maps a type model to Java source code using fully qualified names for robustness. + * + * @param typeName type to map + * @return Java source representation of the type + */ + public static String mapType(TypeName typeName) { + String mappedType; + if (typeName instanceof TypeNameVariable) { + mappedType = typeName.getClassName(); + } else if (typeName instanceof TypeNamePrimitive primitive) { + mappedType = primitive.getFullQualifiedName(); + } else if (typeName instanceof TypeNameArray arrayType) { + mappedType = mapType(arrayType.getTypeOfArray()) + "[]"; + } else if (typeName instanceof TypeNameGeneric genericType) { + mappedType = mapGenericType(genericType); + } else { + mappedType = typeName.getClassName(); + } + + return prependTypeUseAnnotations(mappedType, typeName.getAnnotations()); + } + + /** + * Maps a type to a boxed Java source representation. + * + * @param typeName type to map + * @return boxed Java source representation of the type + */ + public static String mapBoxedType(TypeName typeName) { + if (typeName instanceof TypeNamePrimitive primitive) { + String boxedType = + switch (primitive.getType()) { + case BOOLEAN -> Boolean.class.getSimpleName(); + case BYTE -> Byte.class.getSimpleName(); + case CHAR -> Character.class.getSimpleName(); + case DOUBLE -> Double.class.getSimpleName(); + case FLOAT -> Float.class.getSimpleName(); + case INT -> Integer.class.getSimpleName(); + case LONG -> Long.class.getSimpleName(); + case SHORT -> Short.class.getSimpleName(); + case VOID -> Void.class.getSimpleName(); + }; + return prependTypeUseAnnotations(boxedType, typeName.getAnnotations()); + } + return mapType(typeName); + } + + private static String mapGenericType(TypeNameGeneric genericType) { + if (genericType.getInnerTypeArguments().isEmpty()) { + return genericType.getClassName(); + } + String innerTypes = + genericType.getInnerTypeArguments().stream() + .map(RoasterMapper::mapBoxedType) + .reduce((a, b) -> a + ", " + b) + .orElse(""); + return genericType.getClassName() + "<" + innerTypes + ">"; + } + + /** + * Maps an annotation to source code. + * + * @param annotationDto annotation DTO + * @return Java source representation of the annotation + */ + public static String mapAnnotation(AnnotationDto annotationDto) { + try { + String annotationType = mapType(annotationDto.getAnnotationType()); + if (annotationDto.getMembers().isEmpty()) { + return "@" + annotationType; + } + String members = + annotationDto.getMembers().entrySet().stream() + .map(RoasterMapper::mapAnnotationMember) + .reduce((a, b) -> a + ", " + b) + .orElse(""); + return "@" + annotationType + "(" + members + ")"; + } catch (Exception e) { + throw new RoasterMapperException( + e, + "Failed to map annotation %s: %s", + annotationDto.getAnnotationType().getClassName(), + e.getMessage()); + } + } + + private static String mapAnnotationMember(Map.Entry member) { + if ("value".equals(member.getKey())) { + return member.getValue(); + } + return member.getKey() + " = " + member.getValue(); + } + + /** + * Maps an interface name to Java source code. + * + * @param interfaceName interface model + * @return source representation of the interface type + */ + public static String mapInterfaceToTypeName(InterfaceName interfaceName) { + try { + String qualifiedName = interfaceName.getSimpleName(); + if (interfaceName.hasTypeParameters()) { + String typeParameters = + interfaceName.getTypeParameters().stream() + .map(RoasterMapper::mapType) + .reduce((a, b) -> a + ", " + b) + .orElse(""); + qualifiedName = qualifiedName + "<" + typeParameters + ">"; + } + return prependTypeUseAnnotations(qualifiedName, interfaceName.getAnnotations()); + } catch (Exception e) { + throw new RoasterMapperException( + e, "Failed to map interface %s: %s", interfaceName.toString(), e.getMessage()); + } + } + + /** + * Resolves the JavaPoet-style named template used in MethodCodeDto to plain Java source code. + * + * @param codeDto code template DTO + * @return resolved Java source code + */ + public static String resolveCodeTemplate(MethodCodeDto codeDto) { + String code = codeDto.getCodeFormat(); + for (MethodCodePlaceholder placeHolderValue : codeDto.getCodeArguments()) { + String label = placeHolderValue.getLabel(); + if (placeHolderValue instanceof MethodCodeStringPlaceholder stringPlaceholder) { + code = code.replace("$" + label + ":N", stringPlaceholder.getValue()); + code = code.replace("$" + label + ":L", stringPlaceholder.getValue()); + code = code.replace("$" + label + ":S", quote(stringPlaceholder.getValue())); + } else if (placeHolderValue instanceof MethodCodeTypePlaceholder typePlaceholder) { + code = code.replace("$" + label + ":T", mapType(typePlaceholder.getValue())); + } else { + throw new RoasterMapperException( + "Unsupported placeholder type: %s", placeHolderValue.getClass().getName()); + } + } + code = code.replace("TrackedValue.initialValue", "initialValue"); + code = code.replace("TrackedValue.changedValue", "changedValue"); + code = code.replace("TrackedValue.unsetValue", "unsetValue"); + return code; + } + + /** + * Converts plain text to a Java string literal. + * + * @param value text value + * @return quoted and escaped Java string literal + */ + public static String quote(String value) { + String escaped = + value + .replace("\\", "\\\\") + .replace("\"", "\\\"") + .replace("\n", "\\n") + .replace("\r", "\\r") + .replace("\t", "\\t"); + return "\"" + escaped + "\""; + } + + /** + * Extracts package name from fully qualified name. + * + * @param fqn fully qualified name + * @return package name or empty string if no package + */ + public static String packageNameOf(String fqn) { + int idx = fqn.lastIndexOf('.'); + return idx < 0 ? "" : fqn.substring(0, idx); + } + + private static String prependTypeUseAnnotations( + String baseType, List annotations) { + if (CollectionUtils.isEmpty(annotations)) { + return baseType; + } + String prefix = + annotations.stream() + .map(RoasterMapper::mapAnnotation) + .reduce((a, b) -> a + " " + b) + .orElse(""); + return prefix + " " + baseType; + } +} diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/javapoet/exceptions/JavapoetMapperException.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/exceptions/RoasterMapperException.java similarity index 77% rename from processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/javapoet/exceptions/JavapoetMapperException.java rename to processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/exceptions/RoasterMapperException.java index 75cef175..0d039883 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/javapoet/exceptions/JavapoetMapperException.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/exceptions/RoasterMapperException.java @@ -22,17 +22,17 @@ * SOFTWARE. */ -package org.javahelpers.simple.builders.processor.classgen.javapoet.exceptions; +package org.javahelpers.simple.builders.processor.classgen.roaster.exceptions; -/** Special exception for errors in mapping to Javapoet classes. */ -public class JavapoetMapperException extends RuntimeException { +/** Special exception for errors in mapping to Roaster classes. */ +public class RoasterMapperException extends RuntimeException { /** * Creating an exception with message and parameters. * * @param cause root cause of current exception, containing stacktrace */ - public JavapoetMapperException(Throwable cause) { + public RoasterMapperException(Throwable cause) { super(cause.getMessage(), cause); } @@ -40,9 +40,9 @@ public JavapoetMapperException(Throwable cause) { * Creating an exception with message and parameters. * * @param message A specific message, supports String.format arguments - * @param args Arguments for Stringlformat on message + * @param args Arguments for String.format on message */ - public JavapoetMapperException(String message, Object... args) { + public RoasterMapperException(String message, Object... args) { super(String.format(message, args)); } @@ -51,9 +51,9 @@ public JavapoetMapperException(String message, Object... args) { * * @param cause root cause of current exception, containing stacktrace * @param message A specific message, supports String.format arguments - * @param args Arguments for Stringlformat on message + * @param args Arguments for String.format on message */ - public JavapoetMapperException(Throwable cause, String message, Object... args) { + public RoasterMapperException(Throwable cause, String message, Object... args) { super(String.format(message, args), cause); } } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/exceptions/package-info.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/exceptions/package-info.java new file mode 100644 index 00000000..9c16ae95 --- /dev/null +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/exceptions/package-info.java @@ -0,0 +1,9 @@ +/** + * Package org.javahelpers.simple.builders.processor.classgen.roaster.exceptions + * + *

Roaster-specific exception classes. + * + *

This package contains exceptions specific to Roaster code generation, providing error handling + * for mapping and code generation issues within the Roaster layer. + */ +package org.javahelpers.simple.builders.processor.classgen.roaster.exceptions; diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/package-info.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/package-info.java new file mode 100644 index 00000000..31bbf744 --- /dev/null +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/classgen/roaster/package-info.java @@ -0,0 +1,25 @@ +/** + * Package org.javahelpers.simple.builders.processor.classgen.roaster + * + *

Roaster-based code generation for simple-builders processor. + * + *

This package contains the core code generation components: + * + *

+ * + *

Exception classes are organized in separate packages: + * + *

+ */ +package org.javahelpers.simple.builders.processor.classgen.roaster; diff --git a/processor/src/main/resources/eclipse-java-format.xml b/processor/src/main/resources/eclipse-java-format.xml new file mode 100644 index 00000000..c51be09c --- /dev/null +++ b/processor/src/main/resources/eclipse-java-format.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderConfigurationReaderTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderConfigurationReaderTest.java index f38290aa..9deb48e6 100644 --- a/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderConfigurationReaderTest.java +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/BuilderConfigurationReaderTest.java @@ -463,6 +463,8 @@ public class PersonDto { import static org.javahelpers.simple.builders.core.util.TrackedValue.unsetValue; import java.util.List; + import java.util.function.Consumer; + import java.util.function.Supplier; import org.apache.commons.lang3.builder.ToStringBuilder; import org.javahelpers.simple.builders.core.interfaces.IBuilderBase; import org.javahelpers.simple.builders.core.util.BuilderToStringStyle; @@ -471,17 +473,16 @@ public class PersonDto { /** * Builder for {@code test.PersonDto}. *

- * This builder provides a fluent API for creating instances of test.PersonDto with - * method chaining and validation. Use the static {@code create()} method - * to obtain a new builder instance, configure the desired properties using - * the setter methods, and then call {@code build()} to create the final DTO. + * This builder provides a fluent API for creating instances of test.PersonDto with method chaining and validation. Use + * the static {@code create()} method to obtain a new builder instance, configure the desired properties using the + * setter methods, and then call {@code build()} to create the final DTO. */ public class PersonDtoMinimalBuilder implements IBuilderBase { + /** * Tracked value for name: name. */ private TrackedValue name = unsetValue(); - /** * Tracked value for tags: tags. */ @@ -552,8 +553,7 @@ public PersonDto build() { */ @Override public String toString() { - return new ToStringBuilder(this, BuilderToStringStyle.INSTANCE) - .append("name", this.name) + return new ToStringBuilder(this, BuilderToStringStyle.INSTANCE).append("name", this.name) .append("tags", this.tags) .toString(); } diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/ComprehensiveFeatureIntegrationTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/ComprehensiveFeatureIntegrationTest.java index d8c0f47a..86a4fe7b 100644 --- a/processor/src/test/java/org/javahelpers/simple/builders/processor/ComprehensiveFeatureIntegrationTest.java +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/ComprehensiveFeatureIntegrationTest.java @@ -173,15 +173,12 @@ public PersonDto(String name, int age, Optional email, /** * Builder for {@code test.PersonDto}. *

- * This builder provides a fluent API for creating instances of test.PersonDto with - * method chaining and validation. Use the static {@code create()} method - * to obtain a new builder instance, configure the desired properties using - * the setter methods, and then call {@code build()} to create the final DTO. + * This builder provides a fluent API for creating instances of test.PersonDto with method chaining and validation. Use + * the static {@code create()} method to obtain a new builder instance, configure the desired properties using the + * setter methods, and then call {@code build()} to create the final DTO. */ @Generated("Generated by org.javahelpers.simple.builders.processor.BuilderProcessor") - @BuilderImplementation( - forClass = PersonDto.class - ) + @BuilderImplementation(forClass = PersonDto.class) public class PersonDtoBuilder implements IBuilderBase { /** * Tracked value for name: name. @@ -243,7 +240,8 @@ public PersonDtoBuilder(PersonDto instance) { this.name = initialValue(instance.getName()); this.age = initialValue(instance.getAge()); if (this.age.value() == null) { - throw new IllegalArgumentException("Cannot initialize builder from instance: field 'age' is marked as non-null but source object has null value"); + throw new IllegalArgumentException( + "Cannot initialize builder from instance: field 'age' is marked as non-null but source object has null value"); } this.email = initialValue(instance.getEmail()); this.nicknames = initialValue(instance.getNicknames()); @@ -353,7 +351,9 @@ public PersonDtoBuilder address(AddressDto address) { * @return current instance of builder */ public PersonDtoBuilder address(Consumer addressBuilderConsumer) { - AddressDtoBuilder builder = this.address.isSet() ? new AddressDtoBuilder(this.address.value()) : new AddressDtoBuilder(); + AddressDtoBuilder builder = this.address.isSet() + ? new AddressDtoBuilder(this.address.value()) + : new AddressDtoBuilder(); addressBuilderConsumer.accept(builder); this.address = changedValue(builder.build()); return this; @@ -439,8 +439,8 @@ public PersonDtoBuilder email(Supplier> emailSupplier) { } /** - * Sets the String value for email by using String.format(format, args). - * See {@link String#format(String, Object...)} for details. + * Sets the String value for email by using String.format(format, args). See + * {@link String#format(String, Object...)} for details. * * @param format A format string * @param args Arguments referenced by the format specifiers in the format string. @@ -479,9 +479,10 @@ public PersonDtoBuilder metadata(Map metadata) { * @param metadataBuilderConsumer consumer providing an instance of a builder for metadata * @return current instance of builder */ - public PersonDtoBuilder metadata( - Consumer> metadataBuilderConsumer) { - HashMapBuilder builder = this.metadata.isSet() ? new HashMapBuilder(this.metadata.value()) : new HashMapBuilder(); + public PersonDtoBuilder metadata(Consumer> metadataBuilderConsumer) { + HashMapBuilder builder = this.metadata.isSet() + ? new HashMapBuilder(this.metadata.value()) + : new HashMapBuilder(); metadataBuilderConsumer.accept(builder); this.metadata = changedValue(builder.build()); return this; @@ -534,8 +535,8 @@ public PersonDtoBuilder name(Supplier nameSupplier) { } /** - * Sets the String value for name by using String.format(format, args). - * See {@link String#format(String, Object...)} for details. + * Sets the String value for name by using String.format(format, args). See + * {@link String#format(String, Object...)} for details. * * @param format A format string * @param args Arguments referenced by the format specifiers in the format string. @@ -575,7 +576,9 @@ public PersonDtoBuilder nicknames(List nicknames) { * @return current instance of builder */ public PersonDtoBuilder nicknames(Consumer> nicknamesBuilderConsumer) { - ArrayListBuilder builder = this.nicknames.isSet() ? new ArrayListBuilder(this.nicknames.value()) : new ArrayListBuilder(); + ArrayListBuilder builder = this.nicknames.isSet() + ? new ArrayListBuilder(this.nicknames.value()) + : new ArrayListBuilder(); nicknamesBuilderConsumer.accept(builder); this.nicknames = changedValue(builder.build()); return this; @@ -622,7 +625,9 @@ public PersonDtoBuilder phoneNumbers(LinkedList phoneNumbers) { */ public PersonDtoBuilder phoneNumbers( Consumer> phoneNumbersBuilderConsumer) { - ArrayListBuilder builder = this.phoneNumbers.isSet() ? new ArrayListBuilder(this.phoneNumbers.value()) : new ArrayListBuilder(); + ArrayListBuilder builder = this.phoneNumbers.isSet() + ? new ArrayListBuilder(this.phoneNumbers.value()) + : new ArrayListBuilder(); phoneNumbersBuilderConsumer.accept(builder); this.phoneNumbers = changedValue(new LinkedList<>(builder.build())); return this; @@ -669,7 +674,9 @@ public PersonDtoBuilder previousAddresses(List previousAddresses) { */ public PersonDtoBuilder previousAddresses( Consumer> previousAddressesBuilderConsumer) { - ArrayListBuilderWithElementBuilders builder = this.previousAddresses.isSet() ? new ArrayListBuilderWithElementBuilders(this.previousAddresses.value(), AddressDtoBuilder::create) : new ArrayListBuilderWithElementBuilders(AddressDtoBuilder::create); + ArrayListBuilderWithElementBuilders builder = this.previousAddresses.isSet() + ? new ArrayListBuilderWithElementBuilders(this.previousAddresses.value(), AddressDtoBuilder::create) + : new ArrayListBuilderWithElementBuilders(AddressDtoBuilder::create); previousAddressesBuilderConsumer.accept(builder); this.previousAddresses = changedValue(builder.build()); return this; @@ -715,7 +722,9 @@ public PersonDtoBuilder tags(Set tags) { * @return current instance of builder */ public PersonDtoBuilder tags(Consumer> tagsBuilderConsumer) { - HashSetBuilder builder = this.tags.isSet() ? new HashSetBuilder(this.tags.value()) : new HashSetBuilder(); + HashSetBuilder builder = this.tags.isSet() + ? new HashSetBuilder(this.tags.value()) + : new HashSetBuilder(); tagsBuilderConsumer.accept(builder); this.tags = changedValue(builder.build()); return this; @@ -739,8 +748,7 @@ public PersonDtoBuilder tags(Supplier> tagsSupplier) { * @param yesCondition the consumer to apply if condition is true * @return this builder instance */ - public PersonDtoBuilder conditional(BooleanSupplier condition, - Consumer yesCondition) { + public PersonDtoBuilder conditional(BooleanSupplier condition, Consumer yesCondition) { return conditional(condition, yesCondition, null); } @@ -752,8 +760,7 @@ public PersonDtoBuilder conditional(BooleanSupplier condition, * @param falseCase the consumer to apply if condition is false (can be null) * @return this builder instance */ - public PersonDtoBuilder conditional(BooleanSupplier condition, - Consumer trueCase, Consumer falseCase) { + public PersonDtoBuilder conditional(BooleanSupplier condition, Consumer trueCase, Consumer falseCase) { if (condition.getAsBoolean()) { trueCase.accept(this); } else if (falseCase != null) { @@ -773,7 +780,15 @@ public PersonDto build() { if (this.age.value() == null) { throw new IllegalStateException("Field 'age' is marked as non-null but null value was provided"); } - PersonDto result = new PersonDto(this.name.value(), this.age.value(), this.email.value(), this.nicknames.value(), this.tags.value(), this.metadata.value(), this.address.value(), this.previousAddresses.value(), this.phoneNumbers.value()); + PersonDto result = new PersonDto(this.name.value(), + this.age.value(), + this.email.value(), + this.nicknames.value(), + this.tags.value(), + this.metadata.value(), + this.address.value(), + this.previousAddresses.value(), + this.phoneNumbers.value()); return result; } @@ -784,8 +799,7 @@ public PersonDto build() { */ @Override public String toString() { - return new ToStringBuilder(this, BuilderToStringStyle.INSTANCE) - .append("name", this.name) + return new ToStringBuilder(this, BuilderToStringStyle.INSTANCE).append("name", this.name) .append("age", this.age) .append("email", this.email) .append("nicknames", this.nicknames) @@ -802,7 +816,8 @@ public String toString() { */ public interface With { /** - * Initializes a builder from an instance of this class, using methods of this builder to change values and returns the new built object. + * Initializes a builder from an instance of this class, using methods of this builder to change values and returns + * the new built object. * * @param b the consumer to apply modifications * @return the modified instance @@ -812,7 +827,9 @@ default PersonDto with(Consumer b) { try { builder = new PersonDtoBuilder(PersonDto.class.cast(this)); } catch (ClassCastException ex) { - throw new IllegalArgumentException("The interface 'PersonDtoBuilder.With' should only be implemented by classes, which could be casted to 'PersonDto'", ex); + throw new IllegalArgumentException( + "The interface 'PersonDtoBuilder.With' should only be implemented by classes, which could be casted to 'PersonDto'", + ex); } b.accept(builder); return builder.build(); @@ -827,7 +844,9 @@ default PersonDtoBuilder with() { try { return new PersonDtoBuilder(PersonDto.class.cast(this)); } catch (ClassCastException ex) { - throw new IllegalArgumentException("The interface 'PersonDtoBuilder.With' should only be implemented by classes, which could be casted to 'PersonDto'", ex); + throw new IllegalArgumentException( + "The interface 'PersonDtoBuilder.With' should only be implemented by classes, which could be casted to 'PersonDto'", + ex); } } } diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/RoasterCodeGeneratorEdgeCasesTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/RoasterCodeGeneratorEdgeCasesTest.java new file mode 100644 index 00000000..805028a5 --- /dev/null +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/RoasterCodeGeneratorEdgeCasesTest.java @@ -0,0 +1,282 @@ +/* + * MIT License + * + * Copyright (c) 2026 Andreas Igel + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.javahelpers.simple.builders.processor; + +import static com.google.testing.compile.CompilationSubject.assertThat; +import static org.javahelpers.simple.builders.processor.testing.ProcessorAsserts.assertContaining; +import static org.javahelpers.simple.builders.processor.testing.ProcessorAsserts.assertGenerationSucceeded; +import static org.javahelpers.simple.builders.processor.testing.ProcessorAsserts.assertNotContaining; +import static org.javahelpers.simple.builders.processor.testing.ProcessorTestUtils.loadGeneratedSource; + +import com.google.testing.compile.Compilation; +import com.google.testing.compile.JavaFileObjects; +import javax.tools.JavaFileObject; +import org.javahelpers.simple.builders.processor.testing.ProcessorTestUtils; +import org.junit.jupiter.api.Test; + +/** + * Integration tests for RoasterCodeGenerator edge cases to improve code coverage. + * + *

These tests exercise specific code paths in RoasterCodeGenerator that are typically not + * covered by standard integration tests, such as empty collections, null checks, and edge cases. + */ +class RoasterCodeGeneratorEdgeCasesTest { + + /** + * Tests that builders can be generated for DTOs in the default package (no package declaration). + * + *

This edge case ensures the code generator handles the absence of a package name correctly + * and generates valid builder code without package declarations. + */ + @Test + void shouldHandleBuilderInDefaultPackage() { + JavaFileObject sourceFile = + JavaFileObjects.forSourceString( + "SimpleDto", + """ + import org.javahelpers.simple.builders.core.annotations.SimpleBuilder; + + @SimpleBuilder + public class SimpleDto { + private String name; + public String getName() { return name; } + public void setName(String name) { this.name = name; } + } + """); + + Compilation compilation = ProcessorTestUtils.createCompiler().compile(sourceFile); + + String generatedCode = loadGeneratedSource(compilation, "SimpleDtoBuilder"); + assertGenerationSucceeded(compilation, "SimpleDtoBuilder", generatedCode); + assertContaining(generatedCode, "public class SimpleDtoBuilder"); + assertNotContaining("package"); + } + + /** + * Tests that builders are generated correctly when class-level annotations are disabled. + * + *

This edge case verifies that the code generator correctly handles the empty annotation list + * when {@code usingGeneratedAnnotation} and {@code usingBuilderImplementationAnnotation} are both + * disabled, ensuring the annotation copying logic skips processing when no annotations should be + * added. + */ + @Test + void shouldHandleBuilderWithNoClassAnnotations() { + JavaFileObject sourceFile = + ProcessorTestUtils.simpleBuilderClass( + "test", + "NoAnnotationsDto", + """ + private String value; + public String getValue() { return value; } + public void setValue(String value) { this.value = value; } + """); + + Compilation compilation = + ProcessorTestUtils.createCompiler() + .withOptions( + "-Asimplebuilder.usingGeneratedAnnotation=false", + "-Asimplebuilder.usingBuilderImplementationAnnotation=false") + .compile(sourceFile); + + String generatedCode = loadGeneratedSource(compilation, "NoAnnotationsDtoBuilder"); + assertGenerationSucceeded(compilation, "NoAnnotationsDtoBuilder", generatedCode); + assertContaining(generatedCode, "public class NoAnnotationsDtoBuilder"); + // Verify no class-level annotations are present + // (check for annotations before the class declaration) + assertNotContaining(generatedCode, "@Generated", "@BuilderImplementation"); + } + + /** + * Tests that builders are generated correctly when the With interface is disabled. + * + *

This edge case ensures the code generator handles builders with no nested types (the With + * interface is the only nested type typically generated in builders). When {@code + * generateWithInterface=false}, the builder should have no inner interfaces, and the nested type + * generation logic should correctly skip processing when the nested type list is empty. + */ + @Test + void shouldHandleBuilderWithNoNestedTypes() { + JavaFileObject sourceFile = + ProcessorTestUtils.simpleBuilderClass( + "test", + "NoNestedTypesDto", + """ + private Integer count; + public Integer getCount() { return count; } + public void setCount(Integer count) { this.count = count; } + """); + + Compilation compilation = + ProcessorTestUtils.createCompiler() + .withOptions("-Asimplebuilder.generateWithInterface=false") + .compile(sourceFile); + + String generatedCode = loadGeneratedSource(compilation, "NoNestedTypesDtoBuilder"); + assertGenerationSucceeded(compilation, "NoNestedTypesDtoBuilder", generatedCode); + assertContaining(generatedCode, "public class NoNestedTypesDtoBuilder"); + // Verify no With interface is generated + assertNotContaining(generatedCode, "public interface With"); + } + + /** + * Tests that the code generator detects and reports when a builder class already exists. + * + *

This edge case verifies that if a builder class with the same name already exists (either + * manually written or from a previous compilation), the processor detects the conflict and issues + * a warning, allowing compilation to succeed gracefully. + */ + @Test + void shouldDetectExistingBuilderClass() { + JavaFileObject dto = + JavaFileObjects.forSourceString( + "test.PersonDto", + """ + package test; + import org.javahelpers.simple.builders.core.annotations.SimpleBuilder; + + @SimpleBuilder + public class PersonDto { + private String name; + public String getName() { return name; } + public void setName(String name) { this.name = name; } + } + """); + + // Create a builder class that already exists + JavaFileObject existingBuilder = + JavaFileObjects.forSourceString( + "test.PersonDtoBuilder", + """ + package test; + + public class PersonDtoBuilder { + // Manually written or previously generated builder + } + """); + + Compilation compilation = ProcessorTestUtils.createCompiler().compile(dto, existingBuilder); + + // Verify compilation succeeds with a warning about the existing builder + assertThat(compilation).succeeded(); + assertThat(compilation) + .hadWarningContaining( + "Failed to generate builder - Builder class 'test.PersonDtoBuilder' already exists"); + } + + /** + * Tests that fields without JavaDoc documentation are handled correctly. + * + *

This edge case verifies that the code generator handles blank or missing JavaDoc strings and + * skips JavaDoc generation when documentation is not provided. + */ + @Test + void shouldHandleFieldWithNoJavadoc() { + JavaFileObject sourceFile = + ProcessorTestUtils.simpleBuilderClass( + "test", + "NoJavadocDto", + """ + private String field; + public String getField() { return field; } + public void setField(String field) { this.field = field; } + """); + + Compilation compilation = ProcessorTestUtils.createCompiler().compile(sourceFile); + + String generatedCode = loadGeneratedSource(compilation, "NoJavadocDtoBuilder"); + assertGenerationSucceeded(compilation, "NoJavadocDtoBuilder", generatedCode); + assertContaining(generatedCode, "private TrackedValue field"); + } + + /** + * Tests that primitive types (int, boolean, double) are handled correctly in builders. + * + *

This edge case ensures primitive types are properly boxed in TrackedValue and that primitive + * type imports are skipped (since primitives don't require imports). + */ + @Test + void shouldHandlePrimitiveTypes() { + JavaFileObject sourceFile = + ProcessorTestUtils.simpleBuilderClass( + "test", + "PrimitiveDto", + """ + private int count; + private boolean active; + private double value; + public int getCount() { return count; } + public void setCount(int count) { this.count = count; } + public boolean isActive() { return active; } + public void setActive(boolean active) { this.active = active; } + public double getValue() { return value; } + public void setValue(double value) { this.value = value; } + """); + + Compilation compilation = ProcessorTestUtils.createCompiler().compile(sourceFile); + + String generatedCode = loadGeneratedSource(compilation, "PrimitiveDtoBuilder"); + assertGenerationSucceeded(compilation, "PrimitiveDtoBuilder", generatedCode); + assertContaining( + generatedCode, + "TrackedValue count", + "TrackedValue active", + "TrackedValue value"); + } + + /** + * Tests that generic type variables (T, K, V) are handled correctly in builders. + * + *

This edge case verifies that type variables are properly preserved in the generated builder + * and that type variable imports are skipped (since they're not actual classes). + */ + @Test + void shouldHandleGenericTypeVariables() { + JavaFileObject sourceFile = + JavaFileObjects.forSourceString( + "test.GenericDto", + """ + package test; + import org.javahelpers.simple.builders.core.annotations.SimpleBuilder; + + @SimpleBuilder + public class GenericDto { + private T value; + public T getValue() { return value; } + public void setValue(T value) { this.value = value; } + } + """); + + Compilation compilation = ProcessorTestUtils.createCompiler().compile(sourceFile); + + String generatedCode = loadGeneratedSource(compilation, "GenericDtoBuilder"); + assertContaining( + generatedCode, + "public class GenericDtoBuilder", + "TrackedValue value", + "public GenericDto build()", + "public static GenericDtoBuilder create()"); + } +} diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/ToStringGenerationTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/ToStringGenerationTest.java index d6ab54d6..19096c54 100644 --- a/processor/src/test/java/org/javahelpers/simple/builders/processor/ToStringGenerationTest.java +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/ToStringGenerationTest.java @@ -186,10 +186,9 @@ public class ProductDto { """ @Override public String toString() { - return new ToStringBuilder(this, BuilderToStringStyle.INSTANCE) - .append("name", this.name) - .append("price", this.price) - .toString(); + return new ToStringBuilder(this, BuilderToStringStyle.INSTANCE).append("name", this.name) + .append("price", this.price) + .toString(); } """; diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/WithInterfaceTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/WithInterfaceTest.java index c83522c4..4c2872a7 100644 --- a/processor/src/test/java/org/javahelpers/simple/builders/processor/WithInterfaceTest.java +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/WithInterfaceTest.java @@ -52,7 +52,8 @@ public class Project { */ public interface With { /** - * Initializes a builder from an instance of this class, using methods of this builder to change values and returns the new built object. + * Initializes a builder from an instance of this class, using methods of this builder to change values and returns + * the new built object. * * @param b the consumer to apply modifications * @return the modified instance @@ -62,7 +63,9 @@ default Project with(Consumer b) { try { builder = new ProjectBuilder(Project.class.cast(this)); } catch (ClassCastException ex) { - throw new IllegalArgumentException("The interface 'ProjectBuilder.With' should only be implemented by classes, which could be casted to 'Project'", ex); + throw new IllegalArgumentException( + "The interface 'ProjectBuilder.With' should only be implemented by classes, which could be casted to 'Project'", + ex); } b.accept(builder); return builder.build(); @@ -77,7 +80,9 @@ default ProjectBuilder with() { try { return new ProjectBuilder(Project.class.cast(this)); } catch (ClassCastException ex) { - throw new IllegalArgumentException("The interface 'ProjectBuilder.With' should only be implemented by classes, which could be casted to 'Project'", ex); + throw new IllegalArgumentException( + "The interface 'ProjectBuilder.With' should only be implemented by classes, which could be casted to 'Project'", + ex); } } } @@ -121,7 +126,8 @@ public User(String username) { */ public interface With { /** - * Initializes a builder from an instance of this class, using methods of this builder to change values and returns the new built object. + * Initializes a builder from an instance of this class, using methods of this builder to change values and returns + * the new built object. * * @param b the consumer to apply modifications * @return the modified instance @@ -131,7 +137,9 @@ default User with(Consumer b) { try { builder = new UserBuilder(User.class.cast(this)); } catch (ClassCastException ex) { - throw new IllegalArgumentException("The interface 'UserBuilder.With' should only be implemented by classes, which could be casted to 'User'", ex); + throw new IllegalArgumentException( + "The interface 'UserBuilder.With' should only be implemented by classes, which could be casted to 'User'", + ex); } b.accept(builder); return builder.build(); @@ -146,7 +154,9 @@ default UserBuilder with() { try { return new UserBuilder(User.class.cast(this)); } catch (ClassCastException ex) { - throw new IllegalArgumentException("The interface 'UserBuilder.With' should only be implemented by classes, which could be casted to 'User'", ex); + throw new IllegalArgumentException( + "The interface 'UserBuilder.With' should only be implemented by classes, which could be casted to 'User'", + ex); } } } @@ -187,7 +197,8 @@ public class Config { */ public interface With { /** - * Initializes a builder from an instance of this class, using methods of this builder to change values and returns the new built object. + * Initializes a builder from an instance of this class, using methods of this builder to change values and returns + * the new built object. * * @param b the consumer to apply modifications * @return the modified instance @@ -197,7 +208,9 @@ default Config with(Consumer b) { try { builder = new ConfigBuilder(Config.class.cast(this)); } catch (ClassCastException ex) { - throw new IllegalArgumentException("The interface 'ConfigBuilder.With' should only be implemented by classes, which could be casted to 'Config'", ex); + throw new IllegalArgumentException( + "The interface 'ConfigBuilder.With' should only be implemented by classes, which could be casted to 'Config'", + ex); } b.accept(builder); return builder.build(); @@ -212,7 +225,9 @@ default ConfigBuilder with() { try { return new ConfigBuilder(Config.class.cast(this)); } catch (ClassCastException ex) { - throw new IllegalArgumentException("The interface 'ConfigBuilder.With' should only be implemented by classes, which could be casted to 'Config'", ex); + throw new IllegalArgumentException( + "The interface 'ConfigBuilder.With' should only be implemented by classes, which could be casted to 'Config'", + ex); } } }