Skip to content

Commit 4938036

Browse files
committed
Fix #354 (support xsi:nil)
1 parent c801a96 commit 4938036

File tree

5 files changed

+134
-19
lines changed

5 files changed

+134
-19
lines changed

release-notes/VERSION-2.x

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ Project: jackson-dataformat-xml
88

99
#242: Deserialization of class inheritance depends on attributes order
1010
(reported by Victor K)
11+
#354: Support mapping `xsi:nul` marked elements as `null`s (`JsonToken.VALUE_NULL`)
1112

1213
2.10.0.pr2 (31-Aug-2019)
1314

src/main/java/com/fasterxml/jackson/dataformat/xml/deser/FromXmlParser.java

+19-7
Original file line numberDiff line numberDiff line change
@@ -176,16 +176,25 @@ private Feature(boolean defaultState) {
176176

177177
public FromXmlParser(IOContext ctxt, int genericParserFeatures, int xmlFeatures,
178178
ObjectCodec codec, XMLStreamReader xmlReader)
179+
throws IOException
179180
{
180181
super(genericParserFeatures);
181182
_formatFeatures = xmlFeatures;
182183
_ioContext = ctxt;
183184
_objectCodec = codec;
184185
_parsingContext = XmlReadContext.createRootContext(-1, -1);
185-
// and thereby start a scope
186-
_nextToken = JsonToken.START_OBJECT;
187186
_xmlTokens = new XmlTokenStream(xmlReader, ctxt.getSourceReference(),
188187
_formatFeatures);
188+
switch (_xmlTokens.getCurrentToken()) {
189+
case XmlTokenStream.XML_START_ELEMENT:
190+
_nextToken = JsonToken.START_OBJECT;
191+
break;
192+
case XmlTokenStream.XML_NULL:
193+
_nextToken = JsonToken.VALUE_NULL;
194+
break;
195+
default:
196+
_reportError("Internal problem: invalid starting state (%d)", _xmlTokens.getCurrentToken());
197+
}
189198
}
190199

191200
@Override
@@ -462,11 +471,9 @@ public JsonToken nextToken() throws IOException
462471
}
463472
return t;
464473
}
465-
466-
// public JsonToken nextToken0() throws IOException
467-
*/
468-
474+
*/
469475

476+
// public JsonToken nextToken0() throws IOException
470477
@Override
471478
public JsonToken nextToken() throws IOException
472479
{
@@ -557,7 +564,7 @@ public JsonToken nextToken() throws IOException
557564
_parsingContext = _parsingContext.getParent();
558565
_namesToWrap = _parsingContext.getNamesToWrap();
559566
return _currToken;
560-
567+
561568
case XmlTokenStream.XML_ATTRIBUTE_NAME:
562569
// If there was a chance of leaf node, no more...
563570
if (_mayBeLeaf) {
@@ -615,6 +622,8 @@ public JsonToken nextToken() throws IOException
615622
_parsingContext.setCurrentName(_cfgNameForTextElement);
616623
_nextToken = JsonToken.VALUE_STRING;
617624
return (_currToken = JsonToken.FIELD_NAME);
625+
case XmlTokenStream.XML_NULL:
626+
return (_currToken = JsonToken.VALUE_NULL);
618627
case XmlTokenStream.XML_END:
619628
return (_currToken = null);
620629
}
@@ -732,6 +741,9 @@ public String nextTextValue() throws IOException
732741
_nextToken = JsonToken.VALUE_STRING;
733742
_currToken = JsonToken.FIELD_NAME;
734743
break;
744+
case XmlTokenStream.XML_NULL:
745+
_currToken = JsonToken.VALUE_STRING;
746+
return (_currText = null);
735747
case XmlTokenStream.XML_END:
736748
_currToken = null;
737749
}

src/main/java/com/fasterxml/jackson/dataformat/xml/deser/XmlTokenStream.java

+84-9
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package com.fasterxml.jackson.dataformat.xml.deser;
22

33
import java.io.IOException;
4+
5+
import javax.xml.XMLConstants;
46
import javax.xml.stream.*;
57

68
import org.codehaus.stax2.XMLStreamLocation2;
@@ -29,14 +31,19 @@ public class XmlTokenStream
2931
public final static int XML_ATTRIBUTE_NAME = 3;
3032
public final static int XML_ATTRIBUTE_VALUE = 4;
3133
public final static int XML_TEXT = 5;
32-
public final static int XML_END = 6;
34+
public final static int XML_NULL = 6; // since 2.10
35+
public final static int XML_END = 7;
3336

3437
// // // token replay states
3538

