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

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


The following commit(s) were added to refs/heads/master by this push:
     new 13b5debd17 CAUSEWAY-3722: [Commons] Json/YamlUtils support for 
XmlAdapter
13b5debd17 is described below

commit 13b5debd174caf9e9edc365a70a1ad6f5e79b569
Author: Andi Huber <[email protected]>
AuthorDate: Fri Apr 12 12:18:35 2024 +0200

    CAUSEWAY-3722: [Commons] Json/YamlUtils support for XmlAdapter
---
 .../commons/internal/reflection/_Generics.java     | 54 ++++++++++++++++-
 .../org/apache/causeway/commons/io/JsonUtils.java  | 67 +++++++++++++++++++++-
 2 files changed, 117 insertions(+), 4 deletions(-)

diff --git 
a/commons/src/main/java/org/apache/causeway/commons/internal/reflection/_Generics.java
 
b/commons/src/main/java/org/apache/causeway/commons/internal/reflection/_Generics.java
index e4bd790eab..e12fd926a1 100644
--- 
a/commons/src/main/java/org/apache/causeway/commons/internal/reflection/_Generics.java
+++ 
b/commons/src/main/java/org/apache/causeway/commons/internal/reflection/_Generics.java
@@ -29,6 +29,7 @@ import java.util.stream.Stream;
 
 import org.springframework.lang.Nullable;
 
+import org.apache.causeway.commons.collections.Can;
 import org.apache.causeway.commons.internal.base._NullSafe;
 
 import lombok.NonNull;
@@ -80,11 +81,32 @@ public final class _Generics {
 
     /**
      * Returns a Stream of the actual type arguments for given type {@code 
cls}.
-     * @implNote always returns {@link Stream#empty()}, as we don't know how 
to do this
+     * @apiNote may return {@link Stream#empty()}, as we don't know how to do 
this in the general case
+     * @implNote will work for simple cases, but NOT for wildcards or non 
trivial bounds
+     */
+    public static Stream<Class<?>> streamGenericTypeArgumentsOfType(final 
@NonNull Class<?> cls) {
+        return streamGenericTypeArgumentsOfType(cls, cls);
+    }
+
+    /**
+     * Returns a Stream of the actual type arguments for given type {@code 
cls} after up-castingn to
+     * {@code stopAtSuperClass}.
+     * @apiNote may return {@link Stream#empty()}, as we don't know how to do 
this in the general case
+     * @implNote will work for simple cases, but NOT for wildcards or non 
trivial bounds
      */
     public static Stream<Class<?>> streamGenericTypeArgumentsOfType(
-            final @NonNull Class<?> cls) {
-        // maybe the best one could do is to extract any bounds on the type 
argument
+            final @NonNull Class<?> cls, final @NonNull Class<?> 
stopAtSuperClass) {
+        var superClass = genericUpCast(cls, stopAtSuperClass);
+        if(superClass instanceof ParameterizedType) {
+            final ParameterizedType pt = (ParameterizedType) superClass;
+            final Type[] typeArgs = pt.getActualTypeArguments();
+            final int typeArgCount = _NullSafe.size(typeArgs);
+            var extractedGenericTypes = Can.ofArray(typeArgs)
+                        .<Class<?>>map(typeArg->typeToClass(typeArg));
+            return extractedGenericTypes.size()==typeArgCount
+                ? extractedGenericTypes.stream()
+                : Stream.empty(); // if any of the type to class conversions 
failed, return an empty stream
+        }
         return Stream.empty();
     }
 
@@ -163,4 +185,30 @@ public final class _Generics {
         return Stream.empty();
     }
 
+    @Nullable
+    private static Class<?> typeToClass(final @NonNull Type type) {
+        if(type instanceof Class) {
+            return (Class<?>) type;
+        }
+        if(type instanceof ParameterizedType) {
+            var pType = (ParameterizedType) type;
+            return typeToClass(pType.getRawType());
+        }
+        // don't know how to do this otherwise
+        return null;
+    }
+
+    private static Type genericUpCast(final @NonNull Class<?> cls, final 
@NonNull Class<?> stopAtSuperClass) {
+        var superClass = cls.getGenericSuperclass();
+        if(cls.equals(stopAtSuperClass)) {
+            // don't know how to get a ParameterizedType for cls, so we assume 
there is a super type to the rescue
+            superClass = cls.getGenericSuperclass();
+        }
+        while (superClass instanceof ParameterizedType
+                && stopAtSuperClass != 
((ParameterizedType)superClass).getRawType()) {
+            superClass = ((Class<?>) ((ParameterizedType) 
superClass).getRawType()).getGenericSuperclass();
+        }
+        return superClass;
+    }
+
 }
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 96b9cbbac2..fa96958803 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
@@ -24,7 +24,10 @@ import java.util.List;
 import java.util.Optional;
 import java.util.function.UnaryOperator;
 
