diff --git a/catalog-info.yaml b/catalog-info.yaml index 562e4cfdf..40aea412c 100644 --- a/catalog-info.yaml +++ b/catalog-info.yaml @@ -26,3 +26,33 @@ spec: devtools-team: {} everyone: access_level: READ_ONLY + +--- +# yaml-language-server: $schema=https://gist.githubusercontent.com/elasticmachine/988b80dae436cafea07d9a4a460a011d/raw/rre.schema.json +apiVersion: backstage.io/v1alpha1 +kind: Resource +metadata: + name: buildkite-pipeline-elasticsearch-java-release + description: Buildkite release pipeline for elasticsearch-java + links: + - title: Pipeline + url: https://buildkite.com/elastic/elasticsearch-java-release + +spec: + type: buildkite-pipeline + owner: group:devtools-team + system: buildkite + implementation: + apiVersion: buildkite.elastic.dev/v1 + kind: Pipeline + metadata: + description: Elasticsearch Java Client + name: elasticsearch-java-release + spec: + repository: elastic/elasticsearch-java + pipeline_file: ".buildkite/release.yml" + teams: + devtools-team: + access_level: MANAGE_BUILD_AND_READ + everyone: + access_level: READ_ONLY diff --git a/examples/esql-article/src/main/java/co/elastic/clients/esql/article/EsqlArticle.java b/examples/esql-article/src/main/java/co/elastic/clients/esql/article/EsqlArticle.java index f26e558fb..e73af8fac 100644 --- a/examples/esql-article/src/main/java/co/elastic/clients/esql/article/EsqlArticle.java +++ b/examples/esql-article/src/main/java/co/elastic/clients/esql/article/EsqlArticle.java @@ -38,7 +38,6 @@ import org.apache.http.message.BasicHeader; import org.elasticsearch.client.RestClient; -import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; @@ -46,20 +45,17 @@ import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; -import java.nio.file.Path; -import java.nio.file.Paths; import java.sql.ResultSet; import java.sql.SQLException; import java.time.Duration; import java.time.Instant; import java.util.List; -import java.util.Properties; import java.util.stream.Collectors; public class EsqlArticle { public static void main(String[] args) throws IOException, SQLException, InterruptedException { - + String serverUrl = System.getenv("server-url"); String apiKey = System.getenv("api-key"); String booksUrl = "https://raw.githubusercontent" + diff --git a/java-client/src/main/java/co/elastic/clients/elasticsearch/core/CountRequest.java b/java-client/src/main/java/co/elastic/clients/elasticsearch/core/CountRequest.java index 7fe4999b6..82cd9a5dd 100644 --- a/java-client/src/main/java/co/elastic/clients/elasticsearch/core/CountRequest.java +++ b/java-client/src/main/java/co/elastic/clients/elasticsearch/core/CountRequest.java @@ -760,5 +760,6 @@ protected static void setupCountRequestDeserializer(ObjectDeserializer + * All convenience methods that accept a property name and an event (value, start object, start array) call separately + * {@link #writeKey(String)} and the same method without the key name. This is meant to facilitate overloading + * of methods. + */ +public class DelegatingJsonGenerator implements JsonGenerator { + protected final JsonGenerator generator; + + public DelegatingJsonGenerator(JsonGenerator generator) { + this.generator = generator; + } + + public JsonGenerator unwrap() { + return generator; + }; + + @Override + public JsonGenerator writeStartObject() { + generator.writeStartObject(); + return this; + } + + @Override + public JsonGenerator writeKey(String s) { + generator.writeKey(s); + return this; + } + + @Override + public JsonGenerator writeStartArray() { + generator.writeStartArray(); + return this; + } + + @Override + public JsonGenerator writeEnd() { + generator.writeEnd(); + return this; + } + + @Override + public JsonGenerator write(JsonValue jsonValue) { + generator.write(jsonValue); + return this; + } + + @Override + public JsonGenerator write(String s) { + generator.write(s); + return this; + } + + @Override + public JsonGenerator write(BigDecimal bigDecimal) { + generator.write(bigDecimal); + return this; + } + + @Override + public JsonGenerator write(BigInteger bigInteger) { + generator.write(bigInteger); + return this; + } + + @Override + public JsonGenerator write(int i) { + generator.write(i); + return this; + } + + @Override + public JsonGenerator write(long l) { + generator.write(l); + return this; + } + + @Override + public JsonGenerator write(double v) { + generator.write(v); + return this; + } + + @Override + public JsonGenerator write(boolean b) { + generator.write(b); + return this; + } + + @Override + public JsonGenerator writeNull() { + generator.writeNull(); + return this; + } + + @Override + public void close() { + generator.close(); + } + + @Override + public void flush() { + generator.flush(); + } + + //----- Convenience key+value methods + + @Override + public final JsonGenerator writeStartObject(String s) { + this.writeKey(s); + return this.writeStartObject(); + } + + @Override + public final JsonGenerator writeStartArray(String s) { + this.writeKey(s); + return this.writeStartArray(); + } + + @Override + public final JsonGenerator write(String s, JsonValue jsonValue) { + this.writeKey(s); + return this.write(jsonValue); + } + + @Override + public final JsonGenerator write(String s, String s1) { + this.writeKey(s); + return this.write(s1); + } + + @Override + public final JsonGenerator write(String s, BigInteger bigInteger) { + this.writeKey(s); + return this.write(bigInteger); + } + + @Override + public final JsonGenerator write(String s, BigDecimal bigDecimal) { + this.writeKey(s); + return this.write(bigDecimal); + } + + @Override + public final JsonGenerator write(String s, int i) { + this.writeKey(s); + return this.write(i); + } + + @Override + public final JsonGenerator write(String s, long l) { + this.writeKey(s); + return this.write(l); + } + + @Override + public final JsonGenerator write(String s, double v) { + this.writeKey(s); + return this.write(v); + } + + @Override + public final JsonGenerator write(String s, boolean b) { + this.writeKey(s); + return this.write(b); + } + + @Override + public final JsonGenerator writeNull(String s) { + this.writeKey(s); + return this.writeNull(); + } +} diff --git a/java-client/src/main/java/co/elastic/clients/json/jackson/JacksonJsonpMapper.java b/java-client/src/main/java/co/elastic/clients/json/jackson/JacksonJsonpMapper.java index 84841afee..441b5990c 100644 --- a/java-client/src/main/java/co/elastic/clients/json/jackson/JacksonJsonpMapper.java +++ b/java-client/src/main/java/co/elastic/clients/json/jackson/JacksonJsonpMapper.java @@ -21,6 +21,7 @@ import co.elastic.clients.json.BufferingJsonGenerator; import co.elastic.clients.json.BufferingJsonpMapper; +import co.elastic.clients.json.DelegatingJsonGenerator; import co.elastic.clients.json.JsonpDeserializer; import co.elastic.clients.json.JsonpDeserializerBase; import co.elastic.clients.json.JsonpMapper; @@ -95,16 +96,23 @@ protected JsonpDeserializer getDefaultDeserializer(Type type) { @Override public void serialize(T value, JsonGenerator generator) { - if (!(generator instanceof JacksonJsonpGenerator)) { - throw new IllegalArgumentException("Jackson's ObjectMapper can only be used with the JacksonJsonpProvider"); - } - JsonpSerializer serializer = findSerializer(value); if (serializer != null) { serializer.serialize(value, generator, this); return; } + // Delegating generators are used in higher levels of serialization (e.g. filter empty top-level objects). + // At this point the object is not a JsonpSerializable and we can assume we're in a nested property holding + // a user-provided type and can unwrap to find the underlying non-delegating generator. + while (generator instanceof DelegatingJsonGenerator) { + generator = ((DelegatingJsonGenerator) generator).unwrap(); + } + + if (!(generator instanceof JacksonJsonpGenerator)) { + throw new IllegalArgumentException("Jackson's ObjectMapper can only be used with the JacksonJsonpProvider"); + } + com.fasterxml.jackson.core.JsonGenerator jkGenerator = ((JacksonJsonpGenerator)generator).jacksonGenerator(); try { objectMapper.writeValue(jkGenerator, value); diff --git a/java-client/src/main/java/co/elastic/clients/transport/ElasticsearchTransportBase.java b/java-client/src/main/java/co/elastic/clients/transport/ElasticsearchTransportBase.java index 2fcc42089..26237501e 100644 --- a/java-client/src/main/java/co/elastic/clients/transport/ElasticsearchTransportBase.java +++ b/java-client/src/main/java/co/elastic/clients/transport/ElasticsearchTransportBase.java @@ -259,9 +259,24 @@ private TransportHttpClient.Request prepareTranspo NoCopyByteArrayOutputStream baos = new NoCopyByteArrayOutputStream(); JsonGenerator generator = mapper.jsonProvider().createGenerator(baos); mapper.serialize(body, generator); - generator.close(); - bodyBuffers = Collections.singletonList(baos.asByteBuffer()); - headers = JsonContentTypeHeaders; + + // Some generators (e.g. Parsson) throw an exception if we close a generator + // that hasn't received any event. In that case, we ignore the exception + RuntimeException closeException = null; + try { + generator.close(); + } catch (RuntimeException e) { + closeException = e; + } + + if (baos.size() > 0) { + if (closeException != null) { + // We got some content and close failed + throw closeException; + } + bodyBuffers = Collections.singletonList(baos.asByteBuffer()); + headers = JsonContentTypeHeaders; + } } } diff --git a/java-client/src/main/java/co/elastic/clients/transport/endpoints/EndpointBase.java b/java-client/src/main/java/co/elastic/clients/transport/endpoints/EndpointBase.java index 460370b7c..c0bbc4f7f 100644 --- a/java-client/src/main/java/co/elastic/clients/transport/endpoints/EndpointBase.java +++ b/java-client/src/main/java/co/elastic/clients/transport/endpoints/EndpointBase.java @@ -65,7 +65,7 @@ static Function returnNull() { * that the input and output generic parameters are different, making it suitable for use in a wider range of use cases. */ @SuppressWarnings("unchecked") - static Function returnSelf() { + public static Function returnSelf() { return (Function) RETURN_SELF; } diff --git a/java-client/src/main/java/co/elastic/clients/transport/endpoints/SimpleEndpoint.java b/java-client/src/main/java/co/elastic/clients/transport/endpoints/SimpleEndpoint.java index df07d0c7d..4819b5de1 100644 --- a/java-client/src/main/java/co/elastic/clients/transport/endpoints/SimpleEndpoint.java +++ b/java-client/src/main/java/co/elastic/clients/transport/endpoints/SimpleEndpoint.java @@ -20,8 +20,12 @@ package co.elastic.clients.transport.endpoints; import co.elastic.clients.elasticsearch._types.ErrorResponse; +import co.elastic.clients.json.DelegatingJsonGenerator; import co.elastic.clients.json.JsonpDeserializer; +import co.elastic.clients.json.JsonpMapper; +import co.elastic.clients.json.JsonpSerializable; import co.elastic.clients.transport.JsonEndpoint; +import jakarta.json.stream.JsonGenerator; import java.util.Map; import java.util.function.Function; @@ -52,7 +56,7 @@ public SimpleEndpoint( Function> pathParameters, Function> queryParameters, Function> headers, - boolean hasResponseBody, + boolean hasRequestBody, JsonpDeserializer responseParser ) { this( @@ -62,7 +66,7 @@ public SimpleEndpoint( pathParameters, queryParameters, headers, - hasResponseBody ? returnSelf() : returnNull(), + hasRequestBody ? returnSelf() : returnNull(), responseParser ); } @@ -86,4 +90,56 @@ public SimpleEndpoint withResponseDeseria newResponseParser ); } + + /** + * Wraps a function's result with a serializable object that will serialize to nothing if the wrapped + * object's serialization has no property, i.e. it will either produce an empty object or nothing. + */ + public static Function nonEmptyJsonObject(Function getter) { + return (x -> x == null ? null : new NonEmptySerializable(getter.apply(x))); + } + + private static final class NonEmptySerializable implements JsonpSerializable { + private final Object value; + + NonEmptySerializable(Object value) { + this.value = value; + } + + @Override + public void serialize(JsonGenerator generator, JsonpMapper mapper) { + // Track the first property to start the top-level object, and end it if needed in close() + JsonGenerator filter = new DelegatingJsonGenerator(generator) { + boolean gotKey = false; + + @Override + public JsonGenerator writeStartObject() { + if (gotKey) { + super.writeStartObject(); + } + return this; + } + + @Override + public JsonGenerator writeKey(String s) { + if (!gotKey) { + gotKey = true; + super.writeStartObject(); + } + super.writeKey(s); + return this; + } + + @Override + public JsonGenerator writeEnd() { + if (gotKey) { + super.writeEnd(); + } + return this; + } + }; + + mapper.serialize(value, filter); + } + } } diff --git a/java-client/src/test/java/co/elastic/clients/transport/endpoints/SimpleEndpointTest.java b/java-client/src/test/java/co/elastic/clients/transport/endpoints/SimpleEndpointTest.java new file mode 100644 index 000000000..263e94ac8 --- /dev/null +++ b/java-client/src/test/java/co/elastic/clients/transport/endpoints/SimpleEndpointTest.java @@ -0,0 +1,71 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package co.elastic.clients.transport.endpoints; + +import co.elastic.clients.elasticsearch.ElasticsearchClient; +import co.elastic.clients.elasticsearch.core.CountResponse; +import co.elastic.clients.testkit.MockHttpClient; +import co.elastic.clients.testkit.ModelTestCase; +import co.elastic.clients.transport.ElasticsearchTransport; +import co.elastic.clients.transport.ElasticsearchTransportBase; +import co.elastic.clients.transport.TransportOptions; +import co.elastic.clients.transport.http.TransportHttpClient; +import org.jetbrains.annotations.Nullable; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +public class SimpleEndpointTest extends ModelTestCase { + + @Test + public void testNoBodyForEmptyObject() throws Exception { + + List requests = new ArrayList<>(); + + MockHttpClient httpClient = new MockHttpClient() { + @Override + public Response performRequest( + String endpointId, @Nullable Node node, Request request, TransportOptions option + ) throws IOException { + requests.add(request); + return super.performRequest(endpointId, node, request, option); + } + }; + + httpClient.add("/_count", "application/json", toJson( + CountResponse.of(c -> c + .count(1) + .shards(s -> s.successful(1).failed(0).total(1)) + ))); + + ElasticsearchTransport transport = new ElasticsearchTransportBase(httpClient, null, mapper) {}; + ElasticsearchClient client = new ElasticsearchClient(transport, null); + + client.count(); + client.count(c -> c.q("foo:bar")); + client.count(c -> c.query(q -> q.term(t -> t.field("foo").value("bar")))); + + assertNull(requests.get(0).body()); + assertNull(requests.get(1).body()); + assertNotNull(requests.get(2).body()); + } +}