3639
private final static int REPLAY_START_DUP = 1;
3740
private final static int REPLAY_END = 2;
3841
private final static int REPLAY_START_DELAYED = 3;
39-
42+
43+
// Some helpful XML Constants
44+
45+
private final static String XSI_NAMESPACE = XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI;
46+
4047
/*
4148
/**********************************************************************
4249
/* Configuration
@@ -64,6 +71,13 @@ public class XmlTokenStream
6471

6572
protected int _attributeCount;
6673

74+
/**
75+
* Marker used to indicate presence of `xsi:nil="true"' in current START_ELEMENT.
76+
*
77+
* @since 2.10
78+
*/
79+
protected boolean _xsiNilFound;
80+
6781
/**
6882
* If true we have a START_ELEMENT with mixed text
6983
*
@@ -76,7 +90,7 @@ public class XmlTokenStream
7690
* to return (as field name and value pair), if any; -1
7791
* when no attributes to return
7892
*/
79-
protected int _nextAttributeIndex = 0;
93+
protected int _nextAttributeIndex;
8094

8195
protected String _localName;
8296

@@ -124,11 +138,17 @@ public XmlTokenStream(XMLStreamReader xmlReader, Object sourceRef,
124138
+XMLStreamConstants.START_ELEMENT+"), instead got "+xmlReader.getEventType());
125139
}
126140
_xmlReader = Stax2ReaderAdapter.wrapIfNecessary(xmlReader);
127-
_currentState = XML_START_ELEMENT;
128141
_localName = _xmlReader.getLocalName();
129142
_namespaceURI = _xmlReader.getNamespaceURI();
130-
_attributeCount = _xmlReader.getAttributeCount();
131143
_formatFeatures = formatFeatures;
144+
145+
_checkXsiAttributes(); // sets _attributeCount, _nextAttributeIndex
146+
147+
if (_xsiNilFound) {
148+
_currentState = XML_NULL;
149+
} else {
150+
_currentState = XML_START_ELEMENT;
151+
}
132152
}
133153

