Skip to content

Commit 680f5f6

Browse files
committed
Merge branch '2.x' into 3.0
2 parents 1c87b53 + bc9cd88 commit 680f5f6

File tree

5 files changed

+121
-12
lines changed

5 files changed

+121
-12
lines changed

release-notes/VERSION

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ Version: 3.x (for earlier see VERSION-2.x)
55
=== Releases ===
66
------------------------------------------------------------------------
77

8+
3.0.2 (not yet released)
9+
10+
#114: `XmlMapper` does not support `StreamReadFeature.STRICT_DUPLICATE_DETECTION`
11+
812
3.0.1 (21-Oct-2025)
913

1014
No changes since 3.0.0

release-notes/VERSION-2.x

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ Project: jackson-dataformat-xml
66

77
2.21.0 (not yet released)
88

9-
No changes since 2.20
9+
#114: `XmlMapper` does not support `StreamReadFeature.STRICT_DUPLICATE_DETECTION`
10+
(reported by @cpopp)
1011

1112
2.20.0 (28-Aug-2025)
1213

src/main/java/tools/jackson/dataformat/xml/deser/FromXmlParser.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import tools.jackson.core.exc.StreamReadException;
1919
import tools.jackson.core.io.IOContext;
2020
import tools.jackson.core.io.NumberInput;
21+
import tools.jackson.core.json.DupDetector;
2122
import tools.jackson.core.util.ByteArrayBuilder;
2223
import tools.jackson.core.util.JacksonFeatureSet;
2324
import tools.jackson.dataformat.xml.util.CaseInsensitiveNameSet;
@@ -180,7 +181,9 @@ public FromXmlParser(ObjectReadContext readCtxt, IOContext ioCtxt,
180181
{
181182
super(readCtxt, ioCtxt, parserFeatures);
182183
_formatFeatures = xmlFeatures;
183-
_streamReadContext = XmlReadContext.createRootContext(-1, -1);
184+
DupDetector dups = StreamReadFeature.STRICT_DUPLICATE_DETECTION.enabledIn(parserFeatures)
185+
? DupDetector.rootDetector(this) : null;
186+
_streamReadContext = XmlReadContext.createRootContext(dups, -1, -1);
184187
_xmlTokens = Objects.requireNonNull(tokenStream, "xmlTokenStream cannot be null");
185188
_cfgNameForTextElement = nameForTextElement;
186189
final int firstToken;

src/main/java/tools/jackson/dataformat/xml/deser/XmlReadContext.java

Lines changed: 47 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@
33
import java.util.Set;
44

55
import tools.jackson.core.*;
6+
import tools.jackson.core.exc.StreamReadException;
67
import tools.jackson.core.io.CharTypes;
78
import tools.jackson.core.io.ContentReference;
9+
import tools.jackson.core.json.DupDetector;
810

911
/**
1012
* Extension of {@link TokenStreamContext}, which implements
@@ -23,6 +25,12 @@ public final class XmlReadContext
2325

2426
protected final XmlReadContext _parent;
2527

28+
/**
29+
* Object used for checking for duplicate field names, if enabled
30+
* (null if not enabled)
31+
*/
32+
protected final DupDetector _dups;
33+
2634
// // // Location information (minus source reference)
2735

2836
protected int _lineNr;
@@ -54,18 +62,27 @@ public final class XmlReadContext
5462
/**********************************************************************
5563
*/
5664

57-
public XmlReadContext(int type, XmlReadContext parent, int nestingDepth,
65+
public XmlReadContext(int type, XmlReadContext parent, DupDetector dups,
66+
int nestingDepth,
5867
int lineNr, int colNr)
5968
{
6069
super();
6170
_type = type;
6271
_parent = parent;
72+
_dups = dups;
6373
_nestingDepth = nestingDepth;
6474
_lineNr = lineNr;
6575
_columnNr = colNr;
6676
_index = -1;
6777
}
6878

79+
@Deprecated // @since 3.0.2
80+
public XmlReadContext(int type, XmlReadContext parent, int nestingDepth,
81+
int lineNr, int colNr)
82+
{
83+
this(type, parent, null, nestingDepth, lineNr, colNr);
84+
}
85+
6986
protected final void reset(int type, int lineNr, int colNr)
7087
{
7188
_type = type;
@@ -76,6 +93,9 @@ protected final void reset(int type, int lineNr, int colNr)
7693
_currentValue = null;
7794
_namesToWrap = null;
7895
// _nestingDepth is fine since reused instance at same nesting level
96+
if (_dups != null) {
97+
_dups.reset();
98+
}
7999
}
80100

81101
@Override
@@ -94,17 +114,23 @@ public void assignCurrentValue(Object v) {
94114
/**********************************************************************
95115
*/
96116

117+
public static XmlReadContext createRootContext(DupDetector dups, int lineNr, int colNr) {
118+
return new XmlReadContext(TYPE_ROOT, null, dups, 0, lineNr, colNr);
119+
}
120+
121+
@Deprecated // @since 3.0.2
97122
public static XmlReadContext createRootContext(int lineNr, int colNr) {
98-
return new XmlReadContext(TYPE_ROOT, null, 0, lineNr, colNr);
123+
return createRootContext(null, lineNr, colNr);
99124
}
100125

101126
public final XmlReadContext createChildArrayContext(int lineNr, int colNr)
102127
{
103128
++_index; // not needed for Object, but does not hurt so no need to check curr type
104129
XmlReadContext ctxt = _child;
105130
if (ctxt == null) {
106-
_child = ctxt = new XmlReadContext(TYPE_ARRAY, this, _nestingDepth+1,
107-
lineNr, colNr);
131+
_child = ctxt = new XmlReadContext(TYPE_ARRAY, this,
132+
(_dups == null) ? null : _dups.child(),
133+
_nestingDepth+1, lineNr, colNr);
108134
return ctxt;
109135
}
110136
ctxt.reset(TYPE_ARRAY, lineNr, colNr);
@@ -116,8 +142,9 @@ public final XmlReadContext createChildObjectContext(int lineNr, int colNr)
116142
++_index; // not needed for Object, but does not hurt so no need to check curr type
117143
XmlReadContext ctxt = _child;
118144
if (ctxt == null) {
119-
_child = ctxt = new XmlReadContext(TYPE_OBJECT, this, _nestingDepth+1,
120-
lineNr, colNr);
145+
_child = ctxt = new XmlReadContext(TYPE_OBJECT, this,
146+
(_dups == null) ? null : _dups.child(),
147+
_nestingDepth+1, lineNr, colNr);
121148
return ctxt;
122149
}
123150
ctxt.reset(TYPE_OBJECT, lineNr, colNr);
@@ -159,15 +186,25 @@ public final TokenStreamLocation startLocation(ContentReference srcRef) {
159186
/**
160187
* Method called to mark start of new value, mostly to update `index`
161188
* for Array and Root contexts.
162-
*
163-
* @since 2.12
164189
*/
165190
public final void valueStarted() {
166191
++_index;
167192
}
168193

169-
public void setCurrentName(String name) {
194+
public void setCurrentName(String name) throws StreamReadException {
170195
_currentName = name;
196+
if (_dups != null) {
197+
_checkDup(_dups, name);
198+
}
199+
}
200+
201+
private static void _checkDup(DupDetector dd, String name) throws StreamReadException
202+
{
203+
if (dd.isDup(name)) {
204+
Object src = dd.getSource();
205+
throw new StreamReadException(((src instanceof JsonParser) ? ((JsonParser) src) : null),
206+
"Duplicate Object property \""+name+"\"");
207+
}
171208
}
172209

173210
public void setNamesToWrap(Set<String> namesToWrap) {
@@ -181,7 +218,7 @@ public boolean shouldWrap(String localName) {
181218
protected void convertToArray() {
182219
_type = TYPE_ARRAY;
183220
}
184-
221+
185222
/*
186223
/**********************************************************************
187224
/* Overridden standard methods
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package tools.jackson.dataformat.xml.deser;
2+
3+
import org.junit.jupiter.api.Test;
4+
5+
import tools.jackson.core.StreamReadFeature;
6+
import tools.jackson.core.exc.StreamReadException;
7+
import tools.jackson.dataformat.xml.XmlMapper;
8+
import tools.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+
public class StrictDuplicateDetection114Test extends XmlTestUtil
16+
{
17+
public static class TestBean114 {
18+
public String field1;
19+
public String field2;
20+
}
21+
22+
private final XmlMapper STRICT_MAPPER = XmlMapper.builder()
23+
.enable(StreamReadFeature.STRICT_DUPLICATE_DETECTION)
24+
.build();
25+
26+
// [dataformat-xml#114]
27+
@Test
28+
public void testStrictDuplicateDetectionWithPOJO() throws Exception
29+
{
30+
// Test XML mapper should also reject duplicates
31+
final String xmlWithDup = "<TestBean><field1>value1</field1><field1>value2</field1></TestBean>";
32+
33+
StreamReadException e = assertThrows(StreamReadException.class, () -> {
34+
STRICT_MAPPER.readValue(xmlWithDup, TestBean114.class);
35+
});
36+
final String MATCH = "Duplicate Object property \"field1\"";
37+
assertTrue(e.getMessage().contains(MATCH),
38+
"Expected ["+MATCH+"] error, got: " + e.getMessage());
39+
}
40+
41+
@Test
42+
public void testNoDuplicatesShouldWork() throws Exception
43+
{
44+
final String xml = "<TestBean><field1>value1</field1><field2>value2</field2></TestBean>";
45+
46+
TestBean114 bean = STRICT_MAPPER.readValue(xml, TestBean114.class);
47+
assertNotNull(bean);
48+
assertEquals("value1", bean.field1);
49+
assertEquals("value2", bean.field2);
50+
}
51+
52+
@Test
53+
public void testDuplicateDetectionDisabledByDefault() throws Exception
54+
{
55+
XmlMapper mapper = newMapper(); // default mapper without strict duplicate detection
56+
57+
// Should allow duplicates by default (last value wins)
58+
final String xmlWithDup = "<TestBean><field1>value1</field1><field1>value2</field1></TestBean>";
59+
60+
TestBean114 bean = mapper.readValue(xmlWithDup, TestBean114.class);
61+
assertNotNull(bean);
62+
assertEquals("value2", bean.field1); // last value wins
63+
}
64+
}

0 commit comments

Comments
 (0)