diff --git a/src/main/java/com/fasterxml/jackson/dataformat/xml/deser/FromXmlParser.java b/src/main/java/com/fasterxml/jackson/dataformat/xml/deser/FromXmlParser.java index 7fb2c00e..70a3236f 100644 --- a/src/main/java/com/fasterxml/jackson/dataformat/xml/deser/FromXmlParser.java +++ b/src/main/java/com/fasterxml/jackson/dataformat/xml/deser/FromXmlParser.java @@ -18,6 +18,7 @@ import com.fasterxml.jackson.core.exc.StreamConstraintsException; import com.fasterxml.jackson.core.io.IOContext; import com.fasterxml.jackson.core.io.NumberInput; +import com.fasterxml.jackson.core.json.DupDetector; import com.fasterxml.jackson.core.util.ByteArrayBuilder; import com.fasterxml.jackson.core.util.JacksonFeatureSet; @@ -284,18 +285,21 @@ public FromXmlParser(IOContext ctxt, int genericParserFeatures, int xmlFeatures, * This constructor initializes the parser with the given I/O context, parser features, * and object codec for deserializing XML content into Java objects. * - * @since 2.20 * @param ctxt I/O context used for handling low-level I/O operations and buffering * @param genericParserFeatures set of bitmasked parser features to control parsing behavior * @param codec object codec used for converting between JSON-like structures and Java objects * @param xmlTokenStream the pre-processed XML token stream to parse from * @throws IOException if an I/O error occurs during initialization or parsing setup + * + * @since 2.20 */ public FromXmlParser(IOContext ctxt, int genericParserFeatures, ObjectCodec codec, XmlTokenStream xmlTokenStream) throws IOException { super(genericParserFeatures, ctxt.streamReadConstraints()); _ioContext = ctxt; _objectCodec = codec; - _parsingContext = XmlReadContext.createRootContext(-1, -1); + DupDetector dups = JsonParser.Feature.STRICT_DUPLICATE_DETECTION.enabledIn(genericParserFeatures) + ? DupDetector.rootDetector(this) : null; + _parsingContext = XmlReadContext.createRootContext(dups, -1, -1); _xmlTokens = Objects.requireNonNull(xmlTokenStream, "xmlTokenStream cannot be null"); _formatFeatures = xmlTokenStream.getFormatFeatures(); final int firstToken; @@ -526,7 +530,12 @@ public void overrideCurrentName(String name) if (_currToken == JsonToken.START_OBJECT || _currToken == JsonToken.START_ARRAY) { ctxt = ctxt.getParent(); } - ctxt.setCurrentName(name); + // Unfortunate, but since we did not expose exceptions, need to wrap + try { + ctxt.setCurrentName(name); + } catch (IOException e) { + throw new IllegalStateException(e); + } } @Override @@ -1050,7 +1059,7 @@ public String nextTextValue() throws IOException } - private void _updateState(JsonToken t) + private void _updateState(JsonToken t) throws JsonProcessingException { switch (t) { case START_OBJECT: diff --git a/src/main/java/com/fasterxml/jackson/dataformat/xml/deser/XmlReadContext.java b/src/main/java/com/fasterxml/jackson/dataformat/xml/deser/XmlReadContext.java index dc7ae3bb..aa40167b 100644 --- a/src/main/java/com/fasterxml/jackson/dataformat/xml/deser/XmlReadContext.java +++ b/src/main/java/com/fasterxml/jackson/dataformat/xml/deser/XmlReadContext.java @@ -5,6 +5,7 @@ import com.fasterxml.jackson.core.*; import com.fasterxml.jackson.core.io.CharTypes; import com.fasterxml.jackson.core.io.ContentReference; +import com.fasterxml.jackson.core.json.DupDetector; /** * Extension of {@link JsonStreamContext}, which implements @@ -23,6 +24,14 @@ public final class XmlReadContext protected final XmlReadContext _parent; + /** + * Object used for checking for duplicate field names, if enabled + * (null if not enabled) + * + * @since 2.21 + */ + protected final DupDetector _dups; + // // // Location information (minus source reference) protected int _lineNr; @@ -59,28 +68,26 @@ public final class XmlReadContext */ /** - * @since 2.18 + * @since 2.21 */ - public XmlReadContext(XmlReadContext parent, int nestingDepth, + public XmlReadContext(XmlReadContext parent, DupDetector dups, + int nestingDepth, int type, int lineNr, int colNr) { super(); _type = type; _parent = parent; + _dups = dups; _lineNr = lineNr; _columnNr = colNr; _index = -1; _nestingDepth = nestingDepth; } - /** - * @deprecated Since 2.18 - */ - @Deprecated // since 2.18 - public XmlReadContext(XmlReadContext parent, int type, int lineNr, int colNr) - { - this(parent, (parent == null) ? 0 : parent._nestingDepth + 1, - type, lineNr, colNr); + @Deprecated // @since 2.21 + public XmlReadContext(XmlReadContext parent, int nestingDepth, + int type, int lineNr, int colNr) { + this(parent, null, nestingDepth, type, lineNr, colNr); } protected final void reset(int type, int lineNr, int colNr) @@ -92,7 +99,10 @@ protected final void reset(int type, int lineNr, int colNr) _currentName = null; _currentValue = null; _namesToWrap = null; - // _nestingDepth fine as is, same level for reuse + if (_dups != null) { + _dups.reset(); + } + // _nestingDepth fine as-is, same level for reuse } @Override @@ -111,20 +121,28 @@ public void setCurrentValue(Object v) { /********************************************************************** */ + public static XmlReadContext createRootContext(DupDetector dups, int lineNr, int colNr) { + return new XmlReadContext(null, dups, 0, TYPE_ROOT, lineNr, colNr); + } + + @Deprecated // @since 2.21 public static XmlReadContext createRootContext(int lineNr, int colNr) { - return new XmlReadContext(null, 0, TYPE_ROOT, lineNr, colNr); + return createRootContext(null, lineNr, colNr); } + @Deprecated // @since 2.21 public static XmlReadContext createRootContext() { - return new XmlReadContext(null, 0, TYPE_ROOT, 1, 0); + return createRootContext(null, 1, 0); } - + public final XmlReadContext createChildArrayContext(int lineNr, int colNr) { ++_index; // not needed for Object, but does not hurt so no need to check curr type XmlReadContext ctxt = _child; if (ctxt == null) { - _child = ctxt = new XmlReadContext(this, _nestingDepth+1, TYPE_ARRAY, lineNr, colNr); + _child = ctxt = new XmlReadContext(this, + (_dups == null) ? null : _dups.child(), + _nestingDepth+1, TYPE_ARRAY, lineNr, colNr); return ctxt; } ctxt.reset(TYPE_ARRAY, lineNr, colNr); @@ -136,10 +154,12 @@ public final XmlReadContext createChildObjectContext(int lineNr, int colNr) ++_index; // not needed for Object, but does not hurt so no need to check curr type XmlReadContext ctxt = _child; if (ctxt == null) { - _child = ctxt = new XmlReadContext(this, TYPE_OBJECT, lineNr, colNr); - return ctxt; + _child = ctxt = new XmlReadContext(this, + (_dups == null) ? null : _dups.child(), + _nestingDepth+1, TYPE_OBJECT, lineNr, colNr); + } else { + ctxt.reset(TYPE_OBJECT, lineNr, colNr); } - ctxt.reset(TYPE_OBJECT, lineNr, colNr); return ctxt; } @@ -186,10 +206,22 @@ public final void valueStarted() { ++_index; } - public void setCurrentName(String name) { + public void setCurrentName(String name) throws JsonProcessingException { _currentName = name; + if (_dups != null) { + _checkDup(_dups, name); + } } + // @since 2.21 + private static void _checkDup(DupDetector dd, String name) throws JsonProcessingException + { + if (dd.isDup(name)) { + throw new JsonParseException(null, + "Duplicate field '"+name+"'", dd.findLocation()); + } + } + public void setNamesToWrap(Set namesToWrap) { _namesToWrap = namesToWrap; } diff --git a/src/test/java/com/fasterxml/jackson/dataformat/xml/deser/StrictDuplicateDetection114Test.java b/src/test/java/com/fasterxml/jackson/dataformat/xml/deser/StrictDuplicateDetection114Test.java new file mode 100644 index 00000000..259228cd --- /dev/null +++ b/src/test/java/com/fasterxml/jackson/dataformat/xml/deser/StrictDuplicateDetection114Test.java @@ -0,0 +1,65 @@ +package com.fasterxml.jackson.dataformat.xml.deser; + +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.exc.StreamReadException; +import com.fasterxml.jackson.dataformat.xml.XmlMapper; +import com.fasterxml.jackson.dataformat.xml.XmlTestUtil; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Tests for [dataformat-xml#114]: Support for STRICT_DUPLICATE_DETECTION + * + * @since 2.21 + */ +public class StrictDuplicateDetection114Test extends XmlTestUtil +{ + static class TestBean114 { + public String field1; + public String field2; + } + + private final XmlMapper STRICT_MAPPER = XmlMapper.builder() + .enable(JsonParser.Feature.STRICT_DUPLICATE_DETECTION) + .build(); + + // [dataformat-xml#114] + @Test + public void testStrictDuplicateDetectionWithPOJO() throws Exception + { + // Test XML mapper should also reject duplicates + final String xmlWithDup = "value1value2"; + + StreamReadException e = assertThrows(StreamReadException.class, () -> { + STRICT_MAPPER.readValue(xmlWithDup, TestBean114.class); + }); + assertTrue(e.getMessage().contains("Duplicate field"), + "Expected 'Duplicate field' error, got: " + e.getMessage()); + } + + @Test + public void testNoDuplicatesShouldWork() throws Exception + { + final String xml = "value1value2"; + + TestBean114 bean = STRICT_MAPPER.readValue(xml, TestBean114.class); + assertNotNull(bean); + assertEquals("value1", bean.field1); + assertEquals("value2", bean.field2); + } + + @Test + public void testDuplicateDetectionDisabledByDefault() throws Exception + { + XmlMapper mapper = newMapper(); // default mapper without strict duplicate detection + + // Should allow duplicates by default (last value wins) + final String xmlWithDup = "value1value2"; + + TestBean114 bean = mapper.readValue(xmlWithDup, TestBean114.class); + assertNotNull(bean); + assertEquals("value2", bean.field1); // last value wins + } +}