From 479522112267a0448223d38870bf5d63e77a5c33 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Wed, 14 Aug 2013 22:31:24 -0700 Subject: [PATCH] Implement #38 --- release-notes/VERSION | 1 + .../dataformat/xml/ser/ToXmlGenerator.java | 12 ++ .../dataformat/xml/ser/XmlBeanSerializer.java | 3 +- .../xml/ser/XmlSerializerProvider.java | 138 ++++++++++++++++-- .../jackson/dataformat/xml/XmlTestBase.java | 8 +- .../xml/failing/TestUnwrappedRootList.java | 33 ++++- 6 files changed, 174 insertions(+), 21 deletions(-) diff --git a/release-notes/VERSION b/release-notes/VERSION index 83c605946..c214bb044 100644 --- a/release-notes/VERSION +++ b/release-notes/VERSION @@ -1,6 +1,7 @@ Project: jackson-dataformat-xml Version: 2.3.0 (xx-xxx-2013) +#38: Support root-level Collection serialization - Add support for `JsonGenerator.Feature.WRITE_BIGDECIMAL_AS_PLAIN` - Improved indentation diff --git a/src/main/java/com/fasterxml/jackson/dataformat/xml/ser/ToXmlGenerator.java b/src/main/java/com/fasterxml/jackson/dataformat/xml/ser/ToXmlGenerator.java index b8d9330ce..92420e222 100644 --- a/src/main/java/com/fasterxml/jackson/dataformat/xml/ser/ToXmlGenerator.java +++ b/src/main/java/com/fasterxml/jackson/dataformat/xml/ser/ToXmlGenerator.java @@ -324,6 +324,18 @@ public void finishWrappedValue(QName wrapperName, QName wrappedName) throws IOEx } } + /** + * Trivial helper method called when to add a replicated wrapper name + * + * @since 2.2 + */ + public void writeRepeatedFieldName() throws IOException, JsonGenerationException + { + if (_writeContext.writeFieldName(_nextName.getLocalPart()) == JsonWriteContext.STATUS_EXPECT_VALUE) { + _reportError("Can not write a field name, expecting a value"); + } + } + /* /********************************************************** /* JsonGenerator method overrides diff --git a/src/main/java/com/fasterxml/jackson/dataformat/xml/ser/XmlBeanSerializer.java b/src/main/java/com/fasterxml/jackson/dataformat/xml/ser/XmlBeanSerializer.java index 0bcccc994..38312f95d 100644 --- a/src/main/java/com/fasterxml/jackson/dataformat/xml/ser/XmlBeanSerializer.java +++ b/src/main/java/com/fasterxml/jackson/dataformat/xml/ser/XmlBeanSerializer.java @@ -121,7 +121,6 @@ public BeanSerializer withObjectIdWriter(ObjectIdWriter objectIdWriter) { * extra handling, such as indication of whether to write attributes or * elements. */ - @SuppressWarnings("deprecation") @Override protected void serializeFields(Object bean, JsonGenerator jgen0, SerializerProvider provider) throws IOException, JsonGenerationException @@ -129,7 +128,7 @@ protected void serializeFields(Object bean, JsonGenerator jgen0, SerializerProvi final ToXmlGenerator xgen = (ToXmlGenerator) jgen0; final BeanPropertyWriter[] props; // !!! TODO: change to use non-deprecated version in 2.3 - if (_filteredProps != null && provider.getSerializationView() != null) { + if (_filteredProps != null && provider.getActiveView() != null) { props = _filteredProps; } else { props = _props; 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 53415a275..a361f6e4c 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 @@ -1,16 +1,20 @@ package com.fasterxml.jackson.dataformat.xml.ser; import java.io.IOException; +import java.util.Collection; + import javax.xml.namespace.QName; import javax.xml.stream.XMLStreamException; import com.fasterxml.jackson.core.*; import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.SerializationConfig; import com.fasterxml.jackson.databind.ser.SerializerFactory; import com.fasterxml.jackson.databind.ser.DefaultSerializerProvider; import com.fasterxml.jackson.dataformat.xml.util.StaxUtil; +import com.fasterxml.jackson.dataformat.xml.util.TypeUtil; import com.fasterxml.jackson.dataformat.xml.util.XmlRootNameLookup; @@ -56,37 +60,141 @@ public DefaultSerializerProvider createInstance(SerializationConfig config, { return new XmlSerializerProvider(this, config, jsf); } - + @Override public void serializeValue(JsonGenerator jgen, Object value) throws IOException, JsonProcessingException { - QName rootName = (value == null) ? ROOT_NAME_FOR_NULL - : _rootNameLookup.findRootName(value.getClass(), _config); + if (value == null) { + _serializeNull(jgen); + return; + } + Class cls = value.getClass(); + QName rootName = _rootNameFromConfig(); + if (rootName == null) { + rootName = _rootNameLookup.findRootName(cls, _config); + } _initWithRootName(jgen, rootName); - super.serializeValue(jgen, value); - } + final boolean asArray = Collection.class.isAssignableFrom(cls) || + (cls.isArray() && cls != byte[].class); + if (asArray) { + _startRootArray(jgen, rootName); + } + + // From super-class implementation + final JsonSerializer ser = findTypedValueSerializer(cls, true, null); + try { + ser.serialize(value, jgen, this); + } catch (IOException ioe) { // As per [JACKSON-99], pass IOException and subtypes as-is + throw ioe; + } catch (Exception e) { // but wrap RuntimeExceptions, to get path information + String msg = e.getMessage(); + if (msg == null) { + msg = "[no message for "+e.getClass().getName()+"]"; + } + throw new JsonMappingException(msg, e); + } + // end of super-class implementation + if (asArray) { + jgen.writeEndObject(); + } + } + @Override public void serializeValue(JsonGenerator jgen, Object value, JavaType rootType) throws IOException, JsonProcessingException { - QName rootName = _rootNameLookup.findRootName(rootType, _config); + if (value == null) { + _serializeNull(jgen); + return; + } + QName rootName = _rootNameFromConfig(); + if (rootName == null) { + rootName = _rootNameLookup.findRootName(rootType, _config); + } _initWithRootName(jgen, rootName); - super.serializeValue(jgen, value, rootType); - } + final boolean asArray = TypeUtil.isIndexedType(rootType); + if (asArray) { + _startRootArray(jgen, rootName); + } + final JsonSerializer ser = findTypedValueSerializer(rootType, true, null); + // From super-class implementation + try { + ser.serialize(value, jgen, this); + } catch (IOException ioe) { // no wrapping for IO (and derived) + throw ioe; + } catch (Exception e) { // but others do need to be, to get path etc + String msg = e.getMessage(); + if (msg == null) { + msg = "[no message for "+e.getClass().getName()+"]"; + } + throw new JsonMappingException(msg, e); + } + // end of super-class implementation + + if (asArray) { + jgen.writeEndObject(); + } + } + // @since 2.1 @Override public void serializeValue(JsonGenerator jgen, Object value, JavaType rootType, JsonSerializer ser) throws IOException, JsonGenerationException { - QName rootName = _rootNameLookup.findRootName(rootType, _config); + if (value == null) { + _serializeNull(jgen); + return; + } + QName rootName = _rootNameFromConfig(); + if (rootName == null) { + rootName = _rootNameLookup.findRootName(rootType, _config); + } _initWithRootName(jgen, rootName); - super.serializeValue(jgen, value, rootType, ser); + final boolean asArray = TypeUtil.isIndexedType(rootType); + if (asArray) { + _startRootArray(jgen, rootName); + } + if (ser == null) { + ser = findTypedValueSerializer(rootType, true, null); + } + // From super-class implementation + try { + ser.serialize(value, jgen, this); + } catch (IOException ioe) { // no wrapping for IO (and derived) + throw ioe; + } catch (Exception e) { // but others do need to be, to get path etc + String msg = e.getMessage(); + if (msg == null) { + msg = "[no message for "+e.getClass().getName()+"]"; + } + throw new JsonMappingException(msg, e); + } + // end of super-class implementation + if (asArray) { + jgen.writeEndObject(); + } } - + + protected void _startRootArray(JsonGenerator jgen, QName rootName) + throws IOException, JsonProcessingException + { + jgen.writeStartObject(); + // Could repeat root name, but what's the point? How to customize? + ((ToXmlGenerator) jgen).writeFieldName("item"); + } + + @Override + protected void _serializeNull(JsonGenerator jgen) + throws IOException, JsonProcessingException + { + _initWithRootName(jgen, ROOT_NAME_FOR_NULL); + super.serializeValue(jgen, null); + } + protected void _initWithRootName(JsonGenerator jgen, QName rootName) throws IOException, JsonProcessingException { @@ -103,7 +211,7 @@ protected void _initWithRootName(JsonGenerator jgen, QName rootName) } xgen.initGenerator(); String ns = rootName.getNamespaceURI(); - /* [Issue-26] If we just try writing root element with namespace, + /* [Issue#26] If we just try writing root element with namespace, * we will get an explicit prefix. But we'd rather use the default * namespace, so let's try to force that. */ @@ -115,4 +223,10 @@ protected void _initWithRootName(JsonGenerator jgen, QName rootName) } } } + + protected QName _rootNameFromConfig() + { + String name = _config.getRootName(); + return (name == null) ? null : new QName(name); + } } 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 dca20d81e..030e6b4da 100644 --- a/src/test/java/com/fasterxml/jackson/dataformat/xml/XmlTestBase.java +++ b/src/test/java/com/fasterxml/jackson/dataformat/xml/XmlTestBase.java @@ -274,10 +274,14 @@ protected String readAll(File f) throws IOException return sb.toString(); } - public String jaxbSerialized(Object ob) throws Exception + public String jaxbSerialized(Object ob, Class... classes) throws Exception { StringWriter sw = new StringWriter(); - javax.xml.bind.JAXB.marshal(ob, sw); + if (classes.length == 0) { + javax.xml.bind.JAXB.marshal(ob, sw); + } else { + javax.xml.bind.JAXBContext.newInstance(classes).createMarshaller().marshal(ob, sw); + } sw.close(); return sw.toString(); } diff --git a/src/test/java/com/fasterxml/jackson/dataformat/xml/failing/TestUnwrappedRootList.java b/src/test/java/com/fasterxml/jackson/dataformat/xml/failing/TestUnwrappedRootList.java index 719eb968d..7dab1b66a 100644 --- a/src/test/java/com/fasterxml/jackson/dataformat/xml/failing/TestUnwrappedRootList.java +++ b/src/test/java/com/fasterxml/jackson/dataformat/xml/failing/TestUnwrappedRootList.java @@ -3,6 +3,7 @@ import java.util.*; import com.fasterxml.jackson.databind.AnnotationIntrospector; +import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.introspect.JacksonAnnotationIntrospector; import com.fasterxml.jackson.dataformat.xml.*; @@ -68,13 +69,35 @@ public void testListSerialization() throws Exception l.add(r2); // to see what JAXB might do, uncomment: -// System.out.println("By JAXB: "+jaxbSerialized(l)); +//System.out.println("By JAXB: "+jaxbSerialized(l)); // ArrayList.class, SampleResource.class)); - String result = xmlMapper.writeValueAsString(l); - assertNotNull(result); + String xml = xmlMapper + .writerWithDefaultPrettyPrinter() + .withRootName("RootList") + .writeValueAsString(l) + .trim(); - // TODO: verify actual contents + // first trivial sanity checks + assertNotNull(xml); + if (xml.indexOf("") < 0) { + fail("Unexpected output: should have as root element, got: "+xml); + } + + // and then try reading back + JavaType resListType = xmlMapper.getTypeFactory() + .constructCollectionType(List.class, SampleResource.class); + Object ob = xmlMapper.reader(resListType).readValue(xml); + assertNotNull(ob); + +// System.err.println("XML -> "+xmlMapper.writerWithDefaultPrettyPrinter().writeValueAsString(ob)); -// assertEquals("", result); + assertTrue(ob instanceof List); + List resultList = (List) ob; + assertEquals(2, resultList.size()); + assertEquals(SampleResource.class, resultList.get(0).getClass()); + assertEquals(SampleResource.class, resultList.get(1).getClass()); + SampleResource rr = (SampleResource) resultList.get(1); + assertEquals("William", rr.getName()); + } }