From 21d9fe20c0b89214ec1d952925b53cce744668d5 Mon Sep 17 00:00:00 2001 From: Marco Belladelli Date: Fri, 10 Mar 2023 14:37:23 +0100 Subject: [PATCH] HHH-16280 Fix JacksonXmlFormatMapper handling of array data types --- hibernate-core/hibernate-core.gradle | 2 + .../jackson/JacksonXmlFormatMapper.java | 61 +++++++++++++++++-- settings.gradle | 1 + 3 files changed, 59 insertions(+), 5 deletions(-) diff --git a/hibernate-core/hibernate-core.gradle b/hibernate-core/hibernate-core.gradle index 2917d622c0..d9eb888082 100644 --- a/hibernate-core/hibernate-core.gradle +++ b/hibernate-core/hibernate-core.gradle @@ -72,6 +72,8 @@ dependencies { testRuntimeOnly testLibs.weld testRuntimeOnly testLibs.wildFlyTxnClient testRuntimeOnly libs.jackson + testRuntimeOnly libs.jacksonXml + testRuntimeOnly libs.jacksonJsr310 testAnnotationProcessor project( ':hibernate-jpamodelgen' ) diff --git a/hibernate-core/src/main/java/org/hibernate/type/format/jackson/JacksonXmlFormatMapper.java b/hibernate-core/src/main/java/org/hibernate/type/format/jackson/JacksonXmlFormatMapper.java index e390fddd16..a901e4a2aa 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/format/jackson/JacksonXmlFormatMapper.java +++ b/hibernate-core/src/main/java/org/hibernate/type/format/jackson/JacksonXmlFormatMapper.java @@ -6,13 +6,24 @@ */ package org.hibernate.type.format.jackson; -import org.hibernate.type.format.FormatMapper; +import java.io.IOException; +import java.lang.reflect.Type; +import java.util.ArrayList; + import org.hibernate.type.descriptor.WrapperOptions; import org.hibernate.type.descriptor.java.JavaType; +import org.hibernate.type.format.FormatMapper; +import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.module.SimpleModule; import com.fasterxml.jackson.dataformat.xml.XmlMapper; +import com.fasterxml.jackson.dataformat.xml.ser.ToXmlGenerator; /** * @author Christian Beikov @@ -24,20 +35,37 @@ public final class JacksonXmlFormatMapper implements FormatMapper { private final ObjectMapper objectMapper; public JacksonXmlFormatMapper() { - this(new XmlMapper()); + this( createXmlMapper() ); } public JacksonXmlFormatMapper(ObjectMapper objectMapper) { this.objectMapper = objectMapper; } + private static XmlMapper createXmlMapper() { + final XmlMapper xmlMapper = new XmlMapper(); + // needed to automatically find and register Jackson's jsr310 module for java.time support + xmlMapper.findAndRegisterModules(); + xmlMapper.configure( SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false ); + xmlMapper.enable( ToXmlGenerator.Feature.WRITE_NULLS_AS_XSI_NIL ); + // Workaround for null vs empty string handling inside arrays, + // see: https://github.com/FasterXML/jackson-dataformat-xml/issues/344 + final SimpleModule module = new SimpleModule(); + module.addDeserializer( String[].class, new StringArrayDeserializer() ); + xmlMapper.registerModule( module ); + return xmlMapper; + } + @Override public T fromString(CharSequence charSequence, JavaType javaType, WrapperOptions wrapperOptions) { if ( javaType.getJavaType() == String.class || javaType.getJavaType() == Object.class ) { return (T) charSequence.toString(); } try { - return objectMapper.readValue( charSequence.toString(), objectMapper.constructType( javaType.getJavaType() ) ); + return objectMapper.readValue( + charSequence.toString(), + objectMapper.constructType( javaType.getJavaType() ) + ); } catch (JsonProcessingException e) { throw new IllegalArgumentException( "Could not deserialize string to java type: " + javaType, e ); @@ -49,12 +77,35 @@ public final class JacksonXmlFormatMapper implements FormatMapper { if ( javaType.getJavaType() == String.class || javaType.getJavaType() == Object.class ) { return (String) value; } + else if ( javaType.getJavaTypeClass().isArray() ) { + if ( javaType.getJavaTypeClass().getComponentType().isEnum() ) { + // for enum arrays we need to explicitly pass Byte[] as the writer type + return writeValueAsString( value, javaType, Byte[].class ); + } + } + return writeValueAsString( value, javaType, javaType.getJavaType() ); + } + + private String writeValueAsString(Object value, JavaType javaType, Type type) { try { - return objectMapper.writerFor( objectMapper.constructType( javaType.getJavaType() ) ) - .writeValueAsString( value ); + return objectMapper.writerFor( objectMapper.constructType( type ) ).writeValueAsString( value ); } catch (JsonProcessingException e) { throw new IllegalArgumentException( "Could not serialize object of java type: " + javaType, e ); } } + + private static class StringArrayDeserializer extends JsonDeserializer { + @Override + public String[] deserialize(JsonParser jp, DeserializationContext deserializationContext) throws IOException { + final ArrayList result = new ArrayList<>(); + JsonToken token; + while ( ( token = jp.nextValue() ) != JsonToken.END_OBJECT ) { + if ( token.isScalarValue() ) { + result.add( jp.getValueAsString() ); + } + } + return result.toArray( String[]::new ); + } + } } diff --git a/settings.gradle b/settings.gradle index 07476ab0a3..a4411f718e 100644 --- a/settings.gradle +++ b/settings.gradle @@ -84,6 +84,7 @@ dependencyResolutionManagement { alias( "jackson" ).to ( "com.fasterxml.jackson.core", "jackson-databind" ).version( "2.14.1" ) alias( "jacksonXml" ).to ( "com.fasterxml.jackson.dataformat", "jackson-dataformat-xml" ).version( "2.14.1" ) + alias( "jacksonJsr310" ).to( "com.fasterxml.jackson.datatype", "jackson-datatype-jsr310" ).version( "2.14.1" ) alias( "validator" ).to( "org.hibernate.validator", "hibernate-validator" ).version( "8.0.0.Final" ) alias( "ant" ).to( "org.apache.ant", "ant" ).version( "1.8.2" )