This is an automated email from the ASF dual-hosted git repository.

ahuber pushed a commit to branch spring6
in repository https://gitbox.apache.org/repos/asf/causeway.git


The following commit(s) were added to refs/heads/spring6 by this push:
     new 4589789949 CAUSEWAY-3682: [Commons] YamlUtils to support Java Records
4589789949 is described below

commit 4589789949266b730970781173e967d8d5850659
Author: Andi Huber <[email protected]>
AuthorDate: Thu Jan 25 16:24:13 2024 +0100

    CAUSEWAY-3682: [Commons] YamlUtils to support Java Records
---
 commons/pom.xml                                    | 11 ++-
 commons/src/main/java/module-info.java             |  1 +
 .../org/apache/causeway/commons/io/JsonUtils.java  | 18 +++--
 .../org/apache/causeway/commons/io/YamlUtils.java  | 43 +++++++----
 .../apache/causeway/commons/io/JaxbUtilsTest.java  | 84 +++++++++-------------
 .../apache/causeway/commons/io/JsonUtilsTest.java  | 62 ++++++++++++++++
 ...sTest.toStringUtf8_with_no_options.approved.txt |  1 +
 .../apache/causeway/commons/io/YamlUtilsTest.java  | 57 +++++++++++++++
 ...sTest.toStringUtf8_with_no_options.approved.txt |  2 +
 .../apache/causeway/commons/io/_TestDomain.java    | 39 ++++++++++
 10 files changed, 242 insertions(+), 76 deletions(-)

diff --git a/commons/pom.xml b/commons/pom.xml
index 979eec0111..fccea5e91f 100644
--- a/commons/pom.xml
+++ b/commons/pom.xml
@@ -118,12 +118,14 @@
                        <groupId>com.fasterxml.jackson.core</groupId>
                        <artifactId>jackson-databind</artifactId>
                </dependency>
-
+               <dependency>
+            <groupId>com.fasterxml.jackson.dataformat</groupId>
+            <artifactId>jackson-dataformat-yaml</artifactId>
+        </dependency>
         <dependency>
                    <groupId>com.fasterxml.jackson.module</groupId>
                    
<artifactId>jackson-module-jakarta-xmlbind-annotations</artifactId>
                </dependency>
-               
         <dependency>
                    <groupId>com.fasterxml.jackson.jakarta.rs</groupId>
                    <artifactId>jackson-jakarta-rs-json-provider</artifactId>
@@ -215,6 +217,11 @@
                        <artifactId>hamcrest-library</artifactId>
                        <scope>test</scope>
                </dependency>
+               <dependency>
+            <groupId>com.approvaltests</groupId>
+            <artifactId>approvaltests</artifactId>
+            <scope>test</scope>
+        </dependency>
 
     </dependencies>
 
