Skip to content

Commit 1217de0

Browse files
swallezl-trotta
andauthored
Add support for shortcut properties of non-primitive type (#858)
Co-authored-by: Laura Trotta <153528055+l-trotta@users.noreply.github.com>
1 parent bf86580 commit 1217de0

File tree

13 files changed

+298
-81
lines changed

13 files changed

+298
-81
lines changed

java-client/src/main/java/co/elastic/clients/elasticsearch/_types/query_dsl/FunctionScoreQuery.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,8 @@ protected static void setupFunctionScoreQueryDeserializer(ObjectDeserializer<Fun
359359
op.add(Builder::query, Query._DESERIALIZER, "query");
360360
op.add(Builder::scoreMode, FunctionScoreMode._DESERIALIZER, "score_mode");
361361

362+
op.shortcutProperty("functions", true);
363+
362364
}
363365

364366
}

java-client/src/main/java/co/elastic/clients/elasticsearch/_types/query_dsl/FuzzyQuery.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -389,7 +389,7 @@ protected static void setupFuzzyQueryDeserializer(ObjectDeserializer<FuzzyQuery.
389389
op.add(Builder::value, FieldValue._DESERIALIZER, "value");
390390

391391
op.setKey(Builder::field, JsonpDeserializer.stringDeserializer());
392-
op.shortcutProperty("value");
392+
op.shortcutProperty("value", true);
393393

394394
}
395395

java-client/src/main/java/co/elastic/clients/elasticsearch/_types/query_dsl/MatchQuery.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -671,7 +671,7 @@ protected static void setupMatchQueryDeserializer(ObjectDeserializer<MatchQuery.
671671
op.add(Builder::zeroTermsQuery, ZeroTermsQuery._DESERIALIZER, "zero_terms_query");
672672

673673
op.setKey(Builder::field, JsonpDeserializer.stringDeserializer());
674-
op.shortcutProperty("query");
674+
op.shortcutProperty("query", true);
675675

676676
}
677677

java-client/src/main/java/co/elastic/clients/elasticsearch/_types/query_dsl/TermQuery.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -294,7 +294,7 @@ protected static void setupTermQueryDeserializer(ObjectDeserializer<TermQuery.Bu
294294
op.add(Builder::caseInsensitive, JsonpDeserializer.booleanDeserializer(), "case_insensitive");
295295

296296
op.setKey(Builder::field, JsonpDeserializer.stringDeserializer());
297-
op.shortcutProperty("value");
297+
op.shortcutProperty("value", true);
298298

299299
}
300300

java-client/src/main/java/co/elastic/clients/elasticsearch/core/rank_eval/RankEvalQuery.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@ protected static void setupRankEvalQueryDeserializer(ObjectDeserializer<RankEval
187187
op.add(Builder::query, Query._DESERIALIZER, "query");
188188
op.add(Builder::size, JsonpDeserializer.integerDeserializer(), "size");
189189

190-
op.shortcutProperty("query");
190+
op.shortcutProperty("query", true);
191191

192192
}
193193

java-client/src/main/java/co/elastic/clients/elasticsearch/core/search/CompletionContext.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -362,7 +362,7 @@ protected static void setupCompletionContextDeserializer(ObjectDeserializer<Comp
362362
op.add(Builder::precision, GeoHashPrecision._DESERIALIZER, "precision");
363363
op.add(Builder::prefix, JsonpDeserializer.booleanDeserializer(), "prefix");
364364

365-
op.shortcutProperty("context");
365+
op.shortcutProperty("context", true);
366366

367367
}
368368

java-client/src/main/java/co/elastic/clients/json/JsonpUtils.java

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,7 @@ public static Map.Entry<String, JsonParser> lookAheadFieldValue(
253253
throw new JsonpMappingException("Property '" + name + "' not found", location);
254254
}
255255

256-
JsonParser newParser = objectParser(object, mapper);
256+
JsonParser newParser = jsonValueParser(object, mapper);
257257

258258
// Pin location to the start of the look ahead, as the new parser will return locations in its own buffer
259259
newParser = new DelegatingJsonParser(newParser) {
@@ -272,16 +272,60 @@ public String toString() {
272272
}
273273
}
274274

