Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
2196f9c
Adding a testclass with expectations of the result
AndreasIgel Apr 23, 2026
4af38e4
Finalizing 1st implementation of javadoc generation
AndreasIgel Apr 28, 2026
e682566
Using exampleChainFragment for JavaDoc-Example of method and class
AndreasIgel May 5, 2026
773442f
Improving code which adds exampleCodeFragment by doing it with Method…
AndreasIgel May 5, 2026
09fc62a
Improving handling of templates in generation of example values
AndreasIgel May 5, 2026
ea59804
Adding generation of example blocks
AndreasIgel Jul 31, 2026
4906a64
Extending documentations in helper functions of builders
AndreasIgel Jul 31, 2026
7f39eff
Extending example code to be generated for non-default classes too. F…
AndreasIgel Jul 31, 2026
8f35c46
Refactoring code structure to have specific classes for builder- and …
AndreasIgel Aug 2, 2026
e4801eb
Adding source documentation in javadoc
AndreasIgel Aug 2, 2026
5ad9d79
Refactoring conflict resolution, it is done now primary in BuilderDef…
AndreasIgel Aug 2, 2026
8cbd565
Implementation of Default-Value support
AndreasIgel Aug 4, 2026
749a499
Adding a test for validation of defaultValue handling when creating b…
AndreasIgel Aug 5, 2026
ec1f637
Adding example files for Default-Values
AndreasIgel Aug 5, 2026
c0b81a8
Refactoring code for better reading of it
AndreasIgel Aug 5, 2026
7ba604b
Adding usage in FeatureIntegrationTest
AndreasIgel Aug 5, 2026
6a83cd1
Merge branch 'main' into AndreasIgel/issue227
devin-ai-integration[bot] Aug 5, 2026
1a2b0f6
Adding generated source to building context
AndreasIgel Aug 5, 2026
537f726
Fixing codeformat
AndreasIgel Aug 5, 2026
7fd1eb9
Undo unneeded changes
AndreasIgel Aug 5, 2026
e4fedb8
Fix Sonar findings in default value support
devin-ai-integration[bot] Aug 5, 2026
6910a4b
Improving code quality
AndreasIgel Aug 5, 2026
d0c43e6
Merge branch 'AndreasIgel/issue227' of https://github.com/java-helper…
AndreasIgel Aug 5, 2026
6b1ea92
Increasing code quality and coverage topics
AndreasIgel Aug 5, 2026
99b3236
Improving codequality
AndreasIgel Aug 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,50 @@ ProductBuilder.create()
The `NotNull`/`NonNull` simple-name check is framework-agnostic; use the annotation type already
used by your project.

#### Default Values

Specify default values for fields that are applied when not explicitly set before `build()`:

```java
import org.javahelpers.simple.builders.core.annotations.Default;

@SimpleBuilder
public record Product(
String name,
double price,
@Default("GENERAL") String category,
@Default("true") boolean active
) {}

// category defaults to "GENERAL", active defaults to true
Product product = ProductBuilder.create()
.name("Laptop")
.price(1500.0)
.build();
// product.category() == "GENERAL"
// product.active() == true

// Explicit values override defaults
Product custom = ProductBuilder.create()
.name("Widget")
.price(9.99)
.category("ACCESSORIES")
.active(false)
.build();
// custom.category() == "ACCESSORIES"
```

The `@Default` annotation works on both **constructor parameters** (records) and **fields** (classes with setters). The `value()` is a string expression interpreted based on the field type:

- **String** — wrapped in double quotes automatically (e.g. `@Default("GENERAL")` generates `"GENERAL"`)
- **char** — wrapped in single quotes (e.g. `@Default("A")` generates `'A'`)
- **numeric/boolean primitives** — used as-is (e.g. `@Default("0.0")` generates `0.0`)
- **complex types** — used as a raw Java expression (e.g. `@Default("List.of()")` generates `List.of()`)

**Framework-agnostic detection:** The processor also detects annotations named `Default` or `DefaultValue` from any package (e.g. Jakarta REST `@DefaultValue`) if they have a `String value()` member.

**Interaction with non-null checks:** A field with a `@Default` is never considered "required" — even if annotated with `@NotNull`, no validation error is raised when the field is unset.

#### Conditional Builder Logic

Apply builder modifications conditionally using the `conditional()` method:
Expand Down Expand Up @@ -409,6 +453,7 @@ Examples demonstrating special annotations and nested object relationships:

