From 4938036087bed3ebdfaf6c98db14d269179c30a8 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Fri, 6 Sep 2019 22:59:03 -0700 Subject: [PATCH] Fix #354 (support `xsi:nil`) --- release-notes/VERSION-2.x | 1 + .../dataformat/xml/deser/FromXmlParser.java | 26 ++++-- .../dataformat/xml/deser/XmlTokenStream.java | 93 +++++++++++++++++-- .../xml/deser/SimpleStringValuesTest.java | 5 +- .../dataformat/xml/failing/XsiNil354Test.java | 28 +++++- 5 files changed, 134 insertions(+), 19 deletions(-) diff --git a/release-notes/VERSION-2.x b/release-notes/VERSION-2.x index 9e33bf620..9c66872ae 100644 --- a/release-notes/VERSION-2.x +++ b/release-notes/VERSION-2.x @@ -8,6 +8,7 @@ Project: jackson-dataformat-xml #242: Deserialization of class inheritance depends on attributes order (reported by Victor K) +#354: Support mapping `xsi:nul` marked elements as `null`s (`JsonToken.VALUE_NULL`) 2.10.0.pr2 (31-Aug-2019) diff --git a/src/main/java/com/fasterxml/jackson/dataformat/xml/deser/FromXmlParser.java b/src/main/java/com/fasterxml/jackson/dataformat/xml/deser/FromXmlParser.java index 7dce3c5f8..0cd2ca1dd 100644 --- a/src/main/java/com/fasterxml/jackson/dataformat/xml/deser/FromXmlParser.java +++ b/src/main/java/com/fasterxml/jackson/dataformat/xml/deser/FromXmlParser.java @@ -176,16 +176,25 @@ private Feature(boolean defaultState) { public FromXmlParser(IOContext ctxt, int genericParserFeatures, int xmlFeatures, ObjectCodec codec, XMLStreamReader xmlReader) + throws IOException { super(genericParserFeatures); _formatFeatures = xmlFeatures; _ioContext = ctxt; _objectCodec = codec; _parsingContext = XmlReadContext.createRootContext(-1, -1); - // and thereby start a scope - _nextToken = JsonToken.START_OBJECT; _xmlTokens = new XmlTokenStream(xmlReader, ctxt.getSourceReference(), _formatFeatures); + switch (_xmlTokens.getCurrentToken()) { + case XmlTokenStream.XML_START_ELEMENT: + _nextToken = JsonToken.START_OBJECT; + break; + case XmlTokenStream.XML_NULL: + _nextToken = JsonToken.VALUE_NULL; + break; + default: + _reportError("Internal problem: invalid starting state (%d)", _xmlTokens.getCurrentToken()); + } } @Override @@ -462,11 +471,9 @@ public JsonToken nextToken() throws IOException } return t; } - -// public JsonToken nextToken0() throws IOException - */ - + */ +// public JsonToken nextToken0() throws IOException @Override public JsonToken nextToken() throws IOException { @@ -557,7 +564,7 @@ public JsonToken nextToken() throws IOException _parsingContext = _parsingContext.getParent(); _namesToWrap = _parsingContext.getNamesToWrap(); return _currToken; - + case XmlTokenStream.XML_ATTRIBUTE_NAME: // If there was a chance of leaf node, no more... if (_mayBeLeaf) { @@ -615,6 +622,8 @@ public JsonToken nextToken() throws IOException _parsingContext.setCurrentName(_cfgNameForTextElement); _nextToken = JsonToken.VALUE_STRING; return (_currToken = JsonToken.FIELD_NAME); + case XmlTokenStream.XML_NULL: + return (_currToken = JsonToken.VALUE_NULL); case XmlTokenStream.XML_END: return (_currToken = null); } @@ -732,6 +741,9 @@ public String nextTextValue() throws IOException _nextToken = JsonToken.VALUE_STRING; _currToken = JsonToken.FIELD_NAME; break; + case XmlTokenStream.XML_NULL: + _currToken = JsonToken.VALUE_STRING; + return (_currText = null); case XmlTokenStream.XML_END: _currToken = null; } diff --git a/src/main/java/com/fasterxml/jackson/dataformat/xml/deser/XmlTokenStream.java b/src/main/java/com/fasterxml/jackson/dataformat/xml/deser/XmlTokenStream.java index 8143441fc..d8ea03823 100644 --- a/src/main/java/com/fasterxml/jackson/dataformat/xml/deser/XmlTokenStream.java +++ b/src/main/java/com/fasterxml/jackson/dataformat/xml/deser/XmlTokenStream.java @@ -1,6 +1,8 @@ package com.fasterxml.jackson.dataformat.xml.deser; import java.io.IOException; + +import javax.xml.XMLConstants; import javax.xml.stream.*; import org.codehaus.stax2.XMLStreamLocation2; @@ -29,14 +31,19 @@ public class XmlTokenStream public final static int XML_ATTRIBUTE_NAME = 3; public final static int XML_ATTRIBUTE_VALUE = 4; public final static int XML_TEXT = 5; - public final static int XML_END = 6; + public final static int XML_NULL = 6; // since 2.10 + public final static int XML_END = 7; // // // token replay states private final static int REPLAY_START_DUP = 1; private final static int REPLAY_END = 2; private final static int REPLAY_START_DELAYED = 3; - + + // Some helpful XML Constants + + private final static String XSI_NAMESPACE = XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI; + /* /********************************************************************** /* Configuration @@ -64,6 +71,13 @@ public class XmlTokenStream protected int _attributeCount; + /** + * Marker used to indicate presence of `xsi:nil="true"' in current START_ELEMENT. + * + * @since 2.10 + */ + protected boolean _xsiNilFound; + /** * If true we have a START_ELEMENT with mixed text * @@ -76,7 +90,7 @@ public class XmlTokenStream * to return (as field name and value pair), if any; -1 * when no attributes to return */ - protected int _nextAttributeIndex = 0; + protected int _nextAttributeIndex; protected String _localName; @@ -124,11 +138,17 @@ public XmlTokenStream(XMLStreamReader xmlReader, Object sourceRef, +XMLStreamConstants.START_ELEMENT+"), instead got "+xmlReader.getEventType()); } _xmlReader = Stax2ReaderAdapter.wrapIfNecessary(xmlReader); - _currentState = XML_START_ELEMENT; _localName = _xmlReader.getLocalName(); _namespaceURI = _xmlReader.getNamespaceURI(); - _attributeCount = _xmlReader.getAttributeCount(); _formatFeatures = formatFeatures; + + _checkXsiAttributes(); // sets _attributeCount, _nextAttributeIndex + + if (_xsiNilFound) { + _currentState = XML_NULL; + } else { + _currentState = XML_START_ELEMENT; + } } public XMLStreamReader2 getXmlReader() { @@ -200,10 +220,13 @@ public void skipEndElement() throws IOException, XMLStreamException public String getText() { return _textValue; } public String getLocalName() { return _localName; } public String getNamespaceURI() { return _namespaceURI; } + + /*// not used as of 2.10 public boolean hasAttributes() { return (_currentState == XML_START_ELEMENT) && (_attributeCount > 0); } - + */ + public void closeCompletely() throws XMLStreamException { _xmlReader.closeCompletely(); } @@ -319,6 +342,13 @@ private final int _next() throws XMLStreamException ++_nextAttributeIndex; // fall through case XML_START_ELEMENT: // attributes to return? + + // 06-Sep-2019, tatu: `xsi:nil` to induce "real" null value? + if (_xsiNilFound) { + _xsiNilFound = false; + return (_currentState = XML_NULL); + } + if (_nextAttributeIndex < _attributeCount) { _localName = _xmlReader.getAttributeLocalName(_nextAttributeIndex); _namespaceURI = _xmlReader.getAttributeNamespace(_nextAttributeIndex); @@ -358,11 +388,23 @@ private final int _next() throws XMLStreamException } // text followed by END_ELEMENT return _handleEndElement(); + case XML_NULL: + // at this point we are pointing to START_ELEMENT, need to find + // matching END_ELEMENT, handle it + // 06-Sep-2019, tatu: Should handle error cases better but for now this'll do + switch (_skipUntilTag()) { + case XMLStreamConstants.END_ELEMENT: + return _handleEndElement(); + case XMLStreamConstants.END_DOCUMENT: + throw new IllegalStateException("Unexpected end-of-input after null token"); + default: + throw new IllegalStateException("Unexpected START_ELEMENT after null token"); + } + case XML_END: return XML_END; // throw new IllegalStateException("No more XML tokens available (end of input)"); } - // Ok: must be END_ELEMENT; see what tag we get (or end) switch (_skipUntilTag()) { case XMLStreamConstants.END_DOCUMENT: @@ -463,13 +505,22 @@ private final String _getText(XMLStreamReader2 r) throws XMLStreamException /* Internal methods, other /********************************************************************** */ + + /* + _xmlReader = Stax2ReaderAdapter.wrapIfNecessary(xmlReader); + _currentState = XML_START_ELEMENT; + _localName = _xmlReader.getLocalName(); + _namespaceURI = _xmlReader.getNamespaceURI(); + _attributeCount = _xmlReader.getAttributeCount(); + _formatFeatures = formatFeatures; + */ private final int _initStartElement() throws XMLStreamException { final String ns = _xmlReader.getNamespaceURI(); final String localName = _xmlReader.getLocalName(); - _attributeCount = _xmlReader.getAttributeCount(); - _nextAttributeIndex = 0; + + _checkXsiAttributes(); /* Support for virtual wrapping: in wrapping, may either * create a new wrapper scope (if in sub-tree, or matches @@ -497,6 +548,30 @@ private final int _initStartElement() throws XMLStreamException return (_currentState = XML_START_ELEMENT); } + /** + * @since 2.10 + */ + private final void _checkXsiAttributes() { + int count = _xmlReader.getAttributeCount(); + _attributeCount = count; + + // [dataformat-xml#354]: xsi:nul handling; at first only if first attribute + if (count >= 1) { + if ("nil".equals(_xmlReader.getAttributeLocalName(0))) { + if (XSI_NAMESPACE.equals(_xmlReader.getAttributeNamespace(0))) { + // need to skip, regardless of value + _nextAttributeIndex = 1; + // but only mark as nil marker if enabled + _xsiNilFound = "true".equals(_xmlReader.getAttributeValue(0)); + return; + } + } + } + + _nextAttributeIndex = 0; + _xsiNilFound = false; + } + /** * Method called to handle details of repeating "virtual" * start/end elements, needed for handling 'unwrapped' lists. diff --git a/src/test/java/com/fasterxml/jackson/dataformat/xml/deser/SimpleStringValuesTest.java b/src/test/java/com/fasterxml/jackson/dataformat/xml/deser/SimpleStringValuesTest.java index 6a87b0e08..ce2a9cff3 100644 --- a/src/test/java/com/fasterxml/jackson/dataformat/xml/deser/SimpleStringValuesTest.java +++ b/src/test/java/com/fasterxml/jackson/dataformat/xml/deser/SimpleStringValuesTest.java @@ -99,7 +99,10 @@ public void testEmptyElementToString() throws Exception "\n"; Issue167Bean result = MAPPER.readValue(XML, Issue167Bean.class); assertNotNull(result); - assertEquals("", result.d); + // 06-Sep-2019, tatu: As per [dataformat-xml#354] this should now (2.10) + // produce real `null`: +// assertEquals("", result.d); + assertNull(result.d); } /* diff --git a/src/test/java/com/fasterxml/jackson/dataformat/xml/failing/XsiNil354Test.java b/src/test/java/com/fasterxml/jackson/dataformat/xml/failing/XsiNil354Test.java index 29edfd4e2..be69df9be 100644 --- a/src/test/java/com/fasterxml/jackson/dataformat/xml/failing/XsiNil354Test.java +++ b/src/test/java/com/fasterxml/jackson/dataformat/xml/failing/XsiNil354Test.java @@ -19,18 +19,42 @@ public DoubleWrapper(Double value) { public void testWithDoubleAsNull() throws Exception { DoubleWrapper bean = MAPPER.readValue( - "", +"", DoubleWrapper.class); assertNotNull(bean); assertNull(bean.d); + + bean = MAPPER.readValue( +" ", + DoubleWrapper.class); + assertNotNull(bean); + assertNull(bean.d); + + // actually we should perhaps also verify there is no content but... for now, let's leave it. } public void testWithDoubleAsNonNull() throws Exception { DoubleWrapper bean = MAPPER.readValue( - "0.25", + "0.25", DoubleWrapper.class); assertNotNull(bean); assertEquals(Double.valueOf(0.25), bean.d); } + + public void testRootPojoAsNull() throws Exception + { + Point bean = MAPPER.readValue( + "", + Point.class); + assertNull(bean); + } + + public void testRootPojoAsNonNull() throws Exception + { + Point bean = MAPPER.readValue( + "", + Point.class); + assertNotNull(bean); + } }