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

Reply via email to