diff --git a/java-client-serverless/src/main/java/co/elastic/clients/elasticsearch/_types/ElasticsearchException.java b/java-client-serverless/src/main/java/co/elastic/clients/elasticsearch/_types/ElasticsearchException.java index 5307ed515..aa350d76b 100644 --- a/java-client-serverless/src/main/java/co/elastic/clients/elasticsearch/_types/ElasticsearchException.java +++ b/java-client-serverless/src/main/java/co/elastic/clients/elasticsearch/_types/ElasticsearchException.java @@ -19,6 +19,10 @@ package co.elastic.clients.elasticsearch._types; +import co.elastic.clients.transport.http.TransportHttpClient; + +import javax.annotation.Nullable; + /** * Exception thrown by API client methods when Elasticsearch could not accept or * process a request. @@ -31,11 +35,18 @@ public class ElasticsearchException extends RuntimeException { private final ErrorResponse response; private final String endpointId; + @Nullable + private final TransportHttpClient.Response httpResponse; - public ElasticsearchException(String endpointId, ErrorResponse response) { + public ElasticsearchException(String endpointId, ErrorResponse response, @Nullable TransportHttpClient.Response httpResponse) { super("[" + endpointId + "] failed: [" + response.error().type() + "] " + response.error().reason()); this.response = response; this.endpointId = endpointId; + this.httpResponse = httpResponse; + } + + public ElasticsearchException(String endpointId, ErrorResponse response) { + this(endpointId, response, null); } /** @@ -66,4 +77,12 @@ public ErrorCause error() { public int status() { return this.response.status(); } + + /** + * The underlying http response, if available. + */ + @Nullable + public TransportHttpClient.Response httpResponse() { + return this.httpResponse; + } } diff --git a/java-client/src/main/java/co/elastic/clients/elasticsearch/_types/ElasticsearchException.java b/java-client/src/main/java/co/elastic/clients/elasticsearch/_types/ElasticsearchException.java index 5307ed515..aa350d76b 100644 --- a/java-client/src/main/java/co/elastic/clients/elasticsearch/_types/ElasticsearchException.java +++ b/java-client/src/main/java/co/elastic/clients/elasticsearch/_types/ElasticsearchException.java @@ -19,6 +19,10 @@ package co.elastic.clients.elasticsearch._types; +import co.elastic.clients.transport.http.TransportHttpClient; + +import javax.annotation.Nullable; + /** * Exception thrown by API client methods when Elasticsearch could not accept or * process a request. @@ -31,11 +35,18 @@ public class ElasticsearchException extends RuntimeException { private final ErrorResponse response; private final String endpointId; + @Nullable + private final TransportHttpClient.Response httpResponse; - public ElasticsearchException(String endpointId, ErrorResponse response) { + public ElasticsearchException(String endpointId, ErrorResponse response, @Nullable TransportHttpClient.Response httpResponse) { super("[" + endpointId + "] failed: [" + response.error().type() + "] " + response.error().reason()); this.response = response; this.endpointId = endpointId; + this.httpResponse = httpResponse; + } + + public ElasticsearchException(String endpointId, ErrorResponse response) { + this(endpointId, response, null); } /** @@ -66,4 +77,12 @@ public ErrorCause error() { public int status() { return this.response.status(); } + + /** + * The underlying http response, if available. + */ + @Nullable + public TransportHttpClient.Response httpResponse() { + return this.httpResponse; + } } diff --git a/java-client/src/main/java/co/elastic/clients/elasticsearch/_types/FieldValue.java b/java-client/src/main/java/co/elastic/clients/elasticsearch/_types/FieldValue.java index e68f12532..9267fae3c 100644 --- a/java-client/src/main/java/co/elastic/clients/elasticsearch/_types/FieldValue.java +++ b/java-client/src/main/java/co/elastic/clients/elasticsearch/_types/FieldValue.java @@ -33,6 +33,7 @@ import co.elastic.clients.util.ObjectBuilderBase; import co.elastic.clients.util.TaggedUnion; import co.elastic.clients.util.TaggedUnionUtils; +import jakarta.json.Json; import jakarta.json.stream.JsonGenerator; import jakarta.json.stream.JsonParser; @@ -69,6 +70,10 @@ public static FieldValue of(JsonData value) { return new FieldValue(Kind.Any, value); } + public static FieldValue of(Object value) { + return of(JsonData.of(value)); + } + public static final FieldValue NULL = new FieldValue(Kind.Null, null); public static final FieldValue TRUE = new FieldValue(Kind.Boolean, Boolean.TRUE); public static final FieldValue FALSE = new FieldValue(Kind.Boolean, Boolean.FALSE); diff --git a/java-client/src/main/java/co/elastic/clients/json/BufferingJsonGenerator.java b/java-client/src/main/java/co/elastic/clients/json/BufferingJsonGenerator.java new file mode 100644 index 000000000..7152c0290 --- /dev/null +++ b/java-client/src/main/java/co/elastic/clients/json/BufferingJsonGenerator.java @@ -0,0 +1,38 @@ +/* + * 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.json; + +import jakarta.json.stream.JsonGenerator; +import jakarta.json.stream.JsonParser; + +public interface BufferingJsonGenerator extends JsonGenerator { + + /** + * Close this generator and return the buffered content. + */ + JsonData getJsonData(); + + /** + * Close this generator and return the buffered content as a parser. + */ + JsonParser getParser(); + + void copyValue(JsonParser parser); +} diff --git a/java-client/src/main/java/co/elastic/clients/json/BufferingJsonpMapper.java b/java-client/src/main/java/co/elastic/clients/json/BufferingJsonpMapper.java new file mode 100644 index 000000000..628455423 --- /dev/null +++ b/java-client/src/main/java/co/elastic/clients/json/BufferingJsonpMapper.java @@ -0,0 +1,28 @@ +/* + * 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.json; + +/** + * A Jsonp mapper that has the additional capability of being able to buffer events. + */ +public interface BufferingJsonpMapper extends JsonpMapper { + + BufferingJsonGenerator createBufferingGenerator(); +} diff --git a/java-client/src/main/java/co/elastic/clients/json/DelegatingJsonpMapper.java b/java-client/src/main/java/co/elastic/clients/json/DelegatingJsonpMapper.java index c43aabf72..e8626304f 100644 --- a/java-client/src/main/java/co/elastic/clients/json/DelegatingJsonpMapper.java +++ b/java-client/src/main/java/co/elastic/clients/json/DelegatingJsonpMapper.java @@ -44,6 +44,11 @@ public T deserialize(JsonParser parser, Type type) { return mapper.deserialize(parser, type); } + @Override + public T deserialize(JsonParser parser, Type type, JsonParser.Event event) { + return mapper.deserialize(parser, type, event); + } + @Override public void serialize(T value, JsonGenerator generator) { mapper.serialize(value, generator); diff --git a/java-client/src/main/java/co/elastic/clients/json/JsonpDeserializer.java b/java-client/src/main/java/co/elastic/clients/json/JsonpDeserializer.java index 03a76c562..51af1f999 100644 --- a/java-client/src/main/java/co/elastic/clients/json/JsonpDeserializer.java +++ b/java-client/src/main/java/co/elastic/clients/json/JsonpDeserializer.java @@ -110,7 +110,7 @@ public T deserialize(JsonParser parser, JsonpMapper mapper) { @Override public T deserialize(JsonParser parser, JsonpMapper mapper, JsonParser.Event event) { - throw new UnsupportedOperationException(); + return mapper.deserialize(parser, type, event); } }; } diff --git a/java-client/src/main/java/co/elastic/clients/json/JsonpMapper.java b/java-client/src/main/java/co/elastic/clients/json/JsonpMapper.java index e3b02c4de..880b7d6c0 100644 --- a/java-client/src/main/java/co/elastic/clients/json/JsonpMapper.java +++ b/java-client/src/main/java/co/elastic/clients/json/JsonpMapper.java @@ -54,6 +54,18 @@ default T deserialize(JsonParser parser, Class clazz) { */ T deserialize(JsonParser parser, Type type); + /** + * Deserialize an object, given its class and the current event the parser is at. + */ + default T deserialize(JsonParser parser, Class clazz, JsonParser.Event event) { + return deserialize(parser, (Type)clazz, event); + } + + /** + * Deserialize an object, given its type and the current event the parser is at. + */ + T deserialize(JsonParser parser, Type type, JsonParser.Event event); + /** * Serialize an object. */ diff --git a/java-client/src/main/java/co/elastic/clients/json/JsonpMapperBase.java b/java-client/src/main/java/co/elastic/clients/json/JsonpMapperBase.java index 5f1f2594a..963b46bff 100644 --- a/java-client/src/main/java/co/elastic/clients/json/JsonpMapperBase.java +++ b/java-client/src/main/java/co/elastic/clients/json/JsonpMapperBase.java @@ -61,23 +61,44 @@ protected JsonpMapperBase addAttribute(String name, Object value) { return this; } + //----- Deserialization + @Override public T deserialize(JsonParser parser, Type type) { + @SuppressWarnings("unchecked") + T result = (T)getDeserializer(type).deserialize(parser, this); + return result; + } + + @Override + public T deserialize(JsonParser parser, Type type, JsonParser.Event event) { + @SuppressWarnings("unchecked") + T result = (T)getDeserializer(type).deserialize(parser, this, event); + return result; + } + + private JsonpDeserializer getDeserializer(Type type) { JsonpDeserializer deserializer = findDeserializer(type); if (deserializer != null) { - return deserializer.deserialize(parser, this); + return deserializer; } @SuppressWarnings("unchecked") - T result = (T)getDefaultDeserializer(type).deserialize(parser, this); + JsonpDeserializer result = getDefaultDeserializer(type); return result; } + /** + * Find a built-in deserializer for a given class, if any. + */ @Nullable public static JsonpDeserializer findDeserializer(Class clazz) { return findDeserializer((Type)clazz); } + /** + * Find a built-in deserializer for a given type, if any. + */ @Nullable @SuppressWarnings("unchecked") public static JsonpDeserializer findDeserializer(Type type) { @@ -101,6 +122,8 @@ public static JsonpDeserializer findDeserializer(Type type) { return null; } + //----- Serialization + @Nullable @SuppressWarnings("unchecked") public static JsonpSerializer findSerializer(T value) { diff --git a/java-client/src/main/java/co/elastic/clients/json/JsonpUtils.java b/java-client/src/main/java/co/elastic/clients/json/JsonpUtils.java index fff4b148d..4eea2d430 100644 --- a/java-client/src/main/java/co/elastic/clients/json/JsonpUtils.java +++ b/java-client/src/main/java/co/elastic/clients/json/JsonpUtils.java @@ -412,10 +412,10 @@ public void close() { return dest; } - public static String toJsonString(JsonpSerializable value, JsonpMapper mapper) { + public static String toJsonString(Object value, JsonpMapper mapper) { StringWriter writer = new StringWriter(); JsonGenerator generator = mapper.jsonProvider().createGenerator(writer); - value.serialize(generator, mapper); + mapper.serialize(value, generator); generator.close(); return writer.toString(); } diff --git a/java-client/src/main/java/co/elastic/clients/json/jackson/JacksonJsonpGenerator.java b/java-client/src/main/java/co/elastic/clients/json/jackson/JacksonJsonpGenerator.java index 5f04bcae8..664d3ae9a 100644 --- a/java-client/src/main/java/co/elastic/clients/json/jackson/JacksonJsonpGenerator.java +++ b/java-client/src/main/java/co/elastic/clients/json/jackson/JacksonJsonpGenerator.java @@ -19,12 +19,16 @@ package co.elastic.clients.json.jackson; +import co.elastic.clients.json.BufferingJsonGenerator; +import co.elastic.clients.json.JsonData; import com.fasterxml.jackson.core.JsonStreamContext; +import com.fasterxml.jackson.databind.util.TokenBuffer; import jakarta.json.JsonNumber; import jakarta.json.JsonString; import jakarta.json.JsonValue; import jakarta.json.stream.JsonGenerationException; import jakarta.json.stream.JsonGenerator; +import jakarta.json.stream.JsonParser; import java.io.IOException; import java.math.BigDecimal; @@ -42,6 +46,43 @@ public JacksonJsonpGenerator(com.fasterxml.jackson.core.JsonGenerator generator) this.generator = generator; } + public static class Buffering extends JacksonJsonpGenerator implements BufferingJsonGenerator { + + private final JacksonJsonpMapper mapper; + + public Buffering(JacksonJsonpMapper mapper) { + super(new TokenBuffer(mapper.objectMapper(), false)); + this.mapper = mapper; + } + + @Override + public JsonData getJsonData() { + this.close(); + return new JacksonJsonBuffer((TokenBuffer)jacksonGenerator(), mapper); + } + + @Override + public JsonParser getParser() { + this.close(); + TokenBuffer tokenBuffer = (TokenBuffer) jacksonGenerator(); + return new JacksonJsonpParser(tokenBuffer.asParser(), mapper); + } + + @Override + public void copyValue(JsonParser parser) { + if (!(parser instanceof JacksonJsonpGenerator)) { + throw new IllegalArgumentException("Can only be used with a JacksonJsonpGenerator"); + } + + com.fasterxml.jackson.core.JsonParser jkParser = ((JacksonJsonpParser) parser).jacksonParser(); + try { + jacksonGenerator().copyCurrentStructure(jkParser); + } catch (IOException e) { + throw JacksonUtils.convertException(e); + } + } + } + /** * Returns the underlying Jackson generator. */ 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 6df9c6c85..84841afee 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 @@ -19,12 +19,15 @@ package co.elastic.clients.json.jackson; +import co.elastic.clients.json.BufferingJsonGenerator; +import co.elastic.clients.json.BufferingJsonpMapper; import co.elastic.clients.json.JsonpDeserializer; import co.elastic.clients.json.JsonpDeserializerBase; import co.elastic.clients.json.JsonpMapper; import co.elastic.clients.json.JsonpMapperBase; import co.elastic.clients.json.JsonpSerializer; import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import jakarta.json.spi.JsonProvider; @@ -35,18 +38,20 @@ import java.lang.reflect.Type; import java.util.EnumSet; -public class JacksonJsonpMapper extends JsonpMapperBase { +public class JacksonJsonpMapper extends JsonpMapperBase implements BufferingJsonpMapper { private final JacksonJsonProvider provider; private final ObjectMapper objectMapper; private JacksonJsonpMapper(ObjectMapper objectMapper, JacksonJsonProvider provider) { + // No need to configure here, as this constructor is only called with the objectMapper + // of an existing JacksonJsonpMapper, and has therefore alredy been configured. this.objectMapper = objectMapper; this.provider = provider; } public JacksonJsonpMapper(ObjectMapper objectMapper) { - this.objectMapper = objectMapper; + this.objectMapper = configure(objectMapper); // Order is important as JacksonJsonProvider(this) will get ObjectMapper this.provider = new JacksonJsonProvider(this); } @@ -58,6 +63,13 @@ public JacksonJsonpMapper() { ); } + private static ObjectMapper configure(ObjectMapper objectMapper) { + // Accept single objects as collections. This is useful in the context of Elasticsearch since + // Lucene has no concept of multivalued field and fields with a single value will be returned + // as a single object even if other instances of the same field have multiple values. + return objectMapper.enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY); + } + @Override public JsonpMapper withAttribute(String name, T value) { return new JacksonJsonpMapper(this.objectMapper, this.provider).addAttribute(name, value); @@ -101,6 +113,11 @@ public void serialize(T value, JsonGenerator generator) { } } + @Override + public BufferingJsonGenerator createBufferingGenerator() { + return new JacksonJsonpGenerator.Buffering(this); + } + private class JacksonValueParser extends JsonpDeserializerBase { private final Type type; 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 dcc316189..7d1e98608 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 @@ -342,7 +342,7 @@ private ResponseT getApiResponse( try (JsonParser parser = mapper.jsonProvider().createParser(content)) { ErrorT error = errorDeserializer.deserialize(parser, mapper); // TODO: have the endpoint provide the exception constructor - throw new ElasticsearchException(endpoint.id(), (ErrorResponse) error); + throw new ElasticsearchException(endpoint.id(), (ErrorResponse) error, clientResp); } } catch(JsonException | MissingRequiredPropertyException errorEx) { // Could not decode exception, try the response type diff --git a/java-client/src/main/java/co/elastic/clients/transport/Endpoint.java b/java-client/src/main/java/co/elastic/clients/transport/Endpoint.java index 3ed47254d..e9b0c11d0 100644 --- a/java-client/src/main/java/co/elastic/clients/transport/Endpoint.java +++ b/java-client/src/main/java/co/elastic/clients/transport/Endpoint.java @@ -19,12 +19,15 @@ package co.elastic.clients.transport; +import co.elastic.clients.ApiClient; import co.elastic.clients.json.JsonpDeserializer; import co.elastic.clients.transport.endpoints.BinaryEndpoint; import javax.annotation.Nullable; +import java.io.IOException; import java.util.Collections; import java.util.Map; +import java.util.concurrent.CompletableFuture; /** * An endpoint links requests and responses to HTTP protocol encoding. It also defines the error response @@ -118,4 +121,20 @@ default BinaryEndpoint withBinaryResponse() { null ); } + + default ResponseT call(RequestT request, Transport transport) throws IOException { + return transport.performRequest(request, this, null); + } + + default ResponseT call(RequestT request, ApiClient client) throws IOException { + return client._transport().performRequest(request, this, null); + } + + default CompletableFuture callAsync(RequestT request, Transport transport) throws IOException { + return transport.performRequestAsync(request, this, null); + } + + default CompletableFuture callAsync(RequestT request, ApiClient client) throws IOException { + return client._transport().performRequestAsync(request, this, null); + } } diff --git a/java-client/src/main/java/co/elastic/clients/transport/endpoints/BinaryDataResponse.java b/java-client/src/main/java/co/elastic/clients/transport/endpoints/BinaryDataResponse.java index 937edc10f..f75662a25 100644 --- a/java-client/src/main/java/co/elastic/clients/transport/endpoints/BinaryDataResponse.java +++ b/java-client/src/main/java/co/elastic/clients/transport/endpoints/BinaryDataResponse.java @@ -20,6 +20,7 @@ package co.elastic.clients.transport.endpoints; import co.elastic.clients.util.BinaryData; +import co.elastic.clients.util.ByteArrayBinaryData; import java.io.IOException; import java.io.InputStream; @@ -50,4 +51,8 @@ public InputStream content() throws IOException { @Override public void close() throws IOException { } + + public static BinaryDataResponse of(byte[] data, String contentType) { + return new BinaryDataResponse(new ByteArrayBinaryData(data, contentType)); + } } diff --git a/java-client/src/main/java/co/elastic/clients/util/ByteArrayBinaryData.java b/java-client/src/main/java/co/elastic/clients/util/ByteArrayBinaryData.java index 6509e77ab..9641b013f 100644 --- a/java-client/src/main/java/co/elastic/clients/util/ByteArrayBinaryData.java +++ b/java-client/src/main/java/co/elastic/clients/util/ByteArrayBinaryData.java @@ -43,14 +43,14 @@ public class ByteArrayBinaryData implements BinaryData { private final int length; private final String contentType; - ByteArrayBinaryData(byte[] bytes, int offset, int length, String contentType) { + public ByteArrayBinaryData(byte[] bytes, int offset, int length, String contentType) { this.contentType = contentType; this.bytes = bytes; this.offset = offset; this.length = length; } - ByteArrayBinaryData(byte[] bytes, String contentType) { + public ByteArrayBinaryData(byte[] bytes, String contentType) { this.contentType = contentType; this.bytes = bytes; this.offset = 0; diff --git a/java-client/src/test/java/co/elastic/clients/elasticsearch/ElasticsearchTestServer.java b/java-client/src/test/java/co/elastic/clients/elasticsearch/ElasticsearchTestServer.java index 037fc028d..498bce9ae 100644 --- a/java-client/src/test/java/co/elastic/clients/elasticsearch/ElasticsearchTestServer.java +++ b/java-client/src/test/java/co/elastic/clients/elasticsearch/ElasticsearchTestServer.java @@ -36,11 +36,17 @@ import org.elasticsearch.client.RestClient; import org.testcontainers.elasticsearch.ElasticsearchContainer; import org.testcontainers.images.builder.ImageFromDockerfile; +import org.testcontainers.shaded.org.apache.commons.io.IOUtils; import org.testcontainers.utility.DockerImageName; import javax.net.ssl.SSLContext; import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.charset.StandardCharsets; import java.time.Duration; +import java.util.Base64; public class ElasticsearchTestServer implements AutoCloseable { @@ -55,9 +61,30 @@ public class ElasticsearchTestServer implements AutoCloseable { public static synchronized ElasticsearchTestServer global() { if (global == null) { - System.out.println("Starting global ES test server."); - global = new ElasticsearchTestServer(); - global.start(); + + // Try localhost:9200 + try { + String localUrl = "http://localhost:9200"; + HttpURLConnection connection = (HttpURLConnection) new URL(localUrl).openConnection(); + connection.setRequestProperty("Authorization", "Basic " + + Base64.getEncoder().encodeToString("elastic:changeme".getBytes(StandardCharsets.UTF_8))); + + try (InputStream input = connection.getInputStream()) { + String content = IOUtils.toString(input, StandardCharsets.UTF_8); + if (content.contains("You Know, for Search")) { + System.out.println("Found a running ES server at http://localhost:9200/"); + + global = new ElasticsearchTestServer(); + global.setup(localUrl, null); + } + } + } catch (Exception e) { + // Create container + System.out.println("Starting global ES test server."); + global = new ElasticsearchTestServer(); + global.start(); + } + Runtime.getRuntime().addShutdownHook(new Thread(() -> { System.out.println("Stopping global ES test server."); global.close(); @@ -70,8 +97,27 @@ public ElasticsearchTestServer(String... plugins) { this.plugins = plugins; } + protected void setup(String url, SSLContext sslContext) { + BasicCredentialsProvider credsProv = new BasicCredentialsProvider(); + credsProv.setCredentials( + AuthScope.ANY, new UsernamePasswordCredentials("elastic", "changeme") + ); + restClient = RestClient.builder(HttpHost.create(url)) + .setHttpClientConfigCallback(hc -> hc + .setDefaultCredentialsProvider(credsProv) + .setSSLContext(sslContext) + ) + .build(); + transport = new RestClientTransport(restClient, mapper); + client = new ElasticsearchClient(transport); + } + public synchronized ElasticsearchTestServer start() { - Version version = Version.VERSION.major() < 8 ? new Version(7,17,5,false) : new Version(8,3,3,false); + if (this.client != null) { + return this; + } + + Version version = Version.VERSION.major() < 8 ? new Version(7,17,5,false) : new Version(8,12,0,false); // Note we could use version.major() + "." + version.minor() + "-SNAPSHOT" but plugins won't install on a snapshot version String esImage = "docker.elastic.co/elasticsearch/elasticsearch:" + version; @@ -101,25 +147,12 @@ public synchronized ElasticsearchTestServer start() { .withPassword("changeme"); container.start(); - int port = container.getMappedPort(9200); - boolean useTLS = version.major() >= 8; - HttpHost host = new HttpHost("localhost", port, useTLS ? "https": "http"); SSLContext sslContext = useTLS ? container.createSslContextFromCa() : null; + String url = (useTLS ? "https://" : "http://") + container.getHttpHostAddress(); - BasicCredentialsProvider credsProv = new BasicCredentialsProvider(); - credsProv.setCredentials( - AuthScope.ANY, new UsernamePasswordCredentials("elastic", "changeme") - ); - restClient = RestClient.builder(host) - .setHttpClientConfigCallback(hc -> hc - .setDefaultCredentialsProvider(credsProv) - .setSSLContext(sslContext) - ) - .build(); - transport = new RestClientTransport(restClient, mapper); - client = new ElasticsearchClient(transport); + setup(url, sslContext); return this; } diff --git a/java-client/src/test/java/co/elastic/clients/json/jackson/JacksonMapperTest.java b/java-client/src/test/java/co/elastic/clients/json/jackson/JacksonMapperTest.java index e4bb42bc4..56d42d7ac 100644 --- a/java-client/src/test/java/co/elastic/clients/json/jackson/JacksonMapperTest.java +++ b/java-client/src/test/java/co/elastic/clients/json/jackson/JacksonMapperTest.java @@ -23,14 +23,19 @@ import co.elastic.clients.testkit.ModelTestCase; import co.elastic.clients.json.JsonpDeserializer; import co.elastic.clients.json.JsonpMapper; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.JsonDeserializer; import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonNaming; import org.junit.jupiter.api.Test; import java.io.IOException; +import java.util.Collections; +import java.util.List; public class JacksonMapperTest extends ModelTestCase { @@ -72,4 +77,26 @@ public TestData deserialize(JsonParser jp, DeserializationContext ctx) throws IO } } } + + @Test + public void testSingleValueAsList() { + JsonpMapper jsonpMapper = new JacksonJsonpMapper(); + + String json = "{\"_index\":\"foo\",\"_id\":\"1\",\"_source\":{\"emp_no\":42,\"job_positions\":\"SWE\"}}"; + + Hit testDataHit = fromJson(json, + Hit.createHitDeserializer(JsonpDeserializer.of(EmpData.class)), + jsonpMapper + ); + EmpData data = testDataHit.source(); + assertEquals(42, data.empNo); + assertEquals(Collections.singletonList("SWE"), data.jobPositions); + } + + @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) + @JsonIgnoreProperties(ignoreUnknown = true) + public static class EmpData { + public int empNo; + public List jobPositions; + } } diff --git a/java-client/src/test/java/co/elastic/clients/testkit/MockHttpClient.java b/java-client/src/test/java/co/elastic/clients/testkit/MockHttpClient.java new file mode 100644 index 000000000..cc2c0e878 --- /dev/null +++ b/java-client/src/test/java/co/elastic/clients/testkit/MockHttpClient.java @@ -0,0 +1,156 @@ +/* + * 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.testkit; + +import co.elastic.clients.elasticsearch.ElasticsearchClient; +import co.elastic.clients.json.JsonpMapper; +import co.elastic.clients.transport.ElasticsearchTransportBase; +import co.elastic.clients.transport.TransportException; +import co.elastic.clients.transport.TransportOptions; +import co.elastic.clients.transport.http.TransportHttpClient; +import co.elastic.clients.util.BinaryData; +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; + +public class MockHttpClient implements TransportHttpClient { + + private static final Response NotFound = new MockResponse(404, null); + + Map responses = new ConcurrentHashMap<>(); + + public MockHttpClient add(String path, String contentType, byte[] data) { + responses.put(path, new MockResponse(200, BinaryData.of(data, contentType))); + return this; + } + + public MockHttpClient add(String path, String contentType, String text) { + responses.put(path, new MockResponse(200, BinaryData.of(text.getBytes(StandardCharsets.UTF_8), contentType))); + return this; + } + + public ElasticsearchClient client() { + return client(new ModelTestCase() {}.mapper); + } + + public ElasticsearchClient client(JsonpMapper mapper) { + return new ElasticsearchClient(new ElasticsearchTransportBase(this, null, mapper) { + @Override + public void close() throws IOException { + super.close(); + } + }); + } + + + @Override + public Response performRequest( + String endpointId, @Nullable TransportHttpClient.Node node, Request request, TransportOptions option + ) throws IOException { + Response response = responses.get(request.path()); + + if (response == null) { + throw new TransportException(NotFound, "Not found", endpointId); + } + + return response; + } + + @Override + public CompletableFuture performRequestAsync( + String endpointId, @Nullable TransportHttpClient.Node node, Request request, TransportOptions options + ) { + CompletableFuture result = new CompletableFuture<>(); + try { + Response response = performRequest(endpointId, node, request, options); + result.complete(response); + } catch (Exception e) { + result.completeExceptionally(e); + } + return result; + } + + @Override + public void close() throws IOException { + } + + private static class MockResponse implements TransportHttpClient.Response { + + private final int statusCode; + private final BinaryData body; + private final Map headers; + + MockResponse(int statusCode, BinaryData body) { + this.statusCode = statusCode; + this.headers = new HashMap<>(); + this.body = body; + + if (body != null) { + headers.put("content-type", body.contentType()); + } + headers.put("x-elastic-product", "Elasticsearch"); + } + + @Override + public Node node() { + return null; + } + + @Override + public int statusCode() { + return statusCode; + } + + @Nullable + @Override + public String header(String name) { + return headers.get(name.toLowerCase()); + } + + @Override + public List headers(String name) { + String header = header(name); + return header == null ? null : Collections.singletonList(header); + } + + @Nullable + @Override + public BinaryData body() throws IOException { + return body; + } + + @Nullable + @Override + public Object originalResponse() { + return null; + } + + @Override + public void close() throws IOException { + } + } +}