diff --git a/release-notes/VERSION b/release-notes/VERSION index c214bb044..ac0c04092 100644 --- a/release-notes/VERSION +++ b/release-notes/VERSION @@ -2,6 +2,7 @@ Project: jackson-dataformat-xml Version: 2.3.0 (xx-xxx-2013) #38: Support root-level Collection serialization +#71: Fix issues with `XmlMapper.convertValue()` - Add support for `JsonGenerator.Feature.WRITE_BIGDECIMAL_AS_PLAIN` - Improved indentation diff --git a/src/main/java/com/fasterxml/jackson/dataformat/xml/ser/XmlSerializerProvider.java b/src/main/java/com/fasterxml/jackson/dataformat/xml/ser/XmlSerializerProvider.java index a361f6e4c..905c3083a 100644 --- a/src/main/java/com/fasterxml/jackson/dataformat/xml/ser/XmlSerializerProvider.java +++ b/src/main/java/com/fasterxml/jackson/dataformat/xml/ser/XmlSerializerProvider.java @@ -13,6 +13,7 @@ import com.fasterxml.jackson.databind.SerializationConfig; import com.fasterxml.jackson.databind.ser.SerializerFactory; import com.fasterxml.jackson.databind.ser.DefaultSerializerProvider; +import com.fasterxml.jackson.databind.util.TokenBuffer; import com.fasterxml.jackson.dataformat.xml.util.StaxUtil; import com.fasterxml.jackson.dataformat.xml.util.TypeUtil; import com.fasterxml.jackson.dataformat.xml.util.XmlRootNameLookup; @@ -60,25 +61,31 @@ public DefaultSerializerProvider createInstance(SerializationConfig config, { return new XmlSerializerProvider(this, config, jsf); } - + + @SuppressWarnings("resource") @Override public void serializeValue(JsonGenerator jgen, Object value) throws IOException, JsonProcessingException { + final ToXmlGenerator xgen = _asXmlGenerator(jgen); if (value == null) { - _serializeNull(jgen); + _serializeXmlNull(xgen); return; } - Class cls = value.getClass(); - QName rootName = _rootNameFromConfig(); - if (rootName == null) { - rootName = _rootNameLookup.findRootName(cls, _config); - } - _initWithRootName(jgen, rootName); - final boolean asArray = Collection.class.isAssignableFrom(cls) || - (cls.isArray() && cls != byte[].class); - if (asArray) { - _startRootArray(jgen, rootName); + final Class cls = value.getClass(); + final boolean asArray; + if (xgen == null) { // called by convertValue() + asArray = false; + } else { + QName rootName = _rootNameFromConfig(); + if (rootName == null) { + rootName = _rootNameLookup.findRootName(cls, _config); + } + _initWithRootName(xgen, rootName); + asArray = TypeUtil.isIndexedType(cls); + if (asArray) { + _startRootArray(jgen, rootName); + } } // From super-class implementation @@ -100,23 +107,30 @@ public void serializeValue(JsonGenerator jgen, Object value) jgen.writeEndObject(); } } - + + @SuppressWarnings("resource") @Override public void serializeValue(JsonGenerator jgen, Object value, JavaType rootType) throws IOException, JsonProcessingException { + final ToXmlGenerator xgen = _asXmlGenerator(jgen); if (value == null) { - _serializeNull(jgen); + _serializeXmlNull(xgen); return; } - QName rootName = _rootNameFromConfig(); - if (rootName == null) { - rootName = _rootNameLookup.findRootName(rootType, _config); - } - _initWithRootName(jgen, rootName); - final boolean asArray = TypeUtil.isIndexedType(rootType); - if (asArray) { - _startRootArray(jgen, rootName); + final boolean asArray; + if (xgen == null) { // called by convertValue() + asArray = false; + } else { + QName rootName = _rootNameFromConfig(); + if (rootName == null) { + rootName = _rootNameLookup.findRootName(rootType, _config); + } + _initWithRootName(xgen, rootName); + asArray = TypeUtil.isIndexedType(rootType); + if (asArray) { + _startRootArray(jgen, rootName); + } } final JsonSerializer ser = findTypedValueSerializer(rootType, true, null); @@ -140,23 +154,30 @@ public void serializeValue(JsonGenerator jgen, Object value, JavaType rootType) } // @since 2.1 + @SuppressWarnings("resource") @Override public void serializeValue(JsonGenerator jgen, Object value, JavaType rootType, JsonSerializer ser) throws IOException, JsonGenerationException { + final ToXmlGenerator xgen = _asXmlGenerator(jgen); if (value == null) { - _serializeNull(jgen); + _serializeXmlNull(xgen); return; } - QName rootName = _rootNameFromConfig(); - if (rootName == null) { - rootName = _rootNameLookup.findRootName(rootType, _config); - } - _initWithRootName(jgen, rootName); - final boolean asArray = TypeUtil.isIndexedType(rootType); - if (asArray) { - _startRootArray(jgen, rootName); + final boolean asArray; + if (xgen == null) { // called by convertValue() + asArray = false; + } else { + QName rootName = _rootNameFromConfig(); + if (rootName == null) { + rootName = _rootNameLookup.findRootName(rootType, _config); + } + _initWithRootName(xgen, rootName); + asArray = TypeUtil.isIndexedType(rootType); + if (asArray) { + _startRootArray(jgen, rootName); + } } if (ser == null) { ser = findTypedValueSerializer(rootType, true, null); @@ -187,18 +208,16 @@ protected void _startRootArray(JsonGenerator jgen, QName rootName) ((ToXmlGenerator) jgen).writeFieldName("item"); } - @Override - protected void _serializeNull(JsonGenerator jgen) + protected void _serializeXmlNull(ToXmlGenerator jgen) throws IOException, JsonProcessingException { _initWithRootName(jgen, ROOT_NAME_FOR_NULL); super.serializeValue(jgen, null); } - protected void _initWithRootName(JsonGenerator jgen, QName rootName) + protected void _initWithRootName(ToXmlGenerator xgen, QName rootName) throws IOException, JsonProcessingException { - ToXmlGenerator xgen = (ToXmlGenerator) jgen; /* 28-Nov-2012, tatu: We should only initialize the root * name if no name has been set, as per [Issue#42], * to allow for custom serializers to work. @@ -229,4 +248,19 @@ protected QName _rootNameFromConfig() String name = _config.getRootName(); return (name == null) ? null : new QName(name); } + + protected ToXmlGenerator _asXmlGenerator(JsonGenerator jgen) + throws JsonMappingException + { + // [Issue#71]: When converting, we actually get TokenBuffer, which is fine + if (!(jgen instanceof ToXmlGenerator)) { + // but verify + if (!(jgen instanceof TokenBuffer)) { + throw new JsonMappingException("XmlMapper does not with generators of type other than ToXmlGenerator; got: " + +jgen.getClass().getName()); + } + return null; + } + return (ToXmlGenerator) jgen; + } } diff --git a/src/main/java/com/fasterxml/jackson/dataformat/xml/util/TypeUtil.java b/src/main/java/com/fasterxml/jackson/dataformat/xml/util/TypeUtil.java index dc82049af..4a469e98c 100644 --- a/src/main/java/com/fasterxml/jackson/dataformat/xml/util/TypeUtil.java +++ b/src/main/java/com/fasterxml/jackson/dataformat/xml/util/TypeUtil.java @@ -1,5 +1,6 @@ package com.fasterxml.jackson.dataformat.xml.util; +import java.util.Collection; import java.util.Map; import com.fasterxml.jackson.databind.JavaType; @@ -28,4 +29,8 @@ public static boolean isIndexedType(JavaType type) return false; } + public static boolean isIndexedType(Class cls) + { + return (cls.isArray() && cls != byte[].class) || Collection.class.isAssignableFrom(cls); + } } diff --git a/src/test/java/com/fasterxml/jackson/dataformat/xml/XmlTestBase.java b/src/test/java/com/fasterxml/jackson/dataformat/xml/XmlTestBase.java index 030e6b4da..e54808bee 100644 --- a/src/test/java/com/fasterxml/jackson/dataformat/xml/XmlTestBase.java +++ b/src/test/java/com/fasterxml/jackson/dataformat/xml/XmlTestBase.java @@ -164,7 +164,7 @@ public IntWrapper(int value) { /* /********************************************************** - /* Construction + /* Construction, factory methods /********************************************************** */ @@ -172,6 +172,13 @@ protected XmlTestBase() { super(); } + protected XmlMapper xmlMapper(boolean useListWrapping) + { + JacksonXmlModule module = new JacksonXmlModule(); + module.setDefaultUseWrapper(useListWrapping); + return new XmlMapper(module); + } + /* /********************************************************** /* Additional assertion methods diff --git a/src/test/java/com/fasterxml/jackson/dataformat/xml/convert/TestArrayConversions.java b/src/test/java/com/fasterxml/jackson/dataformat/xml/convert/TestArrayConversions.java new file mode 100644 index 000000000..ce7e33ca5 --- /dev/null +++ b/src/test/java/com/fasterxml/jackson/dataformat/xml/convert/TestArrayConversions.java @@ -0,0 +1,201 @@ +package com.fasterxml.jackson.dataformat.xml.convert; + +import java.util.*; +import java.lang.reflect.Array; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.dataformat.xml.XmlMapper; +import com.fasterxml.jackson.dataformat.xml.XmlTestBase; + +/* NOTE: copied from jackson-databind (with some pruning) + */ + +/** + * Conversion tests to ensure that standard ObjectMapper conversions + * work despite XmlMapper having to add XML-specific work-arounds. + */ +public class TestArrayConversions extends XmlTestBase +{ + public void testNullXform() throws Exception { + _testNullXform(xmlMapper(true)); + _testNullXform(xmlMapper(false)); + } + + private void _testNullXform(ObjectMapper mapper) throws Exception + { + // when given null, null should be returned without conversion (Java null has no type) + assertNull(mapper.convertValue(null, Integer.class)); + assertNull(mapper.convertValue(null, String.class)); + assertNull(mapper.convertValue(null, byte[].class)); + } + + /** + * Tests to verify that primitive number arrays round-trip + * correctly, i.e. type -> type gives equal (although + * not necessarily same) output + */ + public void testArrayIdentityTransforms() throws Exception { + _testArrayIdentityTransforms(xmlMapper(true)); + _testArrayIdentityTransforms(xmlMapper(false)); + } + + private void _testArrayIdentityTransforms(ObjectMapper mapper) throws Exception + { + // first integral types + // (note: byte[] is ok, even if it goes to base64 and back) + verifyByteArrayConversion(mapper, bytes(), byte[].class); + verifyShortArrayConversion(mapper, shorts(), short[].class); + verifyIntArrayConversion(mapper, ints(), int[].class); + verifyLongArrayConversion(mapper, longs(), long[].class); + // then primitive decimal types + verifyFloatArrayConversion(mapper, floats(), float[].class); + verifyDoubleArrayConversion(mapper, doubles(), float[].class); + } + + public void testByteArrayFrom() throws Exception { + _testByteArrayFrom(xmlMapper(true)); + _testByteArrayFrom(xmlMapper(false)); + } + + private void _testByteArrayFrom(ObjectMapper mapper) throws Exception + { + /* Note: byte arrays are tricky, since they are considered + * binary data primarily, not as array of numbers. Hence + * output will be base64 encoded... + */ + byte[] data = _convert(mapper, "c3VyZS4=", byte[].class); + byte[] exp = "sure.".getBytes("Ascii"); + verifyIntegralArrays(exp, data, exp.length); + } + + public void testShortArrayToX() throws Exception + { + final XmlMapper mapper = new XmlMapper(); + short[] data = shorts(); + verifyShortArrayConversion(mapper, data, byte[].class); + verifyShortArrayConversion(mapper, data, int[].class); + verifyShortArrayConversion(mapper, data, long[].class); + } + + public void testIntArrayToX() throws Exception + { + final XmlMapper mapper = new XmlMapper(); + + int[] data = ints(); + verifyIntArrayConversion(mapper, data, byte[].class); + verifyIntArrayConversion(mapper, data, short[].class); + verifyIntArrayConversion(mapper, data, long[].class); + + List expNums = _numberList(data, data.length); + // Alas, due to type erasure, need to use TypeRef, not just class + List actNums = mapper.convertValue(data, new TypeReference>() {}); + assertEquals(expNums, actNums); + } + + public void testLongArrayToX() throws Exception + { + final XmlMapper mapper = new XmlMapper(); + long[] data = longs(); + verifyLongArrayConversion(mapper, data, byte[].class); + verifyLongArrayConversion(mapper, data, short[].class); + verifyLongArrayConversion(mapper, data, int[].class); + + List expNums = _numberList(data, data.length); + List actNums = mapper.convertValue(data, new TypeReference>() {}); + assertEquals(expNums, actNums); + } + + /* + /******************************************************** + /* Helper methods + /******************************************************** + */ + + // note: all value need to be within byte range + + private byte[] bytes() { return new byte[] { 1, -1, 0, 98, 127 }; } + private short[] shorts() { return new short[] { 1, -1, 0, 98, 127 }; } + private int[] ints() { return new int[] { 1, -1, 0, 98, 127 }; } + private long[] longs() { return new long[] { 1, -1, 0, 98, 127 }; } + + // note: use values that are exact in binary + + private double[] doubles() { return new double[] { 0.0, 0.25, -0.125, 10.5, 9875.0 }; } + private float[] floats() { return new float[] { + 0.0f, 0.25f, -0.125f, 10.5f, 9875.0f }; + } + + private void verifyByteArrayConversion(ObjectMapper mapper, byte[] data, Class arrayType) { + T result = _convert(mapper, data, arrayType); + verifyIntegralArrays(data, result, data.length); + } + private void verifyShortArrayConversion(ObjectMapper mapper, short[] data, Class arrayType) { + T result = _convert(mapper, data, arrayType); + verifyIntegralArrays(data, result, data.length); + } + private void verifyIntArrayConversion(ObjectMapper mapper, int[] data, Class arrayType) { + T result = _convert(mapper, data, arrayType); + verifyIntegralArrays(data, result, data.length); + } + private void verifyLongArrayConversion(ObjectMapper mapper, long[] data, Class arrayType) { + T result = _convert(mapper, data, arrayType); + verifyIntegralArrays(data, result, data.length); + } + private void verifyFloatArrayConversion(ObjectMapper mapper, float[] data, Class arrayType) { + T result = _convert(mapper, data, arrayType); + verifyDoubleArrays(data, result, data.length); + } + private void verifyDoubleArrayConversion(ObjectMapper mapper, double[] data, Class arrayType) { + T result = _convert(mapper, data, arrayType); + verifyDoubleArrays(data, result, data.length); + } + + private T _convert(ObjectMapper mapper, Object input, Class outputType) + { + // must be a primitive array, like "int[].class" + if (!outputType.isArray()) throw new IllegalArgumentException(); + if (!outputType.getComponentType().isPrimitive()) throw new IllegalArgumentException(); + T result = mapper.convertValue(input, outputType); + // sanity check first: + assertNotNull(result); + assertEquals(outputType, result.getClass()); + return result; + } + + private List _numberList(Object numberArray, int size) + { + ArrayList result = new ArrayList(size); + for (int i = 0; i < size; ++i) { + result.add((Number) Array.get(numberArray, i)); + } + return result; + } + + /** + * Helper method for checking that given collections contain integral Numbers + * that essentially contain same values in same order + */ + private void verifyIntegralArrays(Object inputArray, Object outputArray, int size) + { + for (int i = 0; i < size; ++i) { + Number n1 = (Number) Array.get(inputArray, i); + Number n2 = (Number) Array.get(outputArray, i); + double value1 = ((Number) n1).longValue(); + double value2 = ((Number) n2).longValue(); + assertEquals("Entry #"+i+"/"+size+" not equal", value1, value2); + } + } + + private void verifyDoubleArrays(Object inputArray, Object outputArray, int size) + { + for (int i = 0; i < size; ++i) { + Number n1 = (Number) Array.get(inputArray, i); + Number n2 = (Number) Array.get(outputArray, i); + double value1 = ((Number) n1).doubleValue(); + double value2 = ((Number) n2).doubleValue(); + assertEquals("Entry #"+i+"/"+size+" not equal", value1, value2); + } + } + +}