+import javax.xml.bind.annotation.adapters.XmlAdapter;
+
 import com.fasterxml.jackson.annotation.JsonInclude.Include;
+import com.fasterxml.jackson.core.JacksonException;
 import com.fasterxml.jackson.core.JsonGenerator;
 import com.fasterxml.jackson.core.JsonParser;
 import com.fasterxml.jackson.databind.BeanProperty;
@@ -36,6 +39,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.databind.SerializationFeature;
 import com.fasterxml.jackson.databind.SerializerProvider;
 import com.fasterxml.jackson.databind.deser.ContextualDeserializer;
+import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
 import com.fasterxml.jackson.databind.module.SimpleModule;
 import com.fasterxml.jackson.databind.ser.std.StdSerializer;
 import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
@@ -46,6 +50,8 @@ import org.springframework.lang.Nullable;
 import org.apache.causeway.commons.collections.Can;
 import org.apache.causeway.commons.functional.Try;
 import org.apache.causeway.commons.internal.base._Casts;
+import org.apache.causeway.commons.internal.exceptions._Exceptions;
+import org.apache.causeway.commons.internal.reflection._Generics;
 
 import lombok.NonNull;
 import lombok.SneakyThrows;
@@ -61,7 +67,26 @@ import lombok.experimental.UtilityClass;
 public class JsonUtils {
 
     @FunctionalInterface
-    public interface JacksonCustomizer extends UnaryOperator<ObjectMapper> {}
+    public interface JacksonCustomizer extends UnaryOperator<ObjectMapper> {
+        public static <T> JacksonCustomizer wrapXmlAdapter(final 
XmlAdapter<String, T> xmlAdapter) {
+            @SuppressWarnings("unchecked")
+            var type = (Class<T>) 
_Generics.streamGenericTypeArgumentsOfType(xmlAdapter.getClass(), 
XmlAdapter.class)
+                .skip(1)
+                .findFirst()
+                .orElseThrow(()->_Exceptions.unsupportedOperation(
+                        "Failed to autodetect second generic type argument of 
class %s. "
+                        + "Use variant JacksonCustomizer.wrapXmlAdapter(type, 
xmlAdapter) "
+                        + "to provide the type explicitely.",
+                        xmlAdapter.getClass().getName()));
+            return wrapXmlAdapter(type, xmlAdapter);
+        }
+        public static <T> JacksonCustomizer wrapXmlAdapter(final Class<T> 
type, final XmlAdapter<String, T> xmlAdapter) {
+            return mapper->
+                mapper.registerModule(new SimpleModule()
+                        .addSerializer(new XSerializer<T>(type, xmlAdapter))
+                        .addDeserializer(type, new XDeserializer<T>(type, 
xmlAdapter)));
+        }
+    }
 
     // -- READING
 
@@ -213,6 +238,46 @@ public class JsonUtils {
         return mapper;
     }
 
+    // -- XML ADAPTER SUPPORT
+
+    static class XSerializer<T> extends StdSerializer<T> {
+        private static final long serialVersionUID = 1L;
+        private final XmlAdapter<String, T> xmlAdapter;
+        protected XSerializer(final Class<T> type, final XmlAdapter<String, T> 
xmlAdapter) {
+            super(type, false);
+            this.xmlAdapter = xmlAdapter;
+        }
+        @Override
+        public void serialize(final T value, final JsonGenerator gen,
+                final SerializerProvider provider) throws IOException {
+            String stringified;
+            try {
+                stringified = this.xmlAdapter.marshal(value);
+            } catch (Exception e) {
+                throw new JsonMappingException(gen, "Unable to marshal: " + 
e.getMessage(), e);
+            }
+            gen.writeObject(stringified);
+        }
+    }
+
+    static class XDeserializer<T> extends StdDeserializer<T> {
+        private static final long serialVersionUID = 1L;
+        private final XmlAdapter<String, T> xmlAdapter;
+        protected XDeserializer(final Class<T> type, final XmlAdapter<String, 
T> xmlAdapter) {
+            super(type);
+            this.xmlAdapter = xmlAdapter;
+        }
+        @Override
+        public T deserialize(final JsonParser p, final DeserializationContext 
ctxt) throws IOException, JacksonException {
+            String stringified = ctxt.readValue(p, String.class);
+            try {
+                return xmlAdapter.unmarshal(stringified);
+            } catch (Exception e) {
+                throw new JsonMappingException(p, "Unable to unmarshal (to 
type " + _valueType + "): " + e.getMessage(), e);
+            }
+        }
+    }
+
     // -- MAPPER FACTORY
 
     private ObjectMapper createJacksonReader(

Reply via email to