This is an automated email from the ASF dual-hosted git repository.
jamesbognar pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/juneau.git
The following commit(s) were added to refs/heads/master by this push:
new 9d9c632765 JUNEAU-219
9d9c632765 is described below
commit 9d9c6327654fbba8620c5d471fbdc818c6229251
Author: James Bognar <[email protected]>
AuthorDate: Tue Oct 14 16:12:58 2025 -0400
JUNEAU-219
---
.../apache/juneau/csv/CsvSerializerSession.java | 37 +++-
juneau-docs/docs/release-notes/9.2.0.md | 66 ++++++
.../test/java/org/apache/juneau/csv/Csv_Test.java | 222 +++++++++++++++++++++
3 files changed, 322 insertions(+), 3 deletions(-)
diff --git
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/csv/CsvSerializerSession.java
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/csv/CsvSerializerSession.java
index 561b50cc62..85a1c313e6 100644
---
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/csv/CsvSerializerSession.java
+++
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/csv/CsvSerializerSession.java
@@ -242,7 +242,9 @@ public class CsvSerializerSession extends
WriterSerializerSession {
BeanMap<?> bean = toBeanMap(x);
bm.forEachProperty(BeanPropertyMeta::canRead, y -> {
addComma2.ifSet(() ->
w.w(',')).set();
-
w.writeEntry(y.get(bean, y.getName()));
+ // Bean property values
are already swapped by BeanPropertyMeta.get() via toSerializedForm()
+ Object value =
y.get(bean, y.getName());
+ w.writeEntry(value);
});
w.w('\n');
});
@@ -259,7 +261,8 @@ public class CsvSerializerSession extends
WriterSerializerSession {
Map map = (Map)x;
map.values().forEach(y -> {
addComma2.ifSet(() ->
w.w(',')).set();
- w.writeEntry(y);
+ Object value =
applySwap(y, getClassMetaForObject(y));
+ w.writeEntry(value);
});
w.w('\n');
});
@@ -267,7 +270,8 @@ public class CsvSerializerSession extends
WriterSerializerSession {
w.writeEntry("value");
w.append('\n');
l.stream().forEach(x -> {
- w.writeEntry(x);
+ Object value = applySwap(x,
getClassMetaForObject(x));
+ w.writeEntry(value);
w.w('\n');
});
}
@@ -275,6 +279,33 @@ public class CsvSerializerSession extends
WriterSerializerSession {
}
}
+ /**
+ * Applies any registered object swap to the specified value.
+ *
+ * <p>
+ * If a swap is registered for the value's type, the value is
transformed using the swap's
+ * {@code swap()} method before being serialized.
+ *
+ * @param value The value to potentially swap.
+ * @param type The class metadata of the value's type.
+ * @return The swapped value, or the original value if no swap is
registered.
+ */
+ @SuppressWarnings({"rawtypes", "unchecked"})
+ private Object applySwap(Object value, ClassMeta<?> type) {
+ try {
+ if (value == null || type == null)
+ return value;
+
+ org.apache.juneau.swap.ObjectSwap swap =
type.getSwap(this);
+ if (swap != null) {
+ return swap(swap, value);
+ }
+ return value;
+ } catch (SerializeException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
CsvWriter getCsvWriter(SerializerPipe out) {
Object output = out.getRawOutput();
if (output instanceof CsvWriter)
diff --git a/juneau-docs/docs/release-notes/9.2.0.md
b/juneau-docs/docs/release-notes/9.2.0.md
index c3cac07fee..734b6ba71d 100644
--- a/juneau-docs/docs/release-notes/9.2.0.md
+++ b/juneau-docs/docs/release-notes/9.2.0.md
@@ -19,6 +19,7 @@ Major changes include:
- **@Schema Annotation** upgraded to JSON Schema Draft 2020-12 with 18 new
properties, while maintaining full backward compatibility with Draft 04
- **HttpPartFormat Enhancement**: Added 18 new format types (email, hostname,
UUID, URI, IPv4/IPv6, etc.) with comprehensive validation
- **Jakarta Bean Validation Integration**: Automatic detection and processing
of Jakarta Validation constraints (`@NotNull`, `@Email`, `@Size`, etc.) with
zero dependencies
+- **CSV Serializer Swap Support**: Added full object swap support to
`CsvSerializer`, enabling date formatting, enum customization, and complex
object transformations
- **Remote Proxy Default Values**: Added `def` attribute to all HTTP part
annotations (`@Header`, `@Query`, `@FormData`, `@Path`, `@Content`) for
specifying method-level default values
- JSON Schema beans upgraded to Draft 2020-12 specification with backward
compatibility for Draft 04
- Comprehensive enhancements to HTML5 beans with improved javadocs and
`HtmlBuilder` integration
@@ -343,6 +344,71 @@ Major changes include:
- **Enhanced OpenAPI Compatibility**: Jakarta Validation constraints are now
seamlessly translated to OpenAPI schema properties, enabling automatic
documentation generation and client-side validation in tools like Swagger UI.
+#### CSV Serializer - Object Swap Support
+
+- **Full Object Swap Support**: The `CsvSerializer` now supports object swaps,
bringing it to feature parity with other Juneau serializers like JSON, XML, and
UON.
+
+ **Key Features**:
+ - **Bean Property Swaps**: Automatically applies swaps registered via
`.swaps()` to bean property values
+ - **Map Value Swaps**: Transforms map values using registered swaps before
CSV serialization
+ - **Simple Value Swaps**: Applies swaps to standalone values (e.g.,
`List<Date>` to formatted date strings)
+ - **@Swap Annotation Support**: Honors `@Swap` annotations on bean fields
for property-specific transformations
+ - **Null-Safe**: Gracefully handles null values without applying swaps
+
+ **Example Usage**:
+ ```java
+ // Define a custom swap
+ public static class DateSwap extends StringSwap<Date> {
+ private static final SimpleDateFormat df = new
SimpleDateFormat("yyyy-MM-dd");
+
+ @Override
+ public String swap(BeanSession session, Date date) {
+ return df.format(date);
+ }
+
+ @Override
+ public Date unswap(BeanSession session, String str, ClassMeta<?> hint)
throws ParseException {
+ return df.parse(str);
+ }
+ }
+
+ // Use the swap with CSV serializer
+ List<User> users = Arrays.asList(
+ new User("john", new Date()),
+ new User("jane", new Date())
+ );
+
+ CsvSerializer serializer = CsvSerializer.create()
+ .swaps(DateSwap.class)
+ .build();
+
+ String csv = serializer.serialize(users);
+ // Output: date,name
+ // 2025-10-14,john
+ // 2025-10-14,jane
+ ```
+
+ **Common Use Cases**:
+ - **Date Formatting**: Transform `Date` objects to formatted strings
(`"yyyy-MM-dd"`, `"MM/dd/yyyy"`, etc.)
+ - **Enum Customization**: Convert enums to custom string representations
+ - **Complex Object Flattening**: Transform nested objects into simple string
representations (e.g., `Address` → `"street|city|state"`)
+ - **Type Conversion**: Convert numeric types to formatted strings (e.g.,
currency, percentages)
+
+ **Implementation Details**:
+ - Bean property values are automatically swapped via
`BeanPropertyMeta.get()` which calls `toSerializedForm()`
+ - Map values and simple values are explicitly swapped via the new
`applySwap()` helper method
+ - Swap exceptions are wrapped as `RuntimeException` to maintain
compatibility with lambda expressions
+ - The implementation follows the same swap resolution pattern as
`JsonSerializerSession`
+
+- **Comprehensive Test Coverage**: Added 8 new unit tests covering:
+ - Swaps on bean properties with serializer-level registration
+ - Swaps on map values
+ - Swaps on simple values (collections of swappable objects)
+ - Null value handling with swaps
+ - Custom object swaps for complex types
+ - `@Swap` annotation on bean fields
+ - Enum serialization
+
#### XML Serialization
- **Text Node Delimiter**: Added `textNodeDelimiter` property to
`XmlSerializer` and `HtmlSerializer` to control spacing between consecutive
text nodes.
diff --git a/juneau-utest/src/test/java/org/apache/juneau/csv/Csv_Test.java
b/juneau-utest/src/test/java/org/apache/juneau/csv/Csv_Test.java
index 863bb985ee..51d669ffdd 100755
--- a/juneau-utest/src/test/java/org/apache/juneau/csv/Csv_Test.java
+++ b/juneau-utest/src/test/java/org/apache/juneau/csv/Csv_Test.java
@@ -18,9 +18,14 @@ package org.apache.juneau.csv;
import static org.junit.jupiter.api.Assertions.*;
+import java.text.*;
import java.util.*;
import org.apache.juneau.*;
+import org.apache.juneau.annotation.*;
+import org.apache.juneau.parser.*;
+import org.apache.juneau.serializer.*;
+import org.apache.juneau.swap.*;
import org.junit.jupiter.api.*;
class Csv_Test extends TestBase {
@@ -48,4 +53,221 @@ class Csv_Test extends TestBase {
this.c = c;
}
}
+
+
//====================================================================================================
+ // Test swaps on bean properties
+
//====================================================================================================
+ @Test void b01_swapOnBeanProperty() throws Exception {
+ var l = new LinkedList<>();
+ l.add(new B("user1", new Date(1000000)));
+ l.add(new B("user2", new Date(2000000)));
+
+ var s = CsvSerializer.create().swaps(DateSwap.class).build();
+ var r = s.serialize(l);
+
+ // Swaps should convert dates to yyyy-MM-dd format
+ assertTrue(r.contains("1970-01-01") ||
r.contains("1969-12-31"), "Should have formatted dates but was: " + r);
+ assertTrue(r.contains("user1"));
+ assertTrue(r.contains("user2"));
+ }
+
+ public static class B {
+ public String name;
+ public Date date;
+
+ public B(String name, Date date) {
+ this.name = name;
+ this.date = date;
+ }
+ }
+
+ public static class DateSwap extends StringSwap<Date> {
+ private static final SimpleDateFormat df = new
SimpleDateFormat("yyyy-MM-dd");
+ @Override
+ public String swap(BeanSession session, Date date) {
+ return df.format(date);
+ }
+ @Override
+ public Date unswap(BeanSession session, String str,
ClassMeta<?> hint) throws java.text.ParseException {
+ return df.parse(str);
+ }
+ }
+
+
//====================================================================================================
+ // Test swaps on map values
+
//====================================================================================================
+ @Test void c01_swapOnMapValues() throws Exception {
+ var l = new LinkedList<>();
+ var m1 = new HashMap<String,Object>();
+ m1.put("name", "user1");
+ m1.put("date", new Date(1000000));
+ var m2 = new HashMap<String,Object>();
+ m2.put("name", "user2");
+ m2.put("date", new Date(2000000));
+ l.add(m1);
+ l.add(m2);
+
+ var s = CsvSerializer.create().swaps(DateSwap.class).build();
+ var r = s.serialize(l);
+
+ // Swaps should format dates
+ assertTrue(r.contains("user1"));
+ assertTrue(r.contains("user2"));
+ assertTrue(r.contains("1970-01-01") ||
r.contains("1969-12-31"), "Should have formatted dates but was: " + r);
+ }
+
+
//====================================================================================================
+ // Test swaps on simple values
+
//====================================================================================================
+ @Test void d01_swapOnSimpleValues() throws Exception {
+ var l = new LinkedList<>();
+ l.add(new Date(1000000));
+ l.add(new Date(2000000));
+ l.add(new Date(3000000));
+
+ var s = CsvSerializer.create().swaps(DateSwap.class).build();
+ var r = s.serialize(l);
+
+ // Should have value header and formatted dates
+ assertTrue(r.startsWith("value\n"));
+ assertTrue(r.contains("1970-01-01") ||
r.contains("1969-12-31"), "Should have formatted dates but was: " + r);
+ // Should have 3 date lines + 1 header = 4 lines total
+ assertEquals(4, r.split("\n").length);
+ }
+
+
//====================================================================================================
+ // Test swap with null values
+
//====================================================================================================
+ @Test void e01_swapWithNullValues() throws Exception {
+ var l = new LinkedList<>();
+ l.add(new B("user1", null));
+ l.add(new B("user2", new Date(2000000)));
+ l.add(new B("user3", null));
+
+ var s = CsvSerializer.create().swaps(DateSwap.class).build();
+ var r = s.serialize(l);
+
+ // Should have users and null values
+ assertTrue(r.contains("user1"));
+ assertTrue(r.contains("user2"));
+ assertTrue(r.contains("user3"));
+ assertTrue(r.contains("null"));
+ assertTrue(r.contains("1970-01-01") ||
r.contains("1969-12-31"), "Should have formatted date but was: " + r);
+ }
+
+
//====================================================================================================
+ // Test custom object swap
+
//====================================================================================================
+ @Test void f01_customObjectSwap() throws Exception {
+ var l = new LinkedList<>();
+ l.add(new C("John", new Address("123 Main St", "Seattle",
"WA")));
+ l.add(new C("Jane", new Address("456 Oak Ave", "Portland",
"OR")));
+
+ var s = CsvSerializer.create().swaps(AddressSwap.class).build();
+ var r = s.serialize(l);
+
+ // Should have names and pipe-delimited addresses
+ assertTrue(r.contains("John"));
+ assertTrue(r.contains("Jane"));
+ assertTrue(r.contains("123 Main St|Seattle|WA"));
+ assertTrue(r.contains("456 Oak Ave|Portland|OR"));
+ }
+
+ public static class C {
+ public String name;
+ public Address address;
+
+ public C(String name, Address address) {
+ this.name = name;
+ this.address = address;
+ }
+ }
+
+ public static class Address {
+ public String street, city, state;
+
+ public Address(String street, String city, String state) {
+ this.street = street;
+ this.city = city;
+ this.state = state;
+ }
+ }
+
+ public static class AddressSwap extends StringSwap<Address> {
+ @Override
+ public String swap(BeanSession session, Address address) {
+ if (address == null) return null;
+ return address.street + "|" + address.city + "|" +
address.state;
+ }
+ @Override
+ public Address unswap(BeanSession session, String str,
ClassMeta<?> hint) {
+ if (str == null) return null;
+ String[] parts = str.split("\\|");
+ return new Address(parts[0], parts[1], parts[2]);
+ }
+ }
+
+
//====================================================================================================
+ // Test @Swap annotation on field
+
//====================================================================================================
+ @Test void g01_swapAnnotationOnField() throws Exception {
+ var l = new LinkedList<>();
+ l.add(new D("user1", new Date(1000000)));
+ l.add(new D("user2", new Date(2000000)));
+
+ var s = CsvSerializer.DEFAULT;
+ var r = s.serialize(l);
+
+ // @Swap annotation on field should apply the swap
+ assertTrue(r.contains("user1"));
+ assertTrue(r.contains("user2"));
+ assertTrue(r.contains("1970-01-01") ||
r.contains("1969-12-31"), "Should have formatted dates but was: " + r);
+ }
+
+ public static class D {
+ public String name;
+
+ @Swap(DateSwap.class)
+ public Date timestamp;
+
+ public D(String name, Date timestamp) {
+ this.name = name;
+ this.timestamp = timestamp;
+ }
+ }
+
+
//====================================================================================================
+ // Test enum swap
+
//====================================================================================================
+ @Test void h01_enumSwap() throws Exception {
+ var l = new LinkedList<>();
+ l.add(new E("Task1", Status.PENDING));
+ l.add(new E("Task2", Status.COMPLETED));
+ l.add(new E("Task3", Status.IN_PROGRESS));
+
+ var s = CsvSerializer.DEFAULT;
+ var r = s.serialize(l);
+
+ // Enums should serialize as their string names
+ assertTrue(r.contains("Task1"));
+ assertTrue(r.contains("Task2"));
+ assertTrue(r.contains("Task3"));
+ assertTrue(r.contains("PENDING"));
+ assertTrue(r.contains("COMPLETED"));
+ assertTrue(r.contains("IN_PROGRESS"));
+ }
+
+ public static class E {
+ public String name;
+ public Status status;
+
+ public E(String name, Status status) {
+ this.name = name;
+ this.status = status;
+ }
+ }
+
+ public enum Status {
+ PENDING, IN_PROGRESS, COMPLETED
+ }
}
\ No newline at end of file