Skip to content

Commit fdbd1e9

Browse files
authored
Fixes #114 in 2.x: support STRICT_DUPLICATE_DETECTION (#783)
1 parent 30ccb47 commit fdbd1e9

File tree

3 files changed

+129
-23
lines changed

3 files changed

+129
-23
lines changed

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

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import com.fasterxml.jackson.core.exc.StreamConstraintsException;
1919
import com.fasterxml.jackson.core.io.IOContext;
2020
import com.fasterxml.jackson.core.io.NumberInput;
21+
import com.fasterxml.jackson.core.json.DupDetector;
2122
import com.fasterxml.jackson.core.util.ByteArrayBuilder;
2223
import com.fasterxml.jackson.core.util.JacksonFeatureSet;
2324

@@ -284,18 +285,21 @@ public FromXmlParser(IOContext ctxt, int genericParserFeatures, int xmlFeatures,
284285
* This constructor initializes the parser with the given I/O context, parser features,
285286
* and object codec for deserializing XML content into Java objects.
286287
*
287-
* @since 2.20
288288
* @param ctxt I/O context used for handling low-level I/O operations and buffering
289289
* @param genericParserFeatures set of bitmasked parser features to control parsing behavior
290290
* @param codec object codec used for converting between JSON-like structures and Java objects
291291
* @param xmlTokenStream the pre-processed XML token stream to parse from
292292
* @throws IOException if an I/O error occurs during initialization or parsing setup
293+
*
294+
* @since 2.20
293295
*/
294296
public FromXmlParser(IOContext ctxt, int genericParserFeatures, ObjectCodec codec, XmlTokenStream xmlTokenStream) throws IOException {
295297
super(genericParserFeatures, ctxt.streamReadConstraints());
296298
_ioContext = ctxt;
297299
_objectCodec = codec;
298-
_parsingContext = XmlReadContext.createRootContext(-1, -1);
300+
DupDetector dups = JsonParser.Feature.STRICT_DUPLICATE_DETECTION.enabledIn(genericParserFeatures)
301+
? DupDetector.rootDetector(this) : null;
302+
_parsingContext = XmlReadContext.createRootContext(dups, -1, -1);
299303
_xmlTokens = Objects.requireNonNull(xmlTokenStream, "xmlTokenStream cannot be null");
300304
_formatFeatures = xmlTokenStream.getFormatFeatures();
301305
final int firstToken;
@@ -526,7 +530,12 @@ public void overrideCurrentName(String name)
526530
if (_currToken == JsonToken.START_OBJECT || _currToken == JsonToken.START_ARRAY) {
527531
ctxt = ctxt.getParent();
528532
}
529-
ctxt.setCurrentName(name);
533+
// Unfortunate, but since we did not expose exceptions, need to wrap
534+
try {
535+
ctxt.setCurrentName(name);
536+
} catch (IOException e) {
537+
throw new IllegalStateException(e);
538+
}
530539
}
531540

532541
@Override
@@ -1050,7 +1059,7 @@ public String nextTextValue() throws IOException
10501059
}
10511060

10521061

