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 3fbcb3bdfd CAUSEWAY-3404: quality of life: YamlUtils to support 
factory customization
3fbcb3bdfd is described below

commit 3fbcb3bdfdc95bc7abc88e039b09d437241ae523
Author: Andi Huber <[email protected]>
AuthorDate: Thu Feb 1 11:57:29 2024 +0100

    CAUSEWAY-3404: quality of life: YamlUtils to support factory
    customization
    
    - NOTE this change is not available for older Jackson version currently
    used in Causeway 2.x - hence cannot backport
---
 .../org/apache/causeway/commons/io/YamlUtils.java  | 212 +++++++++------------
 1 file changed, 85 insertions(+), 127 deletions(-)

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 a3c4a09c92..52154bfac4 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
@@ -18,14 +18,7 @@
  */
 package org.apache.causeway.commons.io;
 
-import java.beans.IntrospectionException;
-import java.beans.PropertyDescriptor;
 import java.io.InputStream;
-import java.lang.reflect.Method;
-import java.lang.reflect.RecordComponent;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
 import java.util.Optional;
 import java.util.function.UnaryOperator;
 
@@ -37,24 +30,11 @@ import org.springframework.lang.Nullable;
 import org.yaml.snakeyaml.DumperOptions;
 import org.yaml.snakeyaml.DumperOptions.LineBreak;
 import org.yaml.snakeyaml.LoaderOptions;
-import org.yaml.snakeyaml.Yaml;
-import org.yaml.snakeyaml.constructor.Constructor;
-import org.yaml.snakeyaml.error.YAMLException;
-import org.yaml.snakeyaml.introspector.BeanAccess;
-import org.yaml.snakeyaml.introspector.MethodProperty;
-import org.yaml.snakeyaml.introspector.Property;
-import org.yaml.snakeyaml.introspector.PropertyUtils;
-import org.yaml.snakeyaml.nodes.Tag;
-import org.yaml.snakeyaml.representer.Representer;
 
-import org.apache.causeway.commons.collections.Can;
 import org.apache.causeway.commons.functional.Try;
 
-import lombok.Getter;
 import lombok.NonNull;
 import lombok.SneakyThrows;
-import lombok.val;
-import lombok.experimental.Accessors;
 import lombok.experimental.UtilityClass;
 
 /**
@@ -65,12 +45,10 @@ 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
 
@@ -94,7 +72,34 @@ public class YamlUtils {
             final @NonNull DataSource source,
             final JsonUtils.JacksonCustomizer ... customizers) {
         return source.tryReadAll((final InputStream is)->{
-            return Try.call(()->createJacksonReader(customizers)
+            return Try.call(()->createJacksonReader(Optional.empty(), 
customizers)
+                    .readValue(is, mappedType));
+        });
+    }
+
+    /**
+     * Tries to deserialize YAML content from given UTF8 encoded {@link String}
+     * into an instance of given {@code mappedType}.
+     */
+    public <T> Try<T> tryReadCustomized(
+            final @NonNull Class<T> mappedType,
+            final @Nullable String stringUtf8,
+            final @NonNull YamlLoadCustomizer loadCustomizer,
+            final JsonUtils.JacksonCustomizer ... customizers) {
+        return tryReadCustomized(mappedType, 
DataSource.ofStringUtf8(stringUtf8), loadCustomizer, customizers);
+    }
+
+    /**
+     * Tries to deserialize YAML content from given {@link DataSource} into an 
instance of
+     * given {@code requiredType}.
+     */
+    public <T> Try<T> tryReadCustomized(
+            final @NonNull Class<T> mappedType,
+            final @NonNull DataSource source,
+            final @NonNull YamlLoadCustomizer loadCustomizer,
+            final JsonUtils.JacksonCustomizer ... customizers) {
+        return source.tryReadAll((final InputStream is)->{
+            return 
Try.call(()->createJacksonReader(Optional.of(loadCustomizer), customizers)
                     .readValue(is, mappedType));
         });
     }
