Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
164 changes: 163 additions & 1 deletion yaml/src/main/java/tools/jackson/dataformat/yaml/YAMLParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -346,16 +346,42 @@ public JsonToken nextToken() throws JacksonException
if (_streamReadContext.inObject()) {
if (_currToken != JsonToken.PROPERTY_NAME) {
if (evt.getEventId() != Event.ID.Scalar) {
_currentAnchor = Optional.empty();
_lastTagEvent = null;
// end is fine
if (evt.getEventId() == Event.ID.MappingEnd) {
_currentAnchor = Optional.empty();
if (!_streamReadContext.inObject()) { // sanity check is optional, but let's do it for now
_reportMismatchedEndMarker('}', ']');
}
_streamReadContext = _streamReadContext.getParent();
return _updateToken(JsonToken.END_OBJECT);
}
// [dataformats-text#589]: complex (non-scalar) mapping keys
if (evt.getEventId() == Event.ID.SequenceStart
|| evt.getEventId() == Event.ID.MappingStart
|| evt.getEventId() == Event.ID.Alias) {
final boolean firstEntry = (_currToken == JsonToken.START_OBJECT);
if (evt.getEventId() == Event.ID.Alias) {
if (!firstEntry) {
_currentAnchor = Optional.empty();
_lastTagEvent = evt;
}
} else {
final Optional<Anchor> newAnchor = ((NodeEvent) evt).getAnchor();
if (newAnchor.isPresent() || !firstEntry) {
_currentAnchor = newAnchor;
}
if (!firstEntry) {
_lastTagEvent = evt;
}
}
final String name = _decodeComplexPropertyName(evt);
_streamReadConstraints.validateNameLength(name.length());
_currentName = name;
_streamReadContext.setCurrentName(name);
return _updateToken(JsonToken.PROPERTY_NAME);
}
_currentAnchor = Optional.empty();
_reportError("Expected a property name (Scalar value in YAML), got this instead: "+evt);
}

Expand Down Expand Up @@ -481,6 +507,142 @@ protected Event nextEvent() {
return _yamlParser.next();
}

/**
* Helper method called when a non-scalar event (Sequence or Mapping start) is encountered
* where a property name is expected -- indicates a "complex key" in YAML terms.
* Consumes the full key structure and converts to a canonical String representation
* (flow-style) so that Jackson's scalar-only property name model can represent it.
*
* @since 3.3
*/
protected String _decodeComplexPropertyName(Event evt) throws JacksonException
{
StringBuilder sb = new StringBuilder(32);
_appendComplexKey(evt, sb, 0);
return sb.toString();
}

private Event _nextComplexKeyEvent()
{
while (true) {
Event evt = nextEvent();
if (evt == null) {
return null;
}
if (evt.getEventId() == Event.ID.Comment) {
continue;
}
return evt;
}
}

private void _appendComplexKey(Event evt, StringBuilder sb, int depth) throws JacksonException
{
final int nestingDepth = _streamReadContext.getNestingDepth() + depth;
switch (evt.getEventId()) {
case Scalar:
_streamReadConstraints.validateNestingDepth(nestingDepth);
_appendComplexKeyScalar(((ScalarEvent) evt).getValue(), sb);
return;
case SequenceStart:
_streamReadConstraints.validateNestingDepth(nestingDepth);
sb.append('[');
_appendComplexKeySequenceContent(sb, depth + 1);
sb.append(']');
return;
case MappingStart:
_streamReadConstraints.validateNestingDepth(nestingDepth);
sb.append('{');
_appendComplexKeyMappingContent(sb, depth + 1);
sb.append('}');
return;
case Alias:
_streamReadConstraints.validateNestingDepth(nestingDepth);
AliasEvent alias = (AliasEvent) evt;
sb.append('*');
sb.append(alias.getAnchor().orElseThrow(() -> new RuntimeException("Alias must be provided.")).getValue());
return;
default:
_reportError("Unexpected YAML event for complex property name, expected Scalar, Sequence or Mapping start, got: "+evt);
}
}

private void _appendComplexKeySequenceContent(StringBuilder sb, int depth) throws JacksonException
{
boolean first = true;
while (true) {
Event evt = _nextComplexKeyEvent();
if (evt == null) {
_reportError("Unexpected end-of-input in complex property name (sequence)");
}
if (evt.getEventId() == Event.ID.SequenceEnd) {
return;
}
if (!first) {
sb.append(", ");
}
first = false;
_appendComplexKey(evt, sb, depth);
}
}

private void _appendComplexKeyMappingContent(StringBuilder sb, int depth) throws JacksonException
{
boolean first = true;
while (true) {
Event keyEvt = _nextComplexKeyEvent();
if (keyEvt == null) {
_reportError("Unexpected end-of-input in complex property name (mapping)");
}
if (keyEvt.getEventId() == Event.ID.MappingEnd) {
return;
}
if (!first) {
sb.append(", ");
}
first = false;
_appendComplexKey(keyEvt, sb, depth);
sb.append(": ");
Event valEvt = _nextComplexKeyEvent();
if (valEvt == null) {
_reportError("Unexpected end-of-input in complex property name (mapping value)");
}
_appendComplexKey(valEvt, sb, depth);
}
}

private void _appendComplexKeyScalar(String value, StringBuilder sb)
{
if (_needsQuotesForComplexKey(value)) {
sb.append('"');
for (int i = 0, len = value.length(); i < len; ++i) {
char c = value.charAt(i);
if (c == '"' || c == '\\') {
sb.append('\\');
}
sb.append(c);
}
sb.append('"');
} else {
sb.append(value);
}
}

private boolean _needsQuotesForComplexKey(String value)
{
if (value.isEmpty()) {
return true;
}
for (int i = 0, len = value.length(); i < len; ++i) {
char c = value.charAt(i);
if (c == ':' || c == ',' || c == '[' || c == ']' || c == '{' || c == '}'
|| c == ' ' || c == '\t' || c == '\n' || c == '\r') {
return true;
}
}
return false;
}

protected JsonToken _decodeScalar(ScalarEvent scalar) throws JacksonException
{
String value = scalar.getValue();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

import tools.jackson.core.JsonParser;
import tools.jackson.core.JsonToken;
import tools.jackson.databind.JsonNode;
import tools.jackson.dataformat.yaml.JacksonYAMLParseException;
import tools.jackson.dataformat.yaml.ModuleTestBase;
import tools.jackson.dataformat.yaml.YAMLFactory;
Expand Down Expand Up @@ -663,6 +664,162 @@ public void testYamlParseFailsWhenCodePointLimitVerySmall() throws Exception
}
}

// [dataformats-text#589]: sequence (and mapping) as mapping key
@Test
public void testComplexMappingKeySequence() throws Exception
{
final String YAML = "? [user, 123]\n: name: Ivan\n age: 30\n";
try (JsonParser p = MAPPER.createParser(YAML)) {
assertToken(JsonToken.START_OBJECT, p.nextToken());
assertToken(JsonToken.PROPERTY_NAME, p.nextToken());
assertEquals("[user, 123]", p.currentName());
assertEquals("[user, 123]", p.getString());
assertToken(JsonToken.START_OBJECT, p.nextToken());
assertToken(JsonToken.PROPERTY_NAME, p.nextToken());
assertEquals("name", p.currentName());
assertToken(JsonToken.VALUE_STRING, p.nextToken());
assertEquals("Ivan", p.getString());
assertToken(JsonToken.PROPERTY_NAME, p.nextToken());
assertEquals("age", p.currentName());
assertToken(JsonToken.VALUE_NUMBER_INT, p.nextToken());
assertEquals(30, p.getIntValue());
assertToken(JsonToken.END_OBJECT, p.nextToken());
assertToken(JsonToken.END_OBJECT, p.nextToken());
assertNull(p.nextToken());
}

JsonNode node = MAPPER.readTree(YAML);
assertTrue(node.has("[user, 123]"));
assertEquals("Ivan", node.get("[user, 123]").get("name").asString());
assertEquals(30, node.get("[user, 123]").get("age").asInt());
}

@Test
public void testComplexMappingKeyMapping() throws Exception
{
final String YAML = "? {foo: bar}\n: 42\n";
try (JsonParser p = MAPPER.createParser(YAML)) {
assertToken(JsonToken.START_OBJECT, p.nextToken());
assertToken(JsonToken.PROPERTY_NAME, p.nextToken());
assertEquals("{foo: bar}", p.currentName());
assertToken(JsonToken.VALUE_NUMBER_INT, p.nextToken());
assertEquals(42, p.getIntValue());
assertToken(JsonToken.END_OBJECT, p.nextToken());
assertNull(p.nextToken());
}

JsonNode node = MAPPER.readTree(YAML);
assertTrue(node.has("{foo: bar}"));
assertEquals(42, node.get("{foo: bar}").asInt());
}

@Test
public void testComplexMappingKeyImplicitFlow() throws Exception
{
final String YAML = "[user, 123]: 42\n";
try (JsonParser p = MAPPER.createParser(YAML)) {
assertToken(JsonToken.START_OBJECT, p.nextToken());
assertToken(JsonToken.PROPERTY_NAME, p.nextToken());
assertEquals("[user, 123]", p.currentName());
assertToken(JsonToken.VALUE_NUMBER_INT, p.nextToken());
assertEquals(42, p.getIntValue());
assertToken(JsonToken.END_OBJECT, p.nextToken());
assertNull(p.nextToken());
}
}

@Test
public void testComplexMappingKeyNested() throws Exception
{
final String YAML = "? {outer: {inner: x}}\n: 42\n";
try (JsonParser p = MAPPER.createParser(YAML)) {
assertToken(JsonToken.START_OBJECT, p.nextToken());
assertToken(JsonToken.PROPERTY_NAME, p.nextToken());
assertEquals("{outer: {inner: x}}", p.currentName());
assertToken(JsonToken.VALUE_NUMBER_INT, p.nextToken());
assertEquals(42, p.getIntValue());
assertToken(JsonToken.END_OBJECT, p.nextToken());
assertNull(p.nextToken());
}
}

@Test
public void testComplexMappingKeyEmptyCollections() throws Exception
{
final String YAML = "? []\n: empty-seq\n? {}\n: empty-map\n";
try (JsonParser p = MAPPER.createParser(YAML)) {
assertToken(JsonToken.START_OBJECT, p.nextToken());

assertToken(JsonToken.PROPERTY_NAME, p.nextToken());
assertEquals("[]", p.currentName());
assertToken(JsonToken.VALUE_STRING, p.nextToken());
assertEquals("empty-seq", p.getString());

assertToken(JsonToken.PROPERTY_NAME, p.nextToken());
assertEquals("{}", p.currentName());
assertToken(JsonToken.VALUE_STRING, p.nextToken());
assertEquals("empty-map", p.getString());

assertToken(JsonToken.END_OBJECT, p.nextToken());
assertNull(p.nextToken());
}
}

@Test
public void testComplexMappingKeyQuotedSpecials() throws Exception
{
final String YAML = "? [\"a:b\", \"c,d\", has space]\n: ok\n";
try (JsonParser p = MAPPER.createParser(YAML)) {
assertToken(JsonToken.START_OBJECT, p.nextToken());
assertToken(JsonToken.PROPERTY_NAME, p.nextToken());
assertEquals("[\"a:b\", \"c,d\", \"has space\"]", p.currentName());
assertToken(JsonToken.VALUE_STRING, p.nextToken());
assertEquals("ok", p.getString());
assertToken(JsonToken.END_OBJECT, p.nextToken());
assertNull(p.nextToken());
}
}

@Test
public void testComplexMappingKeyAliasInKey() throws Exception
{
final String YAML = "? &key1 [user, 123]\n: first\n? *key1\n: second\n";
try (JsonParser p = MAPPER.createParser(YAML)) {
assertToken(JsonToken.START_OBJECT, p.nextToken());

assertToken(JsonToken.PROPERTY_NAME, p.nextToken());
assertEquals("[user, 123]", p.currentName());

assertToken(JsonToken.VALUE_STRING, p.nextToken());
assertEquals("first", p.getString());

assertToken(JsonToken.PROPERTY_NAME, p.nextToken());
assertEquals("*key1", p.currentName());

assertToken(JsonToken.VALUE_STRING, p.nextToken());
assertEquals("second", p.getString());

assertToken(JsonToken.END_OBJECT, p.nextToken());
assertNull(p.nextToken());
}
}

@Test
public void testComplexMappingKeyObjectAnchorDeferredOnFirstEntry() throws Exception
{
final String YAML = "&obj1\n? [user, 123]\n: 42\n";
YAMLParser yp = (YAMLParser) MAPPER.createParser(YAML);
assertToken(JsonToken.START_OBJECT, yp.nextToken());
assertEquals("obj1", yp.getObjectId());
assertToken(JsonToken.PROPERTY_NAME, yp.nextToken());
assertEquals("[user, 123]", yp.currentName());
assertToken(JsonToken.VALUE_NUMBER_INT, yp.nextToken());
assertEquals(42, yp.getIntValue());
assertToken(JsonToken.END_OBJECT, yp.nextToken());
assertNull(yp.nextToken());
yp.close();
}

// In Jackson 3.x, non-Number token should NOT throw exception for parser.getNumberType()
// (like in 2.x) but just return `null`
private void _verifyGetNumberTypeFail(JsonParser p, String token) throws Exception
Expand Down