- **Sponsor DTO**: [`SponsorDto.java`](example/src/main/java/org/javahelpers/simple/builders/example/SponsorDto.java) and [`SponsorDtoBuilder.java`](example/generated-example-builder/org/javahelpers/simple/builders/example/SponsorDtoBuilder.java) - Simple DTO used as nested object in other examples
- **Mannschaft DTO**: [`MannschaftDto.java`](example/src/main/java/org/javahelpers/simple/builders/example/MannschaftDto.java) and [`MannschaftDtoBuilder.java`](example/generated-example-builder/org/javahelpers/simple/builders/example/MannschaftDtoBuilder.java) - Demonstrates `@IgnoreInBuilder` annotation to exclude specific setter methods from the generated builder, plus Set collections with nested objects
- **Default Values**: [`ProductWithDefaults.java`](example/src/main/java/org/javahelpers/simple/builders/example/ProductWithDefaults.java) (record) and [`OrderWithDefaults.java`](example/src/main/java/org/javahelpers/simple/builders/example/OrderWithDefaults.java) (class) - Demonstrate `@Default` annotation for unset builder fields

These examples serve as both documentation and integration tests for the annotation processor.

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/*
* MIT License
*
* Copyright (c) 2026 Andreas Igel
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/

package org.javahelpers.simple.builders.core.annotations;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* Specifies a default value for a builder field that is applied when the field is not explicitly
* set before calling {@code build()}.
*
* <p>The {@link #value()} is a string expression that is interpreted based on the field type:
*
* <ul>
* <li><b>String</b> — wrapped in double quotes, e.g. {@code @Default("GENERAL")} generates {@code
* "GENERAL"}
* <li><b>char</b> — wrapped in single quotes, e.g. {@code @Default("A")} generates {@code 'A'}
* <li><b>numeric/boolean primitives</b> — used as-is, e.g. {@code @Default("0.0")} generates
* {@code 0.0}
* <li><b>complex types</b> — used as a raw Java expression, e.g. {@code @Default("List.of()")}
* generates {@code List.of()}
* </ul>
*
* <p>Can be placed on constructor parameters or fields. When a field has a default value, it is no
* longer considered "required" even if annotated with {@code @NotNull} or {@code @NonNull}.
*
* <p>Example with a record:
*
* <pre>{@code
* @SimpleBuilder
* public record Product(String name, double price,
* @Default("GENERAL") String category) {}
*
* // category defaults to "GENERAL" if not set
* Product p = ProductBuilder.create()
* .name("Widget")
* .price(9.99)
* .build();
* // p.category() == "GENERAL"
* }</pre>
*
* <p>Example with a class:
*
* <pre>{@code
* @SimpleBuilder
* public class Order {
* private String id;
* @Default("PENDING") private String status;
*
* public String getId() { return id; }
* public void setId(String id) { this.id = id; }
* public String getStatus() { return status; }
* public void setStatus(String status) { this.status = status; }
* }
*
* // status defaults to "PENDING" if not set
* Order o = OrderBuilder.create()
* .id("ORD-001")
* .build();
* // o.getStatus() == "PENDING"
* }</pre>
*
* <p><b>Framework-agnostic detection:</b> The builder processor also detects annotations named
* {@code Default} or {@code DefaultValue} from any package (e.g. Jakarta REST {@code
* jakarta.ws.rs.DefaultValue}) if they have a {@code String value()} member.
*/
@Target({ElementType.PARAMETER, ElementType.FIELD})
@Retention(RetentionPolicy.CLASS)
public @interface Default {

/**
* The default value as a string expression, interpreted based on the field type.
*
* <p>For String fields, the value is quoted automatically. For char fields, it is single-quoted.
* For numeric/boolean primitives and complex types, it is used as a raw Java expression.
*
* @return the default value expression
*/
String value();
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,16 +56,60 @@ public boolean isSet() {
return isChanged || isInitial;
}

/**
* Returns the value if set, otherwise the given default.
*
* @param defaultValue the default to return if unset
* @return the value if set, otherwise the default
*/
public T valueOr(T defaultValue) {
return isSet() ? value : defaultValue;
}

/**
* Executes the provided {@link Consumer} only if this value has been set (initial value or
* changed).
* changed). Returns a {@link DefaultValueApplier} that can fluently provide a default via {@code
* .orElse(default)} if the value was unset.
*
* <p>Existing code that ignores the return value (e.g. {@code tracked.ifSet(consumer);})
* continues to work unchanged.
*
* @param consumer action to perform with the current {@link #value()}
* @return a {@link DefaultValueApplier} for fluent default handling
*/
public void ifSet(Consumer<T> consumer) {
public DefaultValueApplier<T> ifSet(Consumer<T> consumer) {
if (isSet()) {
consumer.accept(value);
}
return new DefaultValueApplier<>(isSet(), consumer);
}

/**
* Intermediate result returned by {@link #ifSet(Consumer)} to support fluent default-value
* application via {@code .orElse(default)}.
*
* <p>When {@link #ifSet(Consumer)} is called on a set {@link TrackedValue}, the consumer is
* invoked immediately and {@code alreadyApplied} is {@code true}, making the subsequent {@link
* #orElse(Object)} call a no-op. When called on an unset value, the consumer is not invoked and
* {@code alreadyApplied} is {@code false}, so {@link #orElse(Object)} applies the default value
* to the same consumer.
*
* @param <T> the value type
* @param alreadyApplied whether the consumer has already been invoked
* @param consumer the consumer to apply the default value to if not already applied
*/
public record DefaultValueApplier<T>(boolean alreadyApplied, Consumer<T> consumer) {

/**
* Applies the given default value to the consumer if the original value was unset.
*
* @param defaultValue the default value to apply
*/
public void orElse(T defaultValue) {
if (!alreadyApplied) {
consumer.accept(defaultValue);
}
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -207,4 +207,95 @@ void shouldWorkWithComplexTypes() {
assertTrue(intTracked.isSet());
assertTrue(doubleTracked.isSet());
}

@Test
void valueOr_returnsValueWhenSet() {
TrackedValue<String> tracked = TrackedValue.changedValue("actual");
assertEquals("actual", tracked.valueOr("default"));
}

@Test
void valueOr_returnsValueWhenInitial() {
TrackedValue<String> tracked = TrackedValue.initialValue("initial");
assertEquals("initial", tracked.valueOr("default"));
}

@Test
void valueOr_returnsDefaultWhenUnset() {
TrackedValue<String> tracked = TrackedValue.unsetValue();
assertEquals("default", tracked.valueOr("default"));
}

@Test
void valueOr_returnsNullDefaultWhenUnset() {
TrackedValue<String> tracked = TrackedValue.unsetValue();
assertNull(tracked.valueOr(null));
}

@Test
void valueOr_worksWithPrimitives() {
TrackedValue<Integer> tracked = TrackedValue.unsetValue();
assertEquals(42, tracked.valueOr(42));
}

@Test
void ifSet_orElse_appliesValueWhenChanged() {
TrackedValue<String> tracked = TrackedValue.changedValue("actual");
AtomicReference<String> received = new AtomicReference<>("unchanged");

tracked.ifSet(received::set).orElse("default");

assertEquals("actual", received.get());
}

@Test
void ifSet_orElse_appliesValueWhenInitial() {
TrackedValue<String> tracked = TrackedValue.initialValue("initial");
AtomicReference<String> received = new AtomicReference<>("unchanged");

tracked.ifSet(received::set).orElse("default");

assertEquals("initial", received.get());
}

@Test
void ifSet_orElse_appliesDefaultWhenUnset() {
TrackedValue<String> tracked = TrackedValue.unsetValue();
AtomicReference<String> received = new AtomicReference<>("unchanged");

tracked.ifSet(received::set).orElse("default");

assertEquals("default", received.get());
}

@Test
void ifSet_orElse_appliesNullDefaultWhenUnset() {
TrackedValue<String> tracked = TrackedValue.unsetValue();
AtomicReference<String> received = new AtomicReference<>("unchanged");

tracked.ifSet(received::set).orElse(null);

assertNull(received.get());
}

@Test
void ifSet_returnValueCanBeIgnored() {
TrackedValue<String> tracked = TrackedValue.changedValue("actual");
AtomicReference<String> received = new AtomicReference<>("unchanged");

// Simulate existing generated code that ignores the return value
tracked.ifSet(received::set);

assertEquals("actual", received.get());
}

@Test
void ifSet_orElse_appliesPrimitiveDefaultWhenUnset() {
TrackedValue<Integer> tracked = TrackedValue.unsetValue();
AtomicReference<Integer> received = new AtomicReference<>(0);

tracked.ifSet(received::set).orElse(42);

assertEquals(42, received.get());
}
}
Loading
Loading