Skip to content

Commit f22adc4

Browse files
authored
Refactor ObjectParser and CompatibleObjectParser to support REST Compatible API (elastic#68808)
In order to support compatible fields when parsing XContent additional information has to be set during ParsedField declaration. This commit adds a set of RestApiCompatibleVersion on a ParsedField in order to specify on which versions a field is supported. By default ParsedField is allowed to be parsed on both current and previous major versions. ObjectParser - which is used for constructing objects using 'setters' - has a modified fieldParsersMap to be Map of Maps. with key being RestApiCompatibility. This allows to choose set of field-parsers as specified on a request. Under RestApiCompatibility.minimumSupported key, there is a map that contains field-parsers for both previous and current versions. Under RestApiCompatibility.current there will be only current versions field (compatible fields not a present) ConstructingObjectParser - which is used for constructing objects using 'constructors' - is modified to contain a map of Version To constructorArgInfo , declarations of fields to be set on a constructor depending on a version relates elastic#51816
1 parent f67185f commit f22adc4

File tree

7 files changed

+313
-42
lines changed

7 files changed

+313
-42
lines changed

libs/core/src/main/java/org/elasticsearch/common/compatibility/RestApiCompatibleVersion.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ public enum RestApiCompatibleVersion {
1919
V_8(8),
2020
V_7(7);
2121

22-
public byte major;
23-
private static RestApiCompatibleVersion CURRENT = V_8;
22+
public final byte major;
23+
private static final RestApiCompatibleVersion CURRENT = V_8;
2424

2525
RestApiCompatibleVersion(int major) {
2626
this.major = (byte) major;

libs/x-content/src/main/java/org/elasticsearch/common/ParseField.java

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,15 @@
77
*/
88
package org.elasticsearch.common;
99

10+
import org.elasticsearch.common.compatibility.RestApiCompatibleVersion;
1011
import org.elasticsearch.common.xcontent.DeprecationHandler;
1112
import org.elasticsearch.common.xcontent.XContentLocation;
1213

14+
import java.util.Arrays;
15+
import java.util.Collection;
1316
import java.util.Collections;
1417
import java.util.HashSet;
18+
import java.util.List;
1519
import java.util.Objects;
1620
import java.util.Set;
1721
import java.util.function.Supplier;
@@ -23,21 +27,15 @@
2327
public class ParseField {
2428
private final String name;
2529
private final String[] deprecatedNames;
30+
private final Set<RestApiCompatibleVersion> restApiCompatibleVersions = new HashSet<>(2);
2631
private String allReplacedWith = null;
2732
private final String[] allNames;
2833
private boolean fullyDeprecated = false;
2934

3035
private static final String[] EMPTY = new String[0];
3136

32-
/**
33-
* @param name
34-
* the primary name for this field. This will be returned by
35-
* {@link #getPreferredName()}
36-
* @param deprecatedNames
37-
* names for this field which are deprecated and will not be
38-
* accepted when strict matching is used.
39-
*/
40-
public ParseField(String name, String... deprecatedNames) {
37+
38+
private ParseField(String name, Collection<RestApiCompatibleVersion> restApiCompatibleVersions, String[] deprecatedNames) {
4139
this.name = name;
4240
if (deprecatedNames == null || deprecatedNames.length == 0) {
4341
this.deprecatedNames = EMPTY;
@@ -46,12 +44,25 @@ public ParseField(String name, String... deprecatedNames) {
4644
Collections.addAll(set, deprecatedNames);
4745
this.deprecatedNames = set.toArray(new String[set.size()]);
4846
}
47+
this.restApiCompatibleVersions.addAll(restApiCompatibleVersions);
48+
4949
Set<String> allNames = new HashSet<>();
5050
allNames.add(name);
5151
Collections.addAll(allNames, this.deprecatedNames);
5252
this.allNames = allNames.toArray(new String[allNames.size()]);
5353
}
5454

55+
/**
56+
* Creates a field available for lookup for both current and previous REST API compatible versions
57+
* @param name the primary name for this field. This will be returned by
58+
* {@link #getPreferredName()}
59+
* @param deprecatedNames names for this field which are deprecated and will not be
60+
* accepted when strict matching is used.
61+
*/
62+
public ParseField(String name, String... deprecatedNames) {
63+
this(name, List.of(RestApiCompatibleVersion.currentVersion(), RestApiCompatibleVersion.minimumSupported()) ,deprecatedNames);
64+
}
65+
5566
/**
5667
* @return the preferred name used for this field
5768
*/
@@ -78,6 +89,22 @@ public ParseField withDeprecation(String... deprecatedNames) {
7889
return new ParseField(this.name, deprecatedNames);
7990
}
8091

92+
93+
/**
94+
* Creates a new field with current name and deprecatedNames, but overrides restApiCompatibleVersions
95+
* @param restApiCompatibleVersions rest api compatibility versions under which specifies when a lookup will be allowed
96+
*/
97+
public ParseField withRestApiCompatibilityVersions(RestApiCompatibleVersion... restApiCompatibleVersions) {
98+
return new ParseField(this.name, Arrays.asList(restApiCompatibleVersions), this.deprecatedNames);
99+
}
100+
101+
/**
102+
* @return rest api compatibility versions under which a lookup will be allowed
103+
*/
104+
public Set<RestApiCompatibleVersion> getRestApiCompatibleVersions() {
105+
return restApiCompatibleVersions;
106+
}
107+
81108
/**
82109
* Return a new ParseField where all field names are deprecated and replaced
83110
* with {@code allReplacedWith}.
@@ -169,6 +196,7 @@ public String[] getDeprecatedNames() {
169196
return deprecatedNames;
170197
}
171198

199+
172200
public static class CommonFields {
173201
public static final ParseField FIELD = new ParseField("field");
174202
public static final ParseField FIELDS = new ParseField("fields");

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

Lines changed: 38 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,21 @@
99
package org.elasticsearch.common.xcontent;
1010

1111
import org.elasticsearch.common.ParseField;
12+
import org.elasticsearch.common.compatibility.RestApiCompatibleVersion;
1213
import org.elasticsearch.common.xcontent.ObjectParser.NamedObjectParser;
1314
import org.elasticsearch.common.xcontent.ObjectParser.ValueType;
1415

1516
import java.io.IOException;
1617
import java.util.ArrayList;
18+
import java.util.Collections;
19+
import java.util.EnumMap;
1720
import java.util.List;
21+
import java.util.Map;
1822
import java.util.function.BiConsumer;
1923
import java.util.function.BiFunction;
2024
import java.util.function.Consumer;
2125
import java.util.function.Function;
26+
import java.util.stream.Collectors;
2227

2328
/**
2429
* Like {@link ObjectParser} but works with objects that have constructors whose arguments are mixed in with its other settings. Queries are
@@ -82,7 +87,8 @@ public final class ConstructingObjectParser<Value, Context> extends AbstractObje
8287
/**
8388
* List of constructor names used for generating the error message if not all arrive.
8489
*/
85-
private final List<ConstructorArgInfo> constructorArgInfos = new ArrayList<>();
90+
private final Map<RestApiCompatibleVersion, List<ConstructorArgInfo>> constructorArgInfos =
91+
new EnumMap<>(RestApiCompatibleVersion.class);
8692
private final ObjectParser<Target, Context> objectParser;
8793
private final BiFunction<Object[], Context, Value> builder;
8894
/**
@@ -205,8 +211,8 @@ public <T> void declareField(BiConsumer<Value, T> consumer, ContextParser<Contex
205211
* constructor in the argument list so we don't need to do any fancy
206212
* or expensive lookups whenever the constructor args come in.
207213
*/
208-
int position = addConstructorArg(consumer, parseField);
209-
objectParser.declareField((target, v) -> target.constructorArg(position, v), parser, parseField, type);
214+
Map<RestApiCompatibleVersion, Integer> positions = addConstructorArg(consumer, parseField);
215+
objectParser.declareField((target, v) -> target.constructorArg(positions, v), parser, parseField, type);
210216
} else {
211217
numberOfFields += 1;
212218
objectParser.declareField(queueingConsumer(consumer, parseField), parser, parseField, type);
@@ -234,8 +240,8 @@ public <T> void declareNamedObject(BiConsumer<Value, T> consumer, NamedObjectPar
234240
* constructor in the argument list so we don't need to do any fancy
235241
* or expensive lookups whenever the constructor args come in.
236242
*/
237-
int position = addConstructorArg(consumer, parseField);
238-
objectParser.declareNamedObject((target, v) -> target.constructorArg(position, v), namedObjectParser, parseField);
243+
Map<RestApiCompatibleVersion, Integer> positions = addConstructorArg(consumer, parseField);
244+
objectParser.declareNamedObject((target, v) -> target.constructorArg(positions, v), namedObjectParser, parseField);
239245
} else {
240246
numberOfFields += 1;
241247
objectParser.declareNamedObject(queueingConsumer(consumer, parseField), namedObjectParser, parseField);
@@ -264,8 +270,8 @@ public <T> void declareNamedObjects(BiConsumer<Value, List<T>> consumer, NamedOb
264270
* constructor in the argument list so we don't need to do any fancy
265271
* or expensive lookups whenever the constructor args come in.
266272
*/
267-
int position = addConstructorArg(consumer, parseField);
268-
objectParser.declareNamedObjects((target, v) -> target.constructorArg(position, v), namedObjectParser, parseField);
273+
Map<RestApiCompatibleVersion, Integer> positions = addConstructorArg(consumer, parseField);
274+
objectParser.declareNamedObjects((target, v) -> target.constructorArg(positions, v), namedObjectParser, parseField);
269275
} else {
270276
numberOfFields += 1;
271277
objectParser.declareNamedObjects(queueingConsumer(consumer, parseField), namedObjectParser, parseField);
@@ -296,18 +302,21 @@ public <T> void declareNamedObjects(BiConsumer<Value, List<T>> consumer, NamedOb
296302
* constructor in the argument list so we don't need to do any fancy
297303
* or expensive lookups whenever the constructor args come in.
298304
*/
299-
int position = addConstructorArg(consumer, parseField);
300-
objectParser.declareNamedObjects((target, v) -> target.constructorArg(position, v), namedObjectParser,
301-
wrapOrderedModeCallBack(orderedModeCallback), parseField);
305+
Map<RestApiCompatibleVersion, Integer> positions = addConstructorArg(consumer, parseField);
306+
objectParser.declareNamedObjects((target, v) -> target.constructorArg(positions, v), namedObjectParser,
307+
wrapOrderedModeCallBack(orderedModeCallback), parseField);
302308
} else {
303309
numberOfFields += 1;
304310
objectParser.declareNamedObjects(queueingConsumer(consumer, parseField), namedObjectParser,
305-
wrapOrderedModeCallBack(orderedModeCallback), parseField);
311+
wrapOrderedModeCallBack(orderedModeCallback), parseField);
306312
}
307313
}
308314

309315
int getNumberOfFields() {
310-
return this.constructorArgInfos.size();
316+
assert this.constructorArgInfos.get(RestApiCompatibleVersion.currentVersion()).size()
317+
== this.constructorArgInfos.get(RestApiCompatibleVersion.minimumSupported()).size() :
318+
"Constructors must have same number of arguments per all compatible versions";
319+
return this.constructorArgInfos.get(RestApiCompatibleVersion.currentVersion()).size();
311320
}
312321

313322
/**
@@ -324,11 +333,17 @@ private boolean isConstructorArg(BiConsumer<?, ?> consumer) {
324333
* @param parseField Parse field
325334
* @return The argument position
326335
*/
327-
private int addConstructorArg(BiConsumer<?, ?> consumer, ParseField parseField) {
328-
int position = constructorArgInfos.size();
336+
private Map<RestApiCompatibleVersion, Integer> addConstructorArg(BiConsumer<?, ?> consumer, ParseField parseField) {
337+
329338
boolean required = consumer == REQUIRED_CONSTRUCTOR_ARG_MARKER;
330-
constructorArgInfos.add(new ConstructorArgInfo(parseField, required));
331-
return position;
339+
for (RestApiCompatibleVersion restApiCompatibleVersion : parseField.getRestApiCompatibleVersions()) {
340+
341+
constructorArgInfos.computeIfAbsent(restApiCompatibleVersion, (v)-> new ArrayList<>())
342+
.add(new ConstructorArgInfo(parseField, required));
343+
}
344+
345+
//calculate the positions for the arguments
346+
return constructorArgInfos.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().size()));
332347
}
333348

334349
@Override
@@ -398,7 +413,7 @@ private class Target {
398413
/**
399414
* Array of constructor args to be passed to the {@link ConstructingObjectParser#builder}.
400415
*/
401-
private final Object[] constructorArgs = new Object[constructorArgInfos.size()];
416+
private final Object[] constructorArgs;
402417
/**
403418
* The parser this class is working against. We store it here so we can fetch it conveniently when queueing fields to lookup the
404419
* location of each field so that we can give a useful error message when replaying the queue.
@@ -437,15 +452,18 @@ private class Target {
437452
Target(XContentParser parser, Context context) {
438453
this.parser = parser;
439454
this.context = context;
455+
this.constructorArgs = new Object[constructorArgInfos
456+
.getOrDefault(parser.getRestApiCompatibleVersion(), Collections.emptyList()).size()];
440457
}
441458

442459
/**
443460
* Set a constructor argument and build the target object if all constructor arguments have arrived.
444461
*/
445-
private void constructorArg(int position, Object value) {
462+
private void constructorArg(Map<RestApiCompatibleVersion, Integer> positions, Object value) {
463+
int position = positions.get(parser.getRestApiCompatibleVersion()) - 1;
446464
constructorArgs[position] = value;
447465
constructorArgsCollected++;
448-
if (constructorArgsCollected == constructorArgInfos.size()) {
466+
if (constructorArgsCollected == constructorArgInfos.get(parser.getRestApiCompatibleVersion()).size()) {
449467
buildTarget();
450468
}
451469
}
@@ -480,7 +498,7 @@ private Value finish() {
480498
StringBuilder message = null;
481499
for (int i = 0; i < constructorArgs.length; i++) {
482500
if (constructorArgs[i] != null) continue;
483-
ConstructorArgInfo arg = constructorArgInfos.get(i);
501+
ConstructorArgInfo arg = constructorArgInfos.get(parser.getRestApiCompatibleVersion()).get(i);
484502
if (false == arg.required) continue;
485503
if (message == null) {
486504
message = new StringBuilder("Required [").append(arg.field);

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

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,13 @@
99

1010
import org.elasticsearch.common.Nullable;
1111
import org.elasticsearch.common.ParseField;
12+
import org.elasticsearch.common.compatibility.RestApiCompatibleVersion;
1213

1314
import java.io.IOException;
1415
import java.lang.reflect.Array;
1516
import java.util.ArrayList;
1617
import java.util.Arrays;
18+
import java.util.Collections;
1719
import java.util.EnumSet;
1820
import java.util.HashMap;
1921
import java.util.HashSet;
@@ -88,7 +90,9 @@ private static <Value, Context> UnknownFieldParser<Value, Context> ignoreUnknown
8890

8991
private static <Value, Context> UnknownFieldParser<Value, Context> errorOnUnknown() {
9092
return (op, f, l, p, v, c) -> {
91-
throw new XContentParseException(l, ErrorOnUnknown.IMPLEMENTATION.errorMessage(op.name, f, op.fieldParserMap.keySet()));
93+
throw new XContentParseException(l, ErrorOnUnknown.IMPLEMENTATION.errorMessage(op.name, f,
94+
op.fieldParserMap.getOrDefault(p.getRestApiCompatibleVersion(), Collections.emptyMap())
95+
.keySet()));
9296
};
9397
}
9498

@@ -137,7 +141,9 @@ private static <Value, Category, Context> UnknownFieldParser<Value, Context> unk
137141
try {
138142
o = parser.namedObject(categoryClass, field, context);
139143
} catch (NamedObjectNotFoundException e) {
140-
Set<String> candidates = new HashSet<>(objectParser.fieldParserMap.keySet());
144+
Set<String> candidates = new HashSet<>(objectParser.fieldParserMap
145+
.getOrDefault(parser.getRestApiCompatibleVersion(), Collections.emptyMap())
146+
.keySet());
141147
e.getCandidates().forEach(candidates::add);
142148
String message = ErrorOnUnknown.IMPLEMENTATION.errorMessage(objectParser.name, field, candidates);
143149
throw new XContentParseException(location, message, e);
@@ -146,7 +152,7 @@ private static <Value, Category, Context> UnknownFieldParser<Value, Context> unk
146152
};
147153
}
148154

149-
private final Map<String, FieldParser> fieldParserMap = new HashMap<>();
155+
private final Map<RestApiCompatibleVersion, Map<String, FieldParser>> fieldParserMap = new HashMap<>();
150156
private final String name;
151157
private final Function<Context, Value> valueBuilder;
152158
private final UnknownFieldParser<Value, Context> unknownFieldParser;
@@ -277,7 +283,8 @@ public Value parse(XContentParser parser, Value value, Context context) throws I
277283
if (token == XContentParser.Token.FIELD_NAME) {
278284
currentFieldName = parser.currentName();
279285
currentPosition = parser.getTokenLocation();
280-
fieldParser = fieldParserMap.get(currentFieldName);
286+
fieldParser = fieldParserMap.getOrDefault(parser.getRestApiCompatibleVersion(), Collections.emptyMap())
287+
.get(currentFieldName);
281288
} else {
282289
if (currentFieldName == null) {
283290
throw new XContentParseException(parser.getTokenLocation(), "[" + name + "] no field found");
@@ -359,8 +366,16 @@ public void declareField(Parser<Value, Context> p, ParseField parseField, ValueT
359366
}
360367
FieldParser fieldParser = new FieldParser(p, type.supportedTokens(), parseField, type);
361368
for (String fieldValue : parseField.getAllNamesIncludedDeprecated()) {
362-
fieldParserMap.putIfAbsent(fieldValue, fieldParser);
369+
if (parseField.getRestApiCompatibleVersions().contains(RestApiCompatibleVersion.minimumSupported())) {
370+
fieldParserMap.putIfAbsent(RestApiCompatibleVersion.minimumSupported(), new HashMap<>());
371+
fieldParserMap.get(RestApiCompatibleVersion.minimumSupported()).putIfAbsent(fieldValue, fieldParser);
372+
}
373+
if (parseField.getRestApiCompatibleVersions().contains(RestApiCompatibleVersion.currentVersion())) {
374+
fieldParserMap.putIfAbsent(RestApiCompatibleVersion.currentVersion(), new HashMap<>());
375+
fieldParserMap.get(RestApiCompatibleVersion.currentVersion()).putIfAbsent(fieldValue, fieldParser);
376+
}
363377
}
378+
364379
}
365380

366381
@Override
@@ -657,7 +672,7 @@ public EnumSet<XContentParser.Token> supportedTokens() {
657672
public String toString() {
658673
return "ObjectParser{" +
659674
"name='" + name + '\'' +
660-
", fields=" + fieldParserMap.values() +
675+
", fields=" + fieldParserMap +
661676
'}';
662677
}
663678
}

0 commit comments

Comments
 (0)