Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
*
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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("<e "+XSI_NS_DECL+"><d xsi:nil='true' id='1'/></e>").toString());
assertEquals(a2q("{'d':null}"),
r.readValue("<e "+XSI_NS_DECL+"><d id='1' xsi:nil='true'/></e>").toString());
// and same for an element that has content, not just attributes
assertEquals(a2q("{'d':null}"),
r.readValue("<e "+XSI_NS_DECL+"><d id='1' xsi:nil='true'>0.25</d></e>").toString());
}

@Test
public void testXsiNilAfterOtherAttributeForPojo() throws Exception
{
DoubleWrapper2 bean = MAPPER.readValue(
"<DoubleWrapper "+XSI_NS_DECL+">\n"
+"<a id='1' xsi:nil='true'/>\n"
+"<b id='2' xsi:nil='false'>0.25</b>\n"
+"</DoubleWrapper>",
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("<e "+XSI_NS_DECL+"><d xsi:nil='false' id='1'>0.25</d></e>").toString());
assertEquals(a2q("{'d':{'id':'1','':'0.25'}}"),
r.readValue("<e "+XSI_NS_DECL+"><d id='1' xsi:nil='false'>0.25</d></e>").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("<e "+XSI_NS_DECL+"><d xsi:nil='"+nil+"'>0.25</d></e>").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("<e "+XSI_NS_DECL+"><d xsi:nil='"+nonNil+"'>0.25</d></e>").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("<Point "+XSI_NS_DECL+" xsi:nil='true' id='1'/>").toString());
assertEquals("null",
r.readValue("<Point "+XSI_NS_DECL+" id='1' xsi:nil='true'/>").toString());
}

// [dataformat-xml#714]: trailing bogus `JsonToken.END_OBJECT`
/*
@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">"
+ "<value xsi:schemaLocation=\"http://SomeNamespace schema.xsd\""
+ " xsi:nil=\"true\"/>"
+ "</Dto>",
Dto.class);
assertNull(result.value);
}

// When feature is disabled (default), XSI attributes should be exposed
// and bindable to POJO properties
@Test
Expand Down
Loading