@@ -110,7 +115,7 @@ public class YamlUtils {
             final JsonUtils.JacksonCustomizer ... customizers) {
         if(pojo==null) return;
         sink.writeAll(os->
-            Try.run(()->createJacksonWriter(customizers).writeValue(os, 
pojo)));
+            Try.run(()->createJacksonWriter(Optional.empty(), 
customizers).writeValue(os, pojo)));
     }
 
     /**
@@ -123,9 +128,38 @@ public class YamlUtils {
             final @Nullable Object pojo,
             final JsonUtils.JacksonCustomizer ... customizers) {
         return pojo!=null
-                ? createJacksonWriter(customizers).writeValueAsString(pojo)
+                ? createJacksonWriter(Optional.empty(), 
customizers).writeValueAsString(pojo)
+                : null;
+    }
+
+    /**
+     * Writes given {@code pojo} to given {@link DataSink}.
+     */
+    public void writeCustomized(
+            final @Nullable Object pojo,
+            final @NonNull DataSink sink,
+            final @NonNull YamlDumpCustomizer dumpCustomizer,
+            final JsonUtils.JacksonCustomizer ... customizers) {
+        if(pojo==null) return;
+        sink.writeAll(os->
+            Try.run(()->createJacksonWriter(Optional.of(dumpCustomizer), 
customizers).writeValue(os, pojo)));
+    }
+
+    /**
+     * Converts given {@code pojo} to an UTF8 encoded {@link String}.
+     * @return <code>null</code> if pojo is <code>null</code>
+     */
+    @SneakyThrows
+    @Nullable
+    public static String toStringUtf8Customized(
+            final @Nullable Object pojo,
+            final @NonNull YamlDumpCustomizer dumpCustomizer,
+            final JsonUtils.JacksonCustomizer ... customizers) {
+        return pojo!=null
+                ? createJacksonWriter(Optional.of(dumpCustomizer), 
customizers).writeValueAsString(pojo)
                 : null;
     }
