Skip to content

Commit 480bf09

Browse files
authored
Add utility method to parse named XContent objects with typed prefix (#24240)
This commit adds a XContentParserUtils.parseTypedKeysObject() method that can be used to parse named XContent objects identified by a field name containing a type identifier, a delimiter and the name of the object to parse.
1 parent 251b6d4 commit 480bf09

File tree

4 files changed

+109
-18
lines changed

4 files changed

+109
-18
lines changed

core/src/main/java/org/elasticsearch/common/xcontent/XContentParserUtils.java

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,10 @@
2020
package org.elasticsearch.common.xcontent;
2121

2222
import org.elasticsearch.common.ParsingException;
23+
import org.elasticsearch.common.Strings;
2324
import org.elasticsearch.common.bytes.BytesArray;
2425
import org.elasticsearch.common.xcontent.XContentParser.Token;
26+
import org.elasticsearch.rest.action.search.RestSearchAction;
2527

2628
import java.io.IOException;
2729
import java.util.Locale;
@@ -107,4 +109,37 @@ public static Object parseStoredFieldsValue(XContentParser parser) throws IOExce
107109
}
108110
return value;
109111
}
112+
113+
/**
114+
* This method expects that the current token is a {@code XContentParser.Token.FIELD_NAME} and
115+
* that the current field name is the concatenation of a type, delimiter and name (ex: terms#foo
116+
* where "terms" refers to the type of a registered {@link NamedXContentRegistry.Entry}, "#" is
117+
* the delimiter and "foo" the name of the object to parse).
118+
*
119+
* The method splits the field's name to extract the type and name and then parses the object
120+
* using the {@link XContentParser#namedObject(Class, String, Object)} method.
121+
*
122+
* @param parser the current {@link XContentParser}
123+
* @param delimiter the delimiter to use to splits the field's name
124+
* @param objectClass the object class of the object to parse
125+
* @param <T> the type of the object to parse
126+
* @return the parsed object
127+
* @throws IOException if anything went wrong during parsing or if the type or name cannot be derived
128+
* from the field's name
129+
*/
130+
public static <T> T parseTypedKeysObject(XContentParser parser, String delimiter, Class<T> objectClass) throws IOException {
131+
ensureExpectedToken(XContentParser.Token.FIELD_NAME, parser.currentToken(), parser::getTokenLocation);
132+
String currentFieldName = parser.currentName();
133+
if (Strings.hasLength(currentFieldName)) {
134+
int position = currentFieldName.indexOf(delimiter);
135+
if (position > 0) {
136+
String type = currentFieldName.substring(0, position);
137+
String name = currentFieldName.substring(position + 1);
138+
return parser.namedObject(objectClass, type, name);
139+
}
140+
}
141+
throw new ParsingException(parser.getTokenLocation(), "Cannot parse object of class [" + objectClass.getSimpleName()
142+
+ "] without type information. Set [" + RestSearchAction.TYPED_KEYS_PARAM + "] parameter on the request to ensure the"
143+
+ " type information is added to the response output");
144+
}
110145
}

core/src/main/java/org/elasticsearch/search/suggest/Suggest.java

Lines changed: 2 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
import org.apache.lucene.util.CollectionUtil;
2222
import org.elasticsearch.common.CheckedFunction;
2323
import org.elasticsearch.common.ParseField;
24-
import org.elasticsearch.common.ParsingException;
2524
import org.elasticsearch.common.io.stream.StreamInput;
2625
import org.elasticsearch.common.io.stream.StreamOutput;
2726
import org.elasticsearch.common.io.stream.Streamable;
@@ -33,6 +32,7 @@
3332
import org.elasticsearch.common.xcontent.XContentBuilder;
3433
import org.elasticsearch.common.xcontent.XContentFactory;
3534
import org.elasticsearch.common.xcontent.XContentParser;
35+
import org.elasticsearch.common.xcontent.XContentParserUtils;
3636
import org.elasticsearch.rest.action.search.RestSearchAction;
3737
import org.elasticsearch.search.aggregations.Aggregation;
3838
import org.elasticsearch.search.suggest.Suggest.Suggestion.Entry;
@@ -386,22 +386,7 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws
386386