diff --git a/commons/src/main/java/module-info.java 
b/commons/src/main/java/module-info.java
index 2438711d90..9dea6a4db5 100644
--- a/commons/src/main/java/module-info.java
+++ b/commons/src/main/java/module-info.java
@@ -76,6 +76,7 @@ module org.apache.causeway.commons {
     requires transitive jakarta.inject;
     requires jakarta.annotation;
     requires com.sun.xml.bind;
+    requires com.fasterxml.jackson.dataformat.yaml;
 
     // JAXB JUnit test
     opens org.apache.causeway.commons.internal.resources to jakarta.xml.bind;
diff --git 
a/commons/src/main/java/org/apache/causeway/commons/io/JsonUtils.java 
b/commons/src/main/java/org/apache/causeway/commons/io/JsonUtils.java
index 63046b5132..b6b7997e4e 100644
--- a/commons/src/main/java/org/apache/causeway/commons/io/JsonUtils.java
+++ b/commons/src/main/java/org/apache/causeway/commons/io/JsonUtils.java
@@ -63,7 +63,7 @@ public class JsonUtils {
     }
 
     @FunctionalInterface
-    public interface JsonCustomizer extends UnaryOperator<ObjectMapper> {}
+    public interface JacksonCustomizer extends UnaryOperator<ObjectMapper> {}
 
     // -- READING
 
@@ -74,7 +74,7 @@ public class JsonUtils {
     public <T> Try<T> tryRead(
             final @NonNull Class<T> mappedType,
             final @Nullable String stringUtf8,
-            final JsonUtils.JsonCustomizer ... customizers) {
+            final JsonUtils.JacksonCustomizer ... customizers) {
         return tryRead(mappedType, DataSource.ofStringUtf8(stringUtf8), 
customizers);
     }
 
@@ -85,7 +85,7 @@ public class JsonUtils {
     public <T> Try<T> tryRead(
             final @NonNull Class<T> mappedType,
             final @NonNull DataSource source,
-            final JsonUtils.JsonCustomizer ... customizers) {
+            final JsonUtils.JacksonCustomizer ... customizers) {
         return source.tryReadAll((final InputStream is)->{
             return Try.call(()->createMapper(customizers).readValue(is, 
mappedType));
         });
@@ -98,7 +98,7 @@ public class JsonUtils {
     public <T> Try<List<T>> tryReadAsList(
             final @NonNull Class<T> elementType,
             final @NonNull DataSource source,
-            final JsonUtils.JsonCustomizer ... customizers) {
+            final JsonUtils.JacksonCustomizer ... customizers) {
         return source.tryReadAll((final InputStream is)->{
             return Try.call(()->{
                 val mapper = createMapper(customizers);
@@ -116,7 +116,7 @@ public class JsonUtils {
     public void write(
             final @Nullable Object pojo,
             final @NonNull DataSink sink,
-            final JsonUtils.JsonCustomizer ... customizers) {
+            final JsonUtils.JacksonCustomizer ... customizers) {
         if(pojo==null) return;
         sink.writeAll(os->
             Try.run(()->createMapper(customizers).writeValue(os, pojo)));
@@ -130,7 +130,7 @@ public class JsonUtils {
     @Nullable
     public static String toStringUtf8(
             final @Nullable Object pojo,
-            final JsonUtils.JsonCustomizer ... customizers) {
+            final JsonUtils.JacksonCustomizer ... customizers) {
         return pojo!=null
                 ? createMapper(customizers).writeValueAsString(pojo)
                 : null;
@@ -156,15 +156,13 @@ public class JsonUtils {
     // -- MAPPER FACTORY
 
     private ObjectMapper createMapper(
-            final JsonUtils.JsonCustomizer ... customizers) {
+            final JsonUtils.JacksonCustomizer ... customizers) {
         var mapper = new ObjectMapper();
-        for(JsonUtils.JsonCustomizer customizer : customizers) {
+        for(JsonUtils.JacksonCustomizer customizer : customizers) {
             mapper = Optional.ofNullable(customizer.apply(mapper))
                     .orElse(mapper);
         }
         return mapper;
     }
 
-
-
 }
diff --git 
a/commons/src/main/java/org/apache/causeway/commons/io/YamlUtils.java 
b/commons/src/main/java/org/apache/causeway/commons/io/YamlUtils.java
index 5463fc2ef4..d650e12526 100644
--- a/commons/src/main/java/org/apache/causeway/commons/io/YamlUtils.java
+++ b/commons/src/main/java/org/apache/causeway/commons/io/YamlUtils.java
@@ -30,6 +30,9 @@ import java.util.Map;
 import java.util.Optional;
 import java.util.function.UnaryOperator;
 
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
+
 import org.springframework.lang.Nullable;
 import org.yaml.snakeyaml.DumperOptions;
 import org.yaml.snakeyaml.DumperOptions.LineBreak;
@@ -62,12 +65,13 @@ import lombok.experimental.UtilityClass;
 @UtilityClass
 public class YamlUtils {
 
+    /**
+     * @deprecated We rely on Jackson to parse YAML. Might also replace 
SnakeYaml with Jackson to write YAML.
+     */
+    @Deprecated
     @FunctionalInterface
     public interface YamlDumpCustomizer extends UnaryOperator<DumperOptions> {}
 
-    @FunctionalInterface
-    public interface YamlLoadCustomizer extends UnaryOperator<LoaderOptions> {}
-
     // -- READING
 
     /**
@@ -77,7 +81,7 @@ public class YamlUtils {
     public <T> Try<T> tryRead(
             final @NonNull Class<T> mappedType,
             final @Nullable String stringUtf8,
-            final YamlUtils.YamlLoadCustomizer ... customizers) {
+            final JsonUtils.JacksonCustomizer ... customizers) {
         return tryRead(mappedType, DataSource.ofStringUtf8(stringUtf8), 
customizers);
     }
 
@@ -88,10 +92,10 @@ public class YamlUtils {
     public <T> Try<T> tryRead(
             final @NonNull Class<T> mappedType,
             final @NonNull DataSource source,
-            final YamlUtils.YamlLoadCustomizer ... customizers) {
+            final JsonUtils.JacksonCustomizer ... customizers) {
         return source.tryReadAll((final InputStream is)->{
-            return Try.call(()->createMapper(mappedType, 
Can.ofArray(customizers), Can.empty())
-                    .load(is));
+            return Try.call(()->createJacksonMapperForYaml(customizers)
+                    .readValue(is, mappedType));
         });
     }
 
@@ -106,7 +110,7 @@ public class YamlUtils {
             final YamlUtils.YamlDumpCustomizer ... customizers) {
         if(pojo==null) return;
         sink.writeAll(os->
-            createMapper(pojo.getClass(), Can.empty(), 
Can.ofArray(customizers)).dump(pojo, new OutputStreamWriter(os)));
+            createMapper(pojo.getClass(), Can.ofArray(customizers)).dump(pojo, 
new OutputStreamWriter(os)));
     }
 
     /**
@@ -119,7 +123,7 @@ public class YamlUtils {
             final @Nullable Object pojo,
             final YamlUtils.YamlDumpCustomizer ... customizers) {
         return pojo!=null
-                ? createMapper(pojo.getClass(), Can.empty(), 
Can.ofArray(customizers)).dump(pojo)
+                ? createMapper(pojo.getClass(), 
Can.ofArray(customizers)).dump(pojo)
                 : null;
     }
 
@@ -134,11 +138,24 @@ public class YamlUtils {
         return opts;
     }
 
-    // -- MAPPER FACTORY
+    // -- MAPPER FACTORIES
+
+    /**
+     * SnakeYaml as of 2.2 does not support Java records. So we use Jackson 
instead.
+     */
+    private ObjectMapper createJacksonMapperForYaml(
+            final JsonUtils.JacksonCustomizer ... customizers) {
+        var mapper = new ObjectMapper(new YAMLFactory());
+        for(JsonUtils.JacksonCustomizer customizer : customizers) {
+            mapper = Optional.ofNullable(customizer.apply(mapper))
+                    .orElse(mapper);
+        }
+        return mapper;
+    }
+
 
     private Yaml createMapper(
             final Class<?> mappedType,
-            final Can<YamlUtils.YamlLoadCustomizer> loadCustomizers,
             final Can<YamlUtils.YamlDumpCustomizer> dumpCustomizers) {
         var dumperOptions = new DumperOptions();
         dumperOptions.setIndent(2);
@@ -154,10 +171,6 @@ public class YamlUtils {
         presenter.addClassTag(mappedType, Tag.MAP);
 
         var loaderOptions = new LoaderOptions();
-        for(YamlUtils.YamlLoadCustomizer customizer : loadCustomizers) {
-            loaderOptions = 
Optional.ofNullable(customizer.apply(loaderOptions))
-                    .orElse(loaderOptions);
-        }
         var mapper = new Yaml(new Constructor(mappedType, loaderOptions), 
presenter, dumperOptions, loaderOptions);
         return mapper;
     }
diff --git 
a/commons/src/test/java/org/apache/causeway/commons/io/JaxbUtilsTest.java 
b/commons/src/test/java/org/apache/causeway/commons/io/JaxbUtilsTest.java
index 69238a9eff..6fb8a33a00 100644
--- a/commons/src/test/java/org/apache/causeway/commons/io/JaxbUtilsTest.java
+++ b/commons/src/test/java/org/apache/causeway/commons/io/JaxbUtilsTest.java
@@ -18,6 +18,8 @@
  */
 package org.apache.causeway.commons.io;
 
+import javax.xml.transform.TransformerFactory;
+
 import jakarta.xml.bind.JAXBContext;
 import jakarta.xml.bind.annotation.XmlAccessType;
 import jakarta.xml.bind.annotation.XmlAccessorType;
@@ -25,10 +27,7 @@ import jakarta.xml.bind.annotation.XmlElement;
 import jakarta.xml.bind.annotation.XmlRootElement;
 import jakarta.xml.bind.annotation.XmlType;
 
-//import javax.xml.transform.TransformerFactory;
-//
-//import 
org.apache.causeway.commons.io.JaxbUtils.JaxbOptions.JaxbOptionsBuilder;
-//import org.approvaltests.Approvals;
+import org.approvaltests.Approvals;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Nested;
 import org.junit.jupiter.api.Test;
@@ -37,6 +36,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
 import org.apache.causeway.commons.internal.base._Strings;
+import org.apache.causeway.commons.io.JaxbUtils.JaxbOptions.JaxbOptionsBuilder;
 
 import lombok.EqualsAndHashCode;
 import lombok.Getter;
@@ -109,50 +109,36 @@ class JaxbUtilsTest {
         }
     }
 
-    // commenting this out only because javac v11 fails to compile.  However, 
javac v20 handles it, so we can uncomment in the future.
-    // https://the-asf.slack.com/archives/CFC42LWBV/p1694771499860429
-
-//    @Test
-//    void toStringUtf8_with_no_options() {
-//
-//        val aXml = JaxbUtils.toStringUtf8(a);
-//
-//        System.out.println(aXml);
-//
-//        Approvals.verify(aXml);
-//    }
-//
-//    @Test
-//    void toStringUtf8_with_no_formatted_output() {
-//
-//        val aXml = JaxbUtils.toStringUtf8(a, opt -> {
-//            opt.formattedOutput(false);
-//            return opt;
-//        });
-//
-//        System.out.println(aXml);
-//
-//        Approvals.verify(aXml);
-//    }
-//
-//    @Test
-//    void toStringUtf8_with_indent_number_overridden() {
-//
-//        val aXml = JaxbUtils.toStringUtf8(a, new 
JaxbUtils.TransformerFactoryCustomizer() {
-//            @Override
-//            public void apply(TransformerFactory transformerFactory) {
-//                transformerFactory.setAttribute("indent-number", 2);
-//            }
-//
-//            @Override
-//            public JaxbOptionsBuilder apply(JaxbOptionsBuilder 
jaxbOptionsBuilder) {
-//                return jaxbOptionsBuilder;
-//            }
-//        });
-//
-//        System.out.println(aXml);
-//
-//        Approvals.verify(aXml);
-//    }
+    @Test
+    void toStringUtf8_with_no_options() {
+        val aXml = JaxbUtils.toStringUtf8(a);
+        Approvals.verify(aXml);
+    }
+
+    @Test
+    void toStringUtf8_with_no_formatted_output() {
+        val aXml = JaxbUtils.toStringUtf8(a, opt -> {
+            opt.formattedOutput(false);
+            return opt;
+        });
+        Approvals.verify(aXml);
+    }
+
+    @Test
+    void toStringUtf8_with_indent_number_overridden() {
+        val aXml = JaxbUtils.toStringUtf8(a, new 
JaxbUtils.TransformerFactoryCustomizer() {
+            @Override
+            public void apply(final TransformerFactory transformerFactory) {
+                transformerFactory.setAttribute("indent-number", 2);
+            }
+
+            @Override
+            public JaxbOptionsBuilder apply(final JaxbOptionsBuilder 
jaxbOptionsBuilder) {
+                return jaxbOptionsBuilder;
+            }
+
+        });
+        Approvals.verify(aXml);
+    }
 
 }
diff --git 
a/commons/src/test/java/org/apache/causeway/commons/io/JsonUtilsTest.java 
b/commons/src/test/java/org/apache/causeway/commons/io/JsonUtilsTest.java
new file mode 100644
index 0000000000..fa2239b4aa
--- /dev/null
+++ b/commons/src/test/java/org/apache/causeway/commons/io/JsonUtilsTest.java
@@ -0,0 +1,62 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.apache.causeway.commons.io;
+
+import org.approvaltests.Approvals;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import org.apache.causeway.commons.io._TestDomain.Person;
+
+import lombok.val;
+
+class JsonUtilsTest {
+
+    private Person person;
+
+    @BeforeEach
+    void setup() {
+        this.person = _TestDomain.samplePerson();
+    }
+
+    @Test
+    void toStringUtf8_with_no_options() {
+        val json = JsonUtils.toStringUtf8(person);
+        Approvals.verify(json);
+    }
+
+    @Test
+    void parseRecord() {
+        var json = """
+                {
+                    "name":"sven",
+                    "address": {
+                        "zip":1234,
+                        "street":"backerstreet"
+                    }
+                }
+                """;
+        var person = JsonUtils.tryRead(Person.class, json)
+                .valueAsNonNullElseFail();
+        assertEquals(this.person, person);
+    }
+
+}
diff --git 
a/commons/src/test/java/org/apache/causeway/commons/io/JsonUtilsTest.toStringUtf8_with_no_options.approved.txt
 
b/commons/src/test/java/org/apache/causeway/commons/io/JsonUtilsTest.toStringUtf8_with_no_options.approved.txt
new file mode 100644
index 0000000000..120f413192
--- /dev/null
+++ 
b/commons/src/test/java/org/apache/causeway/commons/io/JsonUtilsTest.toStringUtf8_with_no_options.approved.txt
@@ -0,0 +1 @@
+{"name":"sven","address":{"zip":1234,"street":"backerstreet"}}
\ No newline at end of file
diff --git 
a/commons/src/test/java/org/apache/causeway/commons/io/YamlUtilsTest.java 
b/commons/src/test/java/org/apache/causeway/commons/io/YamlUtilsTest.java
new file mode 100644
index 0000000000..daa17948ed
--- /dev/null
+++ b/commons/src/test/java/org/apache/causeway/commons/io/YamlUtilsTest.java
@@ -0,0 +1,57 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.apache.causeway.commons.io;
+
+import org.approvaltests.Approvals;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import org.apache.causeway.commons.io._TestDomain.Person;
+
+import lombok.val;
+
+class YamlUtilsTest {
+
+    private Person person;
+
+    @BeforeEach
+    void setup() {
+        this.person = _TestDomain.samplePerson();
+    }
+
+    @Test
+    void toStringUtf8_with_no_options() {
+        val yaml = YamlUtils.toStringUtf8(person);
+        Approvals.verify(yaml);
+    }
+
+    @Test
+    void parseRecord() {
+        var yaml = """
+                name: sven
+                address: {street: backerstreet, zip: 1234}
+                """;
+        var person = YamlUtils.tryRead(Person.class, yaml)
+                .valueAsNonNullElseFail();
+        assertEquals(this.person, person);
+    }
+
+}
diff --git 
a/commons/src/test/java/org/apache/causeway/commons/io/YamlUtilsTest.toStringUtf8_with_no_options.approved.txt
 
b/commons/src/test/java/org/apache/causeway/commons/io/YamlUtilsTest.toStringUtf8_with_no_options.approved.txt
new file mode 100644
index 0000000000..bbfcdd376f
--- /dev/null
+++ 
b/commons/src/test/java/org/apache/causeway/commons/io/YamlUtilsTest.toStringUtf8_with_no_options.approved.txt
@@ -0,0 +1,2 @@
+address: {street: backerstreet, zip: 1234}
+name: sven
diff --git 
a/commons/src/test/java/org/apache/causeway/commons/io/_TestDomain.java 
b/commons/src/test/java/org/apache/causeway/commons/io/_TestDomain.java
new file mode 100644
index 0000000000..2e4bb7523c
--- /dev/null
+++ b/commons/src/test/java/org/apache/causeway/commons/io/_TestDomain.java
@@ -0,0 +1,39 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.apache.causeway.commons.io;
+
+import lombok.experimental.UtilityClass;
+
+@UtilityClass
+class _TestDomain {
+
+    public static record Person(
+            String name,
+            Address address) {
+    }
+
+    public static record Address(
+            int zip,
+            String street) {
+    }
+
+    Person samplePerson() {
+        return new Person("sven", new Address(1234, "backerstreet"));
+    }
+}

Reply via email to