275+
/**
276+
* In union types, find the variant to be used by looking up property names in the JSON stream until we find one that
277+
* uniquely identifies the variant.
278+
*
279+
* @param <Variant> the type of variant descriptors used by the caller.
280+
* @param variants a map of variant descriptors, keyed by the property name that uniquely identifies the variant.
281+
* @return a pair containing the variant descriptor (or {@code null} if not found), and a parser to be used to read the JSON object.
282+
*/
283+
284+
public static <Variant> Map.Entry<Variant, JsonParser> findVariant(
285+
Map<String, Variant> variants, JsonParser parser, JsonpMapper mapper
286+
) {
287+
if (parser instanceof LookAheadJsonParser) {
288+
return ((LookAheadJsonParser) parser).findVariant(variants);
289+
} else {
290+
// If it's an object, find matching field names
291+
Variant variant = null;
292+
JsonValue value = parser.getValue();
293+
294+
if (value instanceof JsonObject) {
295+
for (String field: value.asJsonObject().keySet()) {
296+
variant = variants.get(field);
297+
if (variant != null) {
298+
break;
299+
}
300+
}
301+
}
302+
303+
// Traverse the object we have inspected
304+
parser = JsonpUtils.jsonValueParser(value, mapper);
305+
return new AbstractMap.SimpleImmutableEntry<>(variant, parser);
306+
}
307+
}
308+
275309
/**
276310
* Create a parser that traverses a JSON object
311+
*
312+
* @deprecated use {@link #jsonValueParser(JsonValue, JsonpMapper)}
277313
*/
314+
@Deprecated
278315
public static JsonParser objectParser(JsonObject object, JsonpMapper mapper) {
316+
return jsonValueParser(object, mapper);
317+
}
318+
319+
/**
320+
* Create a parser that traverses a JSON value
321+
*/
322+
public static JsonParser jsonValueParser(JsonValue value, JsonpMapper mapper) {
279323
// FIXME: we should have used createParser(object), but this doesn't work as it creates a
280324
// org.glassfish.json.JsonStructureParser that doesn't implement the JsonP 1.0.1 features, in particular
281325
// parser.getObject(). So deserializing recursive internally-tagged union would fail with UnsupportedOperationException
282326
// While glassfish has this issue or until we write our own, we roundtrip through a string.
283327

284-
String strObject = object.toString();
328+
String strObject = value.toString();
285329
return mapper.jsonProvider().createParser(new StringReader(strObject));
286330
}
287331

java-client/src/main/java/co/elastic/clients/json/ObjectDeserializer.java

