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(