1053-
private void _updateState(JsonToken t)
1062+
private void _updateState(JsonToken t) throws JsonProcessingException
10541063
{
10551064
switch (t) {
10561065
case START_OBJECT:

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

Lines changed: 51 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import com.fasterxml.jackson.core.*;
66
import com.fasterxml.jackson.core.io.CharTypes;
77
import com.fasterxml.jackson.core.io.ContentReference;
8+
import com.fasterxml.jackson.core.json.DupDetector;
89

910
/**
1011
* Extension of {@link JsonStreamContext}, which implements
@@ -23,6 +24,14 @@ public final class XmlReadContext
2324

2425
protected final XmlReadContext _parent;
2526

27+
/**
28+
* Object used for checking for duplicate field names, if enabled
29+
* (null if not enabled)
30+
*
31+
* @since 2.21
32+
*/
33+
protected final DupDetector _dups;
34+
2635
// // // Location information (minus source reference)
2736

2837
protected int _lineNr;
@@ -59,28 +68,26 @@ public final class XmlReadContext
5968
*/
6069

6170
/**
62-
* @since 2.18
71+
* @since 2.21
6372
*/
64-
public XmlReadContext(XmlReadContext parent, int nestingDepth,
73+
public XmlReadContext(XmlReadContext parent, DupDetector dups,
74+
int nestingDepth,
6575
int type, int lineNr, int colNr)
6676
{
6777
super();
6878
_type = type;
6979
_parent = parent;
80+
_dups = dups;
7081
_lineNr = lineNr;
7182
_columnNr = colNr;
7283
_index = -1;
7384
_nestingDepth = nestingDepth;
7485
}
7586

76-
/**
77-
* @deprecated Since 2.18
78-
*/
79-
@Deprecated // since 2.18
80-
public XmlReadContext(XmlReadContext parent, int type, int lineNr, int colNr)
81-
{
82-
this(parent, (parent == null) ? 0 : parent._nestingDepth + 1,
83-
type, lineNr, colNr);
87+
@Deprecated // @since 2.21
88+
public XmlReadContext(XmlReadContext parent, int nestingDepth,
89+
int type, int lineNr, int colNr) {
90+
this(parent, null, nestingDepth, type, lineNr, colNr);
8491
}
8592

8693
protected final void reset(int type, int lineNr, int colNr)
@@ -92,7 +99,10 @@ protected final void reset(int type, int lineNr, int colNr)
9299
_currentName = null;
93100
_currentValue = null;
94101
_namesToWrap = null;
95-
// _nestingDepth fine as is, same level for reuse
102+
if (_dups != null) {
103+
_dups.reset();
104+
}
105+
// _nestingDepth fine as-is, same level for reuse
96106
}
97107

98108
@Override
@@ -111,20 +121,28 @@ public void setCurrentValue(Object v) {
111121
/**********************************************************************
112122
*/
113123

124+
public static XmlReadContext createRootContext(DupDetector dups, int lineNr, int colNr) {
125+
return new XmlReadContext(null, dups, 0, TYPE_ROOT, lineNr, colNr);
126+
}
127+
128+
@Deprecated // @since 2.21
114129
public static XmlReadContext createRootContext(int lineNr, int colNr) {
115-
return new XmlReadContext(null, 0, TYPE_ROOT, lineNr, colNr);
130+
return createRootContext(null, lineNr, colNr);
116131
}
117132

133+
@Deprecated // @since 2.21
118134
public static XmlReadContext createRootContext() {
119-
return new XmlReadContext(null, 0, TYPE_ROOT, 1, 0);
135+
return createRootContext(null, 1, 0);
120136
}
121-
137+
122138
public final XmlReadContext createChildArrayContext(int lineNr, int colNr)
123139
{
124140
++_index; // not needed for Object, but does not hurt so no need to check curr type
125141
XmlReadContext ctxt = _child;
126142
if (ctxt == null) {
127-
_child = ctxt = new XmlReadContext(this, _nestingDepth+1, TYPE_ARRAY, lineNr, colNr);
143+
_child = ctxt = new XmlReadContext(this,
144+
(_dups == null) ? null : _dups.child(),
145+
_nestingDepth+1, TYPE_ARRAY, lineNr, colNr);
128146
return ctxt;
129147
}
130148
ctxt.reset(TYPE_ARRAY, lineNr, colNr);
@@ -136,10 +154,12 @@ public final XmlReadContext createChildObjectContext(int lineNr, int colNr)
136154
++_index; // not needed for Object, but does not hurt so no need to check curr type
137155
XmlReadContext ctxt = _child;
138156
if (ctxt == null) {
139-
_child = ctxt = new XmlReadContext(this, TYPE_OBJECT, lineNr, colNr);
140-
return ctxt;
157+
_child = ctxt = new XmlReadContext(this,
158+
(_dups == null) ? null : _dups.child(),
159+
_nestingDepth+1, TYPE_OBJECT, lineNr, colNr);
160+
} else {
161+
ctxt.reset(TYPE_OBJECT, lineNr, colNr);
141162
}
142-
ctxt.reset(TYPE_OBJECT, lineNr, colNr);
143163
return ctxt;
144164
}
145165

@@ -186,10 +206,22 @@ public final void valueStarted() {
186206
++_index;
187207
}
188208

189-
public void setCurrentName(String name) {
209+
public void setCurrentName(String name) throws JsonProcessingException {
190210
_currentName = name;
211+
if (_dups != null) {
212+
_checkDup(_dups, name);
213+
}
191214
}
192215

216+
// @since 2.21
217+
private static void _checkDup(DupDetector dd, String name) throws JsonProcessingException
218+
{
219+
if (dd.isDup(name)) {
220+
throw new JsonParseException(null,
221+
"Duplicate field '"+name+"'", dd.findLocation());
222+
}
223+
}
224+
193225
public void setNamesToWrap(Set<String> namesToWrap) {
194226
_namesToWrap = namesToWrap;
195227
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package com.fasterxml.jackson.dataformat.xml.deser;
2+
3+
import org.junit.jupiter.api.Test;
4+
5+
import com.fasterxml.jackson.core.JsonParser;
6+
import com.fasterxml.jackson.core.exc.StreamReadException;
7+
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
8+
import com.fasterxml.jackson.dataformat.xml.XmlTestUtil;
9+
10+
import static org.junit.jupiter.api.Assertions.*;
11+
12+
/**
13+
* Tests for [dataformat-xml#114]: Support for STRICT_DUPLICATE_DETECTION
14+
*
15+
* @since 2.21
16+
*/
17+
public class StrictDuplicateDetection114Test extends XmlTestUtil
18+
{
19+
static class TestBean114 {
20+
public String field1;
21+
public String field2;
22+
}
23+
24+
private final XmlMapper STRICT_MAPPER = XmlMapper.builder()
25+
.enable(JsonParser.Feature.STRICT_DUPLICATE_DETECTION)
26+
.build();
27+
28+
// [dataformat-xml#114]
29+
@Test
30+
public void testStrictDuplicateDetectionWithPOJO() throws Exception
31+
{
32+
// Test XML mapper should also reject duplicates
33+
final String xmlWithDup = "<TestBean><field1>value1</field1><field1>value2</field1></TestBean>";
34+
35+
StreamReadException e = assertThrows(StreamReadException.class, () -> {
36+
STRICT_MAPPER.readValue(xmlWithDup, TestBean114.class);
37+
});
38+
assertTrue(e.getMessage().contains("Duplicate field"),
39+
"Expected 'Duplicate field' error, got: " + e.getMessage());
40+
}
41+
42+
@Test
43+
public void testNoDuplicatesShouldWork() throws Exception
44+
{
45+
final String xml = "<TestBean><field1>value1</field1><field2>value2</field2></TestBean>";
46+
47+
TestBean114 bean = STRICT_MAPPER.readValue(xml, TestBean114.class);
48+
assertNotNull(bean);
49+
assertEquals("value1", bean.field1);
50+
assertEquals("value2", bean.field2);
51+
}
52+
53+
@Test
54+
public void testDuplicateDetectionDisabledByDefault() throws Exception
55+
{
56+
XmlMapper mapper = newMapper(); // default mapper without strict duplicate detection
57+
58+
// Should allow duplicates by default (last value wins)
59+
final String xmlWithDup = "<TestBean><field1>value1</field1><field1>value2</field1></TestBean>";
60+
61+
TestBean114 bean = mapper.readValue(xmlWithDup, TestBean114.class);
62+
assertNotNull(bean);
63+
assertEquals("value2", bean.field1); // last value wins
64+
}
65+
}

0 commit comments

Comments
 (0)