Lines changed: 88 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,6 @@ public EnumSet<Event> acceptedEvents() {
106106

107107
//---------------------------------------------------------------------------------------------
108108
private static final EnumSet<Event> EventSetObject = EnumSet.of(Event.START_OBJECT, Event.KEY_NAME);
109-
private static final EnumSet<Event> EventSetObjectAndString = EnumSet.of(Event.START_OBJECT, Event.VALUE_STRING, Event.KEY_NAME);
110109

111110
private EnumSet<Event> acceptedEvents = EventSetObject; // May be changed in `shortcutProperty()`
112111
private final Supplier<ObjectType> constructor;
@@ -115,6 +114,7 @@ public EnumSet<Event> acceptedEvents() {
115114
private String typeProperty;
116115
private String defaultType;
117116
private FieldDeserializer<ObjectType> shortcutProperty;
117+
private boolean shortcutIsObject;
118118
private QuadConsumer<ObjectType, String, JsonParser, JsonpMapper> unknownFieldHandler;
119119

120120
public ObjectDeserializer(Supplier<ObjectType> constructor) {
@@ -133,6 +133,10 @@ public Set<String> fieldNames() {
133133
return this.shortcutProperty == null ? null : this.shortcutProperty.name;
134134
}
135135

136+
public boolean shortcutIsObject() {
137+
return this.shortcutIsObject;
138+
}
139+
136140
@Override
137141
public EnumSet<Event> nativeEvents() {
138142
// May also return string if we have a shortcut property. This is needed to identify ambiguous unions.
@@ -145,33 +149,51 @@ public EnumSet<Event> acceptedEvents() {
145149
}
146150

147151
public ObjectType deserialize(JsonParser parser, JsonpMapper mapper, Event event) {
148-
return deserialize(constructor.get(), parser, mapper, event);
149-
}
150-
151-
public ObjectType deserialize(ObjectType value, JsonParser parser, JsonpMapper mapper, Event event) {
152152
if (event == Event.VALUE_NULL) {
153153
return null;
154154
}
155155

156-
String keyName = null;
157-
String fieldName = null;
156+
ObjectType value = constructor.get();
157+
deserialize(value, parser, mapper, event);
158+
return value;
159+
}
158160

159-
try {
161+
public void deserialize(ObjectType value, JsonParser parser, JsonpMapper mapper, Event event) {
162+
// Note: method is public as it's called by `withJson` to augment an already created object
160163

161-
if (singleKey != null) {
162-
// There's a wrapping property whose name is the key value
163-
if (event == Event.START_OBJECT) {
164-
event = JsonpUtils.expectNextEvent(parser, Event.KEY_NAME);
165-
}
164+
if (singleKey == null) {
165+
// Nominal case
166+
deserializeInner(value, parser, mapper, event);
167+
168+
} else {
169+
// Single key dictionary: there's a wrapping property whose name is the key value
170+
if (event == Event.START_OBJECT) {
171+
event = JsonpUtils.expectNextEvent(parser, Event.KEY_NAME);
172+
}
173+
174+
String keyName = parser.getString();
175+
try {
166176
singleKey.deserialize(parser, mapper, null, value, event);
167177
event = parser.next();
178+
deserializeInner(value, parser, mapper, event);
179+
} catch (Exception e) {
180+
throw JsonpMappingException.from(e, value, keyName, parser);
168181
}
169182

170-
if (shortcutProperty != null && event != Event.START_OBJECT && event != Event.KEY_NAME) {
171-
// This is the shortcut property (should be a value event, this will be checked by its deserializer)
172-
shortcutProperty.deserialize(parser, mapper, shortcutProperty.name, value, event);
183+
JsonpUtils.expectNextEvent(parser, Event.END_OBJECT);
184+
}
185+
}
186+
187+
private void deserializeInner(ObjectType value, JsonParser parser, JsonpMapper mapper, Event event) {
188+
String fieldName = null;
173189

174-
} else if (typeProperty == null) {
190+
try {
191+
if ((parser = deserializeWithShortcut(value, parser, mapper, event)) == null) {
192+
// We found the shortcut form
193+
return;
194+
}
195+
196+
if (typeProperty == null) {
175197
if (event != Event.START_OBJECT && event != Event.KEY_NAME) {
176198
// Report we're waiting for a start_object, since this is the most common beginning for object parser
177199
JsonpUtils.expectEvent(parser, Event.START_OBJECT, event);
@@ -209,16 +231,52 @@ public ObjectType deserialize(ObjectType value, JsonParser parser, JsonpMapper m
209231
fieldDeserializer.deserialize(innerParser, mapper, variant, value);
210232
}
211233
}
234+
} catch (Exception e) {
235+
// Add field name if present
236+
throw JsonpMappingException.from(e, value, fieldName, parser);
237+
}
238+
}
212239

213-
if (singleKey != null) {
214-
JsonpUtils.expectNextEvent(parser, Event.END_OBJECT);
240+
/**
241+
* Try to deserialize the value with its shortcut property, if any.
242+
*
243+
* @return {@code null} if the shortcut form was found, and otherwise a parser that should be used to parse the
244+
* non-shortcut form. It may be different from the orginal parser if look-ahead was needed.
245+
*/
246+
private JsonParser deserializeWithShortcut(ObjectType value, JsonParser parser, JsonpMapper mapper, Event event) {
247+
if (shortcutProperty != null) {
248+
if (!shortcutIsObject) {
249+
if (event != Event.START_OBJECT && event != Event.KEY_NAME) {
250+
// This is the shortcut property (should be a value or array event, this will be checked by its deserializer)
251+
shortcutProperty.deserialize(parser, mapper, shortcutProperty.name, value, event);
252+
return null;
253+
}
254+
} else {
255+
// Fast path: we don't need to look ahead if the current event isn't an object
256+
if (event != Event.START_OBJECT) {
257+
shortcutProperty.deserialize(parser, mapper, shortcutProperty.name, value, event);
258+
return null;
259+
}
260+
261+
// Look ahead: does the shortcut property exist? If yes, the shortcut is used
262+
Map.Entry<Object, JsonParser> shortcut = JsonpUtils.findVariant(
263+
Collections.singletonMap(shortcutProperty.name, Boolean.TRUE /* arbitrary non-null value */),
264+
parser, mapper
265+
);
266+
267+
// Parse the buffered events
268+
parser = shortcut.getValue();
269+
event = parser.next();
270+
271+
// If shortcut property was not found, this is a shortcut. Otherwise, keep deserializing as usual
272+
if (shortcut.getKey() == null) {
273+
shortcutProperty.deserialize(parser, mapper, shortcutProperty.name, value, event);
274+
return null;
275+
}
215276
}
216-
} catch (Exception e) {
217-
// Add key name (for single key dicts) and field name if present
218-
throw JsonpMappingException.from(e, value, fieldName, parser).prepend(value, keyName);
219277
}
220278

221-
return value;
279+
return parser;
222280
}
223281

224282
protected void parseUnknownField(JsonParser parser, JsonpMapper mapper, String fieldName, ObjectType object) {
@@ -249,14 +307,18 @@ public void ignore(String name) {
249307
}
250308

251309
public void shortcutProperty(String name) {
310+
shortcutProperty(name, false);
311+
}
312+
313+
public void shortcutProperty(String name, boolean isObject) {
252314
this.shortcutProperty = this.fieldDeserializers.get(name);
253315
if (this.shortcutProperty == null) {
254316
throw new NoSuchElementException("No deserializer was setup for '" + name + "'");
255317
}
256318

257-
//acceptedEvents = EnumSet.copyOf(acceptedEvents);
258-
//acceptedEvents.addAll(shortcutProperty.acceptedEvents());
259-
acceptedEvents = EventSetObjectAndString;
319+
acceptedEvents = EnumSet.copyOf(acceptedEvents);
320+
acceptedEvents.addAll(shortcutProperty.acceptedEvents());
321+
this.shortcutIsObject = isObject;
260322
}
261323

262324
//----- Object types

java-client/src/main/java/co/elastic/clients/json/UnionDeserializer.java

Lines changed: 5 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
package co.elastic.clients.json;
2121

2222
import co.elastic.clients.util.ObjectBuilder;
23-
import jakarta.json.JsonObject;
2423
import jakarta.json.stream.JsonLocation;
2524
import jakarta.json.stream.JsonParser;
2625
import jakarta.json.stream.JsonParser.Event;
@@ -265,28 +264,12 @@ public Union deserialize(JsonParser parser, JsonpMapper mapper, Event event) {
265264
JsonLocation location = parser.getLocation();
266265

267266
if (member == null && event == Event.START_OBJECT && !objectMembers.isEmpty()) {
268-
if (parser instanceof LookAheadJsonParser) {
269-
Map.Entry<EventHandler<Union, Kind, Member>, JsonParser> memberAndParser =
270-
((LookAheadJsonParser) parser).findVariant(objectMembers);
267+
Map.Entry<EventHandler<Union, Kind, Member>, JsonParser> memberAndParser =
268+
JsonpUtils.findVariant(objectMembers, parser, mapper);
271269

272-
member = memberAndParser.getKey();
273-
// Parse the buffered parser
274-
parser = memberAndParser.getValue();
275-
276-
} else {
277-
// Parse as an object to find matching field names
278-
JsonObject object = parser.getObject();
279-
280-
for (String field: object.keySet()) {
281-
member = objectMembers.get(field);
282-
if (member != null) {
283-
break;
284-
}
285-
}
286-
287-
// Traverse the object we have inspected
288-
parser = JsonpUtils.objectParser(object, mapper);
289-
}
270+
member = memberAndParser.getKey();
271+
// Parse the buffered parser
272+
parser = memberAndParser.getValue();
290273

291274
if (member == null) {
292275
member = fallbackObjectMember;

java-client/src/main/java/co/elastic/clients/json/jackson/JacksonJsonpParser.java

Lines changed: 26 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -372,29 +372,34 @@ public <Variant> Map.Entry<Variant, JsonParser> findVariant(Map<String, Variant>
372372
TokenBuffer tb = new TokenBuffer(parser, null);
373373

374374
try {
375-
// The resulting parser must contain the full object, including START_EVENT
376-
tb.copyCurrentEvent(parser);
377-
while (parser.nextToken() != JsonToken.END_OBJECT) {
378-
379-
expectEvent(JsonToken.FIELD_NAME);
380-
String fieldName = parser.getCurrentName();
381-
382-
Variant variant = variants.get(fieldName);
383-
if (variant != null) {
384-
tb.copyCurrentEvent(parser);
385-
return new AbstractMap.SimpleImmutableEntry<>(
386-
variant,
387-
new JacksonJsonpParser(
388-
JsonParserSequence.createFlattened(false, tb.asParser(), parser),
389-
mapper
390-
)
391-
);
392-
} else {
393-
tb.copyCurrentStructure(parser);
375+
if (parser.currentToken() != JsonToken.START_OBJECT) {
376+
// Primitive value or array
377+
tb.copyCurrentStructure(parser);
378+
} else {
379+
// The resulting parser must contain the full object, including START_EVENT
380+
tb.copyCurrentEvent(parser);
381+
while (parser.nextToken() != JsonToken.END_OBJECT) {
382+
383+
expectEvent(JsonToken.FIELD_NAME);
384+
String fieldName = parser.getCurrentName();
385+
386+
Variant variant = variants.get(fieldName);
387+
if (variant != null) {
388+
tb.copyCurrentEvent(parser);
389+
return new AbstractMap.SimpleImmutableEntry<>(
390+
variant,
391+
new JacksonJsonpParser(
392+
JsonParserSequence.createFlattened(false, tb.asParser(), parser),
393+
mapper
394+
)
395+
);
396+
} else {
397+
tb.copyCurrentStructure(parser);
398+
}
394399
}
400+
// Copy ending END_OBJECT
401+
tb.copyCurrentEvent(parser);
395402
}
396-
// Copy ending END_OBJECT
397-
tb.copyCurrentEvent(parser);
398403
} catch (IOException e) {
399404
throw JacksonUtils.convertException(e);
400405
}

0 commit comments

Comments
 (0)