Skip to content

Commit

Permalink
Fix #354 (support xsi:nil)
Browse files Browse the repository at this point in the history
  • Loading branch information
cowtowncoder committed Sep 7, 2019
1 parent c801a96 commit 4938036
Show file tree
Hide file tree
Showing 5 changed files with 134 additions and 19 deletions.
1 change: 1 addition & 0 deletions release-notes/VERSION-2.x
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
{
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
*
Expand All @@ -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;

Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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();
}
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,10 @@ public void testEmptyElementToString() throws Exception
"</a>\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);
}

/*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,42 @@ public DoubleWrapper(Double value) {
public void testWithDoubleAsNull() throws Exception
{
DoubleWrapper bean = MAPPER.readValue(
"<DoubleWrapper xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"><d xsi:nil='true'></DoubleWrapper>",
"<DoubleWrapper xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"><d xsi:nil='true' /></DoubleWrapper>",
DoubleWrapper.class);
assertNotNull(bean);
assertNull(bean.d);

bean = MAPPER.readValue(
"<DoubleWrapper xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"><d xsi:nil='true'> </d></DoubleWrapper>",
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(
"<DoubleWrapper xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"><d xsi:nil='false'>0.25</DoubleWrapper>",
"<DoubleWrapper xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"><d xsi:nil='false'>0.25</d></DoubleWrapper>",
DoubleWrapper.class);
assertNotNull(bean);
assertEquals(Double.valueOf(0.25), bean.d);
}

public void testRootPojoAsNull() throws Exception
{
Point bean = MAPPER.readValue(
"<Point xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance' xsi:nil='true' />",
Point.class);
assertNull(bean);
}

public void testRootPojoAsNonNull() throws Exception
{
Point bean = MAPPER.readValue(
"<Point xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance' xsi:nil='false'></Point>",
Point.class);
assertNotNull(bean);
}
}

0 comments on commit 4938036

Please sign in to comment.