134154
public XMLStreamReader2 getXmlReader() {
@@ -200,10 +220,13 @@ public void skipEndElement() throws IOException, XMLStreamException
200220
public String getText() { return _textValue; }
201221
public String getLocalName() { return _localName; }
202222
public String getNamespaceURI() { return _namespaceURI; }
223+
224+
/*// not used as of 2.10
203225
public boolean hasAttributes() {
204226
return (_currentState == XML_START_ELEMENT) && (_attributeCount > 0);
205227
}
206-
228+
*/
229+
207230
public void closeCompletely() throws XMLStreamException {
208231
_xmlReader.closeCompletely();
209232
}
@@ -319,6 +342,13 @@ private final int _next() throws XMLStreamException
319342
++_nextAttributeIndex;
320343
// fall through
321344
case XML_START_ELEMENT: // attributes to return?
345+
346+
// 06-Sep-2019, tatu: `xsi:nil` to induce "real" null value?
347+
if (_xsiNilFound) {
348+
_xsiNilFound = false;
349+
return (_currentState = XML_NULL);
350+
}
351+
322352
if (_nextAttributeIndex < _attributeCount) {
323353
_localName = _xmlReader.getAttributeLocalName(_nextAttributeIndex);
324354
_namespaceURI = _xmlReader.getAttributeNamespace(_nextAttributeIndex);
@@ -358,11 +388,23 @@ private final int _next() throws XMLStreamException
358388
}
359389
// text followed by END_ELEMENT
360390
return _handleEndElement();
391+
case XML_NULL:
392+
// at this point we are pointing to START_ELEMENT, need to find
393+
// matching END_ELEMENT, handle it
394+
// 06-Sep-2019, tatu: Should handle error cases better but for now this'll do
395+
switch (_skipUntilTag()) {
396+
case XMLStreamConstants.END_ELEMENT:
397+
return _handleEndElement();
398+
case XMLStreamConstants.END_DOCUMENT:
399+
throw new IllegalStateException("Unexpected end-of-input after null token");
400+
default:
401+
throw new IllegalStateException("Unexpected START_ELEMENT after null token");
402+
}
403+
361404
case XML_END:
362405
return XML_END;
363406
// throw new IllegalStateException("No more XML tokens available (end of input)");
364407
}
365-
366408
// Ok: must be END_ELEMENT; see what tag we get (or end)
367409
switch (_skipUntilTag()) {
368410
case XMLStreamConstants.END_DOCUMENT:
@@ -463,13 +505,22 @@ private final String _getText(XMLStreamReader2 r) throws XMLStreamException
463505
/* Internal methods, other
464506
/**********************************************************************
465507
*/
508+
509+
/*
510+
_xmlReader = Stax2ReaderAdapter.wrapIfNecessary(xmlReader);
511+
_currentState = XML_START_ELEMENT;
512+
_localName = _xmlReader.getLocalName();
513+
_namespaceURI = _xmlReader.getNamespaceURI();
514+
_attributeCount = _xmlReader.getAttributeCount();
515+
_formatFeatures = formatFeatures;
516+
*/
466517

467518
private final int _initStartElement() throws XMLStreamException
468519
{
469520
final String ns = _xmlReader.getNamespaceURI();
470521
final String localName = _xmlReader.getLocalName();
471-
_attributeCount = _xmlReader.getAttributeCount();
472-
_nextAttributeIndex = 0;
522+
523+
_checkXsiAttributes();
473524

474525
/* Support for virtual wrapping: in wrapping, may either
475526
* create a new wrapper scope (if in sub-tree, or matches
@@ -497,6 +548,30 @@ private final int _initStartElement() throws XMLStreamException
497548
return (_currentState = XML_START_ELEMENT);
498549
}
499550

551+
/**
552+
* @since 2.10
553+
*/
554+
private final void _checkXsiAttributes() {
555+
int count = _xmlReader.getAttributeCount();
556+
_attributeCount = count;
557+
558+
// [dataformat-xml#354]: xsi:nul handling; at first only if first attribute
559+
if (count >= 1) {
560+
if ("nil".equals(_xmlReader.getAttributeLocalName(0))) {
561+
if (XSI_NAMESPACE.equals(_xmlReader.getAttributeNamespace(0))) {
562+
// need to skip, regardless of value
563+
_nextAttributeIndex = 1;
564+
// but only mark as nil marker if enabled
565+
_xsiNilFound = "true".equals(_xmlReader.getAttributeValue(0));
566+
return;
567+
}
568+
}
569+
}
570+
571+
_nextAttributeIndex = 0;
572+
_xsiNilFound = false;
573+
}
574+
500575
/**
501576
* Method called to handle details of repeating "virtual"
502577
* start/end elements, needed for handling 'unwrapped' lists.

src/test/java/com/fasterxml/jackson/dataformat/xml/deser/SimpleStringValuesTest.java

+4-1
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,10 @@ public void testEmptyElementToString() throws Exception
9999
"</a>\n";
100100
Issue167Bean result = MAPPER.readValue(XML, Issue167Bean.class);
101101
assertNotNull(result);
102-
assertEquals("", result.d);
102+
// 06-Sep-2019, tatu: As per [dataformat-xml#354] this should now (2.10)
103+
// produce real `null`:
104+
// assertEquals("", result.d);
105+
assertNull(result.d);
103106
}
104107

105108
/*

src/test/java/com/fasterxml/jackson/dataformat/xml/failing/XsiNil354Test.java

+26-2
Original file line numberDiff line numberDiff line change
@@ -19,18 +19,42 @@ public DoubleWrapper(Double value) {
1919
public void testWithDoubleAsNull() throws Exception
2020
{
2121
DoubleWrapper bean = MAPPER.readValue(
22-
"<DoubleWrapper xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"><d xsi:nil='true'></DoubleWrapper>",
22+
"<DoubleWrapper xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"><d xsi:nil='true' /></DoubleWrapper>",
2323
DoubleWrapper.class);
2424
assertNotNull(bean);
2525
assertNull(bean.d);
26+
27+
bean = MAPPER.readValue(
28+
"<DoubleWrapper xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"><d xsi:nil='true'> </d></DoubleWrapper>",
29+
DoubleWrapper.class);
30+
assertNotNull(bean);
31+
assertNull(bean.d);
32+
33+
// actually we should perhaps also verify there is no content but... for now, let's leave it.
2634
}
2735

2836
public void testWithDoubleAsNonNull() throws Exception
2937
{
3038
DoubleWrapper bean = MAPPER.readValue(
31-
"<DoubleWrapper xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"><d xsi:nil='false'>0.25</DoubleWrapper>",
39+
"<DoubleWrapper xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"><d xsi:nil='false'>0.25</d></DoubleWrapper>",
3240
DoubleWrapper.class);
3341
assertNotNull(bean);
3442
assertEquals(Double.valueOf(0.25), bean.d);
3543
}
44+
45+
public void testRootPojoAsNull() throws Exception
46+
{
47+
Point bean = MAPPER.readValue(
48+
"<Point xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance' xsi:nil='true' />",
49+
Point.class);
50+
assertNull(bean);
51+
}
52+
53+
public void testRootPojoAsNonNull() throws Exception
54+
{
55+
Point bean = MAPPER.readValue(
56+
"<Point xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance' xsi:nil='false'></Point>",
57+
Point.class);
58+
assertNotNull(bean);
59+
}
3660
}

0 commit comments

Comments
 (0)