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 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(IOContext ctxt, int parserFeatures, int formatFeatures, LoaderOptions loaderOptions, ObjectCodec codec, Reader reader) { + super(ctxt, parserFeatures, formatFeatures, loaderOptions, codec, 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 getEvent() throws IOException { + while(!refEvents.isEmpty()) { + Event event = filterEvent(trackDepth(refEvents.removeFirst())); + if (event != null) return event; + } + + Event event = null; + while (event == null) { + event = trackDepth(super.getEvent()); + if (event == null) return null; + event = filterEvent(event); + } + + if (event instanceof AliasEvent) { + AliasEvent alias = (AliasEvent) event; + List events = referencedObjects.get(alias.getAnchor()); + if (events != null) { + if (refEvents.size() + events.size() > MAX_EVENTS) throw new StreamConstraintsException("too many events to replay"); + refEvents.addAll(events); + return refEvents.removeFirst(); + } + throw new JsonParseException("invalid alias " + alias.getAnchor()); + } + + if (event instanceof NodeEvent) { + String anchor = ((NodeEvent) event).getAnchor(); + 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 scalarEvent = (ScalarEvent) event; + if (scalarEvent.getValue().equals( "<<")) { + // expect next node to be a map + Event next = getEvent(); + if (next instanceof MappingStartEvent) { + if (mergeStack.size() + 1 > MAX_MERGES) throw new StreamConstraintsException("too many merges in the document"); + mergeStack.push(globalDepth); + return getEvent(); + } + throw new JsonParseException("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/main/java/com/fasterxml/jackson/dataformat/yaml/YAMLParser.java b/yaml/src/main/java/com/fasterxml/jackson/dataformat/yaml/YAMLParser.java index eed7a159..49ceff10 100644 --- a/yaml/src/main/java/com/fasterxml/jackson/dataformat/yaml/YAMLParser.java +++ b/yaml/src/main/java/com/fasterxml/jackson/dataformat/yaml/YAMLParser.java @@ -580,13 +580,16 @@ public JsonToken nextToken() throws IOException /** * Since the parserImpl cannot be replaced allow subclasses to at least be able to * influence the events being consumed. - * + *

* 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(); + } +}