diff --git a/release-notes/CREDITS-2.x b/release-notes/CREDITS-2.x index d6339e68..86bf1c68 100644 --- a/release-notes/CREDITS-2.x +++ b/release-notes/CREDITS-2.x @@ -276,6 +276,9 @@ Heiko Boettger (@HeikoBoettger) * Contributed #482: (yaml) Allow passing `ParserImpl` by a subclass or overwrite the events (2.18.0) +* Contributed #502: (yaml) Add an optional extended parser subclass (`YAMLAnchorReplayingFactory`) + able to inline anchors + (2.19.0) Burdyug Pavel (@Pavel38l) diff --git a/release-notes/VERSION-2.x b/release-notes/VERSION-2.x index f7fda5ef..ffb8c7bc 100644 --- a/release-notes/VERSION-2.x +++ b/release-notes/VERSION-2.x @@ -16,7 +16,9 @@ Active Maintainers: 2.19.0 (not yet released) -- +#502: Add an optional extended parser subclass (`YAMLAnchorReplayingFactory`) + able to inline anchors + (contributed by Heiko B) 2.18.2 (27-Nov-2024) diff --git a/yaml/src/main/java/com/fasterxml/jackson/dataformat/yaml/YAMLAnchorReplayingFactory.java b/yaml/src/main/java/com/fasterxml/jackson/dataformat/yaml/YAMLAnchorReplayingFactory.java new file mode 100644 index 00000000..d1fb9b13 --- /dev/null +++ b/yaml/src/main/java/com/fasterxml/jackson/dataformat/yaml/YAMLAnchorReplayingFactory.java @@ -0,0 +1,72 @@ +package com.fasterxml.jackson.dataformat.yaml; + +import java.io.CharArrayReader; +import java.io.InputStream; +import java.io.IOException; +import java.io.Reader; + +import com.fasterxml.jackson.core.JsonEncoding; +import com.fasterxml.jackson.core.ObjectCodec; +import com.fasterxml.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(ObjectCodec oc) { + super(oc); + } + + public YAMLAnchorReplayingFactory(YAMLFactory src, ObjectCodec oc) { + super(src, oc); + } + + protected YAMLAnchorReplayingFactory(YAMLFactoryBuilder b) { + super(b); + } + + @Override + public YAMLAnchorReplayingFactory copy() { + _checkInvalidCopy(YAMLAnchorReplayingFactory.class); + return new YAMLAnchorReplayingFactory(this, (ObjectCodec) null); + } + + @Override + protected Object readResolve() { + return new YAMLAnchorReplayingFactory(this, _objectCodec); + } + + @Override + protected YAMLParser _createParser(InputStream input, IOContext ctxt) throws IOException { + return new YAMLAnchorReplayingParser(ctxt, _parserFeatures, _yamlParserFeatures, + _loaderOptions, _objectCodec, + _createReader(input, (JsonEncoding) null, ctxt)); + } + + @Override + protected YAMLParser _createParser(Reader r, IOContext ctxt) throws IOException { + return new YAMLAnchorReplayingParser(ctxt, _parserFeatures, _yamlParserFeatures, + _loaderOptions, _objectCodec, r); + } + + @Override + protected YAMLParser _createParser(char[] data, int offset, int len, IOContext ctxt, boolean recyclable) throws IOException { + return new YAMLAnchorReplayingParser(ctxt, _parserFeatures, _yamlParserFeatures, + _loaderOptions, _objectCodec, new CharArrayReader(data, offset, len)); + } + + @Override + protected YAMLParser _createParser(byte[] data, int offset, int len, IOContext ctxt) throws IOException { + return new YAMLAnchorReplayingParser(ctxt, _parserFeatures, _yamlParserFeatures, + _loaderOptions, _objectCodec, _createReader(data, offset, len, (JsonEncoding) null, ctxt)); + } +} diff --git a/yaml/src/main/java/com/fasterxml/jackson/dataformat/yaml/YAMLAnchorReplayingParser.java b/yaml/src/main/java/com/fasterxml/jackson/dataformat/yaml/YAMLAnchorReplayingParser.java new file mode 100644 index 00000000..6782725d --- /dev/null +++ b/yaml/src/main/java/com/fasterxml/jackson/dataformat/yaml/YAMLAnchorReplayingParser.java @@ -0,0 +1,189 @@ +package com.fasterxml.jackson.dataformat.yaml; + +import java.io.Reader; +import java.io.IOException; + +import java.util.*; + +import org.yaml.snakeyaml.LoaderOptions; +import org.yaml.snakeyaml.events.*; + +import com.fasterxml.jackson.core.JsonParseException; +import com.fasterxml.jackson.core.ObjectCodec; +import com.fasterxml.jackson.core.exc.StreamConstraintsException; +import com.fasterxml.jackson.core.io.IOContext; + +/** + * 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 getEvent() since the base `super.nextToken()` manages to 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
* A particular use case is working around the lack of anchor and alias support to
* emit additional events.
+ *
+ * NOTE: since 2.18, declared to throw {@link IOException} to allow sub-classes
+ * to do so.
*
* @since 2.18
*/
- protected Event getEvent() {
+ protected Event getEvent() throws IOException {
return _yamlParser.getEvent();
}
diff --git a/yaml/src/test/java/com/fasterxml/jackson/dataformat/yaml/deser/StreamingYAMLAnchorReplayingParseTest.java b/yaml/src/test/java/com/fasterxml/jackson/dataformat/yaml/deser/StreamingYAMLAnchorReplayingParseTest.java
new file mode 100644
index 00000000..6cc53656
--- /dev/null
+++ b/yaml/src/test/java/com/fasterxml/jackson/dataformat/yaml/deser/StreamingYAMLAnchorReplayingParseTest.java
@@ -0,0 +1,375 @@
+package com.fasterxml.jackson.dataformat.yaml.deser;
+
+import com.fasterxml.jackson.core.JsonLocation;
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.JsonToken;
+
+import com.fasterxml.jackson.dataformat.yaml.JacksonYAMLParseException;
+import com.fasterxml.jackson.dataformat.yaml.ModuleTestBase;
+import com.fasterxml.jackson.dataformat.yaml.YAMLAnchorReplayingFactory;
+import com.fasterxml.jackson.dataformat.yaml.YAMLAnchorReplayingParser;
+import org.yaml.snakeyaml.LoaderOptions;
+
+public class StreamingYAMLAnchorReplayingParseTest extends ModuleTestBase {
+
+ private final YAMLAnchorReplayingFactory YAML_F = new YAMLAnchorReplayingFactory();
+
+ public void testBasic() throws Exception
+ {
+ final String YAML =
+"string: 'text'\n"
++"bool: true\n"
++"bool2: false\n"
++"null: null\n"
++"i: 123\n"
++"d: 1.25\n"
+;
+ JsonParser p = YAML_F.createParser(YAML);
+ assertToken(JsonToken.START_OBJECT, p.nextToken());
+
+ assertToken(JsonToken.FIELD_NAME, p.nextToken());
+ assertToken(JsonToken.VALUE_STRING, p.nextToken());
+ assertEquals("text", p.getText());
+ JsonLocation loc = p.currentTokenLocation();
+ assertEquals(1, loc.getLineNr());
+ assertEquals(9, loc.getColumnNr());
+ assertEquals(8, loc.getCharOffset());
+ assertEquals(-1, loc.getByteOffset());
+
+ assertToken(JsonToken.FIELD_NAME, p.nextToken());
+ assertToken(JsonToken.VALUE_TRUE, p.nextToken());
+ assertEquals("true", p.getText());
+ loc = p.currentTokenLocation();
+ assertEquals(2, loc.getLineNr());
+ assertEquals(7, loc.getColumnNr());
+ assertEquals(21, loc.getCharOffset());
+ assertEquals(-1, loc.getByteOffset());
+
+ assertToken(JsonToken.FIELD_NAME, p.nextToken());
+ assertToken(JsonToken.VALUE_FALSE, p.nextToken());
+ assertEquals("false", p.getText());
+ assertToken(JsonToken.FIELD_NAME, p.nextToken());
+ assertToken(JsonToken.VALUE_NULL, p.nextToken());
+ assertEquals("null", p.getText());
+ assertToken(JsonToken.FIELD_NAME, p.nextToken());
+ assertToken(JsonToken.VALUE_NUMBER_INT, p.nextToken());
+ assertEquals("123", p.getText());
+ assertEquals(123, p.getIntValue());
+ assertToken(JsonToken.FIELD_NAME, p.nextToken());
+ assertToken(JsonToken.VALUE_NUMBER_FLOAT, p.nextToken());
+ assertEquals("1.25", p.getText());
+ 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();
+ }
+
+ public void testScalarAnchor() throws Exception
+ {
+ final String YAML =
+"string1: &stringAnchor 'textValue'\n"
++"string2: *stringAnchor\n"
++"int1: &intAnchor 123\n"
++"int2: *intAnchor\n"
+;
+
+ JsonParser p = YAML_F.createParser(YAML);
+ assertToken(JsonToken.START_OBJECT, p.nextToken());
+
+ assertToken(JsonToken.FIELD_NAME, p.nextToken());
+ assertEquals("string1", p.getText());
+ assertToken(JsonToken.VALUE_STRING, p.nextToken());
+ assertEquals("textValue", p.getText());
+ JsonLocation loc = p.currentTokenLocation();
+ assertEquals(1, loc.getLineNr());
+ assertEquals(10, loc.getColumnNr());
+ assertEquals(9, loc.getCharOffset());
+ assertEquals(-1, loc.getByteOffset());
+
+ assertToken(JsonToken.FIELD_NAME, p.nextToken());
+ assertEquals("string2", p.getText());
+ assertToken(JsonToken.VALUE_STRING, p.nextToken());
+ assertEquals("textValue", p.getText());
+ loc = p.currentTokenLocation();
+ assertEquals(1, loc.getLineNr());
+ assertEquals(10, loc.getColumnNr());
+ assertEquals(9, loc.getCharOffset());
+ assertEquals(-1, loc.getByteOffset());
+
+ assertToken(JsonToken.FIELD_NAME, p.nextToken());
+ assertEquals("int1", p.getText());
+ assertToken(JsonToken.VALUE_NUMBER_INT, p.nextToken());
+ assertEquals("123", p.getText());
+ loc = p.currentTokenLocation();
+ assertEquals(3, loc.getLineNr());
+ assertEquals(7, loc.getColumnNr());
+ assertEquals(64, loc.getCharOffset());
+ assertEquals(-1, loc.getByteOffset());
+
+ assertToken(JsonToken.FIELD_NAME, p.nextToken());
+ assertEquals("int2", p.getText());
+ assertToken(JsonToken.VALUE_NUMBER_INT, p.nextToken());
+ assertEquals("123", p.getText());
+ 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();
+ }
+
+ public void testSequenceAnchor() throws Exception
+ {
+ final String YAML =
+"list1: &listAnchor\n"
++" - 1\n"
++" - 2\n"
++" - 3\n"
++"list2: *listAnchor\n"
+;
+ JsonParser p = YAML_F.createParser(YAML);
+ assertToken(JsonToken.START_OBJECT, p.nextToken());
+
+ assertToken(JsonToken.FIELD_NAME, p.nextToken());
+ assertEquals("list1", p.getText());
+
+ assertToken(JsonToken.START_ARRAY, p.nextToken());
+ assertToken(JsonToken.VALUE_NUMBER_INT, p.nextToken());
+ assertEquals("1", p.getText());
+ JsonLocation 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.getText());
+ 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.getText());
+ 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.FIELD_NAME, p.nextToken());
+ assertEquals("list2", p.getText());
+
+ assertToken(JsonToken.START_ARRAY, p.nextToken());
+ assertToken(JsonToken.VALUE_NUMBER_INT, p.nextToken());
+ assertEquals("1", p.getText());
+ 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.getText());
+ 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.getText());
+ 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();
+ }
+
+ public void testObjectAnchor() throws Exception
+ {
+ final String YAML =
+"obj1: &objAnchor\n"
++" string: 'text'\n"
++" bool: True\n"
++"obj2: *objAnchor\n"
+;
+ JsonParser p = YAML_F.createParser(YAML);
+ assertToken(JsonToken.START_OBJECT, p.nextToken());
+
+ assertToken(JsonToken.FIELD_NAME, p.nextToken());
+ assertEquals("obj1", p.getText());
+ assertToken(JsonToken.START_OBJECT, p.nextToken());
+ JsonLocation loc = p.currentTokenLocation();
+ assertEquals(1, loc.getLineNr());
+ assertEquals(7, loc.getColumnNr());
+ assertEquals(6, loc.getCharOffset());
+ assertEquals(-1, loc.getByteOffset());
+
+ assertToken(JsonToken.FIELD_NAME, p.nextToken());
+ assertEquals("string", p.getText());
+ assertToken(JsonToken.VALUE_STRING, p.nextToken());
+ assertEquals("text", p.getText());
+ loc = p.currentTokenLocation();
+ assertEquals(2, loc.getLineNr());
+ assertEquals(11, loc.getColumnNr());
+ assertEquals(27, loc.getCharOffset());
+ assertEquals(-1, loc.getByteOffset());
+
+ assertToken(JsonToken.FIELD_NAME, p.nextToken());
+ assertEquals("bool", p.getText());
+ 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.FIELD_NAME, p.nextToken());
+ assertEquals("obj2", p.getText());
+ 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.FIELD_NAME, p.nextToken());
+ assertEquals("string", p.getText());
+ assertToken(JsonToken.VALUE_STRING, p.nextToken());
+ assertEquals("text", p.getText());
+ loc = p.currentTokenLocation();
+ assertEquals(2, loc.getLineNr());
+ assertEquals(11, loc.getColumnNr());
+ assertEquals(27, loc.getCharOffset());
+ assertEquals(-1, loc.getByteOffset());
+
+ assertToken(JsonToken.FIELD_NAME, p.nextToken());
+ assertEquals("bool", p.getText());
+ 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();
+ }
+
+ public void testMergeAnchor() throws Exception
+ {
+ final String YAML =
+"obj1: &objAnchor\n"
++" string: 'text'\n"
++" bool: True\n"
++"obj2:\n"
++" <<: *objAnchor\n"
++" int: 123\n"
+;
+ JsonParser p = YAML_F.createParser(YAML);
+ assertToken(JsonToken.START_OBJECT, p.nextToken());
+
+ assertToken(JsonToken.FIELD_NAME, p.nextToken());
+ assertEquals("obj1", p.getText());
+ assertToken(JsonToken.START_OBJECT, p.nextToken());
+ JsonLocation loc = p.currentTokenLocation();
+ assertEquals(1, loc.getLineNr());
+ assertEquals(7, loc.getColumnNr());
+ assertEquals(6, loc.getCharOffset());
+ assertEquals(-1, loc.getByteOffset());
+
+ assertToken(JsonToken.FIELD_NAME, p.nextToken());
+ assertEquals("string", p.getText());
+ assertToken(JsonToken.VALUE_STRING, p.nextToken());
+ assertEquals("text", p.getText());
+ loc = p.currentTokenLocation();
+ assertEquals(2, loc.getLineNr());
+ assertEquals(11, loc.getColumnNr());
+ assertEquals(27, loc.getCharOffset());
+ assertEquals(-1, loc.getByteOffset());
+
+ assertToken(JsonToken.FIELD_NAME, p.nextToken());
+ assertEquals("bool", p.getText());
+ 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.FIELD_NAME, p.nextToken());
+ assertEquals("obj2", p.getText());
+ 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.FIELD_NAME, p.nextToken());
+ assertEquals("string", p.getText());
+ assertToken(JsonToken.VALUE_STRING, p.nextToken());
+ assertEquals("text", p.getText());
+ loc = p.currentTokenLocation();
+ assertEquals(2, loc.getLineNr());
+ assertEquals(11, loc.getColumnNr());
+ assertEquals(27, loc.getCharOffset());
+ assertEquals(-1, loc.getByteOffset());
+
+ assertToken(JsonToken.FIELD_NAME, p.nextToken());
+ assertEquals("bool", p.getText());
+ 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.FIELD_NAME, p.nextToken());
+ assertEquals("int", p.getText());
+ 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();
+ }
+}