diff --git a/CHANGELOG.md b/CHANGELOG.md index 78a049f739..68e97c94aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,7 @@ This section is for maintaining a changelog for all breaking changes for the cli ### Added - Add missed fields to MultisearchBody: seqNoPrimaryTerm, storedFields, explain, fields, indicesBoost ([#914](https://github.com/opensearch-project/opensearch-java/pull/914)) +- Add OpenSearchGenericClient with support for raw HTTP request/responses ([#910](https://github.com/opensearch-project/opensearch-java/pull/910)) - Add missed fields to MultisearchBody: collapse, version, timeout ([#916](https://github.com/opensearch-project/opensearch-java/pull/916) - Add missed fields to MultisearchBody: ext, rescore and to SearchRequest: ext ([#918](https://github.com/opensearch-project/opensearch-java/pull/918) @@ -362,4 +363,4 @@ This section is for maintaining a changelog for all breaking changes for the cli [2.5.0]: https://github.com/opensearch-project/opensearch-java/compare/v2.4.0...v2.5.0 [2.4.0]: https://github.com/opensearch-project/opensearch-java/compare/v2.3.0...v2.4.0 [2.3.0]: https://github.com/opensearch-project/opensearch-java/compare/v2.2.0...v2.3.0 -[2.2.0]: https://github.com/opensearch-project/opensearch-java/compare/v2.1.0...v2.2.0 \ No newline at end of file +[2.2.0]: https://github.com/opensearch-project/opensearch-java/compare/v2.1.0...v2.2.0 diff --git a/USER_GUIDE.md b/USER_GUIDE.md index cce234bb3e..eb498cfc71 100644 --- a/USER_GUIDE.md +++ b/USER_GUIDE.md @@ -198,6 +198,7 @@ You can find a working sample of the above code in [IndexingBasics.java](./sampl - [Data Stream APIs](./guides/data_stream.md) - [Point-in-Time APIs](./guides/point_in_time.md) - [Search](./guides/search.md) +- [Generic Client](./guides/generic.md) ## Plugins diff --git a/guides/generic.md b/guides/generic.md new file mode 100644 index 0000000000..f9ee7ff5fa --- /dev/null +++ b/guides/generic.md @@ -0,0 +1,127 @@ +- [Generic Client](#generic-client) + - [Getting the Client](#get-client) + - [Sending Simple Requests](#request-bodyless) + - [Sending JSON Requests](#request-json) + - [Sending JSON Requests using POJOs](#request-pojo) + - [Sending Requests using structured JSON](#request-structured) + +# Generic Client + +There are rare circumstances when the typed OpenSearch client APIs are too constraining and there is a need to communicate with OpenSearch cluster (or individual nodes) over generic HTTP request / response communication. Use `OpenSearchGenericClient` in such cases. + +## Getting the Client +The following sample code gets the `OpenSearchGenericClient` from the `OpenSearchClient` instance. + +```java +final OpenSearchGenericClient generic = javaClient().generic(); +``` + +## Sending Simple Request +The following sample code sends a simple request that does not require any payload to be provided (typically, `GET` requests). + +```java +// compare with what the low level client outputs +try (Response response = javaClient().generic().execute(Requests.builder().endpoint("/").method("GET").build())) { + // ... +} +``` + +Please notice that the `Response` instance should be closed explicitly in order to free up any allocated resource (like response input streams or buffers), the [`try-with-resource`](https://docs.oracle.com/javase/tutorial/essential/exceptions/tryResourceClose.html) pattern is encouraged. + +```java +try (Response response = javaClient().generic().execute(...)) { + // ... +} +``` + +The generic client never interprets status codes and provides the direct access to the response as it was received from the server. This is responsibility of the caller to decide what should happen in case of unsuccessful invocations. + +```java +// compare with what the low level client outputs +try (Response response = javaClient().generic().execute(...)) { + if (response.getStatus() != 200) { + // Request was not successful + } +} +``` + +## Sending JSON Requests +The following sample code a simple request with JSON body. + +```java + try (Response response = javaClient().generic() + .execute( + Requests.builder() + .endpoint("/" + index + "/_search") + .method("POST") + .json("{" + + " \"query\": {" + + " \"match_all\": {}" + + " }" + + "}" + ) + .build())) { + // Retrieve the response body as a simple string + final String body = response.getBody().map(Body::getAsString).orElse(""); + // ... +} +``` + +## Sending JSON Requests using POJOs +Besides providing the ability to deal with raw request and response payloads (bodies), the `OpenSearchGenericClient` could be used mixed with existing OpenSearch typed requests and responses (POJOs), like the following sample code demonstrates. + + +```java +final JsonpMapper jsonpMapper = javaClient()._transport().jsonpMapper(); + +final CreateIndexRequest request = CreateIndexRequest.of( + b -> b.index(index) + .mappings( + m -> m.properties("name", Property.of(p -> p.keyword(v -> v.docValues(true)))) + .properties("size", Property.of(p -> p.keyword(v -> v.docValues(true)))) + ) + .settings(settings -> settings.sort(s -> s.field("name").order(SegmentSortOrder.Asc))) +); + +try (Response response = javaClient().generic() + .execute( + Requests.builder() + .endpoint("/" + index).method("PUT") + .json(request, jsonpMapper).build())) { + // Retrieve the response body as a POJO + final CreateIndexResponse r = response.getBody() + .map(b -> Bodies.json(b, CreateIndexResponse._DESERIALIZER, jsonpMapper)) + .orElse(null); + // ... +} +``` + +## Sending Requests using structured JSON +Dealing with strings or POJOs could be daunting sometimes, using structured JSON APIs is a middle ground of both approaches, as per following sample code that uses (`jakarta.json.Json`)[https://jakarta.ee/specifications/jsonp]. + +```java +try (Response response = javaClient().generic() + .execute( + Requests.builder() + .endpoint("/" + index) + .method("PUT") + .json(Json.createObjectBuilder() + .add("settings", Json.createObjectBuilder() + .add("index", Json.createObjectBuilder() + .add("sort.field", "name")) + .add("sort.order", "asc") + ) + .add("mappings",Json.createObjectBuilder() + .add("properties", Json.createObjectBuilder() + .add("name", Json.createObjectBuilder() + .add("type", "keyword")) + .add("doc_values", true) + .add("size", Json.createObjectBuilder() + .add("type", "keyword")) + .add("doc_values", true)) + ) + ) + .build())) { + // ... +} +``` \ No newline at end of file diff --git a/java-client/src/main/java/org/opensearch/client/opensearch/OpenSearchClient.java b/java-client/src/main/java/org/opensearch/client/opensearch/OpenSearchClient.java index 3b2d8d76ff..ee83bcc20d 100644 --- a/java-client/src/main/java/org/opensearch/client/opensearch/OpenSearchClient.java +++ b/java-client/src/main/java/org/opensearch/client/opensearch/OpenSearchClient.java @@ -123,6 +123,7 @@ import org.opensearch.client.opensearch.core.pit.ListAllPitResponse; import org.opensearch.client.opensearch.dangling_indices.OpenSearchDanglingIndicesClient; import org.opensearch.client.opensearch.features.OpenSearchFeaturesClient; +import org.opensearch.client.opensearch.generic.OpenSearchGenericClient; import org.opensearch.client.opensearch.indices.OpenSearchIndicesClient; import org.opensearch.client.opensearch.ingest.OpenSearchIngestClient; import org.opensearch.client.opensearch.nodes.OpenSearchNodesClient; @@ -155,6 +156,9 @@ public OpenSearchClient withTransportOptions(@Nullable TransportOptions transpor } // ----- Child clients + public OpenSearchGenericClient generic() { + return new OpenSearchGenericClient(this.transport, this.transportOptions); + } public OpenSearchCatClient cat() { return new OpenSearchCatClient(this.transport, this.transportOptions); diff --git a/java-client/src/main/java/org/opensearch/client/opensearch/generic/Bodies.java b/java-client/src/main/java/org/opensearch/client/opensearch/generic/Bodies.java new file mode 100644 index 0000000000..96dafca2db --- /dev/null +++ b/java-client/src/main/java/org/opensearch/client/opensearch/generic/Bodies.java @@ -0,0 +1,58 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.client.opensearch.generic; + +import jakarta.json.JsonObject; +import jakarta.json.JsonObjectBuilder; +import jakarta.json.stream.JsonGenerator; +import jakarta.json.stream.JsonParser; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import org.opensearch.client.json.JsonpDeserializer; +import org.opensearch.client.json.JsonpMapper; + +public final class Bodies { + private static final String APPLICATION_JSON = "application/json; charset=UTF-8"; + + private Bodies() {} + + public static C json(Body body, JsonpDeserializer deserializer, JsonpMapper jsonpMapper) { + try (JsonParser parser = jsonpMapper.jsonProvider().createParser(body.body())) { + return deserializer.deserialize(parser, jsonpMapper); + } + } + + public static C json(Body body, Class clazz, JsonpMapper jsonpMapper) { + try (JsonParser parser = jsonpMapper.jsonProvider().createParser(body.body())) { + return jsonpMapper.deserialize(parser, clazz); + } + } + + public static Body json(C value, JsonpMapper jsonpMapper) throws IOException { + try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { + try (JsonGenerator generator = jsonpMapper.jsonProvider().createGenerator(baos)) { + jsonpMapper.serialize(value, generator); + } + return Body.from(baos.toByteArray(), APPLICATION_JSON); + } + } + + public static Body json(final JsonObjectBuilder builder) { + return json(builder.build()); + } + + public static Body json(final JsonObject json) { + return json(json.toString()); + } + + public static Body json(String str) { + return Body.from(str.getBytes(StandardCharsets.UTF_8), APPLICATION_JSON); + } +} diff --git a/java-client/src/main/java/org/opensearch/client/opensearch/generic/Body.java b/java-client/src/main/java/org/opensearch/client/opensearch/generic/Body.java new file mode 100644 index 0000000000..9957c93507 --- /dev/null +++ b/java-client/src/main/java/org/opensearch/client/opensearch/generic/Body.java @@ -0,0 +1,90 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.client.opensearch.generic; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; +import java.nio.charset.StandardCharsets; +import javax.annotation.Nullable; + +/** + * Generic HTTP request / response body. It is responsibility of the caller to close the body instance + * explicitly (or through {@link GenericResponse} instance) to release all associated resources. + */ +public interface Body extends AutoCloseable { + final int DEFAULT_BUFFER_SIZE = 8192; + + /** + * Constructs the generic response body out of {@link InputStream} with assumed content type + * @param body response body stream + * @param contentType content type + * @return generic response body instance + */ + static @Nullable Body from(@Nullable final InputStream body, @Nullable final String contentType) { + if (body == null) { + return null; + } else { + return new GenericInputStreamBody(body, contentType); + } + } + + /** + * Constructs the generic response body out of {@link InputStream} with assumed content type + * @param body response body stream + * @param contentType content type + * @return generic response body instance + */ + static @Nullable Body from(@Nullable final byte[] body, @Nullable final String contentType) { + if (body == null) { + return null; + } else { + return new GenericByteArrayBody(body, contentType); + } + } + + /** + * Content type of this body + * @return content type + */ + String contentType(); + + /** + * Gets the body as {@link InputStream} + * @return body as {@link InputStream} + */ + InputStream body(); + + /** + * Gets the body as {@link String} + * @return body as {@link String} + */ + default String bodyAsString() { + try (final ByteArrayOutputStream out = new ByteArrayOutputStream()) { + try (final InputStream in = body()) { + final byte[] buffer = new byte[DEFAULT_BUFFER_SIZE]; + int read; + while ((read = in.read(buffer, 0, DEFAULT_BUFFER_SIZE)) >= 0) { + out.write(buffer, 0, read); + } + } + + out.flush(); + return new String(out.toByteArray(), StandardCharsets.UTF_8); + } catch (final IOException ex) { + throw new UncheckedIOException(ex); + } + } + + /** + * Releases all resources associated with this body stream. + */ + void close() throws IOException; +} diff --git a/java-client/src/main/java/org/opensearch/client/opensearch/generic/GenericByteArrayBody.java b/java-client/src/main/java/org/opensearch/client/opensearch/generic/GenericByteArrayBody.java new file mode 100644 index 0000000000..aa8a3f873e --- /dev/null +++ b/java-client/src/main/java/org/opensearch/client/opensearch/generic/GenericByteArrayBody.java @@ -0,0 +1,40 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.client.opensearch.generic; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import javax.annotation.Nullable; + +/** + * The HTTP request / response body that uses {@link byte[]} + */ +final class GenericByteArrayBody implements Body { + private final byte[] bytes; + private final String contentType; + + GenericByteArrayBody(final byte[] bytes, @Nullable final String contentType) { + this.bytes = bytes; + this.contentType = contentType; + } + + @Override + public String contentType() { + return contentType; + } + + @Override + public InputStream body() { + return new ByteArrayInputStream(bytes); + } + + @Override + public void close() throws IOException {} +} diff --git a/java-client/src/main/java/org/opensearch/client/opensearch/generic/GenericInputStreamBody.java b/java-client/src/main/java/org/opensearch/client/opensearch/generic/GenericInputStreamBody.java new file mode 100644 index 0000000000..40f4d241a0 --- /dev/null +++ b/java-client/src/main/java/org/opensearch/client/opensearch/generic/GenericInputStreamBody.java @@ -0,0 +1,41 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.client.opensearch.generic; + +import java.io.IOException; +import java.io.InputStream; +import javax.annotation.Nullable; + +/** + * The HTTP request / response body that uses {@link InputStream} + */ +final class GenericInputStreamBody implements Body { + private final InputStream in; + private final String contentType; + + GenericInputStreamBody(final InputStream in, @Nullable final String contentType) { + this.in = in; + this.contentType = contentType; + } + + @Override + public String contentType() { + return contentType; + } + + @Override + public InputStream body() { + return in; + } + + @Override + public void close() throws IOException { + in.close(); + } +} diff --git a/java-client/src/main/java/org/opensearch/client/opensearch/generic/GenericRequest.java b/java-client/src/main/java/org/opensearch/client/opensearch/generic/GenericRequest.java new file mode 100644 index 0000000000..d9194df9cd --- /dev/null +++ b/java-client/src/main/java/org/opensearch/client/opensearch/generic/GenericRequest.java @@ -0,0 +1,172 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.client.opensearch.generic; + +import static java.util.Collections.unmodifiableMap; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.UncheckedIOException; +import java.util.Collection; +import java.util.Collections; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import javax.annotation.Nullable; +import org.opensearch.client.transport.GenericSerializable; + +/** + * Generic HTTP request to OpenSearch + */ +final class GenericRequest implements GenericSerializable, Request { + private final String method; + private final String endpoint; + private final Collection> headers; + private final Map parameters; + private final Body body; + + /** + * Create the {@linkplain GenericRequest}. + * @param method the HTTP method + * @param endpoint the path of the request (without scheme, host, port, or prefix) + * @param headers list of headers + */ + GenericRequest(String method, String endpoint, Collection> headers) { + this(method, endpoint, headers, Collections.emptyMap(), null); + } + + /** + * Create the {@linkplain GenericRequest}. + * @param method the HTTP method + * @param endpoint the path of the request (without scheme, host, port, or prefix) + * @param headers list of headers + * @param parameters query parameters + * @param body optional body + */ + GenericRequest( + String method, + String endpoint, + Collection> headers, + Map parameters, + @Nullable Body body + ) { + this.method = Objects.requireNonNull(method, "method cannot be null"); + this.endpoint = Objects.requireNonNull(endpoint, "endpoint cannot be null"); + this.headers = Objects.requireNonNull(headers, "headers cannot be null"); + this.parameters = Objects.requireNonNull(parameters, "parameters cannot be null"); + this.body = body; + } + + /** + * The HTTP method. + */ + @Override + public String getMethod() { + return method; + } + + /** + * The path of the request (without scheme, host, port, or prefix). + */ + @Override + public String getEndpoint() { + return endpoint; + } + + /** + * Query string parameters. The returned map is an unmodifiable view of the + * map in the request. + */ + @Override + public Map getParameters() { + return unmodifiableMap(parameters); + } + + @Override + public Collection> getHeaders() { + return Collections.unmodifiableCollection(headers); + } + + /** + * The body of the request. If {@code null} then no body + * is sent with the request. + */ + @Override + public Optional getBody() { + return Optional.ofNullable(body); + } + + /** + * Convert request to string representation + */ + @Override + public String toString() { + StringBuilder b = new StringBuilder(); + b.append("Request{"); + b.append("method='").append(method).append('\''); + b.append(", endpoint='").append(endpoint).append('\''); + if (false == parameters.isEmpty()) { + b.append(", params=").append(parameters); + } + if (body != null) { + b.append(", body=").append(body); + } + return b.append('}').toString(); + } + + /** + * Compare two requests for equality + * @param obj request instance to compare with + */ + @Override + public boolean equals(Object obj) { + if (obj == null || (obj.getClass() != getClass())) { + return false; + } + if (obj == this) { + return true; + } + + final GenericRequest other = (GenericRequest) obj; + return method.equals(other.method) + && endpoint.equals(other.endpoint) + && parameters.equals(other.parameters) + && headers.equals(other.headers) + && Objects.equals(body, other.body); + } + + /** + * Calculate the hash code of the request + */ + @Override + public int hashCode() { + return Objects.hash(method, endpoint, parameters, headers, body); + } + + @Override + public String serialize(OutputStream out) { + if (getBody().isPresent() == false) { + throw new IllegalStateException("The request has no content body provided."); + } + + final Body b = getBody().get(); + try (final InputStream in = b.body()) { + final byte[] buffer = new byte[Body.DEFAULT_BUFFER_SIZE]; + int read; + while ((read = in.read(buffer, 0, Body.DEFAULT_BUFFER_SIZE)) >= 0) { + out.write(buffer, 0, read); + } + } catch (final IOException ex) { + throw new UncheckedIOException(ex); + } + + return b.contentType(); + } +} diff --git a/java-client/src/main/java/org/opensearch/client/opensearch/generic/GenericResponse.java b/java-client/src/main/java/org/opensearch/client/opensearch/generic/GenericResponse.java new file mode 100644 index 0000000000..5a7a27cef8 --- /dev/null +++ b/java-client/src/main/java/org/opensearch/client/opensearch/generic/GenericResponse.java @@ -0,0 +1,116 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.client.opensearch.generic; + +import java.io.IOException; +import java.util.Collection; +import java.util.Collections; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +/** + * Generic HTTP response from OpenSearch + */ +final class GenericResponse implements Response { + private final String protocol; + private final String method; + private final String uri; + private final int status; + private final String reason; + private final Collection> headers; + private final Body body; + + GenericResponse( + String uri, + String protocol, + String method, + int status, + String reason, + Collection> headers, + Body body + ) { + this.uri = Objects.requireNonNull(uri, "uri cannot be null"); + this.protocol = Objects.requireNonNull(protocol, "protocol cannot be null"); + this.method = Objects.requireNonNull(method, "method cannot be null"); + this.status = status; + this.reason = reason; + this.headers = Objects.requireNonNull(headers, "headers cannot be null"); + this.body = body; + } + + @Override + public Optional getBody() { + return Optional.ofNullable(body); + } + + @Override + public String getProtocol() { + return protocol; + } + + @Override + public String getMethod() { + return method; + } + + @Override + public String getReason() { + return reason; + } + + @Override + public int getStatus() { + return status; + } + + @Override + public String getUri() { + return uri; + } + + @Override + public Collection> getHeaders() { + return Collections.unmodifiableCollection(headers); + } + + @Override + public boolean equals(Object obj) { + if (obj == null || (obj.getClass() != getClass())) { + return false; + } + if (obj == this) { + return true; + } + + final GenericResponse other = (GenericResponse) obj; + return method.equals(other.method) + && protocol.equals(other.protocol) + && uri.equals(other.uri) + && Objects.equals(reason, other.reason) + && headers.equals(other.headers) + && status == other.status + && Objects.equals(body, other.body); + } + + /** + * Calculate the hash code of the response + */ + @Override + public int hashCode() { + return Objects.hash(method, protocol, uri, status, reason, headers, body); + } + + @Override + public void close() throws IOException { + if (body != null) { + body.close(); + } + } +} diff --git a/java-client/src/main/java/org/opensearch/client/opensearch/generic/OpenSearchGenericClient.java b/java-client/src/main/java/org/opensearch/client/opensearch/generic/OpenSearchGenericClient.java new file mode 100644 index 0000000000..0d759af935 --- /dev/null +++ b/java-client/src/main/java/org/opensearch/client/opensearch/generic/OpenSearchGenericClient.java @@ -0,0 +1,108 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.client.opensearch.generic; + +import java.io.IOException; +import java.io.InputStream; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; +import javax.annotation.Nullable; +import org.opensearch.client.ApiClient; +import org.opensearch.client.transport.OpenSearchTransport; +import org.opensearch.client.transport.TransportOptions; + +/** + * Client for the generic HTTP requests. + */ +public class OpenSearchGenericClient extends ApiClient { + /** + * Generic endpoint instance + */ + private static final class GenericEndpoint implements org.opensearch.client.transport.GenericEndpoint { + private final Request request; + + public GenericEndpoint(Request request) { + this.request = request; + } + + @Override + public String method(Request request) { + return request.getMethod(); + } + + @Override + public String requestUrl(Request request) { + return request.getEndpoint(); + } + + @Override + public boolean hasRequestBody() { + return request.getBody().isPresent(); + } + + @Override + public Map queryParameters(Request request) { + return request.getParameters(); + } + + @Override + public Map headers(Request request) { + return request.getHeaders().stream().collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (e1, e2) -> e1)); + } + + @Override + public GenericResponse responseDeserializer( + String uri, + String method, + String protocol, + int status, + String reason, + List> headers, + String contentType, + InputStream body + ) { + return new GenericResponse(uri, protocol, method, status, reason, headers, Body.from(body, contentType)); + } + } + + public OpenSearchGenericClient(OpenSearchTransport transport) { + super(transport, null); + } + + public OpenSearchGenericClient(OpenSearchTransport transport, @Nullable TransportOptions transportOptions) { + super(transport, transportOptions); + } + + @Override + public OpenSearchGenericClient withTransportOptions(@Nullable TransportOptions transportOptions) { + return new OpenSearchGenericClient(this.transport, transportOptions); + } + + /** + * Executes generic HTTP request and returns generic HTTP response. + * @param request generic HTTP request + * @return generic HTTP response + * @throws IOException I/O exception + */ + public Response execute(Request request) throws IOException { + return transport.performRequest(request, new GenericEndpoint(request), this.transportOptions); + } + + /** + * Asynchronously executes generic HTTP request and returns generic HTTP response. + * @param request generic HTTP request + * @return generic HTTP response future + */ + public CompletableFuture executeAsync(Request request) { + return transport.performRequestAsync(request, new GenericEndpoint(request), this.transportOptions); + } +} diff --git a/java-client/src/main/java/org/opensearch/client/opensearch/generic/Request.java b/java-client/src/main/java/org/opensearch/client/opensearch/generic/Request.java new file mode 100644 index 0000000000..279c929d3a --- /dev/null +++ b/java-client/src/main/java/org/opensearch/client/opensearch/generic/Request.java @@ -0,0 +1,51 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.client.opensearch.generic; + +import java.util.Collection; +import java.util.Map; +import java.util.Optional; + +/** + * Generic HTTP request to OpenSearch + */ +public interface Request { + + /** + * The HTTP method. + */ + String getMethod(); + + /** + * The path of the request (without scheme, host, port, or prefix). + * @return path of the request + */ + String getEndpoint(); + + /** + * Query string parameters. The returned map is an unmodifiable view of the + * map in the request. + * @return query string parameters + */ + Map getParameters(); + + /** + * List of headers + * @return list of headers + */ + Collection> getHeaders(); + + /** + * The optional body of the request. If {@code Optional.empty()} then no body + * is sent with the request. + * @return optional body of the request + */ + Optional getBody(); + +} diff --git a/java-client/src/main/java/org/opensearch/client/opensearch/generic/Requests.java b/java-client/src/main/java/org/opensearch/client/opensearch/generic/Requests.java new file mode 100644 index 0000000000..e9a39871ea --- /dev/null +++ b/java-client/src/main/java/org/opensearch/client/opensearch/generic/Requests.java @@ -0,0 +1,107 @@ +package org.opensearch.client.opensearch.generic; + +import jakarta.json.JsonObject; +import jakarta.json.JsonObjectBuilder; +import java.io.IOException; +import java.util.Collection; +import java.util.Collections; +import java.util.Map; +import java.util.Objects; +import javax.annotation.Nullable; +import org.opensearch.client.json.JsonpMapper; + +/** + * Helper class to construct requests instances + */ +public final class Requests { + private Requests() {} + + /** + * Creates a new builder for requests with JSON body + * @return a new builder for requests with JSON body + */ + public static JsonBodyBuilder builder() { + return new JsonBodyBuilder(); + } + + /** + * Create request instance + * @param method the HTTP method + * @param endpoint the path of the request (without scheme, host, port, or prefix) + * @param headers list of headers + * @param parameters query parameters + * @param body optional body + * @return request instance + */ + public static Request create( + String method, + String endpoint, + Collection> headers, + Map parameters, + @Nullable Body body + ) { + return new GenericRequest(method, endpoint, headers, parameters, body); + } + + /** + * A new builder for requests with JSON body + */ + public static final class JsonBodyBuilder { + private String method; + private String endpoint; + private Collection> headers = Collections.emptyList(); + private Map parameters = Collections.emptyMap(); + private Body body; + + private JsonBodyBuilder() {} + + public JsonBodyBuilder endpoint(final String endpoint) { + this.endpoint = Objects.requireNonNull(endpoint, "endpoint cannot be null"); + return this; + } + + public JsonBodyBuilder body(final Body body) { + this.body = Objects.requireNonNull(body, "body cannot be null"); + return this; + } + + public JsonBodyBuilder json(final JsonObjectBuilder builder) { + this.body = Bodies.json(builder); + return this; + } + + public JsonBodyBuilder json(final JsonObject json) { + this.body = Bodies.json(json); + return this; + } + + public JsonBodyBuilder json(String str) { + this.body = Bodies.json(str); + return this; + } + + public JsonBodyBuilder json(C value, JsonpMapper mapper) throws IOException { + this.body = Bodies.json(value, mapper); + return this; + } + + public JsonBodyBuilder query(final Map parameters) { + this.parameters = Objects.requireNonNull(parameters, "parameters cannot be null"); + return this; + } + + public JsonBodyBuilder headers(final Collection> headers) { + this.headers = Objects.requireNonNull(headers, "headers cannot be null"); + return this; + } + + public JsonBodyBuilder method(final String method) { + this.method = Objects.requireNonNull(method, "headers cannot be null"); + return this; + } + + public Request build() { + return new GenericRequest(method, endpoint, headers, parameters, body); + } + } +} diff --git a/java-client/src/main/java/org/opensearch/client/opensearch/generic/Response.java b/java-client/src/main/java/org/opensearch/client/opensearch/generic/Response.java new file mode 100644 index 0000000000..d119ad54ba --- /dev/null +++ b/java-client/src/main/java/org/opensearch/client/opensearch/generic/Response.java @@ -0,0 +1,67 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.client.opensearch.generic; + +import java.io.IOException; +import java.util.Collection; +import java.util.Map; +import java.util.Optional; + +/** + * Generic HTTP response from OpenSearch + */ +public interface Response extends AutoCloseable { + /** + * The optional body of the response. If {@code Optional.empty()} then no body + * was sent with the response. + * @return optional body of the response + */ + Optional getBody(); + + /** + * The HTTP protocol version + * @return HTTP protocol version + */ + String getProtocol(); + + /** + * The HTTP method + * @return HTTP method + */ + String getMethod(); + + /** + * The status message + * @return status message + */ + String getReason(); + + /** + * The status code + * @return status code + */ + int getStatus(); + + /** + * Full URI of the request + * @return full URI of the request + */ + String getUri(); + + /** + * List of headers + * @return list of headers + */ + Collection> getHeaders(); + + /** + * Releases all resources associated with this body stream. + */ + void close() throws IOException; +} diff --git a/java-client/src/main/java/org/opensearch/client/transport/GenericEndpoint.java b/java-client/src/main/java/org/opensearch/client/transport/GenericEndpoint.java new file mode 100644 index 0000000000..5616430909 --- /dev/null +++ b/java-client/src/main/java/org/opensearch/client/transport/GenericEndpoint.java @@ -0,0 +1,54 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.client.transport; + +import java.io.InputStream; +import java.util.List; +import java.util.Map; +import javax.annotation.Nullable; +import org.opensearch.client.json.JsonpDeserializer; + +/** + * An endpoint with a raw, unparsed response body. The endpoind does not distinguish between errornous and successful response + * and always return the raw response body. + */ +public interface GenericEndpoint extends Endpoint { + default public boolean isError(int statusCode) { + return false; /* never return an error since errorDeserializer is JSON specific */ + } + + /** + * The error is never deserialized explicitly, represented as the instance of {@link ResponseT} instead. + */ + default public JsonpDeserializer errorDeserializer(int statusCode) { + return null; + } + + /** + * Constructs the {@link ResponseT} instance + * @param uri request URI + * @param method HTTP method + * @param protocol HTTP protocol version + * @param status status code + * @param reason reason phrase + * @param headers response headers + * @param body optional body + * @return the {@link ResponseT} instance + */ + ResponseT responseDeserializer( + final String uri, + final String method, + final String protocol, + int status, + final String reason, + final List> headers, + @Nullable final String contentType, + @Nullable final InputStream body + ); +} diff --git a/java-client/src/main/java/org/opensearch/client/transport/GenericSerializable.java b/java-client/src/main/java/org/opensearch/client/transport/GenericSerializable.java new file mode 100644 index 0000000000..14e07d7434 --- /dev/null +++ b/java-client/src/main/java/org/opensearch/client/transport/GenericSerializable.java @@ -0,0 +1,23 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.client.transport; + +import java.io.OutputStream; + +/** + * The request that takes care of serializing its body (content) into the {@link OutputStream} + */ +public interface GenericSerializable { + /** + * Serializes into the {@link OutputStream} and returns the content type + * @param out {@link OutputStream} to serialize into + * @return content type + */ + String serialize(OutputStream out); +} diff --git a/java-client/src/main/java/org/opensearch/client/transport/httpclient5/ApacheHttpClient5Transport.java b/java-client/src/main/java/org/opensearch/client/transport/httpclient5/ApacheHttpClient5Transport.java index 28be09ae05..74a72e804d 100644 --- a/java-client/src/main/java/org/opensearch/client/transport/httpclient5/ApacheHttpClient5Transport.java +++ b/java-client/src/main/java/org/opensearch/client/transport/httpclient5/ApacheHttpClient5Transport.java @@ -17,6 +17,7 @@ import java.io.OutputStream; import java.net.URI; import java.net.URISyntaxException; +import java.util.AbstractMap; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -37,6 +38,7 @@ import java.util.concurrent.ConcurrentMap; import java.util.concurrent.Future; import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; import java.util.zip.GZIPOutputStream; import javax.annotation.Nullable; import org.apache.commons.logging.Log; @@ -66,6 +68,7 @@ import org.apache.hc.core5.http.io.entity.HttpEntityWrapper; import org.apache.hc.core5.http.message.BasicHeader; import org.apache.hc.core5.http.message.RequestLine; +import org.apache.hc.core5.http.message.StatusLine; import org.apache.hc.core5.http.nio.AsyncRequestProducer; import org.apache.hc.core5.http.nio.AsyncResponseConsumer; import org.apache.hc.core5.net.URIBuilder; @@ -76,6 +79,8 @@ import org.opensearch.client.opensearch._types.ErrorResponse; import org.opensearch.client.opensearch._types.OpenSearchException; import org.opensearch.client.transport.Endpoint; +import org.opensearch.client.transport.GenericEndpoint; +import org.opensearch.client.transport.GenericSerializable; import org.opensearch.client.transport.JsonEndpoint; import org.opensearch.client.transport.OpenSearchTransport; import org.opensearch.client.transport.TransportException; @@ -535,15 +540,18 @@ private HttpUriRequestBase prepareLowLevelRequest( // Request has a body and must implement JsonpSerializable or NdJsonpSerializable ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ContentType contentType = JsonContentType; if (request instanceof NdJsonpSerializable) { writeNdJson((NdJsonpSerializable) request, baos); + } else if (request instanceof GenericSerializable) { + contentType = ContentType.parse(((GenericSerializable) request).serialize(baos)); } else { JsonGenerator generator = mapper.jsonProvider().createGenerator(baos); mapper.serialize(request, generator); generator.close(); } - addRequestBody(clientReq, new ByteArrayEntity(baos.toByteArray(), JsonContentType)); + addRequestBody(clientReq, new ByteArrayEntity(baos.toByteArray(), contentType)); } setHeaders(clientReq, options.headers()); @@ -630,6 +638,31 @@ private ResponseT decodeResponse( ; } return response; + } else if (endpoint instanceof GenericEndpoint) { + @SuppressWarnings("unchecked") + final GenericEndpoint rawEndpoint = (GenericEndpoint) endpoint; + + String contentType = null; + InputStream content = null; + if (entity != null) { + contentType = entity.getContentType(); + content = entity.getContent(); + } + + final RequestLine requestLine = clientResp.getRequestLine(); + final StatusLine statusLine = clientResp.getStatusLine(); + return rawEndpoint.responseDeserializer( + requestLine.getUri(), + requestLine.getMethod(), + requestLine.getProtocolVersion().format(), + statusLine.getStatusCode(), + statusLine.getReasonPhrase(), + Arrays.stream(clientResp.getHeaders()) + .map(h -> new AbstractMap.SimpleEntry(h.getName(), h.getValue())) + .collect(Collectors.toList()), + contentType, + content + ); } else { throw new TransportException("Unhandled endpoint type: '" + endpoint.getClass().getName() + "'"); } diff --git a/java-client/src/main/java/org/opensearch/client/transport/rest_client/RestClientTransport.java b/java-client/src/main/java/org/opensearch/client/transport/rest_client/RestClientTransport.java index e2a40a2fff..7e56409ef6 100644 --- a/java-client/src/main/java/org/opensearch/client/transport/rest_client/RestClientTransport.java +++ b/java-client/src/main/java/org/opensearch/client/transport/rest_client/RestClientTransport.java @@ -37,9 +37,12 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; +import java.util.AbstractMap; +import java.util.Arrays; import java.util.Iterator; import java.util.Map; import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; import javax.annotation.Nullable; import org.apache.hc.core5.http.ContentType; import org.apache.hc.core5.http.HttpEntity; @@ -47,6 +50,8 @@ import org.apache.hc.core5.http.io.entity.BufferedHttpEntity; import org.apache.hc.core5.http.io.entity.ByteArrayEntity; import org.apache.hc.core5.http.io.entity.EntityUtils; +import org.apache.hc.core5.http.message.RequestLine; +import org.apache.hc.core5.http.message.StatusLine; import org.opensearch.client.Cancellable; import org.opensearch.client.RequestOptions; import org.opensearch.client.Response; @@ -59,6 +64,8 @@ import org.opensearch.client.opensearch._types.ErrorResponse; import org.opensearch.client.opensearch._types.OpenSearchException; import org.opensearch.client.transport.Endpoint; +import org.opensearch.client.transport.GenericEndpoint; +import org.opensearch.client.transport.GenericSerializable; import org.opensearch.client.transport.JsonEndpoint; import org.opensearch.client.transport.OpenSearchTransport; import org.opensearch.client.transport.TransportException; @@ -207,15 +214,18 @@ private org.opensearch.client.Request prepareLowLevelRequest( // Request has a body and must implement JsonpSerializable or NdJsonpSerializable ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ContentType contentType = JsonContentType; if (request instanceof NdJsonpSerializable) { writeNdJson((NdJsonpSerializable) request, baos); + } else if (request instanceof GenericSerializable) { + contentType = ContentType.parse(((GenericSerializable) request).serialize(baos)); } else { JsonGenerator generator = mapper.jsonProvider().createGenerator(baos); mapper.serialize(request, generator); generator.close(); } - clientReq.setEntity(new ByteArrayEntity(baos.toByteArray(), JsonContentType)); + clientReq.setEntity(new ByteArrayEntity(baos.toByteArray(), contentType)); } // Request parameter intercepted by LLRC clientReq.addParameter("ignore", "400,401,403,404,405"); @@ -324,6 +334,31 @@ private ResponseT decodeResponse( ; } return response; + } else if (endpoint instanceof GenericEndpoint) { + @SuppressWarnings("unchecked") + final GenericEndpoint rawEndpoint = (GenericEndpoint) endpoint; + + String contentType = null; + InputStream content = null; + if (entity != null) { + contentType = entity.getContentType(); + content = entity.getContent(); + } + + final RequestLine requestLine = clientResp.getRequestLine(); + final StatusLine statusLine = clientResp.getStatusLine(); + return rawEndpoint.responseDeserializer( + requestLine.getUri(), + requestLine.getMethod(), + requestLine.getProtocolVersion().format(), + statusLine.getStatusCode(), + statusLine.getReasonPhrase(), + Arrays.stream(clientResp.getHeaders()) + .map(h -> new AbstractMap.SimpleEntry(h.getName(), h.getValue())) + .collect(Collectors.toList()), + contentType, + content + ); } else { throw new TransportException("Unhandled endpoint type: '" + endpoint.getClass().getName() + "'"); } diff --git a/java-client/src/test/java11/org/opensearch/client/opensearch/integTest/AbstractGenericClientIT.java b/java-client/src/test/java11/org/opensearch/client/opensearch/integTest/AbstractGenericClientIT.java new file mode 100644 index 0000000000..bf3900bfb8 --- /dev/null +++ b/java-client/src/test/java11/org/opensearch/client/opensearch/integTest/AbstractGenericClientIT.java @@ -0,0 +1,263 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.client.opensearch.integTest; + +import static org.hamcrest.CoreMatchers.equalTo; + +import jakarta.json.Json; +import java.io.IOException; +import java.util.Arrays; +import java.util.stream.Collectors; +import org.junit.Test; +import org.opensearch.client.json.JsonpDeserializer; +import org.opensearch.client.json.JsonpMapper; +import org.opensearch.client.opensearch._types.mapping.Property; +import org.opensearch.client.opensearch.core.SearchResponse; +import org.opensearch.client.opensearch.generic.Bodies; +import org.opensearch.client.opensearch.generic.Requests; +import org.opensearch.client.opensearch.generic.Response; +import org.opensearch.client.opensearch.indices.CreateIndexRequest; +import org.opensearch.client.opensearch.indices.CreateIndexResponse; +import org.opensearch.client.opensearch.indices.SegmentSortOrder; + +public abstract class AbstractGenericClientIT extends OpenSearchJavaClientTestCase { + + @Test + public void shouldReturnSearchResults() throws Exception { + final String index = "search_request"; + createIndex(index); + + try ( + Response response = javaClient().generic() + .execute( + Requests.builder() + .endpoint("/" + index + "/_search") + .method("POST") + .json( + "{" + + " \"sort\": [" + + " {" + + " \"name\": \"asc\"" + + " }" + + " ]," + + " \"fields\": [" + + " \"name\"" + + " ]," + + " \"query\": {" + + " \"bool\": {" + + " \"filter\": [" + + " {" + + " \"term\": {" + + " \"size\": \"huge\"" + + " }" + + " }" + + " ]" + + " }" + + " }," + + " \"_source\": true\n" + + "}" + ) + .build() + ) + ) { + assertThat(response.getStatus(), equalTo(200)); + assertThat(response.getBody().isPresent(), equalTo(true)); + + final SearchResponse r = Bodies.json( + response.getBody().get(), + SearchResponse.createSearchResponseDeserializer(JsonpDeserializer.of(ShopItem.class)), + javaClient()._transport().jsonpMapper() + ); + assertEquals(r.hits().hits().size(), 2); + + assertTrue( + Arrays.stream(r.hits().hits().get(0).fields().get("name").to(String[].class)) + .collect(Collectors.toList()) + .contains("hummer") + ); + + assertTrue( + Arrays.stream(r.hits().hits().get(1).fields().get("name").to(String[].class)) + .collect(Collectors.toList()) + .contains("jammer") + ); + } + } + + private void createTestDocuments(String index) throws IOException { + createTestDocument(index, "1", createItem("hummer", "huge", "yes", 2)); + createTestDocument(index, "2", createItem("jammer", "huge", "yes", 1)); + createTestDocument(index, "3", createItem("hammer", "large", "yes", 3)); + createTestDocument(index, "4", createItem("drill", "large", "yes", 3)); + createTestDocument(index, "5", createItem("jack", "medium", "yes", 2)); + createTestDocument(index, "6", createItem("wrench", "medium", "no", 3)); + createTestDocument(index, "7", createItem("screws", "small", "no", 1)); + createTestDocument(index, "8", createItem("nuts", "small", "no", 2)); + } + + private void createTestDocument(String index, String id, ShopItem document) throws IOException { + try ( + Response response = javaClient().generic() + .execute( + Requests.builder() + .endpoint("/" + index + "/_doc/" + id) + .method("PUT") + .json(document, javaClient()._transport().jsonpMapper()) + .build() + ) + ) { + assertThat(response.getStatus(), equalTo(201)); + assertThat(response.getBody().isPresent(), equalTo(true)); + } + } + + private void createIndex(String index) throws IOException { + if (randomBoolean()) { + createIndexUntyped(index); + } else { + createIndexTyped(index); + } + } + + private void createIndexUntyped(String index) throws IOException { + final JsonpMapper jsonpMapper = javaClient()._transport().jsonpMapper(); + + try ( + Response response = javaClient().generic() + .execute( + Requests.builder() + .endpoint("/" + index) + .method("PUT") + .json( + Json.createObjectBuilder() + .add( + "settings", + Json.createObjectBuilder() + .add("index", Json.createObjectBuilder().add("sort.field", "name")) + .add("sort.order", "asc") + ) + .add( + "mappings", + Json.createObjectBuilder() + .add( + "properties", + Json.createObjectBuilder() + .add("name", Json.createObjectBuilder().add("type", "keyword")) + .add("doc_values", true) + + .add("size", Json.createObjectBuilder().add("type", "keyword")) + .add("doc_values", true) + ) + ) + ) + .build() + ) + ) { + assertThat(response.getStatus(), equalTo(200)); + assertThat(response.getBody().isPresent(), equalTo(true)); + + final CreateIndexResponse r = response.getBody() + .map(b -> Bodies.json(b, CreateIndexResponse._DESERIALIZER, jsonpMapper)) + .orElse(null); + assertThat(r.acknowledged(), equalTo(true)); + } + + createTestDocuments(index); + refreshIndex(index); + } + + private void createIndexTyped(String index) throws IOException { + final JsonpMapper jsonpMapper = javaClient()._transport().jsonpMapper(); + + final CreateIndexRequest request = CreateIndexRequest.of( + b -> b.index(index) + .mappings( + m -> m.properties("name", Property.of(p -> p.keyword(v -> v.docValues(true)))) + .properties("size", Property.of(p -> p.keyword(v -> v.docValues(true)))) + ) + .settings(settings -> settings.sort(s -> s.field("name").order(SegmentSortOrder.Asc))) + ); + + try ( + Response response = javaClient().generic() + .execute(Requests.builder().endpoint("/" + index).method("PUT").json(request, jsonpMapper).build()) + ) { + assertThat(response.getStatus(), equalTo(200)); + assertThat(response.getBody().isPresent(), equalTo(true)); + + final CreateIndexResponse r = Bodies.json(response.getBody().get(), CreateIndexResponse._DESERIALIZER, jsonpMapper); + assertThat(r.acknowledged(), equalTo(true)); + } + + createTestDocuments(index); + refreshIndex(index); + } + + private void refreshIndex(String index) throws IOException { + try ( + Response response = javaClient().generic() + .execute(Requests.builder().endpoint("/" + index + "/_refresh").method("POST").build()) + ) { + assertThat(response.getStatus(), equalTo(200)); + assertThat(response.getBody().isPresent(), equalTo(true)); + } + } + + private ShopItem createItem(String name, String size, String company, int quantity) { + return new ShopItem(name, size, company, quantity); + } + + public static class ShopItem { + private String name; + private String size; + private String company; + private int quantity; + + public ShopItem() {} + + public ShopItem(String name, String size, String company, int quantity) { + this.name = name; + this.size = size; + this.company = company; + this.quantity = quantity; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getSize() { + return size; + } + + public void setSize(String size) { + this.size = size; + } + + public String getCompany() { + return company; + } + + public void setCompany(String company) { + this.company = company; + } + + public int getQuantity() { + return quantity; + } + + public void setQuantity(int quantity) { + this.quantity = quantity; + } + } +} diff --git a/java-client/src/test/java11/org/opensearch/client/opensearch/integTest/AbstractPingAndInfoIT.java b/java-client/src/test/java11/org/opensearch/client/opensearch/integTest/AbstractPingAndInfoIT.java index 9310764dba..d951799c33 100644 --- a/java-client/src/test/java11/org/opensearch/client/opensearch/integTest/AbstractPingAndInfoIT.java +++ b/java-client/src/test/java11/org/opensearch/client/opensearch/integTest/AbstractPingAndInfoIT.java @@ -8,12 +8,17 @@ package org.opensearch.client.opensearch.integTest; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; + import java.io.IOException; +import java.util.Collections; import java.util.Map; -import org.apache.hc.client5.http.classic.methods.HttpGet; -import org.opensearch.client.Request; import org.opensearch.client.opensearch.OpenSearchClient; import org.opensearch.client.opensearch.core.InfoResponse; +import org.opensearch.client.opensearch.generic.Bodies; +import org.opensearch.client.opensearch.generic.Requests; +import org.opensearch.client.opensearch.generic.Response; import org.opensearch.client.transport.endpoints.BooleanResponse; public abstract class AbstractPingAndInfoIT extends OpenSearchJavaClientTestCase { @@ -27,19 +32,28 @@ public void testInfo() throws IOException { InfoResponse info = openSearchClient.info(); // compare with what the low level client outputs - Map infoAsMap = entityAsMap(adminClient().performRequest(new Request(HttpGet.METHOD_NAME, "/"))); - assertEquals(infoAsMap.get("cluster_name"), info.clusterName()); - assertEquals(infoAsMap.get("cluster_uuid"), info.clusterUuid()); - - @SuppressWarnings("unchecked") - Map versionMap = (Map) infoAsMap.get("version"); - assertEquals(versionMap.get("build_date"), info.version().buildDate()); - assertEquals(versionMap.get("build_flavor"), info.version().buildFlavor()); - assertEquals(versionMap.get("build_hash"), info.version().buildHash()); - assertEquals(versionMap.get("build_snapshot"), info.version().buildSnapshot()); - assertEquals(versionMap.get("build_type"), info.version().buildType()); - assertEquals(versionMap.get("distribution"), info.version().distribution()); - assertEquals(versionMap.get("lucene_version"), info.version().luceneVersion()); - assertTrue(versionMap.get("number").toString().startsWith(info.version().number())); + try (Response response = javaClient().generic().execute(Requests.builder().endpoint("/").method("GET").build())) { + assertThat(response.getStatus(), equalTo(200)); + assertThat(response.getProtocol(), equalTo("HTTP/1.1")); + assertThat(response.getBody().isEmpty(), is(false)); + + Map infoAsMap = response.getBody() + .map(b -> Bodies.json(b, Map.class, javaClient()._transport().jsonpMapper())) + .orElseGet(Collections::emptyMap); + + assertEquals(infoAsMap.get("cluster_name"), info.clusterName()); + assertEquals(infoAsMap.get("cluster_uuid"), info.clusterUuid()); + + @SuppressWarnings("unchecked") + Map versionMap = (Map) infoAsMap.get("version"); + assertEquals(versionMap.get("build_date"), info.version().buildDate()); + assertEquals(versionMap.get("build_flavor"), info.version().buildFlavor()); + assertEquals(versionMap.get("build_hash"), info.version().buildHash()); + assertEquals(versionMap.get("build_snapshot"), info.version().buildSnapshot()); + assertEquals(versionMap.get("build_type"), info.version().buildType()); + assertEquals(versionMap.get("distribution"), info.version().distribution()); + assertEquals(versionMap.get("lucene_version"), info.version().luceneVersion()); + assertTrue(versionMap.get("number").toString().startsWith(info.version().number())); + } } } diff --git a/java-client/src/test/java11/org/opensearch/client/opensearch/integTest/AbstractRequestIT.java b/java-client/src/test/java11/org/opensearch/client/opensearch/integTest/AbstractRequestIT.java index d78e124f51..f14fd42740 100644 --- a/java-client/src/test/java11/org/opensearch/client/opensearch/integTest/AbstractRequestIT.java +++ b/java-client/src/test/java11/org/opensearch/client/opensearch/integTest/AbstractRequestIT.java @@ -91,7 +91,6 @@ import org.opensearch.client.opensearch.indices.IndexSettingsAnalysis; import org.opensearch.client.opensearch.indices.IndexState; import org.opensearch.client.opensearch.indices.Translog; -import org.opensearch.client.opensearch.model.ModelTestCase; import org.opensearch.client.transport.endpoints.BooleanResponse; public abstract class AbstractRequestIT extends OpenSearchJavaClientTestCase { @@ -342,7 +341,6 @@ public void testDataIngestion() throws Exception { public void testCatRequest() throws IOException { // Cat requests should have the "format=json" added by the transport NodesResponse nodes = javaClient().cat().nodes(_0 -> _0); - System.out.println(ModelTestCase.toJson(nodes, javaClient()._transport().jsonpMapper())); InfoResponse info = javaClient().info(); String version = info.version().number(); diff --git a/java-client/src/test/java11/org/opensearch/client/opensearch/integTest/AbstractSearchTemplateRequestIT.java b/java-client/src/test/java11/org/opensearch/client/opensearch/integTest/AbstractSearchTemplateRequestIT.java index 4489ef8a68..efa2298046 100644 --- a/java-client/src/test/java11/org/opensearch/client/opensearch/integTest/AbstractSearchTemplateRequestIT.java +++ b/java-client/src/test/java11/org/opensearch/client/opensearch/integTest/AbstractSearchTemplateRequestIT.java @@ -89,7 +89,6 @@ public void testTemplateSearchAggregations() throws Exception { @Test public void testMultiSearchTemplate() throws Exception { - System.out.println("Multi search template test"); var index = "test-msearch-template"; createDocuments(index); diff --git a/java-client/src/test/java11/org/opensearch/client/opensearch/integTest/httpclient5/GenericClientIT.java b/java-client/src/test/java11/org/opensearch/client/opensearch/integTest/httpclient5/GenericClientIT.java new file mode 100644 index 0000000000..e480b9f641 --- /dev/null +++ b/java-client/src/test/java11/org/opensearch/client/opensearch/integTest/httpclient5/GenericClientIT.java @@ -0,0 +1,13 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.client.opensearch.integTest.httpclient5; + +import org.opensearch.client.opensearch.integTest.AbstractGenericClientIT; + +public class GenericClientIT extends AbstractGenericClientIT implements HttpClient5TransportSupport {} diff --git a/java-client/src/test/java11/org/opensearch/client/opensearch/integTest/restclient/GenericClientIT.java b/java-client/src/test/java11/org/opensearch/client/opensearch/integTest/restclient/GenericClientIT.java new file mode 100644 index 0000000000..ae486c20d3 --- /dev/null +++ b/java-client/src/test/java11/org/opensearch/client/opensearch/integTest/restclient/GenericClientIT.java @@ -0,0 +1,24 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.client.opensearch.integTest.restclient; + +import java.io.IOException; +import org.apache.hc.core5.http.HttpHost; +import org.opensearch.client.json.jackson.JacksonJsonpMapper; +import org.opensearch.client.opensearch.integTest.AbstractGenericClientIT; +import org.opensearch.client.transport.OpenSearchTransport; +import org.opensearch.client.transport.rest_client.RestClientTransport; +import org.opensearch.common.settings.Settings; + +public class GenericClientIT extends AbstractGenericClientIT { + @Override + public OpenSearchTransport buildTransport(Settings settings, HttpHost[] hosts) throws IOException { + return new RestClientTransport(buildClient(settings, hosts), new JacksonJsonpMapper()); + } +}