diff --git a/release-notes/CREDITS b/release-notes/CREDITS
index 71db2625..38df2704 100644
--- a/release-notes/CREDITS
+++ b/release-notes/CREDITS
@@ -173,3 +173,5 @@ Christian Beikov (@beikov)
* Fixed #887: Apply `XmlNameProcessor` to unwrapped `ObjectNode` root element
name
(3.3.0)
+ * Fixed #888: Honor `xsi:nil` in any attribute position and `xs:boolean` form
+ (3.3.0)
diff --git a/release-notes/VERSION b/release-notes/VERSION
index 19ebaca1..35fa9a15 100644
--- a/release-notes/VERSION
+++ b/release-notes/VERSION
@@ -22,6 +22,8 @@ Version: 3.x (for earlier see VERSION-2.x)
(fix by @Sahana2524)
#887: Apply `XmlNameProcessor` to unwrapped `ObjectNode` root element name
(fix by @Sahana2524)
+#888: Honor `xsi:nil` in any attribute position and `xs:boolean` form
+ (fix by @Sahana2524)
3.2.2 (not yet released)
diff --git a/src/main/java/tools/jackson/dataformat/xml/deser/XmlTokenStream.java b/src/main/java/tools/jackson/dataformat/xml/deser/XmlTokenStream.java
index d52938e2..cbabade7 100644
--- a/src/main/java/tools/jackson/dataformat/xml/deser/XmlTokenStream.java
+++ b/src/main/java/tools/jackson/dataformat/xml/deser/XmlTokenStream.java
@@ -102,6 +102,16 @@ public class XmlTokenStream
*/
protected boolean _xsiNilFound;
+ /**
+ * Index of the {@code xsi:nil} attribute of the current START_ELEMENT, if
+ * one was found (and {@code xsi:nil} processing is enabled); -1 if none.
+ * Kept so that the marker attribute itself is not exposed as a regular
+ * property no matter which position it occurs in.
+ *
+ * @since 3.3
+ */
+ protected int _xsiNilIndex = -1;
+
/**
* Flag set true if current event is {@code XML_TEXT} and there is START_ELEMENT
*
@@ -492,6 +502,11 @@ private final int _next() throws XMLStreamException
//System.out.println(" XmlTokenStream._next(): Got xsi:nil, skipping element");
return _handleEndElement();
}
+ // [dataformat-xml#354]: the xsi:nil marker itself is never exposed as
+ // a property when xsi:nil processing is enabled
+ if (_nextAttributeIndex == _xsiNilIndex) {
+ ++_nextAttributeIndex;
+ }
// [dataformat-xml#358]: Optionally skip XSI namespace attributes other
// than "type" (handled by _decodeAttributeName) and "nil" (when xsi:nil
// processing is disabled, it should be exposed as a regular attribute)
@@ -746,27 +761,43 @@ private final int _initStartElement() throws XMLStreamException
* @since 2.10
*/
private final void _checkXsiAttributes() {
- int count = _xmlReader.getAttributeCount();
+ final int count = _xmlReader.getAttributeCount();
_attributeCount = count;
-
- // [dataformat-xml#354]: xsi:nil handling; at first only if first attribute
- if (count >= 1) {
- // [dataformat-xml#468]: may disable xsi:nil processing
- if (_cfgProcessXsiNil
- && "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));
+ _nextAttributeIndex = 0;
+ _xsiNilFound = false;
+ _xsiNilIndex = -1;
+
+ // [dataformat-xml#354]: xsi:nil handling. Attribute order is not significant
+ // in XML so the marker has to be looked for in any position, not just first
+ // [dataformat-xml#468]: may disable xsi:nil processing
+ if (_cfgProcessXsiNil) {
+ for (int i = 0; i < count; ++i) {
+ if ("nil".equals(_xmlReader.getAttributeLocalName(i))
+ && XSI_NAMESPACE.equals(_xmlReader.getAttributeNamespace(i))) {
+ // need to skip the attribute itself, regardless of value
+ _xsiNilIndex = i;
+ // but only mark as nil marker if value says so
+ _xsiNilFound = _isXsiNilTrue(_xmlReader.getAttributeValue(i));
//System.out.println(" XMLTokenStream._checkXsiAttributes(), _xsiNilFound: "+_xsiNilFound);
return;
}
}
}
+ }
- _nextAttributeIndex = 0;
- _xsiNilFound = false;
+ /**
+ * {@code xsi:nil} is of type {@code xs:boolean}, whose lexical space is
+ * "true", "false", "1" and "0", with surrounding white space collapsed.
+ * Comparison stays case-sensitive since {@code xs:boolean} is.
+ *
+ * @since 3.3
+ */
+ private static boolean _isXsiNilTrue(String value) {
+ if (value == null) {
+ return false;
+ }
+ final String v = value.trim();
+ return "true".equals(v) || "1".equals(v);
}
/**
diff --git a/src/test/java/tools/jackson/dataformat/xml/deser/XsiNilBasicTest.java b/src/test/java/tools/jackson/dataformat/xml/deser/XsiNilBasicTest.java
index 5fe4a596..f2a5f42c 100644
--- a/src/test/java/tools/jackson/dataformat/xml/deser/XsiNilBasicTest.java
+++ b/src/test/java/tools/jackson/dataformat/xml/deser/XsiNilBasicTest.java
@@ -154,6 +154,81 @@ public void testDisableXsiNilLeafProcessing() throws Exception
.readValue(DOC).toString());
}
+ // [dataformat-xml#354]: attribute order is not significant in XML, so the
+ // marker has to be honored wherever it occurs in the attribute list
+ @Test
+ public void testXsiNilAfterOtherAttribute() throws Exception
+ {
+ final ObjectReader r = MAPPER.readerFor(JsonNode.class);
+
+ assertEquals(a2q("{'d':null}"),
+ r.readValue("").toString());
+ assertEquals(a2q("{'d':null}"),
+ r.readValue("").toString());
+ // and same for an element that has content, not just attributes
+ assertEquals(a2q("{'d':null}"),
+ r.readValue("0.25").toString());
+ }
+
+ @Test
+ public void testXsiNilAfterOtherAttributeForPojo() throws Exception
+ {
+ DoubleWrapper2 bean = MAPPER.readValue(
+"\n"
++"\n"
++"0.25\n"
++"",
+ DoubleWrapper2.class);
+ assertNotNull(bean);
+ assertNull(bean.a);
+ assertEquals(Double.valueOf(0.25), bean.b);
+ }
+
+ // Marker attribute itself must not be exposed as a property either, no matter
+ // its position; without this, "xsi:nil" leaks in as a property named "nil"
+ @Test
+ public void testXsiNilFalseAfterOtherAttributeNotExposed() throws Exception
+ {
+ final ObjectReader r = MAPPER.readerFor(JsonNode.class);
+
+ assertEquals(a2q("{'d':{'id':'1','':'0.25'}}"),
+ r.readValue("0.25").toString());
+ assertEquals(a2q("{'d':{'id':'1','':'0.25'}}"),
+ r.readValue("0.25").toString());
+ }
+
+ // `xsi:nil` is `xs:boolean`, so "1" is as much a nil marker as "true", and
+ // surrounding white space is collapsed before the value is read
+ @Test
+ public void testXsiNilBooleanLexicalForms() throws Exception
+ {
+ final ObjectReader r = MAPPER.readerFor(JsonNode.class);
+
+ for (String nil : new String[] { "true", "1", " true ", "\n1\t" }) {
+ assertEquals(a2q("{'d':null}"),
+ r.readValue("0.25").toString(),
+ "xsi:nil='"+nil+"' should mark element as null");
+ }
+ // ... but "false"/"0" do not, and neither does a non-boolean value
+ // ("xs:boolean" is case-sensitive, so "TRUE" is not valid)
+ for (String nonNil : new String[] { "false", "0", "TRUE", "yes" }) {
+ assertEquals(a2q("{'d':'0.25'}"),
+ r.readValue("0.25").toString(),
+ "xsi:nil='"+nonNil+"' should not mark element as null");
+ }
+ }
+
+ @Test
+ public void testXsiNilAfterOtherAttributeOnRoot() throws Exception
+ {
+ final ObjectReader r = MAPPER_NO_TRAILING_CHECK.readerFor(JsonNode.class);
+
+ assertEquals("null",
+ r.readValue("").toString());
+ assertEquals("null",
+ r.readValue("").toString());
+ }
+
// [dataformat-xml#714]: trailing bogus `JsonToken.END_OBJECT`
/*
@Test
diff --git a/src/test/java/tools/jackson/dataformat/xml/deser/XsiSchemaLocationTest.java b/src/test/java/tools/jackson/dataformat/xml/deser/XsiSchemaLocationTest.java
index 4ad8d727..b9b7d8fe 100644
--- a/src/test/java/tools/jackson/dataformat/xml/deser/XsiSchemaLocationTest.java
+++ b/src/test/java/tools/jackson/dataformat/xml/deser/XsiSchemaLocationTest.java
@@ -73,6 +73,21 @@ public void testXsiSchemaLocationWithRegularAttributes() throws Exception
assertEquals(42, result.count);
}
+ // Skipping the "unknown" XSI attributes must not take xsi:nil with it:
+ // schema-annotated documents put xsi:schemaLocation first, leaving the
+ // nil marker in a later position
+ @Test
+ public void testXsiNilAfterXsiSchemaLocation() throws Exception
+ {
+ Dto result = MAPPER_SKIP.readValue(
+ ""
+ + ""
+ + "",
+ Dto.class);
+ assertNull(result.value);
+ }
+
// When feature is disabled (default), XSI attributes should be exposed
// and bindable to POJO properties
@Test