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);
+ }
}