From 104650649f14aa4bfc4c371bdcadd98105f7063a Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Mon, 29 Dec 2025 00:11:52 +0100 Subject: [PATCH 01/17] Adding new configuration usingJacksonDeserializerAnnotation --- .../core/annotations/SimpleBuilder.java | 21 ++++ .../builders/processor/BuilderProcessor.java | 3 +- .../processor/dtos/BuilderConfiguration.java | 24 +++++ .../enums/CompilerArgumentsEnum.java | 3 + .../util/BuilderConfigurationReader.java | 2 + .../util/CompilerArgumentsReader.java | 2 + .../ConfigurationProcessingTest.java | 6 ++ .../JacksonSupportMissingDependencyTest.java | 99 +++++++++++++++++++ .../processor/util/JavaCodeGeneratorTest.java | 86 ++++++++++++++++ 9 files changed, 245 insertions(+), 1 deletion(-) create mode 100644 processor/src/test/java/org/javahelpers/simple/builders/processor/JacksonSupportMissingDependencyTest.java create mode 100644 processor/src/test/java/org/javahelpers/simple/builders/processor/util/JavaCodeGeneratorTest.java diff --git a/core/src/main/java/org/javahelpers/simple/builders/core/annotations/SimpleBuilder.java b/core/src/main/java/org/javahelpers/simple/builders/core/annotations/SimpleBuilder.java index 78c30449..63c45ea8 100644 --- a/core/src/main/java/org/javahelpers/simple/builders/core/annotations/SimpleBuilder.java +++ b/core/src/main/java/org/javahelpers/simple/builders/core/annotations/SimpleBuilder.java @@ -568,6 +568,27 @@ */ OptionState generateWithInterface() default OptionState.UNSET; + /** + * Add Jackson annotations to the generated builder class.
+ * Adds {@code @JsonPOJOBuilder(withPrefix = "...")} to the builder class. + * The prefix matches the configured {@link #setterSuffix()}. + * + *

Example: + * + *

{@code
+     * @JsonDeserialize(builder = PersonDtoBuilder.class)
+     * public class PersonDto { ... }
+     *
+     * // Generated:
+     * @JsonPOJOBuilder(withPrefix = "")
+     * public class PersonDtoBuilder { ... }
+     * }
+ * + * Default: DISABLED
+ * Compiler option: -Asimplebuilder.usingJacksonDeserializerAnnotation + */ + OptionState usingJacksonDeserializerAnnotation() default OptionState.UNSET; + // === Naming === /** * Suffix to append to the DTO name to generate the builder class name.
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 98054a55..5380ed75 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 @@ -76,7 +76,8 @@ public synchronized void init(ProcessingEnvironment processingEnv) { new ProcessingContext( processingEnv.getElementUtils(), processingEnv.getTypeUtils(), logger, globalConfig); context.debug("Loaded global configuration from compiler arguments: %s", globalConfig); - this.codeGenerator = new JavaCodeGenerator(processingEnv.getFiler(), logger); + this.codeGenerator = + new JavaCodeGenerator(processingEnv.getFiler(), processingEnv.getElementUtils(), logger); SourceVersion current = processingEnv.getSourceVersion(); this.supportedJdk = isAtLeastJava17(current); diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderConfiguration.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderConfiguration.java index 3f3f4b93..97c5ec48 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderConfiguration.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderConfiguration.java @@ -86,6 +86,7 @@ public record BuilderConfiguration( OptionState usingBuilderImplementationAnnotation, OptionState implementsBuilderBase, OptionState generateWithInterface, + OptionState usingJacksonDeserializerAnnotation, String builderSuffix, String setterSuffix) { @@ -112,6 +113,7 @@ public record BuilderConfiguration( .usingBuilderImplementationAnnotation(ENABLED) .implementsBuilderBase(ENABLED) .generateWithInterface(ENABLED) + .usingJacksonDeserializerAnnotation(DISABLED) .builderSuffix("Builder") .setterSuffix("") .build(); @@ -137,6 +139,10 @@ public boolean shouldGenerateWithInterface() { return generateWithInterface == ENABLED; } + public boolean shouldUseJacksonDeserializerAnnotation() { + return usingJacksonDeserializerAnnotation == ENABLED; + } + public boolean shouldGenerateVarArgsHelpers() { return generateVarArgsHelpers == ENABLED; } @@ -268,6 +274,9 @@ public BuilderConfiguration merge(BuilderConfiguration other) { mergeOptionState(other.implementsBuilderBase, this.implementsBuilderBase)) .generateWithInterface( mergeOptionState(other.generateWithInterface, this.generateWithInterface)) + .usingJacksonDeserializerAnnotation( + mergeOptionState( + other.usingJacksonDeserializerAnnotation, this.usingJacksonDeserializerAnnotation)) .builderSuffix(mergeString(other.builderSuffix, this.builderSuffix)) .setterSuffix(mergeString(other.setterSuffix, this.setterSuffix)) .build(); @@ -361,6 +370,9 @@ public String toString() { if (generateWithInterface != UNSET) { builder.append("generateWithInterface", generateWithInterface); } + if (usingJacksonDeserializerAnnotation != UNSET) { + builder.append("usingJacksonDeserializerAnnotation", usingJacksonDeserializerAnnotation); + } if (builderSuffix != null && !builderSuffix.equals("Builder")) { builder.append("builderSuffix", builderSuffix); } @@ -406,6 +418,7 @@ public static class Builder { // === Integration === private OptionState implementsBuilderBase = OptionState.UNSET; private OptionState generateWithInterface = OptionState.UNSET; + private OptionState usingJacksonDeserializerAnnotation = OptionState.UNSET; // === Naming === private String builderSuffix = null; @@ -462,6 +475,16 @@ public Builder generateWithInterface(boolean value) { return this; } + public Builder usingJacksonDeserializerAnnotation(OptionState value) { + this.usingJacksonDeserializerAnnotation = value; + return this; + } + + public Builder usingJacksonDeserializerAnnotation(boolean value) { + this.usingJacksonDeserializerAnnotation = value ? ENABLED : DISABLED; + return this; + } + public Builder generateVarArgsHelpers(OptionState value) { this.generateVarArgsHelpers = value; return this; @@ -655,6 +678,7 @@ public BuilderConfiguration build() { usingBuilderImplementationAnnotation, implementsBuilderBase, generateWithInterface, + usingJacksonDeserializerAnnotation, builderSuffix, setterSuffix); } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/enums/CompilerArgumentsEnum.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/enums/CompilerArgumentsEnum.java index 61d93b32..eab3c927 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/enums/CompilerArgumentsEnum.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/enums/CompilerArgumentsEnum.java @@ -107,6 +107,9 @@ public enum CompilerArgumentsEnum { /** Option for With interface generation. */ GENERATE_WITH_INTERFACE("generateWithInterface"), + /** Option for Jackson support. */ + USING_JACKSON_DESERIALIZER_ANNOTATION("usingJacksonDeserializerAnnotation"), + // === Naming === /** Option for builder class name suffix. */ BUILDER_SUFFIX("builderSuffix"), diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderConfigurationReader.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderConfigurationReader.java index 70b113e9..5415897c 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderConfigurationReader.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderConfigurationReader.java @@ -192,6 +192,8 @@ private BuilderConfiguration parseOptionsFromMirror(AnnotationMirror optionsMirr builder.implementsBuilderBase(OptionState.valueOf(enumValue)); case "generateWithInterface" -> builder.generateWithInterface(OptionState.valueOf(enumValue)); + case "usingJacksonDeserializerAnnotation" -> + builder.usingJacksonDeserializerAnnotation(OptionState.valueOf(enumValue)); case "builderSuffix" -> builder.builderSuffix(value.toString()); case "setterSuffix" -> builder.setterSuffix(value.toString()); default -> diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/CompilerArgumentsReader.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/CompilerArgumentsReader.java index 585883f4..e6ec33ae 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/CompilerArgumentsReader.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/CompilerArgumentsReader.java @@ -169,6 +169,8 @@ public BuilderConfiguration readBuilderConfiguration() { readOptionState(CompilerArgumentsEnum.USING_BUILDER_IMPLEMENTATION_ANNOTATION)) .implementsBuilderBase(readOptionState(CompilerArgumentsEnum.IMPLEMENTS_BUILDER_BASE)) .generateWithInterface(readOptionState(CompilerArgumentsEnum.GENERATE_WITH_INTERFACE)) + .usingJacksonDeserializerAnnotation( + readOptionState(CompilerArgumentsEnum.USING_JACKSON_DESERIALIZER_ANNOTATION)) .builderSuffix(readValue(CompilerArgumentsEnum.BUILDER_SUFFIX)) .setterSuffix(readValue(CompilerArgumentsEnum.SETTER_SUFFIX)) .build(); diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/ConfigurationProcessingTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/ConfigurationProcessingTest.java index 6ddaf5fd..69f00876 100644 --- a/processor/src/test/java/org/javahelpers/simple/builders/processor/ConfigurationProcessingTest.java +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/ConfigurationProcessingTest.java @@ -79,6 +79,7 @@ void allConfigurationOptions_MustBeSettableViaBuilder() { // Integration .implementsBuilderBase(OptionState.ENABLED) .generateWithInterface(OptionState.ENABLED) + .usingJacksonDeserializerAnnotation(OptionState.ENABLED) // Naming .builderSuffix("Builder") .setterSuffix("") @@ -106,6 +107,7 @@ void allConfigurationOptions_MustBeSettableViaBuilder() { assertEquals(OptionState.ENABLED, config.usingBuilderImplementationAnnotation()); assertEquals(OptionState.ENABLED, config.implementsBuilderBase()); assertEquals(OptionState.ENABLED, config.generateWithInterface()); + assertEquals(OptionState.ENABLED, config.usingJacksonDeserializerAnnotation()); assertEquals("Builder", config.getBuilderSuffix()); assertEquals("", config.getSetterSuffix()); } @@ -210,6 +212,7 @@ public Address() {} "-Asimplebuilder.usingBuilderImplementationAnnotation=false", "-Asimplebuilder.implementsBuilderBase=false", "-Asimplebuilder.generateWithInterface=false", + "-Asimplebuilder.usingJacksonDeserializerAnnotation=false", "-Asimplebuilder.builderSuffix=CustomBuilder", "-Asimplebuilder.setterSuffix=with") .compile(nestedDto, addressDto, source); @@ -285,6 +288,9 @@ public Address() {} ProcessorAsserts.assertNotContaining( generatedCode, "implements IBuilderBase", "@Override public MinimalDto build()"); + // With usingJacksonDeserializerAnnotation=false, NO @JsonPOJOBuilder annotation should be used + ProcessorAsserts.assertNotContaining(generatedCode, "@JsonPOJOBuilder"); + // With builderAccess=PACKAGE_PRIVATE, builder class should NOT have public modifier ProcessorAsserts.assertNotContaining(generatedCode, "public class MinimalDtoCustomBuilder"); diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/JacksonSupportMissingDependencyTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/JacksonSupportMissingDependencyTest.java new file mode 100644 index 00000000..76637154 --- /dev/null +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/JacksonSupportMissingDependencyTest.java @@ -0,0 +1,99 @@ +package org.javahelpers.simple.builders.processor; + + +import javax.tools.JavaFileObject; +import org.javahelpers.simple.builders.processor.testing.ProcessorTestUtils; +import org.junit.jupiter.api.Test; + +class JacksonSupportMissingDependencyTest { + + @Test + void jacksonSupport_WhenEnabledButDependencyMissing_ShouldNotGenerateAnnotationAndWarn() { + // Given + JavaFileObject source = + ProcessorTestUtils.simpleBuilderClass( + "test", + "MissingJacksonDto", + """ + private String name; + public String getName() { return name; } + public void setName(String name) { this.name = name; } + """); + + // When + // Create compiler WITHOUT Jackson dependency on classpath (default behavior if not added) + // Note: The ProcessorTestUtils.createCompiler() usually adds the processor's classpath + // which might include Jackson if it's in the processor's dependencies. + // However, since we added Jackson as test in pom.xml, it might be on the test + // classpath. + // We need to ensure we run this test in an environment where Jackson is NOT available to the + // compiler. + // BUT: The compiler created by compile-testing typically inherits the test classpath. + // To simulate missing dependency, we might need to rely on the fact that 'jackson-databind' is + // needed for JsonPOJOBuilder? + // Wait, JsonPOJOBuilder is in 'jackson-databind' or 'jackson-annotations'? + // It is in 'jackson-databind'. And we added 'jackson-annotations' and 'jackson-databind' to + // test scope. + // So createCompiler() WILL have it. + + // We need a way to isolate the classpath. + // With Google Compile Testing, it's hard to remove items from classpath. + // However, we can inspect the code to see if the logic works if we can't simulate the missing + // dependency easily. + // Alternatively, we can assume that if we don't supply the option, it works (already tested). + + // Actually, checking for the warning is the key. + // If I cannot remove the dependency, I cannot trigger the missing path. + + // HACK: I can use a mocked Elements utils if I were unit testing JavaCodeGenerator directly. + // But here I am doing integration testing. + + // Let's try to verify the check logic by mocking or simply trusting the implementation if test + // simulation is too hard. + // But I should try. + + // In this specific environment, I added dependencies to pom.xml. + // If I create a compiler that explicitly sets the classpath... + + // Let's skip the "remove dependency" test if it's too complex for the current setup and focus + // on code correctness. + // BUT the user asked for verification. + + // Wait, I can try to run the compiler with an empty classpath? + // .withClasspath(Collections.emptyList()) + + // But then I lose the processor and the input files deps? + // Input files have no deps. Processor is the SUT. + // I need the simple-builders-core dependency for @SimpleBuilder. + + // So I need classpath = core + processor (without jackson). + // This is hard to construct dynamically in the test without knowing paths. + + // Let's write the test assuming I can't easily remove the dependency, + // BUT I can verify that it works when dependency IS present (already done). + + // Wait, I can verify the logic by temporarily hacking the code to look for a non-existent + // class? + // No, that modifies source. + + // Let's rely on the fact that I modified the code to check elementUtils.getTypeElement(...) + // and logged a warning. + + // If I can't easily write a test for "missing dependency", I will just verifying the "present + // dependency" case still works + // and maybe add a manual test note. + + // Actually, `JsonPOJOBuilder` is in `jackson-databind`. + // I added `jackson-databind` to `pom.xml` with `test` scope. + // So it IS available during `mvn test`. + + // If I want to test "missing dependency", I would need to run a test where `jackson-databind` + // is NOT on classpath. + // I can try to use `withClasspath` but identifying the jar for `simple-builders-core` is + // tricky. + + // I will write a unit test for JavaCodeGenerator directly? + // JavaCodeGenerator takes `Elements`. I can mock `Elements`. + + } +} diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/util/JavaCodeGeneratorTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/util/JavaCodeGeneratorTest.java new file mode 100644 index 00000000..f6e43b9a --- /dev/null +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/util/JavaCodeGeneratorTest.java @@ -0,0 +1,86 @@ +package org.javahelpers.simple.builders.processor.util; + +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import javax.annotation.processing.Filer; +import javax.lang.model.element.TypeElement; +import javax.lang.model.util.Elements; +import org.javahelpers.simple.builders.core.enums.OptionState; +import org.javahelpers.simple.builders.processor.dtos.BuilderConfiguration; +import org.javahelpers.simple.builders.processor.dtos.BuilderDefinitionDto; +import org.javahelpers.simple.builders.processor.dtos.TypeName; +import org.junit.jupiter.api.Test; + +class JavaCodeGeneratorTest { + + @Test + void generateBuilder_WhenJacksonEnabledButMissing_ShouldWarnAndNotAddAnnotation() + throws Exception { + // Given + Filer filer = mock(Filer.class); + Elements elementUtils = mock(Elements.class); + ProcessingLogger logger = mock(ProcessingLogger.class); + JavaCodeGenerator generator = new JavaCodeGenerator(filer, elementUtils, logger); + + BuilderDefinitionDto builderDef = new BuilderDefinitionDto(); + builderDef.setBuilderTypeName(new TypeName("test", "TestBuilder")); + builderDef.setBuildingTargetTypeName(new TypeName("test", "Test")); + + BuilderConfiguration config = + BuilderConfiguration.builder() + .usingJacksonDeserializerAnnotation(OptionState.ENABLED) + .build(); + builderDef.setConfiguration(config); + + // Simulate missing JsonPOJOBuilder + when(elementUtils.getTypeElement("com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder")) + .thenReturn(null); + + // When + generator.generateBuilder(builderDef); + + // Then + verify(logger) + .warning( + "Jackson support enabled but 'com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder' not found on classpath. Annotation skipped."); + + // We can't easily verify that annotation was NOT added to the TypeSpec without inspecting + // internal Javapoet logic, + // or by mocking Filer and checking output. + // But determining if checking for the warning is sufficient. + } + + @Test + void generateBuilder_WhenJacksonEnabledAndPresent_ShouldNotWarn() throws Exception { + // Given + Filer filer = mock(Filer.class); + Elements elementUtils = mock(Elements.class); + ProcessingLogger logger = mock(ProcessingLogger.class); + JavaCodeGenerator generator = new JavaCodeGenerator(filer, elementUtils, logger); + + BuilderDefinitionDto builderDef = new BuilderDefinitionDto(); + builderDef.setBuilderTypeName(new TypeName("test", "TestBuilder")); + builderDef.setBuildingTargetTypeName(new TypeName("test", "Test")); + + BuilderConfiguration config = + BuilderConfiguration.builder() + .usingJacksonDeserializerAnnotation(OptionState.ENABLED) + .build(); + builderDef.setConfiguration(config); + + // Simulate present JsonPOJOBuilder + TypeElement typeElement = mock(TypeElement.class); + when(elementUtils.getTypeElement("com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder")) + .thenReturn(typeElement); + + // When + generator.generateBuilder(builderDef); + + // Then + // verify logger.warning was NOT called + verify(logger, org.mockito.Mockito.never()).warning(anyString()); + } +} From fab44b19fe6214bf1c5bc20c0bbfab3b29e32ad3 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Mon, 29 Dec 2025 00:24:19 +0100 Subject: [PATCH 02/17] Undoing adding of Testclasses --- .../JacksonSupportMissingDependencyTest.java | 99 ------------------- .../processor/util/JavaCodeGeneratorTest.java | 86 ---------------- 2 files changed, 185 deletions(-) delete mode 100644 processor/src/test/java/org/javahelpers/simple/builders/processor/JacksonSupportMissingDependencyTest.java delete mode 100644 processor/src/test/java/org/javahelpers/simple/builders/processor/util/JavaCodeGeneratorTest.java diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/JacksonSupportMissingDependencyTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/JacksonSupportMissingDependencyTest.java deleted file mode 100644 index 76637154..00000000 --- a/processor/src/test/java/org/javahelpers/simple/builders/processor/JacksonSupportMissingDependencyTest.java +++ /dev/null @@ -1,99 +0,0 @@ -package org.javahelpers.simple.builders.processor; - - -import javax.tools.JavaFileObject; -import org.javahelpers.simple.builders.processor.testing.ProcessorTestUtils; -import org.junit.jupiter.api.Test; - -class JacksonSupportMissingDependencyTest { - - @Test - void jacksonSupport_WhenEnabledButDependencyMissing_ShouldNotGenerateAnnotationAndWarn() { - // Given - JavaFileObject source = - ProcessorTestUtils.simpleBuilderClass( - "test", - "MissingJacksonDto", - """ - private String name; - public String getName() { return name; } - public void setName(String name) { this.name = name; } - """); - - // When - // Create compiler WITHOUT Jackson dependency on classpath (default behavior if not added) - // Note: The ProcessorTestUtils.createCompiler() usually adds the processor's classpath - // which might include Jackson if it's in the processor's dependencies. - // However, since we added Jackson as test in pom.xml, it might be on the test - // classpath. - // We need to ensure we run this test in an environment where Jackson is NOT available to the - // compiler. - // BUT: The compiler created by compile-testing typically inherits the test classpath. - // To simulate missing dependency, we might need to rely on the fact that 'jackson-databind' is - // needed for JsonPOJOBuilder? - // Wait, JsonPOJOBuilder is in 'jackson-databind' or 'jackson-annotations'? - // It is in 'jackson-databind'. And we added 'jackson-annotations' and 'jackson-databind' to - // test scope. - // So createCompiler() WILL have it. - - // We need a way to isolate the classpath. - // With Google Compile Testing, it's hard to remove items from classpath. - // However, we can inspect the code to see if the logic works if we can't simulate the missing - // dependency easily. - // Alternatively, we can assume that if we don't supply the option, it works (already tested). - - // Actually, checking for the warning is the key. - // If I cannot remove the dependency, I cannot trigger the missing path. - - // HACK: I can use a mocked Elements utils if I were unit testing JavaCodeGenerator directly. - // But here I am doing integration testing. - - // Let's try to verify the check logic by mocking or simply trusting the implementation if test - // simulation is too hard. - // But I should try. - - // In this specific environment, I added dependencies to pom.xml. - // If I create a compiler that explicitly sets the classpath... - - // Let's skip the "remove dependency" test if it's too complex for the current setup and focus - // on code correctness. - // BUT the user asked for verification. - - // Wait, I can try to run the compiler with an empty classpath? - // .withClasspath(Collections.emptyList()) - - // But then I lose the processor and the input files deps? - // Input files have no deps. Processor is the SUT. - // I need the simple-builders-core dependency for @SimpleBuilder. - - // So I need classpath = core + processor (without jackson). - // This is hard to construct dynamically in the test without knowing paths. - - // Let's write the test assuming I can't easily remove the dependency, - // BUT I can verify that it works when dependency IS present (already done). - - // Wait, I can verify the logic by temporarily hacking the code to look for a non-existent - // class? - // No, that modifies source. - - // Let's rely on the fact that I modified the code to check elementUtils.getTypeElement(...) - // and logged a warning. - - // If I can't easily write a test for "missing dependency", I will just verifying the "present - // dependency" case still works - // and maybe add a manual test note. - - // Actually, `JsonPOJOBuilder` is in `jackson-databind`. - // I added `jackson-databind` to `pom.xml` with `test` scope. - // So it IS available during `mvn test`. - - // If I want to test "missing dependency", I would need to run a test where `jackson-databind` - // is NOT on classpath. - // I can try to use `withClasspath` but identifying the jar for `simple-builders-core` is - // tricky. - - // I will write a unit test for JavaCodeGenerator directly? - // JavaCodeGenerator takes `Elements`. I can mock `Elements`. - - } -} diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/util/JavaCodeGeneratorTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/util/JavaCodeGeneratorTest.java deleted file mode 100644 index f6e43b9a..00000000 --- a/processor/src/test/java/org/javahelpers/simple/builders/processor/util/JavaCodeGeneratorTest.java +++ /dev/null @@ -1,86 +0,0 @@ -package org.javahelpers.simple.builders.processor.util; - -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import javax.annotation.processing.Filer; -import javax.lang.model.element.TypeElement; -import javax.lang.model.util.Elements; -import org.javahelpers.simple.builders.core.enums.OptionState; -import org.javahelpers.simple.builders.processor.dtos.BuilderConfiguration; -import org.javahelpers.simple.builders.processor.dtos.BuilderDefinitionDto; -import org.javahelpers.simple.builders.processor.dtos.TypeName; -import org.junit.jupiter.api.Test; - -class JavaCodeGeneratorTest { - - @Test - void generateBuilder_WhenJacksonEnabledButMissing_ShouldWarnAndNotAddAnnotation() - throws Exception { - // Given - Filer filer = mock(Filer.class); - Elements elementUtils = mock(Elements.class); - ProcessingLogger logger = mock(ProcessingLogger.class); - JavaCodeGenerator generator = new JavaCodeGenerator(filer, elementUtils, logger); - - BuilderDefinitionDto builderDef = new BuilderDefinitionDto(); - builderDef.setBuilderTypeName(new TypeName("test", "TestBuilder")); - builderDef.setBuildingTargetTypeName(new TypeName("test", "Test")); - - BuilderConfiguration config = - BuilderConfiguration.builder() - .usingJacksonDeserializerAnnotation(OptionState.ENABLED) - .build(); - builderDef.setConfiguration(config); - - // Simulate missing JsonPOJOBuilder - when(elementUtils.getTypeElement("com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder")) - .thenReturn(null); - - // When - generator.generateBuilder(builderDef); - - // Then - verify(logger) - .warning( - "Jackson support enabled but 'com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder' not found on classpath. Annotation skipped."); - - // We can't easily verify that annotation was NOT added to the TypeSpec without inspecting - // internal Javapoet logic, - // or by mocking Filer and checking output. - // But determining if checking for the warning is sufficient. - } - - @Test - void generateBuilder_WhenJacksonEnabledAndPresent_ShouldNotWarn() throws Exception { - // Given - Filer filer = mock(Filer.class); - Elements elementUtils = mock(Elements.class); - ProcessingLogger logger = mock(ProcessingLogger.class); - JavaCodeGenerator generator = new JavaCodeGenerator(filer, elementUtils, logger); - - BuilderDefinitionDto builderDef = new BuilderDefinitionDto(); - builderDef.setBuilderTypeName(new TypeName("test", "TestBuilder")); - builderDef.setBuildingTargetTypeName(new TypeName("test", "Test")); - - BuilderConfiguration config = - BuilderConfiguration.builder() - .usingJacksonDeserializerAnnotation(OptionState.ENABLED) - .build(); - builderDef.setConfiguration(config); - - // Simulate present JsonPOJOBuilder - TypeElement typeElement = mock(TypeElement.class); - when(elementUtils.getTypeElement("com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder")) - .thenReturn(typeElement); - - // When - generator.generateBuilder(builderDef); - - // Then - // verify logger.warning was NOT called - verify(logger, org.mockito.Mockito.never()).warning(anyString()); - } -} From 647341b9469ef60020394d649b19d8a432260f76 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Mon, 29 Dec 2025 00:33:19 +0100 Subject: [PATCH 03/17] Adding code for supporting PojoBuilderAnnotation --- processor/pom.xml | 9 ++ .../processor/util/JavaCodeGenerator.java | 24 +++- .../processor/JacksonSupportTest.java | 127 ++++++++++++++++++ 3 files changed, 159 insertions(+), 1 deletion(-) create mode 100644 processor/src/test/java/org/javahelpers/simple/builders/processor/JacksonSupportTest.java diff --git a/processor/pom.xml b/processor/pom.xml index 5a741268..02dc3a64 100644 --- a/processor/pom.xml +++ b/processor/pom.xml @@ -59,6 +59,7 @@ 4.5.0 6.0.1 0.23.0 + 2.18.2 17 @@ -142,6 +143,14 @@ ${google-compile-testing.version} test + + + + com.fasterxml.jackson.core + jackson-databind + ${jackson-databind.version} + test + diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavaCodeGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavaCodeGenerator.java index e4c2b58a..a214f692 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavaCodeGenerator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavaCodeGenerator.java @@ -44,6 +44,7 @@ import javax.annotation.processing.Filer; import javax.annotation.processing.Generated; import javax.lang.model.element.Modifier; +import javax.lang.model.util.Elements; import org.apache.commons.lang3.StringUtils; import org.javahelpers.simple.builders.core.annotations.BuilderImplementation; import org.javahelpers.simple.builders.core.interfaces.IBuilderBase; @@ -58,6 +59,7 @@ public class JavaCodeGenerator { private static final String THROW_EXCEPTION_FORMAT = "throw new $T($S)"; private final Filer filer; + private final Elements elementUtils; /** Logger for debug output during code generation. */ private final ProcessingLogger logger; @@ -67,10 +69,12 @@ public class JavaCodeGenerator { * * @param filer Util class for source code generation of type {@code * javax.annotation.processing.Filer} + * @param elementUtils Util class for operating on program elements * @param logger Logger for debug output */ - public JavaCodeGenerator(Filer filer, ProcessingLogger logger) { + public JavaCodeGenerator(Filer filer, Elements elementUtils, ProcessingLogger logger) { this.filer = filer; + this.elementUtils = elementUtils; this.logger = logger; } @@ -210,6 +214,16 @@ public void generateBuilder(BuilderDefinitionDto builderDef) throws BuilderExcep if (builderDef.getConfiguration().shouldUseBuilderImplementationAnnotation()) { classBuilder.addAnnotation(createAnnotationBuilderImplementation(dtoBaseClass)); } + if (builderDef.getConfiguration().shouldUseJacksonDeserializerAnnotation()) { + if (elementUtils.getTypeElement("com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder") + != null) { + classBuilder.addAnnotation( + createAnnotationJsonPOJOBuilder(builderDef.getConfiguration().getSetterSuffix())); + } else { + logger.warning( + "Jackson support enabled but 'com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder' not found on classpath. Annotation skipped."); + } + } logger.debug( "Writing builder class to file: %s.%s", @@ -317,6 +331,14 @@ private AnnotationSpec createAnnotationBuilderImplementation(ClassName dtoClass) .build(); } + private AnnotationSpec createAnnotationJsonPOJOBuilder(String setterPrefix) { + ClassName jsonPojoBuilderClass = + ClassName.get("com.fasterxml.jackson.databind.annotation", "JsonPOJOBuilder"); + return AnnotationSpec.builder(jsonPojoBuilderClass) + .addMember("withPrefix", "$S", setterPrefix == null ? "" : setterPrefix) + .build(); + } + private MethodSpec createEmptyConstructor(ClassName dtoClass, Modifier accessModifier) { MethodSpec.Builder constructorBuilder = MethodSpec.constructorBuilder() diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/JacksonSupportTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/JacksonSupportTest.java new file mode 100644 index 00000000..0b13b2a2 --- /dev/null +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/JacksonSupportTest.java @@ -0,0 +1,127 @@ +package org.javahelpers.simple.builders.processor; + +import static com.google.testing.compile.CompilationSubject.assertThat; + +import com.google.testing.compile.Compilation; +import javax.tools.JavaFileObject; +import org.javahelpers.simple.builders.processor.testing.ProcessorAsserts; +import org.javahelpers.simple.builders.processor.testing.ProcessorTestUtils; +import org.junit.jupiter.api.Test; + +class JacksonSupportTest { + + @Test + void jacksonSupport_WhenEnabled_ShouldGenerateAnnotation() { + // Given + JavaFileObject source = + ProcessorTestUtils.simpleBuilderClass( + "test", + "JacksonDto", + """ + private String name; + public String getName() { return name; } + public void setName(String name) { this.name = name; } + """); + + // When + Compilation compilation = + ProcessorTestUtils.createCompiler() + .withOptions("-Asimplebuilder.usingJacksonDeserializerAnnotation=true") + .compile(source); + + // Then + assertThat(compilation).succeeded(); + String generatedCode = ProcessorTestUtils.loadGeneratedSource(compilation, "JacksonDtoBuilder"); + + ProcessorAsserts.assertContaining( + generatedCode, + "import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder;", + "@JsonPOJOBuilder(withPrefix = \"\")"); + } + + @Test + void jacksonSupport_WhenEnabledWithCustomPrefix_ShouldGenerateAnnotationWithPrefix() { + // Given + JavaFileObject source = + ProcessorTestUtils.simpleBuilderClass( + "test", + "JacksonCustomDto", + """ + private String name; + public String getName() { return name; } + public void setName(String name) { this.name = name; } + """); + + // When + Compilation compilation = + ProcessorTestUtils.createCompiler() + .withOptions( + "-Asimplebuilder.usingJacksonDeserializerAnnotation=true", + "-Asimplebuilder.setterSuffix=with") + .compile(source); + + // Then + assertThat(compilation).succeeded(); + String generatedCode = + ProcessorTestUtils.loadGeneratedSource(compilation, "JacksonCustomDtoBuilder"); + + ProcessorAsserts.assertContaining(generatedCode, "@JsonPOJOBuilder(withPrefix = \"with\")"); + } + + @Test + void jacksonSupport_WhenDisabled_ShouldNotGenerateAnnotation() { + // Given + JavaFileObject source = + ProcessorTestUtils.simpleBuilderClass( + "test", + "NoJacksonDto", + """ + private String name; + public String getName() { return name; } + public void setName(String name) { this.name = name; } + """); + + // When + Compilation compilation = + ProcessorTestUtils.createCompiler() + // usingJacksonDeserializerAnnotation is disabled by default, but we can explicitly + // disable + // it + .withOptions("-Asimplebuilder.usingJacksonDeserializerAnnotation=false") + .compile(source); + + // Then + assertThat(compilation).succeeded(); + String generatedCode = + ProcessorTestUtils.loadGeneratedSource(compilation, "NoJacksonDtoBuilder"); + + ProcessorAsserts.assertNotContaining(generatedCode, "JsonPOJOBuilder"); + } + + @Test + void jacksonSupport_WhenNotConfigured_ShouldDefaultToDisabled() { + // Given + JavaFileObject source = + ProcessorTestUtils.simpleBuilderClass( + "test", + "DefaultJacksonDto", + """ + private String name; + public String getName() { return name; } + public void setName(String name) { this.name = name; } + """); + + // When + Compilation compilation = + ProcessorTestUtils.createCompiler() + // No options provided + .compile(source); + + // Then + assertThat(compilation).succeeded(); + String generatedCode = + ProcessorTestUtils.loadGeneratedSource(compilation, "DefaultJacksonDtoBuilder"); + + ProcessorAsserts.assertNotContaining(generatedCode, "JsonPOJOBuilder"); + } +} From b2e611ef4512833951b2f8f5ef19acb69e7ba813 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Mon, 29 Dec 2025 00:41:03 +0100 Subject: [PATCH 04/17] Extending JacksonDeserialization feature configuration --- README.md | 1 + .../core/annotations/SimpleBuilder.java | 4 ++-- docs/CONFIGURATION.md | 21 +++++++++++++++++++ .../builders/example/ElementaryBuilder.java | 3 ++- .../BuilderConfigurationReaderTest.java | 1 + .../processor/JacksonSupportTest.java | 9 ++++---- 6 files changed, 32 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 21b72b7e..ec6736ce 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,7 @@ Simple Builders is a Java [annotation processor](https://docs.oracle.com/en/java - **Collections Support**: Built-in support for collections and maps - **Annotation Preservation**: Validation annotations are automatically copied to builder methods - **With Interface Pattern**: Type-safe object modifications using generated With interfaces +- **Jackson Support**: Supporting Jackson deserialization (needs to be enabled) ## Requirements diff --git a/core/src/main/java/org/javahelpers/simple/builders/core/annotations/SimpleBuilder.java b/core/src/main/java/org/javahelpers/simple/builders/core/annotations/SimpleBuilder.java index 63c45ea8..cd79b0d2 100644 --- a/core/src/main/java/org/javahelpers/simple/builders/core/annotations/SimpleBuilder.java +++ b/core/src/main/java/org/javahelpers/simple/builders/core/annotations/SimpleBuilder.java @@ -570,8 +570,8 @@ /** * Add Jackson annotations to the generated builder class.
- * Adds {@code @JsonPOJOBuilder(withPrefix = "...")} to the builder class. - * The prefix matches the configured {@link #setterSuffix()}. + * Adds {@code @JsonPOJOBuilder(withPrefix = "...")} to the builder class. The prefix matches + * the configured {@link #setterSuffix()}. * *

Example: * diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 103166a9..c4e9ab92 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -681,6 +681,26 @@ public class PersonDtoBuilder implements IBuilderBase { --- +#### `usingJacksonDeserializerAnnotation` + +**Default**: `DISABLED` | **Compiler Option**: `-Asimplebuilder.usingJacksonDeserializerAnnotation=ENABLED|DISABLED` + +Adds `@JsonPOJOBuilder` annotation to the builder class for Jackson deserialization support. + +**When ENABLED**: +```java +@JsonPOJOBuilder(withPrefix = "") +public class PersonDtoBuilder { + // ... +} +``` + +**When DISABLED**: No `@JsonPOJOBuilder` annotation. + +**Note**: This requires `com.fasterxml.jackson.core:jackson-databind` on the classpath during compilation. If missing, the annotation is skipped with a warning. + +--- + ### Naming #### `builderSuffix` @@ -1115,6 +1135,7 @@ methodAccess = AccessModifier.PRIVATE implementsBuilderBase = OptionState.ENABLED, usingGeneratedAnnotation = OptionState.ENABLED, usingBuilderImplementationAnnotation = OptionState.ENABLED, + usingJacksonDeserializerAnnotation = OptionState.ENABLED, // Naming builderSuffix = "Builder", diff --git a/example/src/main/java/org/javahelpers/simple/builders/example/ElementaryBuilder.java b/example/src/main/java/org/javahelpers/simple/builders/example/ElementaryBuilder.java index 24d1b4d2..6e1cb475 100644 --- a/example/src/main/java/org/javahelpers/simple/builders/example/ElementaryBuilder.java +++ b/example/src/main/java/org/javahelpers/simple/builders/example/ElementaryBuilder.java @@ -47,7 +47,8 @@ usingHashSetBuilderWithElementBuilders = OptionState.DISABLED, usingHashMapBuilder = OptionState.DISABLED, generateWithInterface = OptionState.DISABLED, - usingGeneratedAnnotation = OptionState.DISABLED)) + usingGeneratedAnnotation = OptionState.DISABLED, + usingJacksonDeserializerAnnotation = OptionState.DISABLED)) @Retention(RetentionPolicy.CLASS) @Target(ElementType.TYPE) public @interface ElementaryBuilder {} 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 c84a0e09..fa3cccdc 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 @@ -420,6 +420,7 @@ void resolveConfiguration_AllLayersTogether_CompleteChain() { usingHashMapBuilder = OptionState.DISABLED, usingGeneratedAnnotation = OptionState.DISABLED, usingBuilderImplementationAnnotation = OptionState.DISABLED, + usingJacksonDeserializerAnnotation = OptionState.DISABLED, builderSuffix = "MinimalBuilder", setterSuffix = "with" )) diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/JacksonSupportTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/JacksonSupportTest.java index 0b13b2a2..e77850dc 100644 --- a/processor/src/test/java/org/javahelpers/simple/builders/processor/JacksonSupportTest.java +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/JacksonSupportTest.java @@ -11,7 +11,7 @@ class JacksonSupportTest { @Test - void jacksonSupport_WhenEnabled_ShouldGenerateAnnotation() { + void usingJacksonDeserializerAnnotation_WhenEnabled_ShouldGenerateAnnotation() { // Given JavaFileObject source = ProcessorTestUtils.simpleBuilderClass( @@ -40,7 +40,8 @@ void jacksonSupport_WhenEnabled_ShouldGenerateAnnotation() { } @Test - void jacksonSupport_WhenEnabledWithCustomPrefix_ShouldGenerateAnnotationWithPrefix() { + void + usingJacksonDeserializerAnnotation_WhenEnabledWithCustomPrefix_ShouldGenerateAnnotationWithPrefix() { // Given JavaFileObject source = ProcessorTestUtils.simpleBuilderClass( @@ -69,7 +70,7 @@ void jacksonSupport_WhenEnabledWithCustomPrefix_ShouldGenerateAnnotationWithPref } @Test - void jacksonSupport_WhenDisabled_ShouldNotGenerateAnnotation() { + void usingJacksonDeserializerAnnotation_WhenDisabled_ShouldNotGenerateAnnotation() { // Given JavaFileObject source = ProcessorTestUtils.simpleBuilderClass( @@ -99,7 +100,7 @@ void jacksonSupport_WhenDisabled_ShouldNotGenerateAnnotation() { } @Test - void jacksonSupport_WhenNotConfigured_ShouldDefaultToDisabled() { + void usingJacksonDeserializerAnnotation_WhenNotConfigured_ShouldDefaultToDisabled() { // Given JavaFileObject source = ProcessorTestUtils.simpleBuilderClass( From 99ef07f2cab2f616822e28078401c56592b5288c Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Mon, 29 Dec 2025 00:48:03 +0100 Subject: [PATCH 05/17] Fixing missing annotation-Configuration --- docs/CONFIGURATION.md | 13 ++++++++++++- .../simple/builders/example/ElementaryBuilder.java | 4 ++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index c4e9ab92..605eb04a 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -749,9 +749,20 @@ Generate only essential builder methods: generateBuilderConsumer = OptionState.DISABLED, generateConditionalHelper = OptionState.DISABLED, generateVarArgsHelpers = OptionState.DISABLED, + generateStringFormatHelpers = OptionState.DISABLED, + generateAddToCollectionHelpers = OptionState.DISABLED, + generateUnboxedOptional = OptionState.DISABLED, + copyTypeAnnotations = OptionState.DISABLED, usingArrayListBuilder = OptionState.DISABLED, + usingArrayListBuilderWithElementBuilders = OptionState.DISABLED, + usingHashSetBuilder = OptionState.DISABLED, + usingHashSetBuilderWithElementBuilders = OptionState.DISABLED, usingHashMapBuilder = OptionState.DISABLED, - generateWithInterface = OptionState.DISABLED + generateWithInterface = OptionState.DISABLED, + usingGeneratedAnnotation = OptionState.DISABLED, + usingBuilderImplementationAnnotation = OptionState.DISABLED, + implementsBuilderBase = OptionState.DISABLED, + usingJacksonDeserializerAnnotation = OptionState.DISABLED ) public class MinimalDto { private String name; diff --git a/example/src/main/java/org/javahelpers/simple/builders/example/ElementaryBuilder.java b/example/src/main/java/org/javahelpers/simple/builders/example/ElementaryBuilder.java index 6e1cb475..88037b35 100644 --- a/example/src/main/java/org/javahelpers/simple/builders/example/ElementaryBuilder.java +++ b/example/src/main/java/org/javahelpers/simple/builders/example/ElementaryBuilder.java @@ -40,7 +40,9 @@ generateConditionalHelper = OptionState.DISABLED, generateVarArgsHelpers = OptionState.DISABLED, generateStringFormatHelpers = OptionState.DISABLED, + generateAddToCollectionHelpers = OptionState.DISABLED, generateUnboxedOptional = OptionState.DISABLED, + copyTypeAnnotations = OptionState.DISABLED, usingArrayListBuilder = OptionState.DISABLED, usingArrayListBuilderWithElementBuilders = OptionState.DISABLED, usingHashSetBuilder = OptionState.DISABLED, @@ -48,6 +50,8 @@ usingHashMapBuilder = OptionState.DISABLED, generateWithInterface = OptionState.DISABLED, usingGeneratedAnnotation = OptionState.DISABLED, + usingBuilderImplementationAnnotation = OptionState.DISABLED, + implementsBuilderBase = OptionState.DISABLED, usingJacksonDeserializerAnnotation = OptionState.DISABLED)) @Retention(RetentionPolicy.CLASS) @Target(ElementType.TYPE) From 97d7ccad246fa6d6b904e9acc3df5113b80c556b Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Mon, 29 Dec 2025 01:04:23 +0100 Subject: [PATCH 06/17] Fixing documentation for configurations --- README.md | 2 +- docs/CONFIGURATION.md | 114 ++++++++++++++++++++++++------------------ 2 files changed, 66 insertions(+), 50 deletions(-) diff --git a/README.md b/README.md index ec6736ce..a05054d5 100644 --- a/README.md +++ b/README.md @@ -269,7 +269,7 @@ The `With` interface provides type-safe setter methods that mirror the builder's Simple Builders provides extensive configuration options to customize the generated builder code. You can control: -- Field setter generation (Supplier, Provider, Builder patterns) +- Field setter generation (Supplier, Consumer, Builder patterns) - Conditional logic helpers - Access modifiers for builders and methods - Collection helper methods diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 605eb04a..ed0c2858 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -81,17 +81,25 @@ Create reusable configuration presets with custom template annotations: ```java @SimpleBuilder.Template(options = @SimpleBuilder.Options( - generateFieldSupplier = false, - generateFieldProvider = false, - generateBuilderProvider = false, - generateConditionalHelper = false, - generateVarArgsHelpers = false, - usingArrayListBuilder = false, - usingArrayListBuilderWithElementBuilders = false, - usingHashSetBuilder = false, - usingHashSetBuilderWithElementBuilders = false, - usingHashMapBuilder = false, - generateWithInterface = false + generateFieldSupplier = OptionState.DISABLED, + generateFieldConsumer = OptionState.DISABLED, + generateBuilderConsumer = OptionState.DISABLED, + generateConditionalHelper = OptionState.DISABLED, + generateVarArgsHelpers = OptionState.DISABLED, + generateStringFormatHelpers = OptionState.DISABLED, + generateAddToCollectionHelpers = OptionState.DISABLED, + generateUnboxedOptional = OptionState.DISABLED, + copyTypeAnnotations = OptionState.DISABLED, + usingArrayListBuilder = OptionState.DISABLED, + usingArrayListBuilderWithElementBuilders = OptionState.DISABLED, + usingHashSetBuilder = OptionState.DISABLED, + usingHashSetBuilderWithElementBuilders = OptionState.DISABLED, + usingHashMapBuilder = OptionState.DISABLED, + generateWithInterface = OptionState.DISABLED, + usingGeneratedAnnotation = OptionState.DISABLED, + usingBuilderImplementationAnnotation = OptionState.DISABLED, + implementsBuilderBase = OptionState.DISABLED, + usingJacksonDeserializerAnnotation = OptionState.DISABLED )) @Retention(RetentionPolicy.CLASS) @Target(ElementType.TYPE) @@ -742,27 +750,27 @@ public class PersonDto { Generate only essential builder methods: ```java -@SimpleBuilder -@SimpleBuilder.Options( - generateFieldSupplier = OptionState.DISABLED, - generateFieldConsumer = OptionState.DISABLED, - generateBuilderConsumer = OptionState.DISABLED, - generateConditionalHelper = OptionState.DISABLED, - generateVarArgsHelpers = OptionState.DISABLED, - generateStringFormatHelpers = OptionState.DISABLED, - generateAddToCollectionHelpers = OptionState.DISABLED, - generateUnboxedOptional = OptionState.DISABLED, - copyTypeAnnotations = OptionState.DISABLED, - usingArrayListBuilder = OptionState.DISABLED, - usingArrayListBuilderWithElementBuilders = OptionState.DISABLED, - usingHashSetBuilder = OptionState.DISABLED, - usingHashSetBuilderWithElementBuilders = OptionState.DISABLED, - usingHashMapBuilder = OptionState.DISABLED, - generateWithInterface = OptionState.DISABLED, - usingGeneratedAnnotation = OptionState.DISABLED, - usingBuilderImplementationAnnotation = OptionState.DISABLED, - implementsBuilderBase = OptionState.DISABLED, - usingJacksonDeserializerAnnotation = OptionState.DISABLED +@SimpleBuilder( + options = @SimpleBuilder.Options( + generateFieldSupplier = OptionState.DISABLED, + generateFieldConsumer = OptionState.DISABLED, + generateBuilderConsumer = OptionState.DISABLED, + generateConditionalHelper = OptionState.DISABLED, + generateVarArgsHelpers = OptionState.DISABLED, + generateStringFormatHelpers = OptionState.DISABLED, + generateAddToCollectionHelpers = OptionState.DISABLED, + generateUnboxedOptional = OptionState.DISABLED, + copyTypeAnnotations = OptionState.DISABLED, + usingArrayListBuilder = OptionState.DISABLED, + usingArrayListBuilderWithElementBuilders = OptionState.DISABLED, + usingHashSetBuilder = OptionState.DISABLED, + usingHashSetBuilderWithElementBuilders = OptionState.DISABLED, + usingHashMapBuilder = OptionState.DISABLED, + generateWithInterface = OptionState.DISABLED, + usingGeneratedAnnotation = OptionState.DISABLED, + usingBuilderImplementationAnnotation = OptionState.DISABLED, + implementsBuilderBase = OptionState.DISABLED, + usingJacksonDeserializerAnnotation = OptionState.DISABLED ) public class MinimalDto { private String name; @@ -801,11 +809,11 @@ Optimize for collection manipulation: ```java @SimpleBuilder @SimpleBuilder.Options( - generateVarArgsHelpers = true, - usingArrayListBuilder = true, - usingArrayListBuilderWithElementBuilders = true, - usingHashSetBuilder = true, - usingHashMapBuilder = true + generateVarArgsHelpers = OptionState.ENABLED, + usingArrayListBuilder = OptionState.ENABLED, + usingArrayListBuilderWithElementBuilders = OptionState.ENABLED, + usingHashSetBuilder = OptionState.ENABLED, + usingHashMapBuilder = OptionState.ENABLED ) public class TeamDto { private List memberNames; @@ -833,17 +841,25 @@ Create a reusable template for lightweight builders: ```java @SimpleBuilder.Template(options = @SimpleBuilder.Options( - generateFieldSupplier = false, - generateFieldProvider = false, - generateBuilderProvider = false, - generateConditionalHelper = false, - generateVarArgsHelpers = false, - usingArrayListBuilder = false, - usingArrayListBuilderWithElementBuilders = false, - usingHashSetBuilder = false, - usingHashSetBuilderWithElementBuilders = false, - usingHashMapBuilder = false, - generateWithInterface = false + generateFieldSupplier = OptionState.DISABLED, + generateFieldConsumer = OptionState.DISABLED, + generateBuilderConsumer = OptionState.DISABLED, + generateConditionalHelper = OptionState.DISABLED, + generateVarArgsHelpers = OptionState.DISABLED, + generateStringFormatHelpers = OptionState.DISABLED, + generateAddToCollectionHelpers = OptionState.DISABLED, + generateUnboxedOptional = OptionState.DISABLED, + copyTypeAnnotations = OptionState.DISABLED, + usingArrayListBuilder = OptionState.DISABLED, + usingArrayListBuilderWithElementBuilders = OptionState.DISABLED, + usingHashSetBuilder = OptionState.DISABLED, + usingHashSetBuilderWithElementBuilders = OptionState.DISABLED, + usingHashMapBuilder = OptionState.DISABLED, + generateWithInterface = OptionState.DISABLED, + usingGeneratedAnnotation = OptionState.DISABLED, + usingBuilderImplementationAnnotation = OptionState.DISABLED, + implementsBuilderBase = OptionState.DISABLED, + usingJacksonDeserializerAnnotation = OptionState.DISABLED )) @Retention(RetentionPolicy.CLASS) @Target(ElementType.TYPE) @@ -913,7 +929,7 @@ Configuration resolution follows these priority rules: // Global default: true @SimpleBuilder -@SimpleBuilder.Options(generateFieldSupplier = true) // Annotation wins! +@SimpleBuilder.Options(generateFieldSupplier = OptionState.ENABLED) // Annotation wins! public class Person { private String name; } From fe1e34c3dc85ac0109f9b267aa58dfb1b12cfb11 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Tue, 30 Dec 2025 18:37:50 +0100 Subject: [PATCH 07/17] Adding configurations and documentation for Jackson-Module generation --- README.md | 2 +- .../core/annotations/SimpleBuilder.java | 13 ++++ docs/CONFIGURATION.md | 37 +++++++++ .../processor/dtos/BuilderConfiguration.java | 23 ++++++ .../enums/CompilerArgumentsEnum.java | 3 + .../util/BuilderConfigurationReader.java | 2 + .../util/CompilerArgumentsReader.java | 1 + .../ConfigurationProcessingTest.java | 2 + .../processor/JacksonModuleWarningTest.java | 75 +++++++++++++++++++ 9 files changed, 157 insertions(+), 1 deletion(-) create mode 100644 processor/src/test/java/org/javahelpers/simple/builders/processor/JacksonModuleWarningTest.java diff --git a/README.md b/README.md index a05054d5..14e229d3 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ Simple Builders is a Java [annotation processor](https://docs.oracle.com/en/java - **Collections Support**: Built-in support for collections and maps - **Annotation Preservation**: Validation annotations are automatically copied to builder methods - **With Interface Pattern**: Type-safe object modifications using generated With interfaces -- **Jackson Support**: Supporting Jackson deserialization (needs to be enabled) +- **Jackson Support**: Supporting Jackson deserialization via `@JsonPOJOBuilder` and optional generation of `SimpleModule` (both need to be enabled) ## Requirements diff --git a/core/src/main/java/org/javahelpers/simple/builders/core/annotations/SimpleBuilder.java b/core/src/main/java/org/javahelpers/simple/builders/core/annotations/SimpleBuilder.java index cd79b0d2..6da4f015 100644 --- a/core/src/main/java/org/javahelpers/simple/builders/core/annotations/SimpleBuilder.java +++ b/core/src/main/java/org/javahelpers/simple/builders/core/annotations/SimpleBuilder.java @@ -589,6 +589,19 @@ */ OptionState usingJacksonDeserializerAnnotation() default OptionState.UNSET; + /** + * Generate a Jackson SimpleModule containing registrations for all generated builders.
+ * This module allows Jackson to use the generated builders for deserialization without needing + * to annotate the DTO classes. + * + *

The generated module class will be named {@code SimpleBuildersJacksonModule} (by default) + * and placed in the same package as the first processed DTO. + * + *

Default: DISABLED
+ * Compiler option: -Asimplebuilder.generateJacksonModule + */ + OptionState generateJacksonModule() default OptionState.UNSET; + // === Naming === /** * Suffix to append to the DTO name to generate the builder class name.
diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index ed0c2858..6ff3c2f7 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -709,6 +709,43 @@ public class PersonDtoBuilder { --- +#### `generateJacksonModule` + +**Default**: `DISABLED` | **Compiler Option**: `-Asimplebuilder.generateJacksonModule=ENABLED|DISABLED` + +Generates a Jackson `SimpleModule` (named `SimpleBuildersJacksonModule`) that registers all generated builders via MixIns. This allows deserialization without annotating your DTOs with `@JsonDeserialize`. + +**Requirement**: You MUST also enable [`usingJacksonDeserializerAnnotation`](#usingjacksondeserializerannotation). If `generateJacksonModule` is enabled but `usingJacksonDeserializerAnnotation` is disabled, the processor will issue a warning and skip module generation. + +**When ENABLED**: +1. The builder is generated as usual. +2. A `SimpleBuildersJacksonModule` class is generated in the package of the first processed DTO. +3. The module registers a MixIn for the DTO that points to the Builder. + +**Generated Module Example**: +```java +public class SimpleBuildersJacksonModule extends SimpleModule { + public SimpleBuildersJacksonModule() { + setMixInAnnotation(PersonDto.class, PersonDtoMixin.class); + } + + @JsonDeserialize(builder = PersonDtoBuilder.class) + private interface PersonDtoMixin {} +} +``` + +**Usage**: +```java +ObjectMapper mapper = new ObjectMapper(); +mapper.registerModule(new SimpleBuildersJacksonModule()); + +PersonDto dto = mapper.readValue(json, PersonDto.class); +``` + +**Note**: This requires `com.fasterxml.jackson.core:jackson-databind` on the classpath. + +--- + ### Naming #### `builderSuffix` diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderConfiguration.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderConfiguration.java index 97c5ec48..a36c0d1a 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderConfiguration.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderConfiguration.java @@ -87,6 +87,7 @@ public record BuilderConfiguration( OptionState implementsBuilderBase, OptionState generateWithInterface, OptionState usingJacksonDeserializerAnnotation, + OptionState generateJacksonModule, String builderSuffix, String setterSuffix) { @@ -114,6 +115,7 @@ public record BuilderConfiguration( .implementsBuilderBase(ENABLED) .generateWithInterface(ENABLED) .usingJacksonDeserializerAnnotation(DISABLED) + .generateJacksonModule(DISABLED) .builderSuffix("Builder") .setterSuffix("") .build(); @@ -143,6 +145,10 @@ public boolean shouldUseJacksonDeserializerAnnotation() { return usingJacksonDeserializerAnnotation == ENABLED; } + public boolean shouldGenerateJacksonModule() { + return generateJacksonModule == ENABLED; + } + public boolean shouldGenerateVarArgsHelpers() { return generateVarArgsHelpers == ENABLED; } @@ -277,6 +283,8 @@ public BuilderConfiguration merge(BuilderConfiguration other) { .usingJacksonDeserializerAnnotation( mergeOptionState( other.usingJacksonDeserializerAnnotation, this.usingJacksonDeserializerAnnotation)) + .generateJacksonModule( + mergeOptionState(other.generateJacksonModule, this.generateJacksonModule)) .builderSuffix(mergeString(other.builderSuffix, this.builderSuffix)) .setterSuffix(mergeString(other.setterSuffix, this.setterSuffix)) .build(); @@ -373,6 +381,9 @@ public String toString() { if (usingJacksonDeserializerAnnotation != UNSET) { builder.append("usingJacksonDeserializerAnnotation", usingJacksonDeserializerAnnotation); } + if (generateJacksonModule != UNSET) { + builder.append("generateJacksonModule", generateJacksonModule); + } if (builderSuffix != null && !builderSuffix.equals("Builder")) { builder.append("builderSuffix", builderSuffix); } @@ -419,6 +430,7 @@ public static class Builder { private OptionState implementsBuilderBase = OptionState.UNSET; private OptionState generateWithInterface = OptionState.UNSET; private OptionState usingJacksonDeserializerAnnotation = OptionState.UNSET; + private OptionState generateJacksonModule = OptionState.UNSET; // === Naming === private String builderSuffix = null; @@ -485,6 +497,16 @@ public Builder usingJacksonDeserializerAnnotation(boolean value) { return this; } + public Builder generateJacksonModule(OptionState value) { + this.generateJacksonModule = value; + return this; + } + + public Builder generateJacksonModule(boolean value) { + this.generateJacksonModule = value ? ENABLED : DISABLED; + return this; + } + public Builder generateVarArgsHelpers(OptionState value) { this.generateVarArgsHelpers = value; return this; @@ -679,6 +701,7 @@ public BuilderConfiguration build() { implementsBuilderBase, generateWithInterface, usingJacksonDeserializerAnnotation, + generateJacksonModule, builderSuffix, setterSuffix); } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/enums/CompilerArgumentsEnum.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/enums/CompilerArgumentsEnum.java index eab3c927..86c5bfb5 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/enums/CompilerArgumentsEnum.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/enums/CompilerArgumentsEnum.java @@ -110,6 +110,9 @@ public enum CompilerArgumentsEnum { /** Option for Jackson support. */ USING_JACKSON_DESERIALIZER_ANNOTATION("usingJacksonDeserializerAnnotation"), + /** Option for Jackson Module generation. */ + GENERATE_JACKSON_MODULE("generateJacksonModule"), + // === Naming === /** Option for builder class name suffix. */ BUILDER_SUFFIX("builderSuffix"), diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderConfigurationReader.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderConfigurationReader.java index 5415897c..0afbec00 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderConfigurationReader.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderConfigurationReader.java @@ -194,6 +194,8 @@ private BuilderConfiguration parseOptionsFromMirror(AnnotationMirror optionsMirr builder.generateWithInterface(OptionState.valueOf(enumValue)); case "usingJacksonDeserializerAnnotation" -> builder.usingJacksonDeserializerAnnotation(OptionState.valueOf(enumValue)); + case "generateJacksonModule" -> + builder.generateJacksonModule(OptionState.valueOf(enumValue)); case "builderSuffix" -> builder.builderSuffix(value.toString()); case "setterSuffix" -> builder.setterSuffix(value.toString()); default -> diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/CompilerArgumentsReader.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/CompilerArgumentsReader.java index e6ec33ae..cfa2bfe9 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/CompilerArgumentsReader.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/CompilerArgumentsReader.java @@ -171,6 +171,7 @@ public BuilderConfiguration readBuilderConfiguration() { .generateWithInterface(readOptionState(CompilerArgumentsEnum.GENERATE_WITH_INTERFACE)) .usingJacksonDeserializerAnnotation( readOptionState(CompilerArgumentsEnum.USING_JACKSON_DESERIALIZER_ANNOTATION)) + .generateJacksonModule(readOptionState(CompilerArgumentsEnum.GENERATE_JACKSON_MODULE)) .builderSuffix(readValue(CompilerArgumentsEnum.BUILDER_SUFFIX)) .setterSuffix(readValue(CompilerArgumentsEnum.SETTER_SUFFIX)) .build(); diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/ConfigurationProcessingTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/ConfigurationProcessingTest.java index 69f00876..4393af6f 100644 --- a/processor/src/test/java/org/javahelpers/simple/builders/processor/ConfigurationProcessingTest.java +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/ConfigurationProcessingTest.java @@ -80,6 +80,7 @@ void allConfigurationOptions_MustBeSettableViaBuilder() { .implementsBuilderBase(OptionState.ENABLED) .generateWithInterface(OptionState.ENABLED) .usingJacksonDeserializerAnnotation(OptionState.ENABLED) + .generateJacksonModule(OptionState.ENABLED) // Naming .builderSuffix("Builder") .setterSuffix("") @@ -108,6 +109,7 @@ void allConfigurationOptions_MustBeSettableViaBuilder() { assertEquals(OptionState.ENABLED, config.implementsBuilderBase()); assertEquals(OptionState.ENABLED, config.generateWithInterface()); assertEquals(OptionState.ENABLED, config.usingJacksonDeserializerAnnotation()); + assertEquals(OptionState.ENABLED, config.generateJacksonModule()); assertEquals("Builder", config.getBuilderSuffix()); assertEquals("", config.getSetterSuffix()); } diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/JacksonModuleWarningTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/JacksonModuleWarningTest.java new file mode 100644 index 00000000..3f50f40e --- /dev/null +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/JacksonModuleWarningTest.java @@ -0,0 +1,75 @@ +package org.javahelpers.simple.builders.processor; + +import static com.google.testing.compile.CompilationSubject.assertThat; + +import com.google.testing.compile.Compilation; +import javax.tools.JavaFileObject; +import org.javahelpers.simple.builders.processor.testing.ProcessorTestUtils; +import org.junit.jupiter.api.Test; + +class JacksonModuleWarningTest { + + @Test + void generateJacksonModule_WhenEnabledButDeserializerDisabled_ShouldWarnAndNotGenerate() { + // Given + JavaFileObject dto = + ProcessorTestUtils.simpleBuilderClass( + "test", + "WarningDto", + """ + private String name; + public String getName() { return name; } + public void setName(String name) { this.name = name; } + """); + + // When + Compilation compilation = + ProcessorTestUtils.createCompiler() + .withOptions( + "-Asimplebuilder.generateJacksonModule=true", + "-Asimplebuilder.usingJacksonDeserializerAnnotation=false") + .compile(dto); + + // Then + assertThat(compilation).succeeded(); + assertThat(compilation) + .hadWarningContaining( + "simple-builders: generateJacksonModule is enabled but usingJacksonDeserializerAnnotation is disabled"); + + // Verify file is NOT generated + try { + ProcessorTestUtils.loadGeneratedSource(compilation, "SimpleBuildersJacksonModule"); + throw new AssertionError("SimpleBuildersJacksonModule should not have been generated"); + } catch (AssertionError e) { + // Expected + } + } + + @Test + void generateJacksonModule_WhenEnabledAndDeserializerEnabled_ShouldGenerate() { + // Given + JavaFileObject dto = + ProcessorTestUtils.simpleBuilderClass( + "test", + "SuccessDto", + """ + private String name; + public String getName() { return name; } + public void setName(String name) { this.name = name; } + """); + + // When + Compilation compilation = + ProcessorTestUtils.createCompiler() + .withOptions( + "-Asimplebuilder.generateJacksonModule=true", + "-Asimplebuilder.usingJacksonDeserializerAnnotation=true") + .compile(dto); + + // Then + assertThat(compilation).succeeded(); + + // Verify file IS generated + ProcessorTestUtils.loadGeneratedSource(compilation, "SimpleBuildersJacksonModule"); + } +} From 1b111bd0ae58d3d36ef7e0c04529d3f4f4c13c2f Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Tue, 30 Dec 2025 20:45:53 +0100 Subject: [PATCH 08/17] Adding a configuration and documentation for the package where the JacksonModule should be positioned --- README.md | 2 +- .../core/annotations/SimpleBuilder.java | 19 +++++++++-- docs/CONFIGURATION.md | 34 +++++++++++++++++-- .../example/SimpleBuildersJacksonModule.java | 16 +++++++++ .../processor/dtos/BuilderConfiguration.java | 17 ++++++++++ .../enums/CompilerArgumentsEnum.java | 3 ++ .../util/BuilderConfigurationReader.java | 1 + .../util/CompilerArgumentsReader.java | 1 + 8 files changed, 88 insertions(+), 5 deletions(-) create mode 100644 example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/SimpleBuildersJacksonModule.java diff --git a/README.md b/README.md index 14e229d3..0c9558e7 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ Simple Builders is a Java [annotation processor](https://docs.oracle.com/en/java - **Collections Support**: Built-in support for collections and maps - **Annotation Preservation**: Validation annotations are automatically copied to builder methods - **With Interface Pattern**: Type-safe object modifications using generated With interfaces -- **Jackson Support**: Supporting Jackson deserialization via `@JsonPOJOBuilder` and optional generation of `SimpleModule` (both need to be enabled) +- **Jackson Support**: Supporting Jackson deserialization via `@JsonPOJOBuilder` and optional generation of `SimpleModule`s (one per package) (both need to be enabled) ## Requirements diff --git a/core/src/main/java/org/javahelpers/simple/builders/core/annotations/SimpleBuilder.java b/core/src/main/java/org/javahelpers/simple/builders/core/annotations/SimpleBuilder.java index 6da4f015..ccc79376 100644 --- a/core/src/main/java/org/javahelpers/simple/builders/core/annotations/SimpleBuilder.java +++ b/core/src/main/java/org/javahelpers/simple/builders/core/annotations/SimpleBuilder.java @@ -594,14 +594,29 @@ * This module allows Jackson to use the generated builders for deserialization without needing * to annotate the DTO classes. * - *

The generated module class will be named {@code SimpleBuildersJacksonModule} (by default) - * and placed in the same package as the first processed DTO. + *

The generated module class will be named {@code SimpleBuildersJacksonModule} (by default). + * By default, a module is generated in each package containing processed DTOs. To group + * all registrations into a single module, use {@link #jacksonModulePackage()}. * *

Default: DISABLED
* Compiler option: -Asimplebuilder.generateJacksonModule */ OptionState generateJacksonModule() default OptionState.UNSET; + /** + * Specifies the package name where the {@code SimpleBuildersJacksonModule} class will be + * generated.
+ * This is useful to avoid split-package issues or to group all module registrations into a + * single module. + * + *

If not specified, a separate module will be generated in each package containing + * processed DTOs. + * + *

Default: "" (empty - generate one module per package)
+ * Compiler option: -Asimplebuilder.jacksonModulePackage + */ + String jacksonModulePackage() default ""; + // === Naming === /** * Suffix to append to the DTO name to generate the builder class name.
diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 6ff3c2f7..372f524c 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -719,11 +719,20 @@ Generates a Jackson `SimpleModule` (named `SimpleBuildersJacksonModule`) that re **When ENABLED**: 1. The builder is generated as usual. -2. A `SimpleBuildersJacksonModule` class is generated in the package of the first processed DTO. +2. A `SimpleBuildersJacksonModule` class is generated. 3. The module registers a MixIn for the DTO that points to the Builder. +**Package Name**: +By default, a `SimpleBuildersJacksonModule` is generated in **each package** that contains DTOs configured for Jackson module generation. This ensures deterministic behavior. +To specify a single fixed package name for all generated modules (grouping them into one), use the [`jacksonModulePackage`](#jacksonmodulepackage) option. + **Generated Module Example**: ```java +package com.example.project.dto; // Generated in the same package as DTOs (by default) + +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; + public class SimpleBuildersJacksonModule extends SimpleModule { public SimpleBuildersJacksonModule() { setMixInAnnotation(PersonDto.class, PersonDtoMixin.class); @@ -737,15 +746,36 @@ public class SimpleBuildersJacksonModule extends SimpleModule { **Usage**: ```java ObjectMapper mapper = new ObjectMapper(); -mapper.registerModule(new SimpleBuildersJacksonModule()); + +// Register the module for your package +// Note: If you have DTOs in multiple packages and use the default strategy, +// you need to register the generated module for each package. +mapper.registerModule(new com.example.project.dto.SimpleBuildersJacksonModule()); PersonDto dto = mapper.readValue(json, PersonDto.class); ``` +**Tip**: Use the [`jacksonModulePackage`](#jacksonmodulepackage) option to generate a single module for your entire project, making registration easier: +`mapper.registerModule(new com.example.project.config.SimpleBuildersJacksonModule());` + **Note**: This requires `com.fasterxml.jackson.core:jackson-databind` on the classpath. --- +#### `jacksonModulePackage` + +**Default**: `null` (uses package of each processed DTO) | **Compiler Option**: `-Asimplebuilder.jacksonModulePackage=com.your.package` + +Specifies the package name where the `SimpleBuildersJacksonModule` class will be generated. +This is highly recommended to ensure deterministic output location and avoid split-package issues. + +**Note**: If not specified, a separate `SimpleBuildersJacksonModule` will be generated in **each package** containing processed DTOs. + +**Example**: +`-Asimplebuilder.jacksonModulePackage=com.example.project.config` + +--- + ### Naming #### `builderSuffix` 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 new file mode 100644 index 00000000..5766b3ae --- /dev/null +++ b/example/target/generated-sources/annotations/org/javahelpers/simple/builders/example/SimpleBuildersJacksonModule.java @@ -0,0 +1,16 @@ +package org.javahelpers.simple.builders.example; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.module.SimpleModule; + +public class SimpleBuildersJacksonModule extends SimpleModule { + public SimpleBuildersJacksonModule() { + setMixInAnnotation(JacksonIntegrationDto.class, JacksonIntegrationDtoMixin.class); + } + + @JsonDeserialize( + builder = JacksonIntegrationDtoBuilder.class + ) + private interface JacksonIntegrationDtoMixin { + } +} diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderConfiguration.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderConfiguration.java index a36c0d1a..71de08f9 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderConfiguration.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderConfiguration.java @@ -88,6 +88,7 @@ public record BuilderConfiguration( OptionState generateWithInterface, OptionState usingJacksonDeserializerAnnotation, OptionState generateJacksonModule, + String jacksonModulePackage, String builderSuffix, String setterSuffix) { @@ -116,6 +117,7 @@ public record BuilderConfiguration( .generateWithInterface(ENABLED) .usingJacksonDeserializerAnnotation(DISABLED) .generateJacksonModule(DISABLED) + .jacksonModulePackage(null) .builderSuffix("Builder") .setterSuffix("") .build(); @@ -214,6 +216,10 @@ public AccessModifier getMethodAccess() { return methodAccess; } + public String getJacksonModulePackage() { + return jacksonModulePackage; + } + public String getBuilderSuffix() { return builderSuffix; } @@ -285,6 +291,7 @@ public BuilderConfiguration merge(BuilderConfiguration other) { other.usingJacksonDeserializerAnnotation, this.usingJacksonDeserializerAnnotation)) .generateJacksonModule( mergeOptionState(other.generateJacksonModule, this.generateJacksonModule)) + .jacksonModulePackage(mergeString(other.jacksonModulePackage, this.jacksonModulePackage)) .builderSuffix(mergeString(other.builderSuffix, this.builderSuffix)) .setterSuffix(mergeString(other.setterSuffix, this.setterSuffix)) .build(); @@ -384,6 +391,9 @@ public String toString() { if (generateJacksonModule != UNSET) { builder.append("generateJacksonModule", generateJacksonModule); } + if (jacksonModulePackage != null) { + builder.append("jacksonModulePackage", jacksonModulePackage); + } if (builderSuffix != null && !builderSuffix.equals("Builder")) { builder.append("builderSuffix", builderSuffix); } @@ -431,6 +441,7 @@ public static class Builder { private OptionState generateWithInterface = OptionState.UNSET; private OptionState usingJacksonDeserializerAnnotation = OptionState.UNSET; private OptionState generateJacksonModule = OptionState.UNSET; + private String jacksonModulePackage = null; // === Naming === private String builderSuffix = null; @@ -507,6 +518,11 @@ public Builder generateJacksonModule(boolean value) { return this; } + public Builder jacksonModulePackage(String value) { + this.jacksonModulePackage = value == null ? null : value.trim(); + return this; + } + public Builder generateVarArgsHelpers(OptionState value) { this.generateVarArgsHelpers = value; return this; @@ -702,6 +718,7 @@ public BuilderConfiguration build() { generateWithInterface, usingJacksonDeserializerAnnotation, generateJacksonModule, + jacksonModulePackage, builderSuffix, setterSuffix); } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/enums/CompilerArgumentsEnum.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/enums/CompilerArgumentsEnum.java index 86c5bfb5..0957e6d5 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/enums/CompilerArgumentsEnum.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/enums/CompilerArgumentsEnum.java @@ -113,6 +113,9 @@ public enum CompilerArgumentsEnum { /** Option for Jackson Module generation. */ GENERATE_JACKSON_MODULE("generateJacksonModule"), + /** Option for Jackson Module package name. */ + JACKSON_MODULE_PACKAGE("jacksonModulePackage"), + // === Naming === /** Option for builder class name suffix. */ BUILDER_SUFFIX("builderSuffix"), diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderConfigurationReader.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderConfigurationReader.java index 0afbec00..1cfcc8d5 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderConfigurationReader.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/BuilderConfigurationReader.java @@ -196,6 +196,7 @@ private BuilderConfiguration parseOptionsFromMirror(AnnotationMirror optionsMirr builder.usingJacksonDeserializerAnnotation(OptionState.valueOf(enumValue)); case "generateJacksonModule" -> builder.generateJacksonModule(OptionState.valueOf(enumValue)); + case "jacksonModulePackage" -> builder.jacksonModulePackage(value.toString()); case "builderSuffix" -> builder.builderSuffix(value.toString()); case "setterSuffix" -> builder.setterSuffix(value.toString()); default -> diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/CompilerArgumentsReader.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/CompilerArgumentsReader.java index cfa2bfe9..51bd87aa 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/CompilerArgumentsReader.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/CompilerArgumentsReader.java @@ -172,6 +172,7 @@ public BuilderConfiguration readBuilderConfiguration() { .usingJacksonDeserializerAnnotation( readOptionState(CompilerArgumentsEnum.USING_JACKSON_DESERIALIZER_ANNOTATION)) .generateJacksonModule(readOptionState(CompilerArgumentsEnum.GENERATE_JACKSON_MODULE)) + .jacksonModulePackage(readValue(CompilerArgumentsEnum.JACKSON_MODULE_PACKAGE)) .builderSuffix(readValue(CompilerArgumentsEnum.BUILDER_SUFFIX)) .setterSuffix(readValue(CompilerArgumentsEnum.SETTER_SUFFIX)) .build(); From a1bc29d90386a269ca9246cbda34b8178ce241bd Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Tue, 30 Dec 2025 20:56:37 +0100 Subject: [PATCH 09/17] Adding jackson examples to example-project --- example/pom.xml | 8 +++++ .../example/JacksonIntegrationDto.java | 30 +++++++++++++++++++ .../example/JacksonIntegrationTest.java | 27 +++++++++++++++++ 3 files changed, 65 insertions(+) create mode 100644 example/src/main/java/org/javahelpers/simple/builders/example/JacksonIntegrationDto.java create mode 100644 example/src/test/java/org/javahelpers/simple/builders/example/JacksonIntegrationTest.java diff --git a/example/pom.xml b/example/pom.xml index 50d19132..e58291dc 100644 --- a/example/pom.xml +++ b/example/pom.xml @@ -17,6 +17,7 @@ 2.25 3.18.0 5.11.4 + 2.18.2 3.13.0 3.1.1 @@ -49,6 +50,13 @@ test + + + com.fasterxml.jackson.core + jackson-databind + ${jackson-databind.version} + + diff --git a/example/src/main/java/org/javahelpers/simple/builders/example/JacksonIntegrationDto.java b/example/src/main/java/org/javahelpers/simple/builders/example/JacksonIntegrationDto.java new file mode 100644 index 00000000..8873d45d --- /dev/null +++ b/example/src/main/java/org/javahelpers/simple/builders/example/JacksonIntegrationDto.java @@ -0,0 +1,30 @@ +package org.javahelpers.simple.builders.example; + +import org.javahelpers.simple.builders.core.annotations.SimpleBuilder; +import org.javahelpers.simple.builders.core.enums.OptionState; + +@SimpleBuilder( + options = + @SimpleBuilder.Options( + generateJacksonModule = OptionState.ENABLED, + usingJacksonDeserializerAnnotation = OptionState.ENABLED)) +public class JacksonIntegrationDto { + private String name; + private int age; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int getAge() { + return age; + } + + public void setAge(int age) { + this.age = age; + } +} diff --git a/example/src/test/java/org/javahelpers/simple/builders/example/JacksonIntegrationTest.java b/example/src/test/java/org/javahelpers/simple/builders/example/JacksonIntegrationTest.java new file mode 100644 index 00000000..a03df4a7 --- /dev/null +++ b/example/src/test/java/org/javahelpers/simple/builders/example/JacksonIntegrationTest.java @@ -0,0 +1,27 @@ +package org.javahelpers.simple.builders.example; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +class JacksonIntegrationTest { + + @Test + void shouldDeserializeUsingGeneratedModule() throws Exception { + // Given + ObjectMapper mapper = new ObjectMapper(); + // Register the generated module + mapper.registerModule(new SimpleBuildersJacksonModule()); + + String json = "{\"name\":\"Alice\",\"age\":30}"; + + // When + JacksonIntegrationDto dto = mapper.readValue(json, JacksonIntegrationDto.class); + + // Then + assertNotNull(dto); + assertEquals("Alice", dto.getName()); + assertEquals(30, dto.getAge()); + } +} From d009a97ceff44bcc71e0c045a3e822cad6ac9934 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Tue, 30 Dec 2025 20:59:20 +0100 Subject: [PATCH 10/17] Implementation of Jackson Module generation --- .../builders/processor/BuilderProcessor.java | 14 ++ .../util/JacksonModuleGenerator.java | 195 ++++++++++++++++++ .../JacksonModuleMultiPackageTest.java | 150 ++++++++++++++ .../builders/processor/JacksonModuleTest.java | 92 +++++++++ .../processor/JacksonModuleWarningTest.java | 32 +-- 5 files changed, 467 insertions(+), 16 deletions(-) create mode 100644 processor/src/main/java/org/javahelpers/simple/builders/processor/util/JacksonModuleGenerator.java create mode 100644 processor/src/test/java/org/javahelpers/simple/builders/processor/JacksonModuleMultiPackageTest.java create mode 100644 processor/src/test/java/org/javahelpers/simple/builders/processor/JacksonModuleTest.java 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 5380ed75..1e3db629 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 @@ -47,6 +47,7 @@ import org.javahelpers.simple.builders.processor.exceptions.BuilderException; import org.javahelpers.simple.builders.processor.util.BuilderConfigurationReader; import org.javahelpers.simple.builders.processor.util.CompilerArgumentsReader; +import org.javahelpers.simple.builders.processor.util.JacksonModuleGenerator; import org.javahelpers.simple.builders.processor.util.JavaCodeGenerator; import org.javahelpers.simple.builders.processor.util.ProcessingContext; import org.javahelpers.simple.builders.processor.util.ProcessingLogger; @@ -61,6 +62,7 @@ public class BuilderProcessor extends AbstractProcessor { private ProcessingContext context; private JavaCodeGenerator codeGenerator; + private JacksonModuleGenerator jacksonModuleGenerator; private boolean supportedJdk = true; @Override @@ -78,6 +80,9 @@ public synchronized void init(ProcessingEnvironment processingEnv) { context.debug("Loaded global configuration from compiler arguments: %s", globalConfig); this.codeGenerator = new JavaCodeGenerator(processingEnv.getFiler(), processingEnv.getElementUtils(), logger); + this.jacksonModuleGenerator = + new JacksonModuleGenerator( + processingEnv.getFiler(), processingEnv.getElementUtils(), logger); SourceVersion current = processingEnv.getSourceVersion(); this.supportedJdk = isAtLeastJava17(current); @@ -95,6 +100,12 @@ public boolean process(Set annotations, RoundEnvironment return false; } + // Generate Jackson Module if processing is over and feature is enabled + if (roundEnv.processingOver()) { + jacksonModuleGenerator.generate(); + return false; + } + BuilderConfigurationReader reader = context.getConfigurationReader(); // Find all elements to process: @@ -164,6 +175,9 @@ private void process(Element annotatedElement, BuilderConfiguration config) context.initConfigurationForProcessingTarget(config); BuilderDefinitionDto builderDef = extractFromElement(annotatedElement, context); codeGenerator.generateBuilder(builderDef); + + // Collect info for Jackson Module if enabled + jacksonModuleGenerator.addEntry(builderDef, annotatedElement); } /** diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JacksonModuleGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JacksonModuleGenerator.java new file mode 100644 index 00000000..c260ba60 --- /dev/null +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JacksonModuleGenerator.java @@ -0,0 +1,195 @@ +/* + * MIT License + * + * Copyright (c) 2025 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.util; + +import com.palantir.javapoet.AnnotationSpec; +import com.palantir.javapoet.ClassName; +import com.palantir.javapoet.JavaFile; +import com.palantir.javapoet.MethodSpec; +import com.palantir.javapoet.TypeSpec; +import java.io.IOException; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import javax.annotation.processing.Filer; +import javax.lang.model.element.Element; +import javax.lang.model.element.Modifier; +import javax.lang.model.util.Elements; +import org.javahelpers.simple.builders.processor.dtos.BuilderConfiguration; +import org.javahelpers.simple.builders.processor.dtos.BuilderDefinitionDto; +import org.javahelpers.simple.builders.processor.dtos.TypeName; + +/** + * Generates Jackson SimpleModules to register all generated builders. + * + *

The generated modules include MixIn interfaces to map DTOs to their Builders using + * {@code @JsonDeserialize(builder = ...)}. One module is generated per target package. + */ +public class JacksonModuleGenerator { + + private static final String MODULE_CLASS_NAME = "SimpleBuildersJacksonModule"; + private final Filer filer; + private final Elements elementUtils; + private final ProcessingLogger logger; + private final Map> entriesByPackage = new HashMap<>(); + private final boolean jacksonAvailable; + private boolean enabled = false; + + public JacksonModuleGenerator(Filer filer, Elements elementUtils, ProcessingLogger logger) { + this.filer = filer; + this.elementUtils = elementUtils; + this.logger = logger; + this.jacksonAvailable = + this.elementUtils.getTypeElement("com.fasterxml.jackson.databind.module.SimpleModule") + != null; + } + + public void addEntry(BuilderDefinitionDto builderDef, Element sourceElement) { + BuilderConfiguration config = builderDef.getConfiguration(); + + if (!config.shouldGenerateJacksonModule()) { + return; + } + + if (!jacksonAvailable) { + logger.warning( + sourceElement, + "simple-builders: generateJacksonModule is enabled for %s, but Jackson dependencies (com.fasterxml.jackson.databind.module.SimpleModule) are not found on the classpath. Module generation skipped.", + sourceElement.getSimpleName()); + return; + } + + if (!config.shouldUseJacksonDeserializerAnnotation()) { + logger.warning( + sourceElement, + "simple-builders: generateJacksonModule is enabled but usingJacksonDeserializerAnnotation is disabled. " + + "This is a misconfiguration. Jackson Module will NOT be generated for %s.", + sourceElement.getSimpleName()); + return; + } + + String targetPackage = config.getJacksonModulePackage(); + if (targetPackage == null) { + targetPackage = builderDef.getBuildingTargetTypeName().getPackageName(); + } + + this.enabled = true; + entriesByPackage + .computeIfAbsent(targetPackage, k -> new HashSet<>()) + .add( + new JacksonModuleEntry( + builderDef.getBuildingTargetTypeName(), builderDef.getBuilderTypeName())); + } + + /** + * Generates the Jackson Modules containing registrations for the accumulated builders. + * + *

Logs status and clears state after generation. + */ + public void generate() { + logger.info( + "simple-builders: Processing OVER. JacksonModuleEnabled: %s, Packages: %d", + enabled, entriesByPackage.size()); + + if (enabled && !entriesByPackage.isEmpty()) { + for (Map.Entry> entry : entriesByPackage.entrySet()) { + String packageName = entry.getKey(); + Set moduleEntries = entry.getValue(); + + try { + generateModuleFile(packageName, moduleEntries); + } catch (Exception e) { + logger.error( + "simple-builders: Error generating Jackson module for package %s: %s", + packageName, e.getMessage()); + e.printStackTrace(); + } + } + } + clear(); + } + + private void clear() { + entriesByPackage.clear(); + enabled = false; + } + + private void generateModuleFile(String targetPackage, Set entries) { + logger.info("Generating Jackson Module '%s' in package '%s'", MODULE_CLASS_NAME, targetPackage); + + 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(MODULE_CLASS_NAME) + .addModifiers(Modifier.PUBLIC) + .superclass(simpleModuleClass); + + for (JacksonModuleEntry entry : entries) { + 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 { + JavaFile.builder(targetPackage, classBuilder.build()).build().writeTo(filer); + } catch (IOException e) { + logger.error("Failed to write Jackson Module: %s", e.getMessage()); + } + } + + public record JacksonModuleEntry(TypeName dtoType, TypeName builderType) {} +} diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/JacksonModuleMultiPackageTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/JacksonModuleMultiPackageTest.java new file mode 100644 index 00000000..c46f5775 --- /dev/null +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/JacksonModuleMultiPackageTest.java @@ -0,0 +1,150 @@ +package org.javahelpers.simple.builders.processor; + +import static com.google.testing.compile.CompilationSubject.assertThat; + +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; + +class JacksonModuleMultiPackageTest { + + @Test + void generateJacksonModule_WithMultiplePackages_ShouldGenerateMultipleModules() { + // Given: Two DTOs in different packages, default configuration + JavaFileObject dto1 = + ProcessorTestUtils.simpleBuilderClass( + "pkg.one", + "DtoOne", + """ + private String name; + public String getName() { return name; } + public void setName(String name) { this.name = name; } + """); + + JavaFileObject dto2 = + ProcessorTestUtils.simpleBuilderClass( + "pkg.two", + "DtoTwo", + """ + private int value; + public int getValue() { return value; } + public void setValue(int value) { this.value = value; } + """); + + // When: Compile with Jackson module generation enabled, no global package set + Compilation compilation = + ProcessorTestUtils.createCompiler() + .withOptions( + "-Asimplebuilder.generateJacksonModule=true", + "-Asimplebuilder.usingJacksonDeserializerAnnotation=true") + .compile(dto1, dto2); + + // Then: Should generate TWO modules, one in each package + assertThat(compilation).succeeded(); + + // Verify module in pkg.one + assertThat(compilation) + .generatedSourceFile("pkg.one.SimpleBuildersJacksonModule") + .hasSourceEquivalentTo( + JavaFileObjects.forSourceString( + "pkg.one.SimpleBuildersJacksonModule", + """ + package pkg.one; + import com.fasterxml.jackson.databind.annotation.JsonDeserialize; + import com.fasterxml.jackson.databind.module.SimpleModule; + public class SimpleBuildersJacksonModule extends SimpleModule { + public SimpleBuildersJacksonModule() { + setMixInAnnotation(DtoOne.class, DtoOneMixin.class); + } + @JsonDeserialize(builder = DtoOneBuilder.class) + private interface DtoOneMixin {} + } + """)); + + // Verify module in pkg.two + assertThat(compilation) + .generatedSourceFile("pkg.two.SimpleBuildersJacksonModule") + .hasSourceEquivalentTo( + JavaFileObjects.forSourceString( + "pkg.two.SimpleBuildersJacksonModule", + """ + package pkg.two; + import com.fasterxml.jackson.databind.annotation.JsonDeserialize; + import com.fasterxml.jackson.databind.module.SimpleModule; + public class SimpleBuildersJacksonModule extends SimpleModule { + public SimpleBuildersJacksonModule() { + setMixInAnnotation(DtoTwo.class, DtoTwoMixin.class); + } + @JsonDeserialize(builder = DtoTwoBuilder.class) + private interface DtoTwoMixin {} + } + """)); + } + + @Test + void generateJacksonModule_WithAnnotationOverride_ShouldRespectOverride() { + // Given: DTO with specific package override via annotation + // Note: We simulate this by having one DTO use default (its package) and another potentially + // using a different one if we could mocking the annotation value, + // but here we test the "default package" vs "configured package" behavior. + // Actually, testing annotation override specifically requires a DTO with + // @SimpleBuilder.Options(jacksonModulePackage="...") + // But SimpleBuilder.Options doesn't expose jacksonModulePackage yet in the annotation + // definition? + // Wait, we added it to BuilderConfiguration but did we add it to the @SimpleBuilder.Options + // annotation? + // Checking SimpleBuilder.java... + + // We need to construct the source file manually to include the annotation with options + // because simpleBuilderClass doesn't support adding options easily + JavaFileObject sourceWithAnnotation = + ProcessorTestUtils.forSource( + """ + package pkg.source; + + import org.javahelpers.simple.builders.core.annotations.SimpleBuilder; + import org.javahelpers.simple.builders.core.enums.OptionState; + + @SimpleBuilder(options = @SimpleBuilder.Options( + generateJacksonModule = OptionState.ENABLED, + usingJacksonDeserializerAnnotation = OptionState.ENABLED, + jacksonModulePackage = "pkg.target" + )) + public class DtoWithOverride { + private String name; + public String getName() { return name; } + public void setName(String name) { this.name = name; } + } + """); + + // When + Compilation compilation = ProcessorTestUtils.createCompiler().compile(sourceWithAnnotation); + + // Then + assertThat(compilation).succeeded(); + + // Should generate in pkg.target, NOT pkg.source + assertThat(compilation) + .generatedSourceFile("pkg.target.SimpleBuildersJacksonModule") + .hasSourceEquivalentTo( + JavaFileObjects.forSourceString( + "pkg.target.SimpleBuildersJacksonModule", + """ + package pkg.target; + import com.fasterxml.jackson.databind.annotation.JsonDeserialize; + import com.fasterxml.jackson.databind.module.SimpleModule; + import pkg.source.DtoWithOverride; + import pkg.source.DtoWithOverrideBuilder; + + public class SimpleBuildersJacksonModule extends SimpleModule { + public SimpleBuildersJacksonModule() { + setMixInAnnotation(DtoWithOverride.class, DtoWithOverrideMixin.class); + } + @JsonDeserialize(builder = DtoWithOverrideBuilder.class) + private interface DtoWithOverrideMixin {} + } + """)); + } +} diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/JacksonModuleTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/JacksonModuleTest.java new file mode 100644 index 00000000..52e57297 --- /dev/null +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/JacksonModuleTest.java @@ -0,0 +1,92 @@ +package org.javahelpers.simple.builders.processor; + +import static com.google.testing.compile.CompilationSubject.assertThat; + +import com.google.testing.compile.Compilation; +import javax.tools.JavaFileObject; +import org.javahelpers.simple.builders.processor.testing.ProcessorAsserts; +import org.javahelpers.simple.builders.processor.testing.ProcessorTestUtils; +import org.junit.jupiter.api.Test; + +class JacksonModuleTest { + + @Test + void generateJacksonModule_WhenEnabled_ShouldGenerateModuleClass() { + // Given + JavaFileObject dto1 = + ProcessorTestUtils.simpleBuilderClass( + "test", + "FirstDto", + """ + private String name; + public String getName() { return name; } + public void setName(String name) { this.name = name; } + """); + + JavaFileObject dto2 = + ProcessorTestUtils.simpleBuilderClass( + "test", + "SecondDto", + """ + private int age; + public int getAge() { return age; } + public void setAge(int age) { this.age = age; } + """); + + // When + Compilation compilation = + ProcessorTestUtils.createCompiler() + .withOptions( + "-Asimplebuilder.generateJacksonModule=true", + "-Asimplebuilder.usingJacksonDeserializerAnnotation=true") + .compile(dto1, dto2); + + // Then + assertThat(compilation).succeeded(); + + // Check for generated module class + // It should be in the same package as the first DTO ("test") + String generatedModule = + ProcessorTestUtils.loadGeneratedSource(compilation, "SimpleBuildersJacksonModule"); + + ProcessorAsserts.assertContaining( + generatedModule, + "package test;", + "public class SimpleBuildersJacksonModule extends SimpleModule", + "setMixInAnnotation(FirstDto.class, FirstDtoMixin.class)", + "setMixInAnnotation(SecondDto.class, SecondDtoMixin.class)", + "@JsonDeserialize(builder = FirstDtoBuilder.class)", + "@JsonDeserialize(builder = SecondDtoBuilder.class)"); + } + + @Test + void generateJacksonModule_WhenDisabled_ShouldNotGenerateModuleClass() { + // Given + JavaFileObject dto = + ProcessorTestUtils.simpleBuilderClass( + "test", + "ModuleDisabledDto", + """ + private String name; + public String getName() { return name; } + public void setName(String name) { this.name = name; } + """); + + // When + Compilation compilation = + ProcessorTestUtils.createCompiler() + .withOptions("-Asimplebuilder.generateJacksonModule=false") + .compile(dto); + + // Then + assertThat(compilation).succeeded(); + + // Verify file is NOT generated + try { + ProcessorTestUtils.loadGeneratedSource(compilation, "SimpleBuildersJacksonModule"); + throw new AssertionError("SimpleBuildersJacksonModule should not have been generated"); + } catch (AssertionError e) { + // Expected + } + } +} diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/JacksonModuleWarningTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/JacksonModuleWarningTest.java index 3f50f40e..7f871006 100644 --- a/processor/src/test/java/org/javahelpers/simple/builders/processor/JacksonModuleWarningTest.java +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/JacksonModuleWarningTest.java @@ -1,6 +1,8 @@ package org.javahelpers.simple.builders.processor; import static com.google.testing.compile.CompilationSubject.assertThat; +import static org.javahelpers.simple.builders.processor.testing.ProcessorTestUtils.createCompiler; +import static org.javahelpers.simple.builders.processor.testing.ProcessorTestUtils.simpleBuilderClass; import com.google.testing.compile.Compilation; import javax.tools.JavaFileObject; @@ -10,12 +12,12 @@ class JacksonModuleWarningTest { @Test - void generateJacksonModule_WhenEnabledButDeserializerDisabled_ShouldWarnAndNotGenerate() { + void generateJacksonModuleEnabled_ButAnnotationDisabled_ShouldEmitWarningAndSkipGeneration() { // Given JavaFileObject dto = - ProcessorTestUtils.simpleBuilderClass( - "test", - "WarningDto", + simpleBuilderClass( + "pkg.test", + "MisconfiguredDto", """ private String name; public String getName() { return name; } @@ -23,26 +25,24 @@ void generateJacksonModule_WhenEnabledButDeserializerDisabled_ShouldWarnAndNotGe """); // When + // Enable generateJacksonModule but NOT usingJacksonDeserializerAnnotation (which defaults to + // DISABLED) Compilation compilation = - ProcessorTestUtils.createCompiler() - .withOptions( - "-Asimplebuilder.generateJacksonModule=true", - "-Asimplebuilder.usingJacksonDeserializerAnnotation=false") - .compile(dto); + createCompiler().withOptions("-Asimplebuilder.generateJacksonModule=true").compile(dto); // Then assertThat(compilation).succeeded(); + + // Should have a warning assertThat(compilation) .hadWarningContaining( "simple-builders: generateJacksonModule is enabled but usingJacksonDeserializerAnnotation is disabled"); - // Verify file is NOT generated - try { - ProcessorTestUtils.loadGeneratedSource(compilation, "SimpleBuildersJacksonModule"); - throw new AssertionError("SimpleBuildersJacksonModule should not have been generated"); - } catch (AssertionError e) { - // Expected - } + // Should NOT have generated the Jackson module + // We verify this by ensuring the warning is present, which implies the generation branch was + // skipped. + // Testing explicitly for "file not generated" is tricky with compile-testing without complex + // custom assertions. } @Test From 90a8ec948d076b7dc768f2c861178ce3b5975da8 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Tue, 30 Dec 2025 21:18:56 +0100 Subject: [PATCH 11/17] Replacing Map> by HashSetValuedHashMap in JacksonModuleGenerator --- .../util/JacksonModuleGenerator.java | 24 +++++++++---------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JacksonModuleGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JacksonModuleGenerator.java index c260ba60..74c39d38 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JacksonModuleGenerator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JacksonModuleGenerator.java @@ -30,14 +30,13 @@ import com.palantir.javapoet.MethodSpec; import com.palantir.javapoet.TypeSpec; import java.io.IOException; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; import java.util.Set; import javax.annotation.processing.Filer; import javax.lang.model.element.Element; import javax.lang.model.element.Modifier; import javax.lang.model.util.Elements; +import org.apache.commons.collections4.SetValuedMap; +import org.apache.commons.collections4.multimap.HashSetValuedHashMap; import org.javahelpers.simple.builders.processor.dtos.BuilderConfiguration; import org.javahelpers.simple.builders.processor.dtos.BuilderDefinitionDto; import org.javahelpers.simple.builders.processor.dtos.TypeName; @@ -54,7 +53,8 @@ public class JacksonModuleGenerator { private final Filer filer; private final Elements elementUtils; private final ProcessingLogger logger; - private final Map> entriesByPackage = new HashMap<>(); + private final SetValuedMap entriesByPackage = + new HashSetValuedHashMap<>(); private final boolean jacksonAvailable; private boolean enabled = false; @@ -97,11 +97,10 @@ public void addEntry(BuilderDefinitionDto builderDef, Element sourceElement) { } this.enabled = true; - entriesByPackage - .computeIfAbsent(targetPackage, k -> new HashSet<>()) - .add( - new JacksonModuleEntry( - builderDef.getBuildingTargetTypeName(), builderDef.getBuilderTypeName())); + entriesByPackage.put( + targetPackage, + new JacksonModuleEntry( + builderDef.getBuildingTargetTypeName(), builderDef.getBuilderTypeName())); } /** @@ -112,12 +111,11 @@ public void addEntry(BuilderDefinitionDto builderDef, Element sourceElement) { public void generate() { logger.info( "simple-builders: Processing OVER. JacksonModuleEnabled: %s, Packages: %d", - enabled, entriesByPackage.size()); + enabled, entriesByPackage.keySet().size()); if (enabled && !entriesByPackage.isEmpty()) { - for (Map.Entry> entry : entriesByPackage.entrySet()) { - String packageName = entry.getKey(); - Set moduleEntries = entry.getValue(); + for (String packageName : entriesByPackage.keySet()) { + Set moduleEntries = entriesByPackage.get(packageName); try { generateModuleFile(packageName, moduleEntries); From 5db9aff875d3dab24ef4ab55ca3b2b52797e20f6 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Tue, 30 Dec 2025 21:46:24 +0100 Subject: [PATCH 12/17] Refactoring JacksonModuleGenerator for code quality and separating generation of class from extracting functionality --- .../builders/processor/BuilderProcessor.java | 8 +- .../dtos/JacksonModuleDefinitionDto.java | 58 +++++++ .../processor/dtos/JacksonModuleEntryDto.java | 33 ++++ .../util/JacksonModuleGenerator.java | 149 ++++++------------ .../processor/util/JavaCodeGenerator.java | 78 ++++++++- 5 files changed, 218 insertions(+), 108 deletions(-) create mode 100644 processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/JacksonModuleDefinitionDto.java create mode 100644 processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/JacksonModuleEntryDto.java 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 1e3db629..042db372 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 @@ -81,8 +81,7 @@ public synchronized void init(ProcessingEnvironment processingEnv) { this.codeGenerator = new JavaCodeGenerator(processingEnv.getFiler(), processingEnv.getElementUtils(), logger); this.jacksonModuleGenerator = - new JacksonModuleGenerator( - processingEnv.getFiler(), processingEnv.getElementUtils(), logger); + new JacksonModuleGenerator(processingEnv.getElementUtils(), logger); SourceVersion current = processingEnv.getSourceVersion(); this.supportedJdk = isAtLeastJava17(current); @@ -102,7 +101,10 @@ public boolean process(Set annotations, RoundEnvironment // Generate Jackson Module if processing is over and feature is enabled if (roundEnv.processingOver()) { - jacksonModuleGenerator.generate(); + var modules = jacksonModuleGenerator.getModuleDefinitions(); + for (var module : modules) { + codeGenerator.generateJacksonModule(module); + } return false; } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/JacksonModuleDefinitionDto.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/JacksonModuleDefinitionDto.java new file mode 100644 index 00000000..b1acfdce --- /dev/null +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/JacksonModuleDefinitionDto.java @@ -0,0 +1,58 @@ +/* + * MIT License + * + * Copyright (c) 2025 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.dtos; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; + +/** + * Definition for generating a Jackson SimpleModule. Contains the target package and the entries to + * be registered in the module. + */ +public class JacksonModuleDefinitionDto { + private final String targetPackage; + private final Set entries = new HashSet<>(); + + public JacksonModuleDefinitionDto(String targetPackage) { + this.targetPackage = targetPackage; + } + + public String getTargetPackage() { + return targetPackage; + } + + public Set getEntries() { + return entries; + } + + public void addEntry(JacksonModuleEntryDto entry) { + this.entries.add(entry); + } + + public void addAllEntries(Collection entries) { + this.entries.addAll(entries); + } +} diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/JacksonModuleEntryDto.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/JacksonModuleEntryDto.java new file mode 100644 index 00000000..53a236a9 --- /dev/null +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/JacksonModuleEntryDto.java @@ -0,0 +1,33 @@ +/* + * MIT License + * + * Copyright (c) 2025 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.dtos; + +/** + * Represents an entry for a Jackson module registration. Maps a DTO type to its builder type. + * + * @param dtoType the TypeName of the DTO + * @param builderType the TypeName of the builder + */ +public record JacksonModuleEntryDto(TypeName dtoType, TypeName builderType) {} diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JacksonModuleGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JacksonModuleGenerator.java index 74c39d38..fdf36740 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JacksonModuleGenerator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JacksonModuleGenerator.java @@ -24,22 +24,17 @@ package org.javahelpers.simple.builders.processor.util; -import com.palantir.javapoet.AnnotationSpec; -import com.palantir.javapoet.ClassName; -import com.palantir.javapoet.JavaFile; -import com.palantir.javapoet.MethodSpec; -import com.palantir.javapoet.TypeSpec; -import java.io.IOException; +import java.util.ArrayList; +import java.util.List; import java.util.Set; -import javax.annotation.processing.Filer; import javax.lang.model.element.Element; -import javax.lang.model.element.Modifier; import javax.lang.model.util.Elements; import org.apache.commons.collections4.SetValuedMap; import org.apache.commons.collections4.multimap.HashSetValuedHashMap; import org.javahelpers.simple.builders.processor.dtos.BuilderConfiguration; import org.javahelpers.simple.builders.processor.dtos.BuilderDefinitionDto; -import org.javahelpers.simple.builders.processor.dtos.TypeName; +import org.javahelpers.simple.builders.processor.dtos.JacksonModuleDefinitionDto; +import org.javahelpers.simple.builders.processor.dtos.JacksonModuleEntryDto; /** * Generates Jackson SimpleModules to register all generated builders. @@ -49,17 +44,13 @@ */ public class JacksonModuleGenerator { - private static final String MODULE_CLASS_NAME = "SimpleBuildersJacksonModule"; - private final Filer filer; private final Elements elementUtils; private final ProcessingLogger logger; - private final SetValuedMap entriesByPackage = + private final SetValuedMap entriesByPackage = new HashSetValuedHashMap<>(); private final boolean jacksonAvailable; - private boolean enabled = false; - public JacksonModuleGenerator(Filer filer, Elements elementUtils, ProcessingLogger logger) { - this.filer = filer; + public JacksonModuleGenerator(Elements elementUtils, ProcessingLogger logger) { this.elementUtils = elementUtils; this.logger = logger; this.jacksonAvailable = @@ -70,16 +61,37 @@ public JacksonModuleGenerator(Filer filer, Elements elementUtils, ProcessingLogg public void addEntry(BuilderDefinitionDto builderDef, Element sourceElement) { BuilderConfiguration config = builderDef.getConfiguration(); - if (!config.shouldGenerateJacksonModule()) { + if (!validateForModuleGeneration(config, sourceElement)) { return; } + String targetPackage = getTargetPackage(config, builderDef); + + entriesByPackage.put( + targetPackage, + new JacksonModuleEntryDto( + builderDef.getBuildingTargetTypeName(), builderDef.getBuilderTypeName())); + } + + private String getTargetPackage(BuilderConfiguration config, BuilderDefinitionDto builderDef) { + String targetPackage = config.getJacksonModulePackage(); + if (targetPackage == null) { + targetPackage = builderDef.getBuildingTargetTypeName().getPackageName(); + } + return targetPackage; + } + + private boolean validateForModuleGeneration(BuilderConfiguration config, Element sourceElement) { + if (!config.shouldGenerateJacksonModule()) { + return false; + } + if (!jacksonAvailable) { logger.warning( sourceElement, "simple-builders: generateJacksonModule is enabled for %s, but Jackson dependencies (com.fasterxml.jackson.databind.module.SimpleModule) are not found on the classpath. Module generation skipped.", sourceElement.getSimpleName()); - return; + return false; } if (!config.shouldUseJacksonDeserializerAnnotation()) { @@ -88,106 +100,37 @@ public void addEntry(BuilderDefinitionDto builderDef, Element sourceElement) { "simple-builders: generateJacksonModule is enabled but usingJacksonDeserializerAnnotation is disabled. " + "This is a misconfiguration. Jackson Module will NOT be generated for %s.", sourceElement.getSimpleName()); - return; + return false; } - - String targetPackage = config.getJacksonModulePackage(); - if (targetPackage == null) { - targetPackage = builderDef.getBuildingTargetTypeName().getPackageName(); - } - - this.enabled = true; - entriesByPackage.put( - targetPackage, - new JacksonModuleEntry( - builderDef.getBuildingTargetTypeName(), builderDef.getBuilderTypeName())); + return true; } /** - * Generates the Jackson Modules containing registrations for the accumulated builders. + * Returns the definitions for the Jackson Modules to be generated. * - *

Logs status and clears state after generation. + *

Logs status and clears state after retrieval. + * + * @return list of JacksonModuleDefinitionDto */ - public void generate() { - logger.info( - "simple-builders: Processing OVER. JacksonModuleEnabled: %s, Packages: %d", - enabled, entriesByPackage.keySet().size()); + public List getModuleDefinitions() { + List definitions = new ArrayList<>(); + if (!entriesByPackage.isEmpty()) { + logger.info( + "simple-builders: Processing OVER. JacksonModuleEnabled: true, Packages: %d", + entriesByPackage.keySet().size()); - if (enabled && !entriesByPackage.isEmpty()) { for (String packageName : entriesByPackage.keySet()) { - Set moduleEntries = entriesByPackage.get(packageName); - - try { - generateModuleFile(packageName, moduleEntries); - } catch (Exception e) { - logger.error( - "simple-builders: Error generating Jackson module for package %s: %s", - packageName, e.getMessage()); - e.printStackTrace(); - } + Set moduleEntries = entriesByPackage.get(packageName); + JacksonModuleDefinitionDto definition = new JacksonModuleDefinitionDto(packageName); + definition.addAllEntries(moduleEntries); + definitions.add(definition); } } clear(); + return definitions; } private void clear() { entriesByPackage.clear(); - enabled = false; - } - - private void generateModuleFile(String targetPackage, Set entries) { - logger.info("Generating Jackson Module '%s' in package '%s'", MODULE_CLASS_NAME, targetPackage); - - 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(MODULE_CLASS_NAME) - .addModifiers(Modifier.PUBLIC) - .superclass(simpleModuleClass); - - for (JacksonModuleEntry entry : entries) { - 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 { - JavaFile.builder(targetPackage, classBuilder.build()).build().writeTo(filer); - } catch (IOException e) { - logger.error("Failed to write Jackson Module: %s", e.getMessage()); - } } - - public record JacksonModuleEntry(TypeName dtoType, TypeName builderType) {} } diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavaCodeGenerator.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavaCodeGenerator.java index a214f692..db6856c5 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavaCodeGenerator.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/util/JavaCodeGenerator.java @@ -229,12 +229,13 @@ public void generateBuilder(BuilderDefinitionDto builderDef) throws BuilderExcep "Writing builder class to file: %s.%s", builderDef.getBuilderTypeName().getPackageName(), builderDef.getBuilderTypeName().getClassName()); - writeClassToFile(builderDef.getBuilderTypeName().getPackageName(), classBuilder.build()); + writeBuilderClassToFile(builderDef.getBuilderTypeName().getPackageName(), classBuilder.build()); logger.debug( "Successfully generated builder: %s", builderDef.getBuilderTypeName().getClassName()); } - private void writeClassToFile(String packageName, TypeSpec typeSpec) throws BuilderException { + private void writeBuilderClassToFile(String packageName, TypeSpec typeSpec) + throws BuilderException { try { JavaFile.builder(packageName, typeSpec) .skipJavaLangImports(true) @@ -248,6 +249,15 @@ private void writeClassToFile(String packageName, TypeSpec typeSpec) throws Buil } } + private void writeSimpleClassToFile(String packageName, TypeSpec typeSpec) + throws BuilderException { + try { + JavaFile.builder(packageName, typeSpec).skipJavaLangImports(true).build().writeTo(filer); + } catch (IOException ex) { + throw new BuilderException(null, ex); + } + } + /** * Resolves method conflicts by keeping only the highest priority method for each unique * signature. This prevents compilation errors when methods from different fields have the same @@ -727,4 +737,68 @@ private ParameterSpec createParameter(MethodParameterDto paramDto) { } return paramBuilder.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())); + } + } } From c838a93b7e8bd135085b63b23e72a6f2c38cf752 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Tue, 30 Dec 2025 22:01:56 +0100 Subject: [PATCH 13/17] Fixing BookDtoBuilder, the ElementalBuilder should have only elementary builder-functions --- .../builders/example/BookDtoBuilder.java | 46 +------------------ 1 file changed, 1 insertion(+), 45 deletions(-) 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 3b8dbb0b..704c96fd 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 @@ -7,25 +7,18 @@ import java.math.BigDecimal; import java.time.LocalDate; import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import org.apache.commons.lang3.builder.ToStringBuilder; -import org.javahelpers.simple.builders.core.annotations.BuilderImplementation; -import org.javahelpers.simple.builders.core.interfaces.IBuilderBase; import org.javahelpers.simple.builders.core.util.BuilderToStringStyle; import org.javahelpers.simple.builders.core.util.TrackedValue; /** * Builder for {@code org.javahelpers.simple.builders.example.BookDto}. */ -@BuilderImplementation( - forClass = BookDto.class -) -public class BookDtoBuilder implements IBuilderBase { +public class BookDtoBuilder { /** * Tracked value for title: the book title to set. */ @@ -178,42 +171,6 @@ public BookDtoBuilder(BookDto instance) { public BookDtoBuilder() { } - /** - * Adds a single element to genres. - * - * @param element the element to add - * @return current instance of builder - */ - public BookDtoBuilder add2Genres(String element) { - Set newCollection; - if (this.genres.isSet()) { - newCollection = new HashSet<>(this.genres.value()); - } else { - newCollection = new HashSet<>(); - } - newCollection.add(element); - this.genres = changedValue(newCollection); - return this; - } - - /** - * Adds a single element to tags. - * - * @param element the element to add - * @return current instance of builder - */ - public BookDtoBuilder add2Tags(String element) { - List newCollection; - if (this.tags.isSet()) { - newCollection = new ArrayList<>(this.tags.value()); - } else { - newCollection = new ArrayList<>(); - } - newCollection.add(element); - this.tags = changedValue(newCollection); - return this; - } - /** * Sets the value for author. * @@ -423,7 +380,6 @@ public BookDtoBuilder title(String title) { return this; } - @Override public BookDto build() { if (this.pages.isSet() && this.pages.value() == null) { throw new IllegalStateException("Field 'pages' is marked as non-null but null value was provided"); From 2f7ce38fc9e3ce650013b05dc48384d4f2ab96c8 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Tue, 30 Dec 2025 22:18:41 +0100 Subject: [PATCH 14/17] Refactoring JacksonModuleMultiPackageTest to use the test-util functions --- .../JacksonModuleMultiPackageTest.java | 112 +++++++++--------- 1 file changed, 57 insertions(+), 55 deletions(-) diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/JacksonModuleMultiPackageTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/JacksonModuleMultiPackageTest.java index c46f5775..12ef4b70 100644 --- a/processor/src/test/java/org/javahelpers/simple/builders/processor/JacksonModuleMultiPackageTest.java +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/JacksonModuleMultiPackageTest.java @@ -1,9 +1,9 @@ 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 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; @@ -45,42 +45,44 @@ void generateJacksonModule_WithMultiplePackages_ShouldGenerateMultipleModules() assertThat(compilation).succeeded(); // Verify module in pkg.one - assertThat(compilation) - .generatedSourceFile("pkg.one.SimpleBuildersJacksonModule") - .hasSourceEquivalentTo( - JavaFileObjects.forSourceString( - "pkg.one.SimpleBuildersJacksonModule", - """ - package pkg.one; - import com.fasterxml.jackson.databind.annotation.JsonDeserialize; - import com.fasterxml.jackson.databind.module.SimpleModule; - public class SimpleBuildersJacksonModule extends SimpleModule { - public SimpleBuildersJacksonModule() { - setMixInAnnotation(DtoOne.class, DtoOneMixin.class); - } - @JsonDeserialize(builder = DtoOneBuilder.class) - private interface DtoOneMixin {} - } - """)); + JavaFileObject module1 = + compilation.generatedSourceFile("pkg.one.SimpleBuildersJacksonModule").orElseThrow(); + String content1; + try { + content1 = module1.getCharContent(false).toString(); + } catch (Exception e) { + content1 = module1.toString(); + } + assertContaining( + content1, + "package pkg.one;", + "import com.fasterxml.jackson.databind.annotation.JsonDeserialize;", + "import com.fasterxml.jackson.databind.module.SimpleModule;", + "public class SimpleBuildersJacksonModule extends SimpleModule", + "setMixInAnnotation(DtoOne.class, DtoOneMixin.class)", + "@JsonDeserialize", + "builder = DtoOneBuilder.class", + "private interface DtoOneMixin"); // Verify module in pkg.two - assertThat(compilation) - .generatedSourceFile("pkg.two.SimpleBuildersJacksonModule") - .hasSourceEquivalentTo( - JavaFileObjects.forSourceString( - "pkg.two.SimpleBuildersJacksonModule", - """ - package pkg.two; - import com.fasterxml.jackson.databind.annotation.JsonDeserialize; - import com.fasterxml.jackson.databind.module.SimpleModule; - public class SimpleBuildersJacksonModule extends SimpleModule { - public SimpleBuildersJacksonModule() { - setMixInAnnotation(DtoTwo.class, DtoTwoMixin.class); - } - @JsonDeserialize(builder = DtoTwoBuilder.class) - private interface DtoTwoMixin {} - } - """)); + JavaFileObject module2 = + compilation.generatedSourceFile("pkg.two.SimpleBuildersJacksonModule").orElseThrow(); + String content2; + try { + content2 = module2.getCharContent(false).toString(); + } catch (Exception e) { + content2 = module2.toString(); + } + assertContaining( + content2, + "package pkg.two;", + "import com.fasterxml.jackson.databind.annotation.JsonDeserialize;", + "import com.fasterxml.jackson.databind.module.SimpleModule;", + "public class SimpleBuildersJacksonModule extends SimpleModule", + "setMixInAnnotation(DtoTwo.class, DtoTwoMixin.class)", + "@JsonDeserialize", + "builder = DtoTwoBuilder.class", + "private interface DtoTwoMixin"); } @Test @@ -126,25 +128,25 @@ public class DtoWithOverride { assertThat(compilation).succeeded(); // Should generate in pkg.target, NOT pkg.source - assertThat(compilation) - .generatedSourceFile("pkg.target.SimpleBuildersJacksonModule") - .hasSourceEquivalentTo( - JavaFileObjects.forSourceString( - "pkg.target.SimpleBuildersJacksonModule", - """ - package pkg.target; - import com.fasterxml.jackson.databind.annotation.JsonDeserialize; - import com.fasterxml.jackson.databind.module.SimpleModule; - import pkg.source.DtoWithOverride; - import pkg.source.DtoWithOverrideBuilder; - - public class SimpleBuildersJacksonModule extends SimpleModule { - public SimpleBuildersJacksonModule() { - setMixInAnnotation(DtoWithOverride.class, DtoWithOverrideMixin.class); - } - @JsonDeserialize(builder = DtoWithOverrideBuilder.class) - private interface DtoWithOverrideMixin {} - } - """)); + JavaFileObject module3 = + compilation.generatedSourceFile("pkg.target.SimpleBuildersJacksonModule").orElseThrow(); + String content3; + try { + content3 = module3.getCharContent(false).toString(); + } catch (Exception e) { + content3 = module3.toString(); + } + assertContaining( + content3, + "package pkg.target;", + "import com.fasterxml.jackson.databind.annotation.JsonDeserialize;", + "import com.fasterxml.jackson.databind.module.SimpleModule;", + "import pkg.source.DtoWithOverride;", + "import pkg.source.DtoWithOverrideBuilder;", + "public class SimpleBuildersJacksonModule extends SimpleModule", + "setMixInAnnotation(DtoWithOverride.class, DtoWithOverrideMixin.class)", + "@JsonDeserialize", + "builder = DtoWithOverrideBuilder.class", + "private interface DtoWithOverrideMixin"); } } From 5770886c31630626e3c7acd944085e349032998a Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Tue, 30 Dec 2025 22:51:21 +0100 Subject: [PATCH 15/17] Updating JacksonIntegrationTest to show the SimpleBuildersJacksonModule working correctly --- .../example/JacksonIntegrationDto.java | 20 ++++++------- .../example/JacksonIntegrationTest.java | 28 +++++++++++++++++-- 2 files changed, 34 insertions(+), 14 deletions(-) diff --git a/example/src/main/java/org/javahelpers/simple/builders/example/JacksonIntegrationDto.java b/example/src/main/java/org/javahelpers/simple/builders/example/JacksonIntegrationDto.java index 8873d45d..f0f7e190 100644 --- a/example/src/main/java/org/javahelpers/simple/builders/example/JacksonIntegrationDto.java +++ b/example/src/main/java/org/javahelpers/simple/builders/example/JacksonIntegrationDto.java @@ -9,22 +9,20 @@ generateJacksonModule = OptionState.ENABLED, usingJacksonDeserializerAnnotation = OptionState.ENABLED)) public class JacksonIntegrationDto { - private String name; - private int age; + private final String name; + private final int age; - public String getName() { - return name; - } - - public void setName(String name) { + // Protected constructor - Jackson can't access this, but the builder can + protected JacksonIntegrationDto(String name, int age) { this.name = name; + this.age = age; } - public int getAge() { - return age; + public String name() { + return name; } - public void setAge(int age) { - this.age = age; + public int age() { + return age; } } diff --git a/example/src/test/java/org/javahelpers/simple/builders/example/JacksonIntegrationTest.java b/example/src/test/java/org/javahelpers/simple/builders/example/JacksonIntegrationTest.java index a03df4a7..cb2d08ff 100644 --- a/example/src/test/java/org/javahelpers/simple/builders/example/JacksonIntegrationTest.java +++ b/example/src/test/java/org/javahelpers/simple/builders/example/JacksonIntegrationTest.java @@ -1,14 +1,36 @@ package org.javahelpers.simple.builders.example; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.exc.InvalidDefinitionException; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; class JacksonIntegrationTest { @Test - void shouldDeserializeUsingGeneratedModule() throws Exception { + void shouldFailWithoutJacksonModule() throws Exception { + // Given + ObjectMapper mapper = new ObjectMapper(); + // NOTE: NOT registering the generated module + + String json = "{\"name\":\"Alice\",\"age\":30}"; + + // When & Then + InvalidDefinitionException exception = assertThrows( + InvalidDefinitionException.class, + () -> mapper.readValue(json, JacksonIntegrationDto.class) + ); + + // Should fail because Jackson can't access protected constructor + assertNotNull(exception.getMessage()); + assertTrue(exception.getMessage().contains("no Creators, like default constructor, exist")); + } + + @Test + void shouldSucceedWithJacksonModule() throws Exception { // Given ObjectMapper mapper = new ObjectMapper(); // Register the generated module @@ -21,7 +43,7 @@ void shouldDeserializeUsingGeneratedModule() throws Exception { // Then assertNotNull(dto); - assertEquals("Alice", dto.getName()); - assertEquals(30, dto.getAge()); + assertEquals("Alice", dto.name()); + assertEquals(30, dto.age()); } } From 470f6ad1310846aeaec02f8941464eb3b363f4e9 Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Tue, 30 Dec 2025 23:10:53 +0100 Subject: [PATCH 16/17] Refactoring for code quality --- .../processor/dtos/BuilderConfiguration.java | 3 +- .../JacksonModuleMultiPackageTest.java | 37 ++++++++----------- 2 files changed, 17 insertions(+), 23 deletions(-) diff --git a/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderConfiguration.java b/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderConfiguration.java index 71de08f9..87def7aa 100644 --- a/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderConfiguration.java +++ b/processor/src/main/java/org/javahelpers/simple/builders/processor/dtos/BuilderConfiguration.java @@ -27,6 +27,7 @@ import static org.javahelpers.simple.builders.core.enums.AccessModifier.*; import static org.javahelpers.simple.builders.core.enums.OptionState.*; +import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.lang3.builder.ToStringStyle; import org.javahelpers.simple.builders.core.annotations.SimpleBuilder; @@ -519,7 +520,7 @@ public Builder generateJacksonModule(boolean value) { } public Builder jacksonModulePackage(String value) { - this.jacksonModulePackage = value == null ? null : value.trim(); + this.jacksonModulePackage = StringUtils.trimToNull(value); return this; } diff --git a/processor/src/test/java/org/javahelpers/simple/builders/processor/JacksonModuleMultiPackageTest.java b/processor/src/test/java/org/javahelpers/simple/builders/processor/JacksonModuleMultiPackageTest.java index 12ef4b70..3cd791e5 100644 --- a/processor/src/test/java/org/javahelpers/simple/builders/processor/JacksonModuleMultiPackageTest.java +++ b/processor/src/test/java/org/javahelpers/simple/builders/processor/JacksonModuleMultiPackageTest.java @@ -10,6 +10,14 @@ class JacksonModuleMultiPackageTest { + private static String extractContent(JavaFileObject fileObject) { + try { + return fileObject.getCharContent(false).toString(); + } catch (Exception e) { + return fileObject.toString(); + } + } + @Test void generateJacksonModule_WithMultiplePackages_ShouldGenerateMultipleModules() { // Given: Two DTOs in different packages, default configuration @@ -47,14 +55,9 @@ void generateJacksonModule_WithMultiplePackages_ShouldGenerateMultipleModules() // Verify module in pkg.one JavaFileObject module1 = compilation.generatedSourceFile("pkg.one.SimpleBuildersJacksonModule").orElseThrow(); - String content1; - try { - content1 = module1.getCharContent(false).toString(); - } catch (Exception e) { - content1 = module1.toString(); - } + String pkgOneContent = extractContent(module1); assertContaining( - content1, + pkgOneContent, "package pkg.one;", "import com.fasterxml.jackson.databind.annotation.JsonDeserialize;", "import com.fasterxml.jackson.databind.module.SimpleModule;", @@ -67,14 +70,9 @@ void generateJacksonModule_WithMultiplePackages_ShouldGenerateMultipleModules() // Verify module in pkg.two JavaFileObject module2 = compilation.generatedSourceFile("pkg.two.SimpleBuildersJacksonModule").orElseThrow(); - String content2; - try { - content2 = module2.getCharContent(false).toString(); - } catch (Exception e) { - content2 = module2.toString(); - } + String pkgTwoContent = extractContent(module2); assertContaining( - content2, + pkgTwoContent, "package pkg.two;", "import com.fasterxml.jackson.databind.annotation.JsonDeserialize;", "import com.fasterxml.jackson.databind.module.SimpleModule;", @@ -127,17 +125,12 @@ public class DtoWithOverride { // Then assertThat(compilation).succeeded(); - // Should generate in pkg.target, NOT pkg.source + // Verify module in pkg.target (not pkg.source) JavaFileObject module3 = compilation.generatedSourceFile("pkg.target.SimpleBuildersJacksonModule").orElseThrow(); - String content3; - try { - content3 = module3.getCharContent(false).toString(); - } catch (Exception e) { - content3 = module3.toString(); - } + String pkgTargetContent = extractContent(module3); assertContaining( - content3, + pkgTargetContent, "package pkg.target;", "import com.fasterxml.jackson.databind.annotation.JsonDeserialize;", "import com.fasterxml.jackson.databind.module.SimpleModule;", From ae1acb1d6fac4f7447b22b79b590835cc4ae17ff Mon Sep 17 00:00:00 2001 From: AndreasIgel Date: Tue, 30 Dec 2025 23:12:49 +0100 Subject: [PATCH 17/17] Removing unneeded Exception --- .../simple/builders/example/JacksonIntegrationTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/src/test/java/org/javahelpers/simple/builders/example/JacksonIntegrationTest.java b/example/src/test/java/org/javahelpers/simple/builders/example/JacksonIntegrationTest.java index cb2d08ff..f5859f29 100644 --- a/example/src/test/java/org/javahelpers/simple/builders/example/JacksonIntegrationTest.java +++ b/example/src/test/java/org/javahelpers/simple/builders/example/JacksonIntegrationTest.java @@ -11,7 +11,7 @@ class JacksonIntegrationTest { @Test - void shouldFailWithoutJacksonModule() throws Exception { + void shouldFailWithoutJacksonModule() { // Given ObjectMapper mapper = new ObjectMapper(); // NOTE: NOT registering the generated module