Skip to content

Commit d92e4fc

Browse files
committed
Add support for maps with enum keys and overflow properties maps
1 parent 81e8de9 commit d92e4fc

File tree

10 files changed

+196
-67
lines changed

10 files changed

+196
-67
lines changed

java-client/src/main/java/co/elastic/clients/base/rest_client/RestClientTransport.java

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
import java.io.ByteArrayOutputStream;
4444
import java.io.IOException;
4545
import java.io.InputStream;
46+
import java.util.Iterator;
4647
import java.util.Map;
4748
import java.util.concurrent.CompletableFuture;
4849
import java.util.function.Function;
@@ -168,8 +169,8 @@ private <RequestT> org.elasticsearch.client.Request prepareLowLevelRequest(
168169
// Request has a body and must implement JsonpSerializable or NdJsonpSerializable
169170
ByteArrayOutputStream baos = new ByteArrayOutputStream();
170171

171-
if (request instanceof NdJsonpSerializable<?>) {
172-
writeNdJson((NdJsonpSerializable<?>) request, baos);
172+
if (request instanceof NdJsonpSerializable) {
173+
writeNdJson((NdJsonpSerializable) request, baos);
173174
} else {
174175
JsonGenerator generator = mapper.jsonProvider().createGenerator(baos);
175176
mapper.serialize(request, generator);
@@ -184,15 +185,15 @@ private <RequestT> org.elasticsearch.client.Request prepareLowLevelRequest(
184185
}
185186

186187
/**
187-
* Write an nd-json value by serializing each of its items on a separate line.
188-
* <p>
189-
* If an item itself implements {@link NdJsonpSerializable}, it is output as nd-json. This allows flattening
190-
* nested structures.
188+
* Write an nd-json value by serializing each of its items on a separate line, recursing if its items themselves implement
189+
* {@link NdJsonpSerializable} to flattening nested structures.
191190
*/
192-
private void writeNdJson(NdJsonpSerializable<?> value, ByteArrayOutputStream baos) {
193-
for (Object item: value) {
194-
if (item instanceof NdJsonpSerializable<?>) {
195-
writeNdJson((NdJsonpSerializable<?>) item, baos);
191+
private void writeNdJson(NdJsonpSerializable value, ByteArrayOutputStream baos) {
192+
Iterator<?> values = value._serializables();
193+
while(values.hasNext()) {
194+
Object item = values.next();
195+
if (item instanceof NdJsonpSerializable && item != value) { // do not recurse on the item itself
196+
writeNdJson((NdJsonpSerializable) item, baos);
196197
} else {
197198
JsonGenerator generator = mapper.jsonProvider().createGenerator(baos);
198199
mapper.serialize(item, generator);

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

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,12 @@ public abstract <FieldType> void add(
4141
* This structure is flattened in the corresponding Java classes, and this method should be used to register
4242
* its setter.
4343
*
44-
* @param keySetter the key setter
44+
* @param setter the key setter
45+
* @param deserializer the key deserializer (from a KEY_NAME event)
4546
*/
46-
public abstract void setKey(
47-
BiConsumer<ObjectType, String> keySetter
48-
);
47+
public <FieldType> void setKey(BiConsumer<ObjectType, FieldType> setter, JsonpDeserializer<FieldType> deserializer) {
48+
throw new UnsupportedOperationException();
49+
}
4950

5051
/**
5152
* Used for internally tagged variants containers to indicate the object's property that defines the variant type

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

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

2222
import co.elastic.clients.util.ObjectBuilder;
23+
import co.elastic.clients.util.StringEnum;
2324
import co.elastic.clients.util.TriFunction;
2425
import jakarta.json.JsonNumber;
2526
import jakarta.json.JsonValue;
@@ -161,7 +162,8 @@ public T deserialize(JsonParser parser, JsonpMapper mapper, Event event) {
161162
//----- Lazily created parser
162163

163164
public static <T> JsonpDeserializer<T> lazy(Supplier<JsonpDeserializer<T>> builder) {
164-
return new LazyDeserializer<>(builder, EnumSet.of(Event.START_OBJECT));
165+
//return new LazyDeserializer<>(builder, EnumSet.of(Event.START_OBJECT));
166+
return new LazyDeserializer<>(builder, EnumSet.allOf(Event.class));
165167
}
166168

167169
public static <T> JsonpDeserializer<T> lazy(Supplier<JsonpDeserializer<T>> builder, EnumSet<Event> acceptedEvents) {
@@ -278,7 +280,9 @@ public static JsonpDeserializer<String> stringDeserializer() {
278280

279281
private static final JsonpDeserializer<String> STRING =
280282
// String parsing is lenient and accepts any other primitive type
281-
new JsonpDeserializer<String>(EnumSet.of(Event.VALUE_STRING, Event.VALUE_NUMBER, Event.VALUE_FALSE, Event.VALUE_TRUE)) {
283+
new JsonpDeserializer<String>(
284+
EnumSet.of(Event.KEY_NAME, Event.VALUE_STRING, Event.VALUE_NUMBER, Event.VALUE_FALSE, Event.VALUE_TRUE)
285+
) {
282286
@Override
283287
public String deserialize(JsonParser parser, JsonpMapper mapper, Event event) {
284288
if (event == Event.VALUE_TRUE) {
@@ -468,4 +472,33 @@ public Map<String, T> deserialize(JsonParser parser, JsonpMapper mapper, Event e
468472
public static <T> JsonpDeserializer<Map<String, T>> stringMapDeserializer(JsonpDeserializer<T> itemDeserializer) {
469473
return new StringMapDeserializer<T>(itemDeserializer);
470474
}
475+
476+
private static class EnumMapDeserializer<K, V> extends JsonpDeserializer<Map<K, V>> {
477+
private final JsonpDeserializer<K> keyDeserializer;
478+
private final JsonpDeserializer<V> valueDeserializer;
479+
480+
protected EnumMapDeserializer(JsonpDeserializer<K> keyDeserializer, JsonpDeserializer<V> valueDeserializer) {
481+
super(EnumSet.of(Event.START_OBJECT));
482+
this.keyDeserializer = keyDeserializer;
483+
this.valueDeserializer = valueDeserializer;
484+
}
485+
486+
@Override
487+
public Map<K, V> deserialize(JsonParser parser, JsonpMapper mapper, Event event) {
488+
Map<K, V> result = new HashMap<>();
489+
while ((event = parser.next()) != Event.END_OBJECT) {
490+
JsonpUtils.expectEvent(parser, Event.KEY_NAME, event);
491+
K key = keyDeserializer.deserialize(parser, mapper, event);
492+
V value = valueDeserializer.deserialize(parser, mapper);
493+
result.put(key, value);
494+
}
495+
return result;
496+
}
497+
}
498+
499+
public static <K extends StringEnum, V> JsonpDeserializer<Map<K, V>> enumMapDeserializer(
500+
JsonpDeserializer<K> keyDeserializer, JsonpDeserializer<V> valueDeserializer
501+
) {
502+
return new EnumMapDeserializer<K, V>(keyDeserializer, valueDeserializer);
503+
}
471504
}

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,15 @@
1919

2020
package co.elastic.clients.json;
2121

22+
import java.util.Iterator;
23+
2224
/**
2325
* Marks a class a being serialized as nd-json (e.g. bulk requests).
26+
* <p>
27+
* If an item returned by the iterator implements {@link NdJsonpSerializable}, it should also be output as nd-json.
28+
* This allows flattening nested structures (e.g. a bulk index operation and its document). However, if this object returns itself
29+
* as part of the iterator values, it then has to be serialized and not flattened again (which would lead to an infinite loop).
2430
*/
25-
public interface NdJsonpSerializable<T> extends Iterable<T> {
31+
public interface NdJsonpSerializable {
32+
Iterator<?> _serializables();
2633
}

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

Lines changed: 53 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,10 @@ public FieldDeserializer(String name, String[] deprecatedNames) {
4444
this.name = name;
4545
this.deprecatedNames = deprecatedNames;
4646
}
47+
4748
public abstract void deserialize(JsonParser parser, JsonpMapper mapper, String fieldName, ObjectType object);
49+
50+
public abstract void deserialize(JsonParser parser, JsonpMapper mapper, String fieldName, ObjectType object, Event event);
4851
}
4952

5053
/** Field deserializer for objects (and boxed primitives) */
@@ -69,26 +72,39 @@ public void deserialize(JsonParser parser, JsonpMapper mapper, String fieldName,
6972
FieldType fieldValue = deserializer.deserialize(parser, mapper);
7073
setter.accept(object, fieldValue);
7174
}
75+
76+
public void deserialize(JsonParser parser, JsonpMapper mapper, String fieldName, ObjectType object, Event event) {
77+
deserializer.ensureAccepts(parser, event);
78+
FieldType fieldValue = deserializer.deserialize(parser, mapper, event);
79+
setter.accept(object, fieldValue);
80+
}
7281
}
7382

7483
private static final FieldDeserializer<?> IGNORED_FIELD = new FieldDeserializer<Object>("-", null) {
84+
7585
@Override
7686
public void deserialize(JsonParser parser, JsonpMapper mapper, String fieldName, Object object) {
7787
JsonpUtils.skipValue(parser);
7888
}
89+
90+
@Override
91+
public void deserialize(JsonParser parser, JsonpMapper mapper, String fieldName, Object object, Event event) {
92+
JsonpUtils.skipValue(parser, event);
93+
}
7994
};
8095

8196
//---------------------------------------------------------------------------------------------
8297

8398
private final Supplier<ObjectType> constructor;
8499
protected final Map<String, FieldDeserializer<ObjectType>> fieldDeserializers;
85-
private BiConsumer<ObjectType, String> keySetter;
100+
private FieldDeserializer<ObjectType> singleKey;
86101
private String typeProperty;
87102
private FieldDeserializer<ObjectType> shortcutProperty;
88103
private QuadConsumer<ObjectType, String, JsonParser, JsonpMapper> unknownFieldHandler;
89104

90105
public ObjectDeserializer(Supplier<ObjectType> constructor) {
91-
super(EnumSet.of(Event.START_OBJECT));
106+
//super(EnumSet.of(Event.START_OBJECT));
107+
super(EnumSet.allOf(Event.class));
92108
this.constructor = constructor;
93109
this.fieldDeserializers = new HashMap<>();
94110
}
@@ -98,51 +114,54 @@ public ObjectType deserialize(JsonParser parser, JsonpMapper mapper, Event event
98114
}
99115

100116
public ObjectType deserialize(ObjectType value, JsonParser parser, JsonpMapper mapper, Event event) {
101-
ensureAccepts(parser, event);
102117
if (event == Event.VALUE_NULL) {
103118
return null;
104119
}
105120

106-
if (event != Event.START_OBJECT && shortcutProperty != null) {
107-
shortcutProperty.deserialize(parser, mapper, shortcutProperty.name, value);
121+
if (singleKey != null) {
122+
// There's a wrapping property whose name is the key value
123+
event = JsonpUtils.expectNextEvent(parser, Event.KEY_NAME);
124+
singleKey.deserialize(parser, mapper, null, value, event);
125+
event = parser.next();
108126
}
109127

110-
if (keySetter != null) {
111-
String key = JsonpUtils.expectKeyName(parser, parser.next());
112-
keySetter.accept(value, key);
113-
JsonpUtils.expectNextEvent(parser, Event.START_OBJECT);
114-
}
128+
if (shortcutProperty != null && event != Event.START_OBJECT) {
129+
// This is the shortcut property (should be a value event, this will be checked by its deserializer)
130+
shortcutProperty.deserialize(parser, mapper, shortcutProperty.name, value, event);
115131

116-
if (typeProperty == null) {
117-
// Regular object: read all properties until we reach the end of the object
118-
while ((event = parser.next()) != Event.END_OBJECT) {
132+
} else {
133+
JsonpUtils.expectEvent(parser, Event.START_OBJECT, event);
134+
if (typeProperty == null) {
135+
// Regular object: read all properties until we reach the end of the object
136+
while ((event = parser.next()) != Event.END_OBJECT) {
137+
138+
JsonpUtils.expectEvent(parser, Event.KEY_NAME, event);
139+
String fieldName = parser.getString();
140+
141+
FieldDeserializer<ObjectType> fieldDeserializer = fieldDeserializers.get(fieldName);
142+
if (fieldDeserializer == null) {
143+
parseUnknownField(parser, mapper, fieldName, value);
144+
} else {
145+
fieldDeserializer.deserialize(parser, mapper, fieldName, value);
146+
}
147+
}
119148

120-
JsonpUtils.expectEvent(parser, Event.KEY_NAME, event);
121-
String fieldName = parser.getString();
149+
} else {
150+
// Union variant: find the property to find the proper deserializer
151+
Map.Entry<String, JsonParser> unionInfo = JsonpUtils.lookAheadFieldValue(typeProperty, parser, mapper);
152+
String variant = unionInfo.getKey();
153+
JsonParser innerParser = unionInfo.getValue();
122154

123-
FieldDeserializer<ObjectType> fieldDeserializer = fieldDeserializers.get(fieldName);
155+
FieldDeserializer<ObjectType> fieldDeserializer = fieldDeserializers.get(variant);
124156
if (fieldDeserializer == null) {
125-
parseUnknownField(parser, mapper, fieldName, value);
157+
parseUnknownField(parser, mapper, variant, value);
126158
} else {
127-
fieldDeserializer.deserialize(parser, mapper, fieldName, value);
159+
fieldDeserializer.deserialize(innerParser, mapper, variant, value);
128160
}
129161
}
130-
131-
} else {
132-
// Union variant: find the property to find the proper deserializer
133-
Map.Entry<String, JsonParser> unionInfo = JsonpUtils.lookAheadFieldValue(typeProperty, parser, mapper);
134-
String variant = unionInfo.getKey();
135-
JsonParser innerParser = unionInfo.getValue();
136-
137-
FieldDeserializer<ObjectType> fieldDeserializer = fieldDeserializers.get(variant);
138-
if (fieldDeserializer == null) {
139-
parseUnknownField(parser, mapper, variant, value);
140-
} else {
141-
fieldDeserializer.deserialize(innerParser, mapper, variant, value);
142-
}
143162
}
144163

145-
if (keySetter != null) {
164+
if (singleKey != null) {
146165
JsonpUtils.expectNextEvent(parser, Event.END_OBJECT);
147166
}
148167

@@ -198,8 +217,8 @@ public <FieldType> void add(
198217
}
199218

200219
@Override
201-
public void setKey(BiConsumer<ObjectType, String> setter) {
202-
this.keySetter = setter;
220+
public <FieldType> void setKey(BiConsumer<ObjectType, FieldType> setter, JsonpDeserializer<FieldType> deserializer) {
221+
this.singleKey = new FieldObjectDeserializer<>(setter, deserializer, null, null);
203222
}
204223

205224
@Override

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

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -71,15 +71,6 @@ public <FieldType> void add(
7171
this.valueDeserializer = valueParser;
7272
}
7373

74-
/**
75-
* Not supported in this implementation
76-
* @throws UnsupportedOperationException not implemented.
77-
*/
78-
@Override
79-
public void setKey(BiConsumer<ObjectType, String> setter) {
80-
throw new UnsupportedOperationException();
81-
}
82-
8374
/**
8475
* Not supported in this implementation
8576
* @throws UnsupportedOperationException not implemented.

java-client/src/main/java/co/elastic/clients/util/StringEnum.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ class Deserializer<T extends Enum<T> & StringEnum> extends JsonpDeserializer<T>
4646
private final Map<String, T> lookupTable;
4747

4848
public Deserializer(T[] values) {
49-
super(EnumSet.of(JsonParser.Event.VALUE_STRING));
49+
super(EnumSet.of(JsonParser.Event.VALUE_STRING, JsonParser.Event.KEY_NAME));
5050

5151
// Use the same size calculation as in java.lang.Enum.enumConstantDirectory
5252
this.lookupTable = new HashMap<>((int)(values.length / 0.75f) + 1);

java-client/src/main/java/co/elastic/clients/util/TaggedUnionUtils.java

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@
1919

2020
package co.elastic.clients.util;
2121

22+
import co.elastic.clients.json.NdJsonpSerializable;
23+
24+
import java.util.Collections;
25+
import java.util.Iterator;
26+
2227
public class TaggedUnionUtils {
2328
public static <V, U extends TaggedUnion<?>> V get(U union, String type) {
2429
if (union._is(type)) {
@@ -29,4 +34,35 @@ public static <V, U extends TaggedUnion<?>> V get(U union, String type) {
2934
throw new IllegalStateException("Cannot get '" + type + "' variant: current variant is '" + union._type() + "'.");
3035
}
3136
}
37+
38+
public static <T> Iterator<?> ndJsonIterator(TaggedUnion<T> union) {
39+
40+
T value = union._get();
41+
42+
if (value instanceof NdJsonpSerializable) {
43+
// Iterate on value's items, replacing value, if it appears, by the union. This allows JSON wrapping
44+
// done by the container to happen.
45+
Iterator<?> valueIterator = ((NdJsonpSerializable) value)._serializables();
46+
47+
return new Iterator<Object>() {
48+
@Override
49+
public boolean hasNext() {
50+
return valueIterator.hasNext();
51+
}
52+
53+
@Override
54+
public Object next() {
55+
Object next = valueIterator.next();
56+
if (next == value) {
57+
return union;
58+
} else {
59+
return next;
60+
}
61+
}
62+
};
63+
} else {
64+
// Nothing to flatten
65+
return Collections.singletonList(union).iterator();
66+
}
67+
}
3268
}

java-client/src/test/java/co/elastic/clients/elasticsearch/end_to_end/RequestTest.java

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
import co.elastic.clients.elasticsearch.core.GetResponse;
3131
import co.elastic.clients.elasticsearch.core.IndexResponse;
3232
import co.elastic.clients.elasticsearch.core.SearchResponse;
33-
import co.elastic.clients.elasticsearch.core.bulk.ResponseItem;
33+
import co.elastic.clients.elasticsearch.core.bulk.OperationType;
3434
import co.elastic.clients.elasticsearch.cat.NodesResponse;
3535
import co.elastic.clients.elasticsearch.indices.CreateIndexResponse;
3636
import co.elastic.clients.elasticsearch.indices.GetIndexResponse;
@@ -208,20 +208,20 @@ public void testBulkRequest() throws IOException {
208208
appData.setMsg("Some message");
209209

210210
BulkResponse bulk = client.bulk(_0 -> _0
211-
.addOperation(_1 -> _1
211+
.addOperations(_1 -> _1
212212
.create(_2 -> _2
213213
.index("foo")
214214
.id("abc")
215+
.document(appData)
215216
)
216217
)
217-
.addDocument(appData)
218218
);
219219

220220
assertFalse(bulk.errors());
221221
assertEquals(1, bulk.items().size());
222-
assertEquals(ResponseItem.CREATE, bulk.items().get(0)._type());
223-
assertEquals("foo", bulk.items().get(0).create().index());
224-
assertEquals(1L, bulk.items().get(0).create().version().longValue());
222+
assertEquals(OperationType.Create, bulk.items().get(0).operationType());
223+
assertEquals("foo", bulk.items().get(0).index());
224+
assertEquals(1L, bulk.items().get(0).version().longValue());
225225
}
226226

227227
@Test

0 commit comments

Comments
 (0)