387387
@SuppressWarnings("unchecked")
388388
public static Suggestion<? extends Entry<? extends Option>> fromXContent(XContentParser parser) throws IOException {
389-
ensureExpectedToken(XContentParser.Token.FIELD_NAME, parser.currentToken(), parser::getTokenLocation);
390-
String typeAndName = parser.currentName();
391-
// we need to extract the type prefix from the name and throw error if it is not present
392-
int delimiterPos = typeAndName.indexOf(Aggregation.TYPED_KEYS_DELIMITER);
393-
String type;
394-
String name;
395-
if (delimiterPos > 0) {
396-
type = typeAndName.substring(0, delimiterPos);
397-
name = typeAndName.substring(delimiterPos + 1);
398-
} else {
399-
throw new ParsingException(parser.getTokenLocation(),
400-
"Cannot parse suggestion response without type information. Set [" + RestSearchAction.TYPED_KEYS_PARAM
401-
+ "] parameter on the request to ensure the type information is added to the response output");
402-
}
403-
404-
return parser.namedObject(Suggestion.class, type, name);
389+
return XContentParserUtils.parseTypedKeysObject(parser, Aggregation.TYPED_KEYS_DELIMITER, Suggestion.class);
405390
}
406391

407392
protected static <E extends Suggestion.Entry<?>> void parseEntries(XContentParser parser, Suggestion<E> suggestion,

core/src/test/java/org/elasticsearch/common/xcontent/XContentParserUtilsTests.java

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,22 @@
1919

2020
package org.elasticsearch.common.xcontent;
2121

22+
import org.elasticsearch.common.ParseField;
2223
import org.elasticsearch.common.ParsingException;
24+
import org.elasticsearch.common.bytes.BytesReference;
2325
import org.elasticsearch.common.xcontent.json.JsonXContent;
2426
import org.elasticsearch.test.ESTestCase;
2527

2628
import java.io.IOException;
29+
import java.util.ArrayList;
30+
import java.util.List;
2731

32+
import static org.elasticsearch.common.xcontent.XContentHelper.toXContent;
2833
import static org.elasticsearch.common.xcontent.XContentParserUtils.ensureExpectedToken;
34+
import static org.elasticsearch.common.xcontent.XContentParserUtils.parseTypedKeysObject;
2935

3036
public class XContentParserUtilsTests extends ESTestCase {
37+
3138
public void testEnsureExpectedToken() throws IOException {
3239
final XContentParser.Token randomToken = randomFrom(XContentParser.Token.values());
3340
try (XContentParser parser = createParser(JsonXContent.jsonXContent, "{}")) {
@@ -40,4 +47,68 @@ public void testEnsureExpectedToken() throws IOException {
4047
ensureExpectedToken(XContentParser.Token.END_OBJECT, parser.nextToken(), parser::getTokenLocation);
4148
}
4249
}
50+
51+
public void testParseTypedKeysObject() throws IOException {
52+
final String delimiter = randomFrom("#", ":", "/", "-", "_", "|", "_delim_");
53+
final XContentType xContentType = randomFrom(XContentType.values());
54+
55+
List<NamedXContentRegistry.Entry> namedXContents = new ArrayList<>();
56+
namedXContents.add(new NamedXContentRegistry.Entry(Boolean.class, new ParseField("bool"), parser -> {
57+
ensureExpectedToken(XContentParser.Token.VALUE_BOOLEAN, parser.nextToken(), parser::getTokenLocation);
58+
return parser.booleanValue();
59+
}));
60+
namedXContents.add(new NamedXContentRegistry.Entry(Long.class, new ParseField("long"), parser -> {
61+
ensureExpectedToken(XContentParser.Token.VALUE_NUMBER, parser.nextToken(), parser::getTokenLocation);
62+
return parser.longValue();
63+
}));
64+
final NamedXContentRegistry namedXContentRegistry = new NamedXContentRegistry(namedXContents);
65+
66+
BytesReference bytes = toXContent((builder, params) -> builder.field("test", 0), xContentType, randomBoolean());
67+
try (XContentParser parser = xContentType.xContent().createParser(namedXContentRegistry, bytes)) {
68+
parser.nextToken();
69+
ParsingException e = expectThrows(ParsingException.class, () -> parseTypedKeysObject(parser, delimiter, Boolean.class));
70+
assertEquals("Failed to parse object: expecting token of type [FIELD_NAME] but found [START_OBJECT]", e.getMessage());
71+
72+
parser.nextToken();
73+
e = expectThrows(ParsingException.class, () -> parseTypedKeysObject(parser, delimiter, Boolean.class));
74+
assertEquals("Cannot parse object of class [Boolean] without type information. Set [typed_keys] parameter " +
75+
"on the request to ensure the type information is added to the response output", e.getMessage());
76+
}
77+
78+
bytes = toXContent((builder, params) -> builder.field("type" + delimiter + "name", 0), xContentType, randomBoolean());
79+
try (XContentParser parser = xContentType.xContent().createParser(namedXContentRegistry, bytes)) {
80+
ensureExpectedToken(XContentParser.Token.START_OBJECT, parser.nextToken(), parser::getTokenLocation);
81+
ensureExpectedToken(XContentParser.Token.FIELD_NAME, parser.nextToken(), parser::getTokenLocation);
82+
83+
NamedXContentRegistry.UnknownNamedObjectException e = expectThrows(NamedXContentRegistry.UnknownNamedObjectException.class,
84+
() -> parseTypedKeysObject(parser, delimiter, Boolean.class));
85+
assertEquals("Unknown Boolean [type]", e.getMessage());
86+
assertEquals("type", e.getName());
87+
assertEquals("java.lang.Boolean", e.getCategoryClass());
88+
}
89+
90+
final long longValue = randomLong();
91+
final boolean boolValue = randomBoolean();
92+
bytes = toXContent((builder, params) -> {
93+
builder.field("long" + delimiter + "l", longValue);
94+
builder.field("bool" + delimiter + "b", boolValue);
95+
return builder;
96+
}, xContentType, randomBoolean());
97+
98+
try (XContentParser parser = xContentType.xContent().createParser(namedXContentRegistry, bytes)) {
99+
ensureExpectedToken(XContentParser.Token.START_OBJECT, parser.nextToken(), parser::getTokenLocation);
100+
101+
ensureExpectedToken(XContentParser.Token.FIELD_NAME, parser.nextToken(), parser::getTokenLocation);
102+
Long parsedLong = parseTypedKeysObject(parser, delimiter, Long.class);
103+
assertNotNull(parsedLong);
104+
assertEquals(longValue, parsedLong.longValue());
105+
106+
ensureExpectedToken(XContentParser.Token.FIELD_NAME, parser.nextToken(), parser::getTokenLocation);
107+
Boolean parsedBoolean = parseTypedKeysObject(parser, delimiter, Boolean.class);
108+
assertNotNull(parsedBoolean);
109+
assertEquals(boolValue, parsedBoolean);
110+
111+
ensureExpectedToken(XContentParser.Token.END_OBJECT, parser.nextToken(), parser::getTokenLocation);
112+
}
113+
}
43114
}

core/src/test/java/org/elasticsearch/search/suggest/SuggestionTests.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ public void testFromXContentFailsWithoutTypeParam() throws IOException {
133133
ensureExpectedToken(XContentParser.Token.FIELD_NAME, parser.nextToken(), parser::getTokenLocation);
134134
ParsingException e = expectThrows(ParsingException.class, () -> Suggestion.fromXContent(parser));
135135
assertEquals(
136-
"Cannot parse suggestion response without type information. "
136+
"Cannot parse object of class [Suggestion] without type information. "
137137
+ "Set [typed_keys] parameter on the request to ensure the type information "
138138
+ "is added to the response output", e.getMessage());
139139
}

0 commit comments

Comments
 (0)