Skip to content

Commit 0211e36

Browse files
authored
Allow requests to be serialized as nothing instead of an empty object (#926) (#928)
1 parent b9941ae commit 0211e36

File tree

9 files changed

+367
-13
lines changed

9 files changed

+367
-13
lines changed

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -760,5 +760,6 @@ protected static void setupCountRequestDeserializer(ObjectDeserializer<CountRequ
760760
}
761761
return params;
762762

763-
}, SimpleEndpoint.emptyMap(), true, CountResponse._DESERIALIZER);
763+
}, SimpleEndpoint.emptyMap(), SimpleEndpoint.nonEmptyJsonObject(SimpleEndpoint.returnSelf()),
764+
CountResponse._DESERIALIZER);
764765
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -704,7 +704,8 @@ protected static void setupExplainRequestDeserializer(ObjectDeserializer<Explain
704704
}
705705
return params;
706706

707-
}, SimpleEndpoint.emptyMap(), true, ExplainResponse._DESERIALIZER);
707+
}, SimpleEndpoint.emptyMap(), SimpleEndpoint.nonEmptyJsonObject(SimpleEndpoint.returnSelf()),
708+
ExplainResponse._DESERIALIZER);
708709

709710
/**
710711
* Create an "{@code explain}" endpoint.

java-client/src/main/java/co/elastic/clients/elasticsearch/indices/ValidateQueryRequest.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -698,5 +698,6 @@ protected static void setupValidateQueryRequestDeserializer(ObjectDeserializer<V
698698
}
699699
return params;
700700

701-
}, SimpleEndpoint.emptyMap(), true, ValidateQueryResponse._DESERIALIZER);
701+
}, SimpleEndpoint.emptyMap(), SimpleEndpoint.nonEmptyJsonObject(SimpleEndpoint.returnSelf()),
702+
ValidateQueryResponse._DESERIALIZER);
702703
}
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
/*
2+
* Licensed to Elasticsearch B.V. under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch B.V. licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package co.elastic.clients.json;
21+
22+
import jakarta.json.JsonValue;
23+
import jakarta.json.stream.JsonGenerator;
24+
25+
import java.math.BigDecimal;
26+
import java.math.BigInteger;
27+
28+
/**
29+
* A JSON generator that delegates to another generator.
30+
* <p>
31+
* All convenience methods that accept a property name and an event (value, start object, start array) call separately
32+
* {@link #writeKey(String)} and the same method without the key name. This is meant to facilitate overloading
33+
* of methods.
34+
*/
35+
public class DelegatingJsonGenerator implements JsonGenerator {
36+
protected final JsonGenerator generator;
37+
38+
public DelegatingJsonGenerator(JsonGenerator generator) {
39+
this.generator = generator;
40+
}
41+
42+
public JsonGenerator unwrap() {
43+
return generator;
44+
};
45+
46+
@Override
47+
public JsonGenerator writeStartObject() {
48+
generator.writeStartObject();
49+
return this;
50+
}
51+
52+
@Override
53+
public JsonGenerator writeKey(String s) {
54+
generator.writeKey(s);
55+
return this;
56+
}
57+
58+
@Override
59+
public JsonGenerator writeStartArray() {
60+
generator.writeStartArray();
61+
return this;
62+
}
63+
64+
@Override
65+
public JsonGenerator writeEnd() {
66+
generator.writeEnd();
67+
return this;
68+
}
69+
70+
@Override
71+
public JsonGenerator write(JsonValue jsonValue) {
72+
generator.write(jsonValue);
73+
return this;
74+
}
75+
76+
@Override
77+
public JsonGenerator write(String s) {
78+
generator.write(s);
79+
return this;
80+
}
81+
82+
@Override
83+
public JsonGenerator write(BigDecimal bigDecimal) {
84+
generator.write(bigDecimal);
85+
return this;
86+
}
87+
88+
@Override
89+
public JsonGenerator write(BigInteger bigInteger) {
90+
generator.write(bigInteger);
91+
return this;
92+
}
93+
94+
@Override
95+
public JsonGenerator write(int i) {
96+
generator.write(i);
97+
return this;
98+
}
99+
100+
@Override
101+
public JsonGenerator write(long l) {
102+
generator.write(l);
103+
return this;
104+
}
105+
106+
@Override
107+
public JsonGenerator write(double v) {
108+
generator.write(v);
109+
return this;
110+
}
111+
112+
@Override
113+
public JsonGenerator write(boolean b) {
114+
generator.write(b);
115+
return this;
116+
}
117+
118+
@Override
119+
public JsonGenerator writeNull() {
120+
generator.writeNull();
121+
return this;
122+
}
123+
124+
@Override
125+
public void close() {
126+
generator.close();
127+
}
128+
129+
@Override
130+
public void flush() {
131+
generator.flush();
132+
}
133+
134+
//----- Convenience key+value methods
135+
136+
@Override
137+
public final JsonGenerator writeStartObject(String s) {
138+
this.writeKey(s);
139+
return this.writeStartObject();
140+
}
141+
142+
@Override
143+
public final JsonGenerator writeStartArray(String s) {
144+
this.writeKey(s);
145+
return this.writeStartArray();
146+
}
147+
148+
@Override
149+
public final JsonGenerator write(String s, JsonValue jsonValue) {
150+
this.writeKey(s);
151+
return this.write(jsonValue);
152+
}
153+
154+
@Override
155+
public final JsonGenerator write(String s, String s1) {
156+
this.writeKey(s);
157+
return this.write(s1);
158+
}
159+
160+
@Override
161+
public final JsonGenerator write(String s, BigInteger bigInteger) {
162+
this.writeKey(s);
163+
return this.write(bigInteger);
164+
}
165+
166+
@Override
167+
public final JsonGenerator write(String s, BigDecimal bigDecimal) {
168+
this.writeKey(s);
169+
return this.write(bigDecimal);
170+
}
171+
172+
@Override
173+
public final JsonGenerator write(String s, int i) {
174+
this.writeKey(s);
175+
return this.write(i);
176+
}
177+
178+
@Override
179+
public final JsonGenerator write(String s, long l) {
180+
this.writeKey(s);
181+
return this.write(l);
182+
}
183+
184+
@Override
185+
public final JsonGenerator write(String s, double v) {
186+
this.writeKey(s);
187+
return this.write(v);
188+
}
189+
190+
@Override
191+
public final JsonGenerator write(String s, boolean b) {
192+
this.writeKey(s);
193+
return this.write(b);
194+
}
195+
196+
@Override
197+
public final JsonGenerator writeNull(String s) {
198+
this.writeKey(s);
199+
return this.writeNull();
200+
}
201+
}

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

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
import co.elastic.clients.json.BufferingJsonGenerator;
2323
import co.elastic.clients.json.BufferingJsonpMapper;
24+
import co.elastic.clients.json.DelegatingJsonGenerator;
2425
import co.elastic.clients.json.JsonpDeserializer;
2526
import co.elastic.clients.json.JsonpDeserializerBase;
2627
import co.elastic.clients.json.JsonpMapper;
@@ -95,16 +96,23 @@ protected <T> JsonpDeserializer<T> getDefaultDeserializer(Type type) {
9596
@Override
9697
public <T> void serialize(T value, JsonGenerator generator) {
9798

98-
if (!(generator instanceof JacksonJsonpGenerator)) {
99-
throw new IllegalArgumentException("Jackson's ObjectMapper can only be used with the JacksonJsonpProvider");
100-
}
101-
10299
JsonpSerializer<T> serializer = findSerializer(value);
103100
if (serializer != null) {
104101
serializer.serialize(value, generator, this);
105102
return;
106103
}
107104

105+
// Delegating generators are used in higher levels of serialization (e.g. filter empty top-level objects).
106+
// At this point the object is not a JsonpSerializable and we can assume we're in a nested property holding
107+
// a user-provided type and can unwrap to find the underlying non-delegating generator.
108+
while (generator instanceof DelegatingJsonGenerator) {
109+
generator = ((DelegatingJsonGenerator) generator).unwrap();
110+
}
111+
112+
if (!(generator instanceof JacksonJsonpGenerator)) {
113+
throw new IllegalArgumentException("Jackson's ObjectMapper can only be used with the JacksonJsonpProvider");
114+
}
115+
108116
com.fasterxml.jackson.core.JsonGenerator jkGenerator = ((JacksonJsonpGenerator)generator).jacksonGenerator();
109117
try {
110118
objectMapper.writeValue(jkGenerator, value);

java-client/src/main/java/co/elastic/clients/transport/ElasticsearchTransportBase.java

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -257,9 +257,24 @@ private <RequestT, ResponseT, ErrorT> TransportHttpClient.Request prepareTranspo
257257
NoCopyByteArrayOutputStream baos = new NoCopyByteArrayOutputStream();
258258
JsonGenerator generator = mapper.jsonProvider().createGenerator(baos);
259259
mapper.serialize(body, generator);
260-
generator.close();
261-
bodyBuffers = Collections.singletonList(baos.asByteBuffer());
262-
headers = JsonContentTypeHeaders;
260+
261+
// Some generators (e.g. Parsson) throw an exception if we close a generator
262+
// that hasn't received any event. In that case, we ignore the exception
263+
RuntimeException closeException = null;
264+
try {
265+
generator.close();
266+
} catch (RuntimeException e) {
267+
closeException = e;
268+
}
269+
270+
if (baos.size() > 0) {
271+
if (closeException != null) {
272+
// We got some content and close failed
273+
throw closeException;
274+
}
275+
bodyBuffers = Collections.singletonList(baos.asByteBuffer());
276+
headers = JsonContentTypeHeaders;
277+
}
263278
}
264279
}
265280

java-client/src/main/java/co/elastic/clients/transport/endpoints/EndpointBase.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ static <T, U> Function<T, U> returnNull() {
6565
* that the input and output generic parameters are different, making it suitable for use in a wider range of use cases.
6666
*/
6767
@SuppressWarnings("unchecked")
68-
static <T, U> Function<T, U> returnSelf() {
68+
public static <T, U> Function<T, U> returnSelf() {
6969
return (Function<T, U>) RETURN_SELF;
7070
}
7171

java-client/src/main/java/co/elastic/clients/transport/endpoints/SimpleEndpoint.java

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,12 @@
2020
package co.elastic.clients.transport.endpoints;
2121

2222
import co.elastic.clients.elasticsearch._types.ErrorResponse;
23+
import co.elastic.clients.json.DelegatingJsonGenerator;
2324
import co.elastic.clients.json.JsonpDeserializer;
25+
import co.elastic.clients.json.JsonpMapper;
26+
import co.elastic.clients.json.JsonpSerializable;
2427
import co.elastic.clients.transport.JsonEndpoint;
28+
import jakarta.json.stream.JsonGenerator;
2529

2630
import java.util.Map;
2731
import java.util.function.Function;
@@ -52,7 +56,7 @@ public SimpleEndpoint(
5256
Function<RequestT, Map<String, String>> pathParameters,
5357
Function<RequestT, Map<String, String>> queryParameters,
5458
Function<RequestT, Map<String, String>> headers,
55-
boolean hasResponseBody,
59+
boolean hasRequestBody,
5660
JsonpDeserializer<ResponseT> responseParser
5761
) {
5862
this(
@@ -62,7 +66,7 @@ public SimpleEndpoint(
6266
pathParameters,
6367
queryParameters,
6468
headers,
65-
hasResponseBody ? returnSelf() : returnNull(),
69+
hasRequestBody ? returnSelf() : returnNull(),
6670
responseParser
6771
);
6872
}
@@ -86,4 +90,56 @@ public <NewResponseT> SimpleEndpoint<RequestT, NewResponseT> withResponseDeseria
8690
newResponseParser
8791
);
8892
}
93+
94+
/**
95+
* Wraps a function's result with a serializable object that will serialize to nothing if the wrapped
96+
* object's serialization has no property, i.e. it will either produce an empty object or nothing.
97+
*/
98+
public static <T, U extends JsonpSerializable> Function<T, Object> nonEmptyJsonObject(Function<T, U> getter) {
99+
return (x -> x == null ? null : new NonEmptySerializable(getter.apply(x)));
100+
}
101+
102+
private static final class NonEmptySerializable implements JsonpSerializable {
103+
private final Object value;
104+
105+
NonEmptySerializable(Object value) {
106+
this.value = value;
107+
}
108+
109+
@Override
110+
public void serialize(JsonGenerator generator, JsonpMapper mapper) {
111+
// Track the first property to start the top-level object, and end it if needed in close()
112+
JsonGenerator filter = new DelegatingJsonGenerator(generator) {
113+
boolean gotKey = false;
114+
115+
@Override
116+
public JsonGenerator writeStartObject() {
117+
if (gotKey) {
118+
super.writeStartObject();
119+
}
120+
return this;
121+
}
122+
123+
@Override
124+
public JsonGenerator writeKey(String s) {
125+
if (!gotKey) {
126+
gotKey = true;
127+
super.writeStartObject();
128+
}
129+
super.writeKey(s);
130+
return this;
131+
}
132+
133+
@Override
134+
public JsonGenerator writeEnd() {
135+
if (gotKey) {
136+
super.writeEnd();
137+
}
138+
return this;
139+
}
140+
};
141+
142+
mapper.serialize(value, filter);
143+
}
144+
}
89145
}

0 commit comments

Comments
 (0)