Skip to content

Commit 626ac5a

Browse files
committed
DotExpandingXContentParser to expose the original token location
With elastic#79922 we have introduced a parser that expands dots in fields names on the fly, so that the expansion no longer needs to be handled by consumers. The token location exposed by such parser can be confusing to interpret: consumers are parsing the expanded version which requires jumping ahead reading tokens and exposing additional field names and start objects, while users have sent the unexpanded version and would like errors to refer to the original content. This commit adds a test for this scenario and tweaks the DotExpandingXContentParser to cache the token location before jumping ahead to expand dots in field names.
1 parent 351a410 commit 626ac5a

File tree

2 files changed

+91
-4
lines changed

2 files changed

+91
-4
lines changed

libs/x-content/src/main/java/org/elasticsearch/xcontent/DotExpandingXContentParser.java

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@
1515
/**
1616
* An XContentParser that reinterprets field names containing dots as an object structure.
1717
*
18-
* A fieldname named {@code "foo.bar.baz":...} will be parsed instead as {@code 'foo':{'bar':{'baz':...}}}
18+
* A field name named {@code "foo.bar.baz":...} will be parsed instead as {@code 'foo':{'bar':{'baz':...}}}.
19+
* The token location is preserved so that error messages refer to the original content being parsed.
1920
*/
2021
public class DotExpandingXContentParser extends FilterXContentParserWrapper {
2122

@@ -59,13 +60,14 @@ private void expandDots() throws IOException {
5960
if (subpaths.length == 1 && field.endsWith(".") == false) {
6061
return;
6162
}
63+
XContentLocation location = delegate().getTokenLocation();
6264
Token token = delegate().nextToken();
6365
if (token == Token.START_OBJECT || token == Token.START_ARRAY) {
64-
parsers.push(new DotExpandingXContentParser(new XContentSubParser(delegate()), delegate(), subpaths));
66+
parsers.push(new DotExpandingXContentParser(new XContentSubParser(delegate()), delegate(), subpaths, location));
6567
} else if (token == Token.END_OBJECT || token == Token.END_ARRAY) {
6668
throw new IllegalStateException("Expecting START_OBJECT or START_ARRAY or VALUE but got [" + token + "]");
6769
} else {
68-
parsers.push(new DotExpandingXContentParser(new SingletonValueXContentParser(delegate()), delegate(), subpaths));
70+
parsers.push(new DotExpandingXContentParser(new SingletonValueXContentParser(delegate()), delegate(), subpaths, location));
6971
}
7072
}
7173

@@ -118,14 +120,16 @@ private enum State {
118120
final String[] subPaths;
119121
final XContentParser subparser;
120122

123+
private XContentLocation currentLocation;
121124
private int expandedTokens = 0;
122125
private int innerLevel = -1;
123126
private State state = State.EXPANDING_START_OBJECT;
124127

125-
private DotExpandingXContentParser(XContentParser subparser, XContentParser root, String[] subPaths) {
128+
private DotExpandingXContentParser(XContentParser subparser, XContentParser root, String[] subPaths, XContentLocation startLocation) {
126129
super(root);
127130
this.subPaths = subPaths;
128131
this.subparser = subparser;
132+
this.currentLocation = startLocation;
129133
}
130134

131135
@Override
@@ -158,13 +162,22 @@ public Token nextToken() throws IOException {
158162
if (token != null) {
159163
return token;
160164
}
165+
currentLocation = getTokenLocation();
161166
state = State.ENDING_EXPANDED_OBJECT;
162167
}
163168
assert expandedTokens % 2 == 1;
164169
expandedTokens -= 2;
165170
return expandedTokens < 0 ? null : Token.END_OBJECT;
166171
}
167172

173+
@Override
174+
public XContentLocation getTokenLocation() {
175+
if (state == State.PARSING_ORIGINAL_CONTENT) {
176+
return super.getTokenLocation();
177+
}
178+
return currentLocation;
179+
}
180+
168181
@Override
169182
public Token currentToken() {
170183
return switch (state) {

libs/x-content/src/test/java/org/elasticsearch/xcontent/DotExpandingXContentParserTests.java

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,4 +166,78 @@ public void testNestedExpansions() throws IOException {
166166
{"first.dot":{"second.dot":"value","third":"value"},"nodots":"value"}\
167167
""");
168168
}
169+
170+
public void test() throws IOException {
171+
String jsonInput = """
172+
{"first.dot":{"second.dot":"value",
173+
"value":null}}\
174+
""";
175+
XContentParser dotExpandedParser = DotExpandingXContentParser.expandDots(createParser(JsonXContent.jsonXContent, jsonInput));
176+
177+
dotExpandedParser.nextToken();
178+
XContentParser.Token token;
179+
while ((token = dotExpandedParser.nextToken()) != null) {
180+
System.out.println(token + " - " + dotExpandedParser.currentName());
181+
}
182+
}
183+
184+
public void testGetTokenLocation() throws IOException {
185+
String jsonInput = """
186+
{"first.dot":{"second.dot":"value",
187+
"value":null}}\
188+
""";
189+
XContentParser expectedParser = createParser(JsonXContent.jsonXContent, jsonInput);
190+
XContentParser dotExpandedParser = DotExpandingXContentParser.expandDots(createParser(JsonXContent.jsonXContent, jsonInput));
191+
192+
assertEquals(expectedParser.getTokenLocation(), dotExpandedParser.getTokenLocation());
193+
assertEquals(XContentParser.Token.START_OBJECT, dotExpandedParser.nextToken());
194+
assertEquals(XContentParser.Token.START_OBJECT, expectedParser.nextToken());
195+
assertEquals(expectedParser.getTokenLocation(), dotExpandedParser.getTokenLocation());
196+
assertEquals(XContentParser.Token.FIELD_NAME, expectedParser.nextToken());
197+
assertEquals(XContentParser.Token.FIELD_NAME, dotExpandedParser.nextToken());
198+
assertEquals("first", dotExpandedParser.currentName());
199+
assertEquals("first.dot", expectedParser.currentName());
200+
assertEquals(expectedParser.getTokenLocation(), dotExpandedParser.getTokenLocation());
201+
assertEquals(XContentParser.Token.START_OBJECT, dotExpandedParser.nextToken());
202+
assertEquals(expectedParser.getTokenLocation(), dotExpandedParser.getTokenLocation());
203+
assertEquals(XContentParser.Token.FIELD_NAME, dotExpandedParser.nextToken());
204+
assertEquals(expectedParser.getTokenLocation(), dotExpandedParser.getTokenLocation());
205+
assertEquals("dot", dotExpandedParser.currentName());
206+
assertEquals(XContentParser.Token.START_OBJECT, expectedParser.nextToken());
207+
assertEquals(XContentParser.Token.START_OBJECT, dotExpandedParser.nextToken());
208+
assertEquals(expectedParser.getTokenLocation(), dotExpandedParser.getTokenLocation());
209+
assertEquals(XContentParser.Token.FIELD_NAME, expectedParser.nextToken());
210+
assertEquals(XContentParser.Token.FIELD_NAME, dotExpandedParser.nextToken());
211+
assertEquals("second", dotExpandedParser.currentName());
212+
assertEquals("second.dot", expectedParser.currentName());
213+
assertEquals(expectedParser.getTokenLocation(), dotExpandedParser.getTokenLocation());
214+
assertEquals(XContentParser.Token.START_OBJECT, dotExpandedParser.nextToken());
215+
assertEquals(expectedParser.getTokenLocation(), dotExpandedParser.getTokenLocation());
216+
assertEquals(XContentParser.Token.FIELD_NAME, dotExpandedParser.nextToken());
217+
assertEquals(expectedParser.getTokenLocation(), dotExpandedParser.getTokenLocation());
218+
assertEquals("dot", dotExpandedParser.currentName());
219+
assertEquals(XContentParser.Token.VALUE_STRING, expectedParser.nextToken());
220+
assertEquals(XContentParser.Token.VALUE_STRING, dotExpandedParser.nextToken());
221+
assertEquals(expectedParser.getTokenLocation(), dotExpandedParser.getTokenLocation());
222+
assertEquals(XContentParser.Token.END_OBJECT, dotExpandedParser.nextToken());
223+
assertEquals(expectedParser.getTokenLocation(), dotExpandedParser.getTokenLocation());
224+
assertEquals(XContentParser.Token.FIELD_NAME, expectedParser.nextToken());
225+
assertEquals(XContentParser.Token.FIELD_NAME, dotExpandedParser.nextToken());
226+
assertEquals("value", dotExpandedParser.currentName());
227+
assertEquals("value", expectedParser.currentName());
228+
assertEquals(expectedParser.getTokenLocation(), dotExpandedParser.getTokenLocation());
229+
assertEquals(XContentParser.Token.VALUE_NULL, expectedParser.nextToken());
230+
assertEquals(XContentParser.Token.VALUE_NULL, dotExpandedParser.nextToken());
231+
assertEquals(expectedParser.getTokenLocation(), dotExpandedParser.getTokenLocation());
232+
assertEquals(XContentParser.Token.END_OBJECT, dotExpandedParser.nextToken());
233+
assertEquals(XContentParser.Token.END_OBJECT, expectedParser.nextToken());
234+
assertEquals(expectedParser.getTokenLocation(), dotExpandedParser.getTokenLocation());
235+
assertEquals(XContentParser.Token.END_OBJECT, dotExpandedParser.nextToken());
236+
assertEquals(expectedParser.getTokenLocation(), dotExpandedParser.getTokenLocation());
237+
assertEquals(XContentParser.Token.END_OBJECT, dotExpandedParser.nextToken());
238+
assertEquals(XContentParser.Token.END_OBJECT, expectedParser.nextToken());
239+
assertEquals(expectedParser.getTokenLocation(), dotExpandedParser.getTokenLocation());
240+
assertNull(dotExpandedParser.nextToken());
241+
assertNull(expectedParser.nextToken());
242+
}
169243
}

0 commit comments

Comments
 (0)