From 6b7ecfeb1f8823bd99176f02d65f161e8dcfa596 Mon Sep 17 00:00:00 2001 From: indyteo Date: Thu, 11 Dec 2025 12:09:52 +0100 Subject: [PATCH 1/2] Ported YAMLAnchorReplayingParser from 2.x to 3.x Co-authored-by: "Boettger, Heiko" --- yaml/src/main/java/module-info.java | 3 +- .../yaml/YAMLAnchorReplayingFactory.java | 92 +++++ .../YAMLAnchorReplayingFactoryBuilder.java | 20 + .../yaml/YAMLAnchorReplayingParser.java | 194 +++++++++ ...StreamingYAMLAnchorReplayingParseTest.java | 376 ++++++++++++++++++ 5 files changed, 684 insertions(+), 1 deletion(-) create mode 100644 yaml/src/main/java/tools/jackson/dataformat/yaml/YAMLAnchorReplayingFactory.java create mode 100644 yaml/src/main/java/tools/jackson/dataformat/yaml/YAMLAnchorReplayingFactoryBuilder.java create mode 100644 yaml/src/main/java/tools/jackson/dataformat/yaml/YAMLAnchorReplayingParser.java create mode 100644 yaml/src/test/java/tools/jackson/dataformat/yaml/deser/StreamingYAMLAnchorReplayingParseTest.java diff --git a/yaml/src/main/java/module-info.java b/yaml/src/main/java/module-info.java index db5402d3..15a01a6c 100644 --- a/yaml/src/main/java/module-info.java +++ b/yaml/src/main/java/module-info.java @@ -11,7 +11,8 @@ exports tools.jackson.dataformat.yaml.util; provides tools.jackson.core.TokenStreamFactory with - tools.jackson.dataformat.yaml.YAMLFactory; + tools.jackson.dataformat.yaml.YAMLFactory, + tools.jackson.dataformat.yaml.YAMLAnchorReplayingFactory; provides tools.jackson.databind.ObjectMapper with tools.jackson.dataformat.yaml.YAMLMapper; } diff --git a/yaml/src/main/java/tools/jackson/dataformat/yaml/YAMLAnchorReplayingFactory.java b/yaml/src/main/java/tools/jackson/dataformat/yaml/YAMLAnchorReplayingFactory.java new file mode 100644 index 00000000..1f98ae9b --- /dev/null +++ b/yaml/src/main/java/tools/jackson/dataformat/yaml/YAMLAnchorReplayingFactory.java @@ -0,0 +1,92 @@ +package tools.jackson.dataformat.yaml; + +import java.io.*; + +import tools.jackson.core.ObjectReadContext; +import tools.jackson.core.io.IOContext; + +/** + * A subclass of YAMLFactory with the only purpose to replace the YAMLParser by + * the YAMLAnchorReplayingParser subclass. + * + * @since 2.19 + */ +public class YAMLAnchorReplayingFactory extends YAMLFactory { + private static final long serialVersionUID = 1L; + + public YAMLAnchorReplayingFactory() { + super(); + } + + public YAMLAnchorReplayingFactory(YAMLFactory src) { + super(src); + } + + protected YAMLAnchorReplayingFactory(YAMLAnchorReplayingFactoryBuilder b) { + super(b); + } + + @Override + public YAMLAnchorReplayingFactoryBuilder rebuild() { + return new YAMLAnchorReplayingFactoryBuilder(this); + } + + /** + * Main factory method to use for constructing {@link YAMLAnchorReplayingFactory} instances with + * different configuration. + */ + public static YAMLAnchorReplayingFactoryBuilder builder() { + return new YAMLAnchorReplayingFactoryBuilder(); + } + + @Override + public YAMLAnchorReplayingFactory copy() { + return new YAMLAnchorReplayingFactory(this); + } + + @Override + protected Object readResolve() { + return new YAMLAnchorReplayingFactory(this); + } + + @Override + protected YAMLAnchorReplayingParser _createParser(ObjectReadContext readCtxt, IOContext ioCtxt, InputStream in) { + return new YAMLAnchorReplayingParser(readCtxt, ioCtxt, + _getBufferRecycler(), + readCtxt.getStreamReadFeatures(_streamReadFeatures), + readCtxt.getFormatReadFeatures(_formatReadFeatures), + _loadSettings, + _createReader(in, null, ioCtxt)); + } + + @Override + protected YAMLAnchorReplayingParser _createParser(ObjectReadContext readCtxt, IOContext ioCtxt, Reader r) { + return new YAMLAnchorReplayingParser(readCtxt, ioCtxt, + _getBufferRecycler(), + readCtxt.getStreamReadFeatures(_streamReadFeatures), + readCtxt.getFormatReadFeatures(_formatReadFeatures), + _loadSettings, + r); + } + + @Override + protected YAMLAnchorReplayingParser _createParser(ObjectReadContext readCtxt, IOContext ioCtxt, + char[] data, int offset, int len, + boolean recyclable) { + return new YAMLAnchorReplayingParser(readCtxt, ioCtxt, _getBufferRecycler(), + readCtxt.getStreamReadFeatures(_streamReadFeatures), + readCtxt.getFormatReadFeatures(_formatReadFeatures), + _loadSettings, + new CharArrayReader(data, offset, len)); + } + + @Override + protected YAMLAnchorReplayingParser _createParser(ObjectReadContext readCtxt, IOContext ioCtxt, + byte[] data, int offset, int len) { + return new YAMLAnchorReplayingParser(readCtxt, ioCtxt, _getBufferRecycler(), + readCtxt.getStreamReadFeatures(_streamReadFeatures), + readCtxt.getFormatReadFeatures(_formatReadFeatures), + _loadSettings, + _createReader(data, offset, len, null, ioCtxt)); + } +} diff --git a/yaml/src/main/java/tools/jackson/dataformat/yaml/YAMLAnchorReplayingFactoryBuilder.java b/yaml/src/main/java/tools/jackson/dataformat/yaml/YAMLAnchorReplayingFactoryBuilder.java new file mode 100644 index 00000000..f714e423 --- /dev/null +++ b/yaml/src/main/java/tools/jackson/dataformat/yaml/YAMLAnchorReplayingFactoryBuilder.java @@ -0,0 +1,20 @@ +package tools.jackson.dataformat.yaml; + +/** + * A subclass of YAMLFactoryBuilder with the only purpose to replace the YAMLFactory by + * the YAMLAnchorReplayingFactory subclass. + */ +public class YAMLAnchorReplayingFactoryBuilder extends YAMLFactoryBuilder { + protected YAMLAnchorReplayingFactoryBuilder() { + super(); + } + + public YAMLAnchorReplayingFactoryBuilder(YAMLAnchorReplayingFactory base) { + super(base); + } + + @Override + public YAMLAnchorReplayingFactory build() { + return new YAMLAnchorReplayingFactory(this); + } +} diff --git a/yaml/src/main/java/tools/jackson/dataformat/yaml/YAMLAnchorReplayingParser.java b/yaml/src/main/java/tools/jackson/dataformat/yaml/YAMLAnchorReplayingParser.java new file mode 100644 index 00000000..b3f12c0c --- /dev/null +++ b/yaml/src/main/java/tools/jackson/dataformat/yaml/YAMLAnchorReplayingParser.java @@ -0,0 +1,194 @@ +package tools.jackson.dataformat.yaml; + +import java.io.Reader; + +import java.util.*; + +import org.snakeyaml.engine.v2.api.LoadSettings; +import org.snakeyaml.engine.v2.common.Anchor; +import org.snakeyaml.engine.v2.events.*; + +import tools.jackson.core.ObjectReadContext; +import tools.jackson.core.exc.StreamConstraintsException; +import tools.jackson.core.io.IOContext; +import tools.jackson.core.util.BufferRecycler; + +/** + * A parser that remembers the events of anchored parts in yaml and repeats them + * to inline these parts when an alias if found instead of only returning an alias. + *

+ * Note: this overwrites the nextEvent() since the base {@code super.nextToken()} + * manages too much state, and it seems to be much simpler to re-emit the events. + * + * @since 2.19 + */ +public class YAMLAnchorReplayingParser extends YAMLParser { + private static class AnchorContext { + public final String anchor; + public final List events = new ArrayList<>(); + public int depth = 1; + + public AnchorContext(String anchor) { + this.anchor = anchor; + } + } + + /** + * the maximum number of events that can be replayed + */ + public static final int MAX_EVENTS = 9999; + + /** + * the maximum limit of anchors to remember + */ + public static final int MAX_ANCHORS = 9999; + + /** + * the maximum limit of merges to follow + */ + public static final int MAX_MERGES = 9999; + + /** + * the maximum limit of references to remember + */ + public static final int MAX_REFS = 9999; + + /** + * Remembers when a merge has been started in order to skip the corresponding + * sequence end which needs to be excluded + */ + private final ArrayDeque mergeStack = new ArrayDeque<>(); + + /** + * Collects nested anchor definitions + */ + private final ArrayDeque tokenStack = new ArrayDeque<>(); + + /** + * Keeps track of the last sequentially found definition of each anchor + */ + private final Map> referencedObjects = new HashMap<>(); + + /** + * Keeps track of events that have been insert when processing alias + */ + private final ArrayDeque refEvents = new ArrayDeque<>(); + + /** + * keeps track of the global depth of nested collections + */ + private int globalDepth = 0; + + public YAMLAnchorReplayingParser(ObjectReadContext readCtxt, IOContext ioCtxt, BufferRecycler br, + int streamReadFeatures, int formatFeatures, + LoadSettings loadSettings, Reader reader) { + super(readCtxt, ioCtxt, br, streamReadFeatures, formatFeatures, loadSettings, reader); + } + + private void finishContext(AnchorContext context) throws StreamConstraintsException { + if (referencedObjects.size() + 1 > MAX_REFS) + throw new StreamConstraintsException("too many references in the document"); + referencedObjects.put(context.anchor, context.events); + if (!tokenStack.isEmpty()) { + List events = tokenStack.peek().events; + if (events.size() + context.events.size() > MAX_EVENTS) + throw new StreamConstraintsException("too many events to replay"); + events.addAll(context.events); + } + } + + protected Event trackDepth(Event event) { + if (event instanceof CollectionStartEvent) { + ++globalDepth; + } else if (event instanceof CollectionEndEvent) { + --globalDepth; + } + return event; + } + + protected Event filterEvent(Event event) { + if (event instanceof MappingEndEvent) { + if (!mergeStack.isEmpty()) { + if (mergeStack.peek() > globalDepth) { + mergeStack.pop(); + return null; + } + } + } + return event; + } + + @Override + protected Event nextEvent() { + while (!refEvents.isEmpty()) { + Event event = filterEvent(trackDepth(refEvents.removeFirst())); + if (event != null) return event; + } + + Event event = null; + while (event == null) { + event = trackDepth(super.nextEvent()); + if (event == null) return null; + event = filterEvent(event); + } + + if (event instanceof AliasEvent alias) { + List events = referencedObjects.get(alias.getAlias().getValue()); + if (events != null) { + if (refEvents.size() + events.size() > MAX_EVENTS) + throw new StreamConstraintsException("too many events to replay"); + refEvents.addAll(events); + return refEvents.removeFirst(); + } + _reportError("invalid alias: " + alias.getAlias()); + } + + if (event instanceof NodeEvent nodeEvent) { + String anchor = nodeEvent.getAnchor().map(Anchor::getValue).orElse(null); + if (anchor != null) { + AnchorContext context = new AnchorContext(anchor); + context.events.add(event); + if (event instanceof CollectionStartEvent) { + if (tokenStack.size() + 1 > MAX_ANCHORS) + throw new StreamConstraintsException("too many anchors in the document"); + tokenStack.push(context); + } else { + // directly store it + finishContext(context); + } + return event; + } + } + + if (event instanceof ScalarEvent scalarEvent) { + if (scalarEvent.getValue().equals("<<")) { + // expect next node to be a map + Event next = nextEvent(); + if (next instanceof MappingStartEvent) { + if (mergeStack.size() + 1 > MAX_MERGES) + throw new StreamConstraintsException("too many merges in the document"); + mergeStack.push(globalDepth); + return nextEvent(); + } + _reportError("found field '<<' but value isn't a map"); + } + } + + if (!tokenStack.isEmpty()) { + AnchorContext context = tokenStack.peek(); + if (context.events.size() + 1 > MAX_EVENTS) + throw new StreamConstraintsException("too many events to replay"); + context.events.add(event); + if (event instanceof CollectionStartEvent) { + ++context.depth; + } else if (event instanceof CollectionEndEvent) { + --context.depth; + if (context.depth == 0) { + tokenStack.pop(); + finishContext(context); + } + } + } + return event; + } +} diff --git a/yaml/src/test/java/tools/jackson/dataformat/yaml/deser/StreamingYAMLAnchorReplayingParseTest.java b/yaml/src/test/java/tools/jackson/dataformat/yaml/deser/StreamingYAMLAnchorReplayingParseTest.java new file mode 100644 index 00000000..eeadd105 --- /dev/null +++ b/yaml/src/test/java/tools/jackson/dataformat/yaml/deser/StreamingYAMLAnchorReplayingParseTest.java @@ -0,0 +1,376 @@ +package tools.jackson.dataformat.yaml.deser; + +import org.junit.jupiter.api.Test; +import tools.jackson.core.TokenStreamLocation; +import tools.jackson.core.JsonParser; +import tools.jackson.core.JsonToken; + +import tools.jackson.dataformat.yaml.ModuleTestBase; +import tools.jackson.dataformat.yaml.YAMLAnchorReplayingFactory; +import tools.jackson.dataformat.yaml.YAMLMapper; + +import static org.junit.jupiter.api.Assertions.*; + +public class StreamingYAMLAnchorReplayingParseTest extends ModuleTestBase { + + private final YAMLMapper MAPPER = mapperBuilder(new YAMLAnchorReplayingFactory()).build(); + + @Test + public void testBasic() { + final String YAML = """ + string: 'text' + bool: true + bool2: false + null: null + i: 123 + d: 1.25 + """; + JsonParser p = MAPPER.createParser(YAML); + assertToken(JsonToken.START_OBJECT, p.nextToken()); + + assertToken(JsonToken.PROPERTY_NAME, p.nextToken()); + assertToken(JsonToken.VALUE_STRING, p.nextToken()); + assertEquals("text", p.getString()); + TokenStreamLocation loc = p.currentTokenLocation(); + assertEquals(1, loc.getLineNr()); + assertEquals(9, loc.getColumnNr()); + assertEquals(8, loc.getCharOffset()); + assertEquals(-1, loc.getByteOffset()); + + assertToken(JsonToken.PROPERTY_NAME, p.nextToken()); + assertToken(JsonToken.VALUE_TRUE, p.nextToken()); + assertEquals("true", p.getString()); + loc = p.currentTokenLocation(); + assertEquals(2, loc.getLineNr()); + assertEquals(7, loc.getColumnNr()); + assertEquals(21, loc.getCharOffset()); + assertEquals(-1, loc.getByteOffset()); + + assertToken(JsonToken.PROPERTY_NAME, p.nextToken()); + assertToken(JsonToken.VALUE_FALSE, p.nextToken()); + assertEquals("false", p.getString()); + assertToken(JsonToken.PROPERTY_NAME, p.nextToken()); + assertToken(JsonToken.VALUE_NULL, p.nextToken()); + assertEquals("null", p.getString()); + assertToken(JsonToken.PROPERTY_NAME, p.nextToken()); + assertToken(JsonToken.VALUE_NUMBER_INT, p.nextToken()); + assertEquals("123", p.getString()); + assertEquals(123, p.getIntValue()); + assertToken(JsonToken.PROPERTY_NAME, p.nextToken()); + assertToken(JsonToken.VALUE_NUMBER_FLOAT, p.nextToken()); + assertEquals("1.25", p.getString()); + assertEquals(1.25, p.getDoubleValue()); + assertEquals(1, p.getIntValue()); + + assertToken(JsonToken.END_OBJECT, p.nextToken()); + assertNull(p.nextToken()); + assertNull(p.nextToken()); + assertNull(p.nextToken()); + p.close(); + } + + @Test + public void testScalarAnchor() { + final String YAML = """ + string1: &stringAnchor 'textValue' + string2: *stringAnchor + int1: &intAnchor 123 + int2: *intAnchor + """; + + JsonParser p = MAPPER.createParser(YAML); + assertToken(JsonToken.START_OBJECT, p.nextToken()); + + assertToken(JsonToken.PROPERTY_NAME, p.nextToken()); + assertEquals("string1", p.getString()); + assertToken(JsonToken.VALUE_STRING, p.nextToken()); + assertEquals("textValue", p.getString()); + TokenStreamLocation loc = p.currentTokenLocation(); + assertEquals(1, loc.getLineNr()); + assertEquals(10, loc.getColumnNr()); + assertEquals(9, loc.getCharOffset()); + assertEquals(-1, loc.getByteOffset()); + + assertToken(JsonToken.PROPERTY_NAME, p.nextToken()); + assertEquals("string2", p.getString()); + assertToken(JsonToken.VALUE_STRING, p.nextToken()); + assertEquals("textValue", p.getString()); + loc = p.currentTokenLocation(); + assertEquals(1, loc.getLineNr()); + assertEquals(10, loc.getColumnNr()); + assertEquals(9, loc.getCharOffset()); + assertEquals(-1, loc.getByteOffset()); + + assertToken(JsonToken.PROPERTY_NAME, p.nextToken()); + assertEquals("int1", p.getString()); + assertToken(JsonToken.VALUE_NUMBER_INT, p.nextToken()); + assertEquals("123", p.getString()); + loc = p.currentTokenLocation(); + assertEquals(3, loc.getLineNr()); + assertEquals(7, loc.getColumnNr()); + assertEquals(64, loc.getCharOffset()); + assertEquals(-1, loc.getByteOffset()); + + assertToken(JsonToken.PROPERTY_NAME, p.nextToken()); + assertEquals("int2", p.getString()); + assertToken(JsonToken.VALUE_NUMBER_INT, p.nextToken()); + assertEquals("123", p.getString()); + loc = p.currentTokenLocation(); + assertEquals(3, loc.getLineNr()); + assertEquals(7, loc.getColumnNr()); + assertEquals(64, loc.getCharOffset()); + assertEquals(-1, loc.getByteOffset()); + + assertToken(JsonToken.END_OBJECT, p.nextToken()); + assertNull(p.nextToken()); + assertNull(p.nextToken()); + assertNull(p.nextToken()); + p.close(); + } + + @Test + public void testSequenceAnchor() { + final String YAML = """ + list1: &listAnchor + - 1 + - 2 + - 3 + list2: *listAnchor + """; + JsonParser p = MAPPER.createParser(YAML); + assertToken(JsonToken.START_OBJECT, p.nextToken()); + + assertToken(JsonToken.PROPERTY_NAME, p.nextToken()); + assertEquals("list1", p.getString()); + + assertToken(JsonToken.START_ARRAY, p.nextToken()); + assertToken(JsonToken.VALUE_NUMBER_INT, p.nextToken()); + assertEquals("1", p.getString()); + TokenStreamLocation loc = p.currentTokenLocation(); + assertEquals(2, loc.getLineNr()); + assertEquals(5, loc.getColumnNr()); + assertEquals(23, loc.getCharOffset()); + assertEquals(-1, loc.getByteOffset()); + + assertToken(JsonToken.VALUE_NUMBER_INT, p.nextToken()); + assertEquals("2", p.getString()); + loc = p.currentTokenLocation(); + assertEquals(3, loc.getLineNr()); + assertEquals(5, loc.getColumnNr()); + assertEquals(29, loc.getCharOffset()); + assertEquals(-1, loc.getByteOffset()); + + assertToken(JsonToken.VALUE_NUMBER_INT, p.nextToken()); + assertEquals("3", p.getString()); + loc = p.currentTokenLocation(); + assertEquals(4, loc.getLineNr()); + assertEquals(5, loc.getColumnNr()); + assertEquals(35, loc.getCharOffset()); + assertEquals(-1, loc.getByteOffset()); + + assertToken(JsonToken.END_ARRAY, p.nextToken()); + + assertToken(JsonToken.PROPERTY_NAME, p.nextToken()); + assertEquals("list2", p.getString()); + + assertToken(JsonToken.START_ARRAY, p.nextToken()); + assertToken(JsonToken.VALUE_NUMBER_INT, p.nextToken()); + assertEquals("1", p.getString()); + loc = p.currentTokenLocation(); + assertEquals(2, loc.getLineNr()); + assertEquals(5, loc.getColumnNr()); + assertEquals(23, loc.getCharOffset()); + assertEquals(-1, loc.getByteOffset()); + + assertToken(JsonToken.VALUE_NUMBER_INT, p.nextToken()); + assertEquals("2", p.getString()); + loc = p.currentTokenLocation(); + assertEquals(3, loc.getLineNr()); + assertEquals(5, loc.getColumnNr()); + assertEquals(29, loc.getCharOffset()); + assertEquals(-1, loc.getByteOffset()); + + assertToken(JsonToken.VALUE_NUMBER_INT, p.nextToken()); + assertEquals("3", p.getString()); + loc = p.currentTokenLocation(); + assertEquals(4, loc.getLineNr()); + assertEquals(5, loc.getColumnNr()); + assertEquals(35, loc.getCharOffset()); + assertEquals(-1, loc.getByteOffset()); + + assertToken(JsonToken.END_ARRAY, p.nextToken()); + + assertToken(JsonToken.END_OBJECT, p.nextToken()); + + assertNull(p.nextToken()); + + p.close(); + } + + @Test + public void testObjectAnchor() { + final String YAML = """ + obj1: &objAnchor + string: 'text' + bool: true + obj2: *objAnchor + """; + JsonParser p = MAPPER.createParser(YAML); + assertToken(JsonToken.START_OBJECT, p.nextToken()); + + assertToken(JsonToken.PROPERTY_NAME, p.nextToken()); + assertEquals("obj1", p.getString()); + assertToken(JsonToken.START_OBJECT, p.nextToken()); + TokenStreamLocation loc = p.currentTokenLocation(); + assertEquals(1, loc.getLineNr()); + assertEquals(7, loc.getColumnNr()); + assertEquals(6, loc.getCharOffset()); + assertEquals(-1, loc.getByteOffset()); + + assertToken(JsonToken.PROPERTY_NAME, p.nextToken()); + assertEquals("string", p.getString()); + assertToken(JsonToken.VALUE_STRING, p.nextToken()); + assertEquals("text", p.getString()); + loc = p.currentTokenLocation(); + assertEquals(2, loc.getLineNr()); + assertEquals(11, loc.getColumnNr()); + assertEquals(27, loc.getCharOffset()); + assertEquals(-1, loc.getByteOffset()); + + assertToken(JsonToken.PROPERTY_NAME, p.nextToken()); + assertEquals("bool", p.getString()); + assertToken(JsonToken.VALUE_TRUE, p.nextToken()); + loc = p.currentTokenLocation(); + assertEquals(3, loc.getLineNr()); + assertEquals(9, loc.getColumnNr()); + assertEquals(42, loc.getCharOffset()); + assertEquals(-1, loc.getByteOffset()); + + assertToken(JsonToken.END_OBJECT, p.nextToken()); + + assertToken(JsonToken.PROPERTY_NAME, p.nextToken()); + assertEquals("obj2", p.getString()); + assertToken(JsonToken.START_OBJECT, p.nextToken()); + loc = p.currentTokenLocation(); + assertEquals(1, loc.getLineNr()); + assertEquals(7, loc.getColumnNr()); + assertEquals(6, loc.getCharOffset()); + assertEquals(-1, loc.getByteOffset()); + + assertToken(JsonToken.PROPERTY_NAME, p.nextToken()); + assertEquals("string", p.getString()); + assertToken(JsonToken.VALUE_STRING, p.nextToken()); + assertEquals("text", p.getString()); + loc = p.currentTokenLocation(); + assertEquals(2, loc.getLineNr()); + assertEquals(11, loc.getColumnNr()); + assertEquals(27, loc.getCharOffset()); + assertEquals(-1, loc.getByteOffset()); + + assertToken(JsonToken.PROPERTY_NAME, p.nextToken()); + assertEquals("bool", p.getString()); + assertToken(JsonToken.VALUE_TRUE, p.nextToken()); + loc = p.currentTokenLocation(); + assertEquals(3, loc.getLineNr()); + assertEquals(9, loc.getColumnNr()); + assertEquals(42, loc.getCharOffset()); + assertEquals(-1, loc.getByteOffset()); + + assertToken(JsonToken.END_OBJECT, p.nextToken()); + + assertToken(JsonToken.END_OBJECT, p.nextToken()); + + assertNull(p.nextToken()); + + p.close(); + } + + @Test + public void testMergeAnchor() { + final String YAML = """ + obj1: &objAnchor + string: 'text' + bool: true + obj2: + <<: *objAnchor + int: 123 + """; + JsonParser p = MAPPER.createParser(YAML); + assertToken(JsonToken.START_OBJECT, p.nextToken()); + + assertToken(JsonToken.PROPERTY_NAME, p.nextToken()); + assertEquals("obj1", p.getString()); + assertToken(JsonToken.START_OBJECT, p.nextToken()); + TokenStreamLocation loc = p.currentTokenLocation(); + assertEquals(1, loc.getLineNr()); + assertEquals(7, loc.getColumnNr()); + assertEquals(6, loc.getCharOffset()); + assertEquals(-1, loc.getByteOffset()); + + assertToken(JsonToken.PROPERTY_NAME, p.nextToken()); + assertEquals("string", p.getString()); + assertToken(JsonToken.VALUE_STRING, p.nextToken()); + assertEquals("text", p.getString()); + loc = p.currentTokenLocation(); + assertEquals(2, loc.getLineNr()); + assertEquals(11, loc.getColumnNr()); + assertEquals(27, loc.getCharOffset()); + assertEquals(-1, loc.getByteOffset()); + + assertToken(JsonToken.PROPERTY_NAME, p.nextToken()); + assertEquals("bool", p.getString()); + assertToken(JsonToken.VALUE_TRUE, p.nextToken()); + loc = p.currentTokenLocation(); + assertEquals(3, loc.getLineNr()); + assertEquals(9, loc.getColumnNr()); + assertEquals(42, loc.getCharOffset()); + assertEquals(-1, loc.getByteOffset()); + + assertToken(JsonToken.END_OBJECT, p.nextToken()); + + assertToken(JsonToken.PROPERTY_NAME, p.nextToken()); + assertEquals("obj2", p.getString()); + assertToken(JsonToken.START_OBJECT, p.nextToken()); + loc = p.currentTokenLocation(); + assertEquals(5, loc.getLineNr()); + assertEquals(3, loc.getColumnNr()); + assertEquals(55, loc.getCharOffset()); + assertEquals(-1, loc.getByteOffset()); + + assertToken(JsonToken.PROPERTY_NAME, p.nextToken()); + assertEquals("string", p.getString()); + assertToken(JsonToken.VALUE_STRING, p.nextToken()); + assertEquals("text", p.getString()); + loc = p.currentTokenLocation(); + assertEquals(2, loc.getLineNr()); + assertEquals(11, loc.getColumnNr()); + assertEquals(27, loc.getCharOffset()); + assertEquals(-1, loc.getByteOffset()); + + assertToken(JsonToken.PROPERTY_NAME, p.nextToken()); + assertEquals("bool", p.getString()); + assertToken(JsonToken.VALUE_TRUE, p.nextToken()); + loc = p.currentTokenLocation(); + assertEquals(3, loc.getLineNr()); + assertEquals(9, loc.getColumnNr()); + assertEquals(42, loc.getCharOffset()); + assertEquals(-1, loc.getByteOffset()); + + assertToken(JsonToken.PROPERTY_NAME, p.nextToken()); + assertEquals("int", p.getString()); + assertToken(JsonToken.VALUE_NUMBER_INT, p.nextToken()); + loc = p.currentTokenLocation(); + assertEquals(6, loc.getLineNr()); + assertEquals(8, loc.getColumnNr()); + assertEquals(77, loc.getCharOffset()); + assertEquals(-1, loc.getByteOffset()); + + assertToken(JsonToken.END_OBJECT, p.nextToken()); + + assertToken(JsonToken.END_OBJECT, p.nextToken()); + + assertNull(p.nextToken()); + + p.close(); + } +} From 14da14113a9ece9988691857636386f66570077d Mon Sep 17 00:00:00 2001 From: indyteo Date: Thu, 11 Dec 2025 15:03:06 +0100 Subject: [PATCH 2/2] Made YAMLAnchorReplayingParser able to handle nested anchors --- .../yaml/YAMLAnchorReplayingParser.java | 41 +++++---- ...StreamingYAMLAnchorReplayingParseTest.java | 89 +++++++++++++++++++ 2 files changed, 113 insertions(+), 17 deletions(-) diff --git a/yaml/src/main/java/tools/jackson/dataformat/yaml/YAMLAnchorReplayingParser.java b/yaml/src/main/java/tools/jackson/dataformat/yaml/YAMLAnchorReplayingParser.java index b3f12c0c..1b7653c7 100644 --- a/yaml/src/main/java/tools/jackson/dataformat/yaml/YAMLAnchorReplayingParser.java +++ b/yaml/src/main/java/tools/jackson/dataformat/yaml/YAMLAnchorReplayingParser.java @@ -118,11 +118,31 @@ protected Event filterEvent(Event event) { return event; } + protected void recordEvent(Event event) { + if (tokenStack.isEmpty()) return; + AnchorContext context = tokenStack.peek(); + if (context.events.size() + 1 > MAX_EVENTS) + throw new StreamConstraintsException("too many events to replay"); + context.events.add(event); + if (event instanceof CollectionStartEvent) { + ++context.depth; + } else if (event instanceof CollectionEndEvent) { + --context.depth; + if (context.depth == 0) { + tokenStack.pop(); + finishContext(context); + } + } + } + @Override protected Event nextEvent() { while (!refEvents.isEmpty()) { Event event = filterEvent(trackDepth(refEvents.removeFirst())); - if (event != null) return event; + if (event != null) { + recordEvent(event); + return event; + } } Event event = null; @@ -138,7 +158,7 @@ protected Event nextEvent() { if (refEvents.size() + events.size() > MAX_EVENTS) throw new StreamConstraintsException("too many events to replay"); refEvents.addAll(events); - return refEvents.removeFirst(); + return nextEvent(); } _reportError("invalid alias: " + alias.getAlias()); } @@ -156,6 +176,7 @@ protected Event nextEvent() { // directly store it finishContext(context); } + // no need to record this event as it was handled above return event; } } @@ -174,21 +195,7 @@ protected Event nextEvent() { } } - if (!tokenStack.isEmpty()) { - AnchorContext context = tokenStack.peek(); - if (context.events.size() + 1 > MAX_EVENTS) - throw new StreamConstraintsException("too many events to replay"); - context.events.add(event); - if (event instanceof CollectionStartEvent) { - ++context.depth; - } else if (event instanceof CollectionEndEvent) { - --context.depth; - if (context.depth == 0) { - tokenStack.pop(); - finishContext(context); - } - } - } + recordEvent(event); return event; } } diff --git a/yaml/src/test/java/tools/jackson/dataformat/yaml/deser/StreamingYAMLAnchorReplayingParseTest.java b/yaml/src/test/java/tools/jackson/dataformat/yaml/deser/StreamingYAMLAnchorReplayingParseTest.java index eeadd105..1f164251 100644 --- a/yaml/src/test/java/tools/jackson/dataformat/yaml/deser/StreamingYAMLAnchorReplayingParseTest.java +++ b/yaml/src/test/java/tools/jackson/dataformat/yaml/deser/StreamingYAMLAnchorReplayingParseTest.java @@ -373,4 +373,93 @@ public void testMergeAnchor() { p.close(); } + + @Test + public void testNestedAnchor() { + final String YAML = """ + value: &valAnchor 'text' + obj1: &objAnchor + string: *valAnchor + bool: true + obj2: *objAnchor + """; + JsonParser p = MAPPER.createParser(YAML); + assertToken(JsonToken.START_OBJECT, p.nextToken()); + + assertToken(JsonToken.PROPERTY_NAME, p.nextToken()); + assertEquals("value", p.getString()); + assertToken(JsonToken.VALUE_STRING, p.nextToken()); + assertEquals("text", p.getString()); + TokenStreamLocation loc = p.currentTokenLocation(); + assertEquals(1, loc.getLineNr()); + assertEquals(8, loc.getColumnNr()); + assertEquals(7, loc.getCharOffset()); + assertEquals(-1, loc.getByteOffset()); + + assertToken(JsonToken.PROPERTY_NAME, p.nextToken()); + assertEquals("obj1", p.getString()); + assertToken(JsonToken.START_OBJECT, p.nextToken()); + loc = p.currentTokenLocation(); + assertEquals(2, loc.getLineNr()); + assertEquals(7, loc.getColumnNr()); + assertEquals(31, loc.getCharOffset()); + assertEquals(-1, loc.getByteOffset()); + + assertToken(JsonToken.PROPERTY_NAME, p.nextToken()); + assertEquals("string", p.getString()); + assertToken(JsonToken.VALUE_STRING, p.nextToken()); + assertEquals("text", p.getString()); + loc = p.currentTokenLocation(); + assertEquals(1, loc.getLineNr()); + assertEquals(8, loc.getColumnNr()); + assertEquals(7, loc.getCharOffset()); + assertEquals(-1, loc.getByteOffset()); + + assertToken(JsonToken.PROPERTY_NAME, p.nextToken()); + assertEquals("bool", p.getString()); + assertToken(JsonToken.VALUE_TRUE, p.nextToken()); + loc = p.currentTokenLocation(); + assertEquals(4, loc.getLineNr()); + assertEquals(9, loc.getColumnNr()); + assertEquals(71, loc.getCharOffset()); + assertEquals(-1, loc.getByteOffset()); + + assertToken(JsonToken.END_OBJECT, p.nextToken()); + + assertToken(JsonToken.PROPERTY_NAME, p.nextToken()); + assertEquals("obj2", p.getString()); + assertToken(JsonToken.START_OBJECT, p.nextToken()); + loc = p.currentTokenLocation(); + assertEquals(2, loc.getLineNr()); + assertEquals(7, loc.getColumnNr()); + assertEquals(31, loc.getCharOffset()); + assertEquals(-1, loc.getByteOffset()); + + assertToken(JsonToken.PROPERTY_NAME, p.nextToken()); + assertEquals("string", p.getString()); + assertToken(JsonToken.VALUE_STRING, p.nextToken()); + assertEquals("text", p.getString()); + loc = p.currentTokenLocation(); + assertEquals(1, loc.getLineNr()); + assertEquals(8, loc.getColumnNr()); + assertEquals(7, loc.getCharOffset()); + assertEquals(-1, loc.getByteOffset()); + + assertToken(JsonToken.PROPERTY_NAME, p.nextToken()); + assertEquals("bool", p.getString()); + assertToken(JsonToken.VALUE_TRUE, p.nextToken()); + loc = p.currentTokenLocation(); + assertEquals(4, loc.getLineNr()); + assertEquals(9, loc.getColumnNr()); + assertEquals(71, loc.getCharOffset()); + assertEquals(-1, loc.getByteOffset()); + + assertToken(JsonToken.END_OBJECT, p.nextToken()); + + assertToken(JsonToken.END_OBJECT, p.nextToken()); + + assertNull(p.nextToken()); + + p.close(); + } }