+
     // -- CUSTOMIZERS
 
     /**
@@ -141,10 +175,17 @@ public class YamlUtils {
 
     /**
      * SnakeYaml as of 2.2 does not support Java records. So we use Jackson 
instead.
+     * @param loadCustomizer
      */
     private ObjectMapper createJacksonReader(
+            final Optional<YamlLoadCustomizer> loadCustomizer,
             final JsonUtils.JacksonCustomizer ... customizers) {
-        var mapper = new ObjectMapper(new YAMLFactory());
+        var yamlFactory = YAMLFactory.builder()
+                .loaderOptions(loadCustomizer
+                        .map(YamlUtils::createLoaderOptions)
+                        .orElseGet(YamlUtils::createLoaderOptions))
+                .build();
+        var mapper = new ObjectMapper(yamlFactory);
         mapper = JsonUtils.readingJavaTimeSupport(mapper);
         mapper = JsonUtils.readingCanSupport(mapper);
         for(JsonUtils.JacksonCustomizer customizer : customizers) {
@@ -158,9 +199,15 @@ public class YamlUtils {
      * Use Jackson to write YAML.
      */
     private ObjectMapper createJacksonWriter(
+            final Optional<YamlDumpCustomizer> dumpCustomizer,
             final JsonUtils.JacksonCustomizer ... customizers) {
-        var mapper = new ObjectMapper(new YAMLFactory()
-                .disable(YAMLGenerator.Feature.WRITE_DOC_START_MARKER));
+        var yamlFactory = YAMLFactory.builder()
+                .dumperOptions(dumpCustomizer
+                        .map(YamlUtils::createDumperOptions)
+                        .orElseGet(YamlUtils::createDumperOptions))
+                .build()
+                .disable(YAMLGenerator.Feature.WRITE_DOC_START_MARKER);
+        var mapper = new ObjectMapper(yamlFactory);
         mapper = JsonUtils.writingJavaTimeSupport(mapper);
         mapper = JsonUtils.writingCanSupport(mapper);
         for(JsonUtils.JacksonCustomizer customizer : customizers) {
@@ -170,10 +217,7 @@ public class YamlUtils {
         return mapper;
     }
 
-    @Deprecated
-    private Yaml createMapperLegacy(
-            final Class<?> mappedType,
-            final Can<YamlUtils.YamlDumpCustomizer> dumpCustomizers) {
+    private DumperOptions createDumperOptions(final YamlDumpCustomizer ... 
dumpCustomizers) {
         var dumperOptions = new DumperOptions();
         dumperOptions.setIndent(2);
         dumperOptions.setLineBreak(LineBreak.UNIX); // fixated for consistency
@@ -183,102 +227,16 @@ public class YamlUtils {
             dumperOptions = 
Optional.ofNullable(customizer.apply(dumperOptions))
                     .orElse(dumperOptions);
         }
-        var presenter = new Representer(dumperOptions);
-        presenter.setPropertyUtils(new PropertyUtils2());
-        presenter.addClassTag(mappedType, Tag.MAP);
-
-        var loaderOptions = new LoaderOptions();
-        var mapper = new Yaml(new Constructor(mappedType, loaderOptions), 
presenter, dumperOptions, loaderOptions);
-        return mapper;
+        return dumperOptions;
     }
 
-    // -- REPRESENTING RECORD TYPES
-
-    static class PropertyUtils2 extends PropertyUtils {
-
-        @Override
-        protected Map<String, Property> getPropertiesMap(final Class<?> type, 
final BeanAccess bAccess) {
-            if(type==Class.class) {
-                setAllowReadOnlyProperties(true);
-                try {
-                    val properties = new LinkedHashMap<String, Property>();
-                    val propertyDescriptor = new PropertyDescriptor("name", 
className(), null);
-                    properties.put("name", new 
MethodProperty(propertyDescriptor));
-                    return properties;
-                } catch (IntrospectionException e) {
-                    throw new YAMLException(e);
-                }
-            }
-            if(type.isRecord()) {
-                setAllowReadOnlyProperties(true);
-                try {
-                    val properties = new LinkedHashMap<String, Property>();
-                    for(RecordComponent rc: type.getRecordComponents()) {
-                        val propertyDescriptor = new 
PropertyDescriptor(rc.getName(), rc.getAccessor(), null);
-                        properties.put(rc.getName(), new 
MethodProperty(propertyDescriptor));
-                    }
-                    return postProcessMap(properties);
-                } catch (IntrospectionException e) {
-                    throw new YAMLException(e);
-                }
-            }
-            val map = super.getPropertiesMap(type, bAccess);
-            return postProcessMap(map);
-        }
-
-        private Map<String, Property> postProcessMap(final Map<String, 
Property> map) {
-            //debug
-            //System.err.printf("%s map: %s%n", type.getName(), map);
-            map.replaceAll((k, v)->
-                Can.class.isAssignableFrom(v.getType())
-                    && v instanceof MethodProperty // no field support yet
-                    ? MethodPropertyFromCanToList.wrap((MethodProperty)v)
-                    : v);
-            return map;
-        }
-
-        @Getter(lazy = true) @Accessors(fluent=true)
-        private final static Method className = lookupClassName();
-        @SneakyThrows
-        private static Method lookupClassName() {
-            return Class.class.getMethod("getName");
-        }
-
-        @Getter(lazy = true) @Accessors(fluent=true)
-        private final static Method canToList = lookupCanToList();
-        @SneakyThrows
-        private static Method lookupCanToList() {
-            return Can.class.getMethod("toList");
-        }
-
-    }
-
-    /** Wraps any {@link MethodProperty} that represent a {@link Can} type
-     * and acts as a {@link List} representing MethodProperty facade instead. 
*/
-    static class MethodPropertyFromCanToList extends MethodProperty {
-
-        @SneakyThrows
-        static MethodPropertyFromCanToList wrap(final MethodProperty 
wrappedMethodProperty) {
-            return new MethodPropertyFromCanToList(
-                    wrappedMethodProperty,
-                    new PropertyDescriptor(wrappedMethodProperty.getName(), 
null, null));
-        }
-
-        final MethodProperty wrappedMethodProperty;
-
-        MethodPropertyFromCanToList(
-                final MethodProperty wrappedMethodProperty,
-                final PropertyDescriptor property) {
-            super(property);
-            this.wrappedMethodProperty = wrappedMethodProperty;
-        }
-
-        @Override public Object get(final Object object) {
-            return ((Can<?>)wrappedMethodProperty.get(object)).toList();
+    private LoaderOptions createLoaderOptions(final YamlLoadCustomizer ... 
loadCustomizers) {
+        var loaderOptions = new LoaderOptions();
+        for(YamlUtils.YamlLoadCustomizer customizer : loadCustomizers) {
+            loaderOptions = 
Optional.ofNullable(customizer.apply(loaderOptions))
+                    .orElse(loaderOptions);
         }
-        @Override public Class<?> getType() { return List.class; }
-        @Override public boolean isReadable() { return true; }
-
+        return loaderOptions;
     }
 
 }

Reply via email to