Skip to content

Commit f690a21

Browse files
authored
Add XContentParserTsidFunnel (#133457)
1 parent 6086437 commit f690a21

File tree

2 files changed

+202
-0
lines changed

2 files changed

+202
-0
lines changed
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
package org.elasticsearch.cluster.routing;
11+
12+
import org.elasticsearch.common.ParsingException;
13+
import org.elasticsearch.core.Nullable;
14+
import org.elasticsearch.xcontent.XContentParser;
15+
import org.elasticsearch.xcontent.XContentParserConfiguration;
16+
17+
import java.io.IOException;
18+
import java.util.Set;
19+
20+
import static org.elasticsearch.common.xcontent.XContentParserUtils.ensureExpectedToken;
21+
import static org.elasticsearch.common.xcontent.XContentParserUtils.expectValueToken;
22+
23+
/**
24+
* A funnel that extracts dimensions from an {@link XContentParser} and adds them to a {@link TsidBuilder}.
25+
*/
26+
class XContentParserTsidFunnel implements TsidBuilder.ThrowingTsidFunnel<XContentParser, IOException> {
27+
28+
private static final XContentParserTsidFunnel INSTANCE = new XContentParserTsidFunnel();
29+
30+
static XContentParserTsidFunnel get() {
31+
return INSTANCE;
32+
}
33+
34+
/**
35+
* Adds dimensions extracted from the provided {@link XContentParser} to the given {@link TsidBuilder}.
36+
* To only extract dimensions, the parser should be configured via
37+
* {@link XContentParserConfiguration#withFiltering(String, Set, Set, boolean)}.
38+
*
39+
* @param parser the parser from which to read the JSON content
40+
* @param tsidBuilder the builder to which dimensions will be added
41+
* @throws IOException if an error occurs while reading from the parser
42+
*/
43+
@Override
44+
public void add(XContentParser parser, TsidBuilder tsidBuilder) throws IOException {
45+
ensureExpectedToken(null, parser.currentToken(), parser);
46+
if (parser.nextToken() != XContentParser.Token.START_OBJECT) {
47+
throw new IllegalArgumentException("Error extracting tsid: source didn't contain any dimension fields");
48+
}
49+
ensureExpectedToken(XContentParser.Token.FIELD_NAME, parser.nextToken(), parser);
50+
extractObject(tsidBuilder, null, parser);
51+
ensureExpectedToken(null, parser.nextToken(), parser);
52+
}
53+
54+
private void extractObject(TsidBuilder tsidBuilder, @Nullable String path, XContentParser source) throws IOException {
55+
while (source.currentToken() != XContentParser.Token.END_OBJECT) {
56+
ensureExpectedToken(XContentParser.Token.FIELD_NAME, source.currentToken(), source);
57+
String fieldName = source.currentName();
58+
String subPath = path == null ? fieldName : path + "." + fieldName;
59+
source.nextToken();
60+
extractItem(tsidBuilder, subPath, source);
61+
}
62+
}
63+
64+
private void extractArray(TsidBuilder tsidBuilder, @Nullable String path, XContentParser source) throws IOException {
65+
while (source.currentToken() != XContentParser.Token.END_ARRAY) {
66+
expectValueToken(source.currentToken(), source);
67+
extractItem(tsidBuilder, path, source);
68+
}
69+
}
70+
71+
private void extractItem(TsidBuilder tsidBuilder, String path, XContentParser source) throws IOException {
72+
switch (source.currentToken()) {
73+
case START_OBJECT:
74+
source.nextToken();
75+
extractObject(tsidBuilder, path, source);
76+
source.nextToken();
77+
break;
78+
case VALUE_NUMBER:
79+
switch (source.numberType()) {
80+
case INT -> tsidBuilder.addIntDimension(path, source.intValue());
81+
case LONG -> tsidBuilder.addLongDimension(path, source.longValue());
82+
case FLOAT -> tsidBuilder.addDoubleDimension(path, source.floatValue());
83+
case DOUBLE -> tsidBuilder.addDoubleDimension(path, source.doubleValue());
84+
case BIG_DECIMAL, BIG_INTEGER -> tsidBuilder.addStringDimension(path, source.optimizedText().bytes());
85+
}
86+
source.nextToken();
87+
break;
88+
case VALUE_BOOLEAN:
89+
tsidBuilder.addBooleanDimension(path, source.booleanValue());
90+
source.nextToken();
91+
break;
92+
case VALUE_STRING:
93+
tsidBuilder.addStringDimension(path, source.optimizedText().bytes());
94+
source.nextToken();
95+
break;
96+
case START_ARRAY:
97+
source.nextToken();
98+
extractArray(tsidBuilder, path, source);
99+
source.nextToken();
100+
break;
101+
case VALUE_NULL:
102+
source.nextToken();
103+
break;
104+
default:
105+
throw new ParsingException(
106+
source.getTokenLocation(),
107+
"Cannot extract dimension due to unexpected token [{}]",
108+
source.currentToken()
109+
);
110+
}
111+
}
112+
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
package org.elasticsearch.cluster.routing;
11+
12+
import org.elasticsearch.test.ESTestCase;
13+
import org.elasticsearch.xcontent.XContentParserConfiguration;
14+
import org.elasticsearch.xcontent.json.JsonXContent;
15+
16+
import java.io.IOException;
17+
import java.util.Set;
18+
19+
import static org.hamcrest.Matchers.equalTo;
20+
21+
public class XContentParserTsidFunnelTests extends ESTestCase {
22+
23+
public void testTsidFunnel() throws IOException {
24+
TsidBuilder xContentTsidBuilder = new TsidBuilder();
25+
xContentTsidBuilder.add(createParser(JsonXContent.jsonXContent, """
26+
{
27+
"string": "value",
28+
"int": 42,
29+
"long": 1234567890123,
30+
"double": 3.14159,
31+
"boolean": true,
32+
"null_value": null,
33+
"object": {
34+
"nested_string": "nested_value"
35+
},
36+
"array": ["elem1", "elem2", 3, 4.5, false]
37+
}
38+
"""), XContentParserTsidFunnel.get());
39+
TsidBuilder manualTsidBuilder = TsidBuilder.newBuilder()
40+
.addStringDimension("string", "value")
41+
.addIntDimension("int", 42)
42+
.addLongDimension("long", 1234567890123L)
43+
.addDoubleDimension("double", 3.14159)
44+
.addBooleanDimension("boolean", true)
45+
.addStringDimension("object.nested_string", "nested_value")
46+
.addStringDimension("array", "elem1")
47+
.addStringDimension("array", "elem2")
48+
.addIntDimension("array", 3)
49+
.addDoubleDimension("array", 4.5)
50+
.addBooleanDimension("array", false);
51+
assertThat(xContentTsidBuilder.hash(), equalTo(manualTsidBuilder.hash()));
52+
assertThat(xContentTsidBuilder.buildTsid(), equalTo(manualTsidBuilder.buildTsid()));
53+
}
54+
55+
public void testFilteredTsidFunnel() throws IOException {
56+
TsidBuilder xContentTsidBuilder = new TsidBuilder();
57+
xContentTsidBuilder.add(JsonXContent.jsonXContent.createParser(getFilteredConfig(Set.of("attributes.*")), """
58+
{
59+
"attributes": {
60+
"string": "value",
61+
"int": 42,
62+
"long": 1234567890123
63+
},
64+
"other_field": "should_not_be_included"
65+
}
66+
"""), XContentParserTsidFunnel.get());
67+
TsidBuilder manualTsidBuilder = TsidBuilder.newBuilder()
68+
.addStringDimension("attributes.string", "value")
69+
.addIntDimension("attributes.int", 42)
70+
.addLongDimension("attributes.long", 1234567890123L);
71+
assertThat(xContentTsidBuilder.hash(), equalTo(manualTsidBuilder.hash()));
72+
assertThat(xContentTsidBuilder.buildTsid(), equalTo(manualTsidBuilder.buildTsid()));
73+
}
74+
75+
public void testNoMatchingDimensions() {
76+
IllegalArgumentException e = expectThrows(
77+
IllegalArgumentException.class,
78+
() -> new TsidBuilder().add(JsonXContent.jsonXContent.createParser(getFilteredConfig(Set.of("attributes.*")), """
79+
{
80+
"other_field": "should_not_be_included"
81+
}
82+
"""), XContentParserTsidFunnel.get())
83+
);
84+
assertThat(e.getMessage(), equalTo("Error extracting tsid: source didn't contain any dimension fields"));
85+
}
86+
87+
private static XContentParserConfiguration getFilteredConfig(Set<String> includePaths) {
88+
return XContentParserConfiguration.EMPTY.withFiltering(null, includePaths, null, true);
89+
}
90+
}

0 commit comments

Comments
 (0)