diff --git a/java-client/src/main/java/co/elastic/clients/base/AcceptType.java b/java-client/src/main/java/co/elastic/clients/base/AcceptType.java new file mode 100644 index 000000000..614d5b1cf --- /dev/null +++ b/java-client/src/main/java/co/elastic/clients/base/AcceptType.java @@ -0,0 +1,47 @@ +/* + * 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.base; + +/** + * Wraps a {@link MediaType} in the context of an HTTP + * Accept header. + */ +public class AcceptType implements ConvertibleToHeader { + + public static AcceptType forMediaType(MediaType mediaType) { + return new AcceptType(mediaType); + } + + private final MediaType mediaType; + + private AcceptType(MediaType mediaType) { + this.mediaType = mediaType; + } + + public MediaType mediaType() { + return mediaType; + } + + @Override + public Header toHeader() { + return Header.raw("Accept", mediaType); + } + +} diff --git a/java-client/src/main/java/co/elastic/clients/base/ClientMetadata.java b/java-client/src/main/java/co/elastic/clients/base/ClientMetadata.java new file mode 100644 index 000000000..3a6b3f0a0 --- /dev/null +++ b/java-client/src/main/java/co/elastic/clients/base/ClientMetadata.java @@ -0,0 +1,276 @@ +/* + * 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.base; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; + +/** + * This class models a set of client metadata, including client + * version, Java platform version and {@link Transport} version. This + * information is typically compiled into a header field called + * {@code X-Elastic-Client-Meta} and sent to the server with each request. + * + * TODO: optional fields beyond "es", "jv" and "t" + * + * @see + * Structured HTTP Header for Client Metadata + */ +public class ClientMetadata implements ConvertibleToHeader { + + /** + * Location of the properties file containing project + * version metadata. + */ + private static final String VERSION_PROPERTIES = "/co.elastic.clients.elasticsearch/version.properties"; + + /** + * Empty static instance of {@link ClientMetadata} used to + * disable sending the X-Elastic-Client-Meta header. + */ + public static final ClientMetadata EMPTY = new ClientMetadata(); + + /** + * Construct an instance of {@link ClientMetadata} containing + * versions of components currently in use. This will be the + * method generally used in a production context for + * obtaining an instance of this class. + * + * @return {@link ClientMetadata} instance + */ + public static ClientMetadata forLocalSystem() { + Version clientVersion = getClientVersion(); + return new Builder() + .withClientVersion(clientVersion) + .withJavaVersion(getJavaVersion()) + .withTransportVersion(clientVersion) + .build(); + } + + /** + * Builder for constructing {@link ClientMetadata} instances. + * This exists mainly for use in a non-production context. + */ + public static class Builder { + + private Version clientVersion; + private Version javaVersion; + private Version transportVersion; + + public Builder() { + clientVersion = null; + javaVersion = null; + transportVersion = null; + } + + public Builder withClientVersion(Version version) { + clientVersion = version; + return this; + } + + public Builder withJavaVersion(Version version) { + javaVersion = version; + return this; + } + + public Builder withTransportVersion(Version version) { + transportVersion = version; + return this; + } + + public ClientMetadata build() { + return new ClientMetadata( + clientVersion, + javaVersion, + transportVersion); + } + + } + + private final Version clientVersion; + private final Version javaVersion; + private final Version transportVersion; + + /** + * The class constructor is private, as it is intended for + * instances to be constructed via the {@link Builder} or + * the {@link #forLocalSystem()} method. + * + * @param clientVersion {@link Version} of the client + * @param javaVersion {@link Version} of the Java platform + * @param transportVersion {@link Version} of {@link Transport} + */ + private ClientMetadata(Version clientVersion, Version javaVersion, Version transportVersion) { + if (clientVersion == null) { + throw new IllegalArgumentException("Client version may not be omitted from client metadata"); + } + else { + this.clientVersion = clientVersion; + } + if (javaVersion == null) { + throw new IllegalArgumentException("Java version may not be omitted from client metadata"); + } + else { + this.javaVersion = javaVersion; + } + if (transportVersion == null) { + throw new IllegalArgumentException("Transport version may not be omitted from client metadata"); + } + else { + this.transportVersion = transportVersion; + } + } + + /** + * Separate constructor used for the {@link #EMPTY} instance. + */ + private ClientMetadata() { + this.clientVersion = null; + this.javaVersion = null; + this.transportVersion = null; + } + + /** + * {@link Version} of the client represented by this metadata. + * + * @return Elasticsearch {@link Version} + */ + public Version clientVersion() { + return clientVersion; + } + + /** + * {@link Version} of the Java platform represented by this metadata. + * + * @return Java platform {@link Version} + */ + public Version javaVersion() { + return javaVersion; + } + + /** + * {@link Version} of {@link Transport} represented by this metadata. + * + * @return {@link Transport} {@link Version} + */ + public Version transportVersion() { + return transportVersion; + } + + @Override + public String toString() { + return String.join(",", pairStrings()); + } + + /** + * Construct a list of "key=value" strings for all + * non-null values. + * + * @return list of strings + */ + private List pairStrings() { + List bits = new ArrayList<>(); + if (clientVersion != null) { + bits.add("es=" + clientVersion); + } + if (javaVersion != null) { + bits.add("jv=" + javaVersion); + } + if (transportVersion != null) { + bits.add("t=" + transportVersion); + } + return bits; + } + + /** + * Convert this client metadata instance into a {@link Header} + * for inclusion in an HTTP request. + * + * The resulting {@link Header#value()} may be null, which denotes + * that metadata tracking should be disabled. + * + * @return {@code X-Elastic-Client-Meta} {@link Header} + */ + @Override + public Header toHeader() { + // According to the spec, "There must be at least one key-value + // pair if the header is added to a request. An empty header + // is not valid." + // + // To that end, if no key-value pairs have been populated, we + // return a null-valued header which will be excluded from the + // headers, disabling client metadata. + if (this.pairStrings().size() == 0) { + return Header.raw("X-Elastic-Client-Meta", null); + } + else { + return Header.raw("X-Elastic-Client-Meta", this); + } + } + + /** + * Fetch and return Java version information as a + * {@link Version} object. + * + * @return Java {@link Version} + */ + public static Version getJavaVersion() { + return Version.parse(System.getProperty("java.version")); + } + + /** + * Fetch and return Elasticsearch version information + * in raw string form. + * + * @return client version string + */ + public static String getClientVersionString() { + InputStream in = ApiClient.class.getResourceAsStream(VERSION_PROPERTIES); + if (in == null) { + // Failed to locate version.properties file + return null; + } + Properties properties = new Properties(); + try { + properties.load(in); + // This will return null if no version information is + // found in the version.properties file + return properties.getProperty("version"); + } catch (IOException e) { + // Failed to read version.properties file + return null; + } + } + + /** + * Fetch and return Elasticsearch version information + * as a {@link Version} object. + * + * @return Elasticsearch {@link Version} + */ + public static Version getClientVersion() { + String versionString = getClientVersionString(); + return versionString == null ? null : Version.parse(versionString); + } + +} diff --git a/java-client/src/main/java/co/elastic/clients/base/ContentType.java b/java-client/src/main/java/co/elastic/clients/base/ContentType.java new file mode 100644 index 000000000..c7a38f784 --- /dev/null +++ b/java-client/src/main/java/co/elastic/clients/base/ContentType.java @@ -0,0 +1,47 @@ +/* + * 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.base; + +/** + * Wraps a {@link MediaType} in the context of an HTTP + * Content-Type header. + */ +public class ContentType implements ConvertibleToHeader { + + public static ContentType forMediaType(MediaType mediaType) { + return new ContentType(mediaType); + } + + private final MediaType mediaType; + + private ContentType(MediaType mediaType) { + this.mediaType = mediaType; + } + + public MediaType mediaType() { + return mediaType; + } + + @Override + public Header toHeader() { + return Header.raw("Content-Type", mediaType); + } + +} diff --git a/java-client/src/main/java/co/elastic/clients/base/ConvertibleToHeader.java b/java-client/src/main/java/co/elastic/clients/base/ConvertibleToHeader.java new file mode 100644 index 000000000..d253238cf --- /dev/null +++ b/java-client/src/main/java/co/elastic/clients/base/ConvertibleToHeader.java @@ -0,0 +1,35 @@ +/* + * 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.base; + +/** + * Interface implemented by any classes whose instances + * can represent an HTTP header value, such as {@link UserAgent}. + */ +public interface ConvertibleToHeader { + + /** + * Convert this object into an HTTP header. + * + * @return {@link Header} instance + */ + Header toHeader(); + +} diff --git a/java-client/src/main/java/co/elastic/clients/base/Header.java b/java-client/src/main/java/co/elastic/clients/base/Header.java new file mode 100644 index 000000000..59f79555c --- /dev/null +++ b/java-client/src/main/java/co/elastic/clients/base/Header.java @@ -0,0 +1,58 @@ +/* + * 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.base; + +import co.elastic.clients.util.NamedString; + +import java.util.Locale; + +/** + * Raw HTTP header field, consisting of a string name and value. + */ +public class Header extends NamedString implements ConvertibleToHeader { + + /** + * Construct a raw header field. + * + * Header names are coerced to lower case and the + * {@link Object#toString()} method of the value is + * used to obtain the field value sent over the wire. + * + * By convention, a null value denotes that the header with that + * name is disabled, and will not be sent. + * + * @param name header field name + * @param value header field value + * @return new {@link Header} object + */ + public static Header raw(String name, Object value) { + return new Header(name, value == null ? null : value.toString()); + } + + private Header(String name, String value) { + super(name.toLowerCase(Locale.ROOT), value); + } + + @Override + public Header toHeader() { + return this; + } + +} diff --git a/java-client/src/main/java/co/elastic/clients/base/MediaType.java b/java-client/src/main/java/co/elastic/clients/base/MediaType.java new file mode 100644 index 000000000..4aedf40d1 --- /dev/null +++ b/java-client/src/main/java/co/elastic/clients/base/MediaType.java @@ -0,0 +1,128 @@ +/* + * 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.base; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +/** + * A media type as defined for use in Content-Type and + * Accept headers. Also historically known as a MIME type. + * + * Note that this class only has very limited support for the + * media types used by the Elasticsearch client. It is not usable + * as a general purpose utility class. + * + * @see Media Types at IANA + */ +public class MediaType { + + /** + * Construct an Elasticsearch vendor-specific media type. + * If the {@code ELASTIC_CLIENT_APIVERSIONING} environment + * variable is set to 1, this also appends a + * {@code compatible-with} parameter that points to the + * major client version. + * + * The base type is {@code application/vnd.elasticsearch+json}. + * + * @return new {@link MediaType} for ES-specific JSON + */ + public static MediaType vendorElasticsearchJSON() { + Map parameters = new HashMap<>(); + if (Objects.equals(System.getenv("ELASTIC_CLIENT_APIVERSIONING"), "1")) { + Version clientVersion = ClientMetadata.getClientVersion(); + if (clientVersion != null) { + parameters.put("compatible-with", clientVersion.major()); + } + } + return new MediaType("application", "vnd.elasticsearch+json", parameters); + } + + private final String type; + private final String subtype; + private final Map parameters; + + public MediaType(String type, String subtype, Map parameters) { + this.type = type; + this.subtype = subtype; + this.parameters = parameters; + } + + /** + * The top-level type, such as "text" or "application". + * + * @return type string + */ + public String type() { + return type; + } + + /** + * The subtype, such as "plain" or "json". + * + * @return subtype string + */ + public String subtype() { + return subtype; + } + + /** + * Map of parameters to append to the media type string, + * such as "charset=utf-8". + * + * @return map of parameter keys and values + */ + public Map parameters() { + return parameters; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof MediaType)) return false; + MediaType mediaType = (MediaType) o; + return (Objects.equals(type, mediaType.type) && + Objects.equals(subtype, mediaType.subtype) && + Objects.equals(parameters, mediaType.parameters)); + } + + @Override + public int hashCode() { + return Objects.hash(type, subtype, parameters); + } + + @Override + public String toString() { + StringBuilder s = new StringBuilder(); + s.append(type); + s.append('/'); + s.append(subtype); + for (Map.Entry entry : parameters.entrySet()) { + s.append("; "); + s.append(entry.getKey()); + s.append('='); + s.append(entry.getValue()); + } + return s.toString(); + } + +} diff --git a/java-client/src/main/java/co/elastic/clients/base/OpaqueID.java b/java-client/src/main/java/co/elastic/clients/base/OpaqueID.java new file mode 100644 index 000000000..ad41998cc --- /dev/null +++ b/java-client/src/main/java/co/elastic/clients/base/OpaqueID.java @@ -0,0 +1,57 @@ +/* + * 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.base; + +import java.util.Objects; + +/** + * A user-specified Opaque ID as used in the X-Opaque-ID header. + */ +public class OpaqueID implements ConvertibleToHeader { + + private final Object value; + + public OpaqueID(Object value) { + this.value = value; + } + + public Object value() { + return value; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof OpaqueID)) return false; + OpaqueID opaqueID = (OpaqueID) o; + return Objects.equals(value, opaqueID.value); + } + + @Override + public int hashCode() { + return Objects.hash(value); + } + + @Override + public Header toHeader() { + return Header.raw("X-Opaque-ID", value); + } + +} diff --git a/java-client/src/main/java/co/elastic/clients/base/QueryParameter.java b/java-client/src/main/java/co/elastic/clients/base/QueryParameter.java new file mode 100644 index 000000000..ea8f4637c --- /dev/null +++ b/java-client/src/main/java/co/elastic/clients/base/QueryParameter.java @@ -0,0 +1,50 @@ +/* + * 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.base; + +import co.elastic.clients.util.NamedString; + +/** + * Raw URI query parameter, consisting of a string name and value. + */ +public class QueryParameter extends NamedString { + + /** + * Construct a raw URI query parameter. + * + * The {@link Object#toString()} method of the value passed is + * used to obtain the field value sent over the wire. + * + * By convention, a null value denotes that the parameter with that + * name is disabled, and will not be sent. + * + * @param name query parameter name + * @param value query parameter value + * @return new {@link QueryParameter} object + */ + public static QueryParameter raw(String name, Object value) { + return new QueryParameter(name, value == null ? null : value.toString()); + } + + private QueryParameter(String name, String value) { + super(name, value); + } + +} diff --git a/java-client/src/main/java/co/elastic/clients/base/RequestOptions.java b/java-client/src/main/java/co/elastic/clients/base/RequestOptions.java new file mode 100644 index 000000000..c1fab4703 --- /dev/null +++ b/java-client/src/main/java/co/elastic/clients/base/RequestOptions.java @@ -0,0 +1,235 @@ +/* + * 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.base; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +/** + * Container for all application-specific or request-specific + * options, including headers, query parameters, timeouts and + * warning handlers. + * + * This class is not publicly constructable. Instead, users + * should use the {@link #DEFAULT} instance either directly or + * as a basis for a {@link Builder} via the {@link #toBuilder()} + * method. + */ +public class RequestOptions { + + public static final RequestOptions DEFAULT = new RequestOptions( + + // Default headers + Arrays.asList( + AcceptType.forMediaType(MediaType.vendorElasticsearchJSON()).toHeader(), + ContentType.forMediaType(MediaType.vendorElasticsearchJSON()).toHeader(), + ClientMetadata.forLocalSystem().toHeader(), + UserAgent.DEFAULT.toHeader() + ), + + // Default query parameters + Collections.emptyList(), + + // Timeout + null, + + // Warning callback + null + + ); + + /** + * Builder for constructing {@link RequestOptions} instances. + * This is typically obtained via the {@link #toBuilder()} + * method, i.e. {@code RequestOptions.DEFAULT.toBuilder()}. + */ + public static class Builder { + + private final Map headers; + private final Map queryParameters; + private Duration timeout; + private Consumer> onWarning; + + private Builder() { + this.headers = new HashMap<>(); + this.queryParameters = new HashMap<>(); + this.timeout = null; + this.onWarning = null; + } + + /** + * Add a {@link Header}. + * + * {@link Header} instances can be constructed via a {@code toHeader} + * method, such as {@code UserAgent.DEFAULT.toHeader()} or by using + * the {@code Header.raw(...)} factory method. + * + * @param header {@link Header} to add + * @return this {@link Builder} instance (for chaining) + */ + public Builder withHeader(Header header) { + headers.put(header.name(), header); + return this; + } + + public Builder withQueryParameter(QueryParameter parameter) { + queryParameters.put(parameter.name(), parameter); + return this; + } + + public Builder withTimeout(Duration value) { + timeout = value; + return this; + } + + public Builder withWarningHandler(Consumer> callback) { + onWarning = callback; + return this; + } + + /** + * Return a {@link List} of all {@link Header} objects, + * including those with null values. + * + * @return {@link List} of {@link Header} objects + */ + public List
headers() { + return new ArrayList<>(headers.values()); + } + + /** + * Return a {@link List} of all {@link QueryParameter} + * objects, including those with null values. + * + * @return {@link List} of {@link QueryParameter} objects + */ + public List queryParameters() { + return new ArrayList<>(queryParameters.values()); + } + + public Duration timeout() { + return timeout; + } + + public Consumer> onWarning() { + return onWarning; + } + + public RequestOptions build() { + return new RequestOptions(headers.values(), queryParameters.values(), timeout, onWarning); + } + + } + + private final Map headers; + private final Map queryParameters; + private final Duration timeout; + private final Consumer> onWarning; + + private RequestOptions(Iterable
headers, + Iterable queryParameters, + Duration timeout, + Consumer> onWarning) { + this.headers = new HashMap<>(); + headers.forEach(header -> this.headers.put(header.name(), header)); + this.queryParameters = new HashMap<>(); + queryParameters.forEach(parameter -> this.queryParameters.put(parameter.name(), parameter)); + this.timeout = timeout; + this.onWarning = onWarning; + } + + + /** + * Return all headers with a non-null value. + * + * Internally, headers may contain null values, which can be used to + * "silence" features such as tracking. While this information needs to + * be propagated through the {@link Builder} process, it is not required + * in the final compiled list of headers, which this method provides + * access to. + * + * To access the full list of headers, including null-valued headers, + * first convert to a {@link Builder}, e.g.: + * {@code List
allHeaders = options.toBuilder().headers();} + * + * @return list of {@link Header} objects + */ + public List
headers() { + return headers.values().stream().filter(header -> + header.value() != null).collect(Collectors.toList()); + } + + /** + * Return all query parameters with a non-null value. + * + * Internally, parameters may contain null values, which can be used to + * "silence" features if required. While this information needs to + * be propagated through the {@link Builder} process, it is not required + * in the final compiled list of parameters, which this method provides + * access to. + * + * To access the full list of parameters, including null-valued parameters, + * first convert to a {@link Builder}, e.g.: + * {@code List allParameters = options.toBuilder().queryParameters();} + * + * @return list of {@link QueryParameter} objects + */ + public List queryParameters() { + return queryParameters.values().stream().filter(parameter -> + parameter.value() != null).collect(Collectors.toList()); + } + + public Duration timeout() { + return timeout; + } + + public Consumer> onWarning() { + return onWarning; + } + + /** + * Obtain a {@link Builder} instance based on this object, copying + * across all contained values. + * + * @return new {@link Builder} instance + */ + public Builder toBuilder() { + Builder builder = new Builder(); + // Use headers instead of headers() for full list + headers.values().forEach(builder::withHeader); + // Use queryParameters instead of queryParameters() for full list + queryParameters.values().forEach(builder::withQueryParameter); + if (timeout != null) { + builder.withTimeout(timeout); + } + if (onWarning != null) { + builder.withWarningHandler(onWarning); + } + return builder; + } + +} diff --git a/java-client/src/main/java/co/elastic/clients/base/Transport.java b/java-client/src/main/java/co/elastic/clients/base/Transport.java index 6685e8b5e..a18e6c8ec 100644 --- a/java-client/src/main/java/co/elastic/clients/base/Transport.java +++ b/java-client/src/main/java/co/elastic/clients/base/Transport.java @@ -23,6 +23,7 @@ import java.io.Closeable; import java.io.IOException; +import java.util.Map; import java.util.concurrent.CompletableFuture; /** @@ -41,4 +42,21 @@ CompletableFuture performRequestAsync( ); JsonpMapper jsonpMapper(); + + /** + * Get a map of key-value pairs representing the base headers + * used by all requests going via this transport. + * + * @return Map of header key-value pairs + */ + Map headers(); + + /** + * Get a map of key-value pairs representing the base query + * parameters used by all requests going via this transport. + * + * @return Map of query parameter key-value pairs + */ + Map queryParameters(); + } diff --git a/java-client/src/main/java/co/elastic/clients/base/UserAgent.java b/java-client/src/main/java/co/elastic/clients/base/UserAgent.java new file mode 100644 index 000000000..ecbab35a5 --- /dev/null +++ b/java-client/src/main/java/co/elastic/clients/base/UserAgent.java @@ -0,0 +1,86 @@ +/* + * 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.base; + +import java.util.Collections; +import java.util.Map; + +/** + * Models a user agent, consisting of a name, version, + * and optional key-value metadata. + */ +public class UserAgent implements ConvertibleToHeader { + + static final String DEFAULT_NAME = "elasticsearch-java"; + + static final String DEFAULT_VERSION = ClientMetadata.getClientVersionString(); + + public static final UserAgent DEFAULT = new UserAgent(DEFAULT_NAME, DEFAULT_VERSION); + + private final String name; + private final String version; + private final Map metadata; + + public UserAgent(String name, String version, Map metadata) { + this.name = name; + this.version = version; + this.metadata = metadata; + } + + public UserAgent(String repoName, String version) { + this(repoName, version, Collections.emptyMap()); + } + + public String name() { + return name; + } + + public String version() { + return version; + } + + public Map metadata() { + return metadata; + } + + public String toString() { + if (metadata.isEmpty()) { + return String.format("%s/%s", name, version == null ? '?' : version); + } + else { + StringBuilder metadataString = new StringBuilder(); + for (Map.Entry entry : metadata.entrySet()) { + if (metadataString.length() > 0) { + metadataString.append("; "); + } + metadataString.append(entry.getKey()); + metadataString.append(' '); + metadataString.append(entry.getValue()); + } + return String.format("%s/%s (%s)", name, version == null ? '?' : version, metadataString); + } + } + + @Override + public Header toHeader() { + return Header.raw("User-Agent", this); + } + +} diff --git a/java-client/src/main/java/co/elastic/clients/base/Version.java b/java-client/src/main/java/co/elastic/clients/base/Version.java new file mode 100644 index 000000000..a1e201079 --- /dev/null +++ b/java-client/src/main/java/co/elastic/clients/base/Version.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.base; + +import java.util.Objects; + +/** + * This class represents an immutable product version, as specified in + * + * Structured HTTP Header for Client Metadata. + * + * In that specification it is stated that a version string must match + * the following regex: + * ^[0-9]{1,2}\.[0-9]{1,2}(?:\.[0-9]{1,3})?p?$. + * + * Therefore, the following rules are encoded within this class: + * + * 1. The major and minor versions are mandatory and can be any value + * between 0 and 99 inclusive. It is unstated in the specification, + * but the assumption is made that leading zeros do not affect the + * value of the field, i.e. "05" is equal to "5". Values outside + * the range 0..99 will throw an exception. + * 2. The maintenance version is optional can be any value between + * 0 and 999 inclusive. As above, it is assumed that leading zeros + * do not affect the field value, and values outside the given + * range will throw an exception. To model optionality, the special + * value -1 can be used to denote omission. + * + * @see + * Time-based releases &emdash; Versioning + * + */ +public class Version { + + private final int major; + private final int minor; + private final int maintenance; + private final boolean isPreRelease; + + /** + * Parse a version string formatted using the standard Maven version format. + * + * @param version + * @return + */ + public static Version parse(String version) { + int hyphen = version.indexOf('-'); + boolean isPreRelease; + if (hyphen >= 0) { + version = version.substring(0, hyphen); + isPreRelease = true; + } + else { + isPreRelease = false; + } + String[] bits = version.split("\\."); + try { + int major = (bits.length >= 1) ? Integer.parseInt(bits[0]) : 0; + int minor = (bits.length >= 2) ? Integer.parseInt(bits[1]) : 0; + int maintenance = (bits.length >= 3) ? Integer.parseInt(bits[2]) : -1; + return new Version(major, minor, maintenance, isPreRelease); + } + catch(NumberFormatException ex) { + throw new IllegalArgumentException("Failed to parse numeric version components in " + version); + } + } + + public Version(int major, int minor, int maintenance, boolean isPreRelease) { + // Set major version + if (major < 0 || major > 99) { + throw new IllegalArgumentException("Major version must be between 0 and 99 inclusive"); + } + this.major = major; + + // Set minor version + if (minor < 0 || minor > 99) { + throw new IllegalArgumentException("Minor version must be between 0 and 99 inclusive"); + } + this.minor = minor; + + // Set maintenance version + if (maintenance < -1 || maintenance > 999) { + throw new IllegalArgumentException("Maintenance version must be between 0 and 999 inclusive, or -1 if omitted"); + } + this.maintenance = maintenance; + + // Set the pre-release flag + this.isPreRelease = isPreRelease; + } + + public int major() { + return major; + } + + public int minor() { + return minor; + } + + public int maintenance() { + return maintenance; + } + + public boolean isPreRelease() { + return isPreRelease; + } + + @Override + public boolean equals(Object other) { + if (this == other) return true; + if (!(other instanceof Version)) return false; + Version that = (Version) other; + return (major == that.major && + minor == that.minor && + maintenance == that.maintenance && + isPreRelease == that.isPreRelease); + } + + @Override + public int hashCode() { + return Objects.hash(major, minor, maintenance, isPreRelease); + } + + @Override + public String toString() { + StringBuilder s = new StringBuilder(); + s.append(major); + s.append('.'); + s.append(minor); + if (maintenance != -1) { + s.append('.'); + s.append(maintenance); + } + if (isPreRelease) { + s.append('p'); + } + return s.toString(); + } + +} diff --git a/java-client/src/main/java/co/elastic/clients/base/rest_client/RestClientTransport.java b/java-client/src/main/java/co/elastic/clients/base/rest_client/RestClientTransport.java index d242fd9e4..aac0d6d4b 100644 --- a/java-client/src/main/java/co/elastic/clients/base/rest_client/RestClientTransport.java +++ b/java-client/src/main/java/co/elastic/clients/base/rest_client/RestClientTransport.java @@ -22,9 +22,10 @@ import co.elastic.clients.base.BooleanEndpoint; import co.elastic.clients.base.BooleanResponse; import co.elastic.clients.base.ElasticsearchCatRequest; -import co.elastic.clients.elasticsearch._types.ElasticsearchException; import co.elastic.clients.base.Endpoint; +import co.elastic.clients.base.RequestOptions; import co.elastic.clients.base.Transport; +import co.elastic.clients.elasticsearch._types.ElasticsearchException; import co.elastic.clients.elasticsearch._types.ErrorResponse; import co.elastic.clients.json.JsonpDeserializer; import co.elastic.clients.json.JsonpMapper; @@ -34,7 +35,6 @@ import org.apache.http.entity.ByteArrayEntity; import org.apache.http.entity.ContentType; import org.elasticsearch.client.Cancellable; -import org.elasticsearch.client.RequestOptions; import org.elasticsearch.client.Response; import org.elasticsearch.client.ResponseListener; import org.elasticsearch.client.RestClient; @@ -44,21 +44,21 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; +import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.concurrent.CompletableFuture; -import java.util.function.Function; public class RestClientTransport implements Transport { private final RestClient restClient; private final JsonpMapper mapper; - private RequestOptions requestOptions; + private final RequestOptions requestOptions; public RestClientTransport(RestClient restClient, JsonpMapper mapper, @Nullable RequestOptions options) { this.restClient = restClient; this.mapper = mapper; - this.requestOptions = options; + this.requestOptions = options == null ? RequestOptions.DEFAULT : options; } public RestClientTransport(RestClient restClient, JsonpMapper mapper) { @@ -72,23 +72,27 @@ public RestClientTransport withRequestOptions(@Nullable RequestOptions options) return new RestClientTransport(this.restClient, this.mapper, options); } - /** - * Creates a new {@link #RestClientTransport} with specific request options, inheriting existing options. - * - * @param fn a function taking an options builder initialized with the current request options, or initialized - * with default values. - */ - public RestClientTransport withRequestOptions(Function fn) { - RequestOptions.Builder builder = requestOptions == null ? - RequestOptions.DEFAULT.toBuilder() : - requestOptions.toBuilder(); + @Override + public JsonpMapper jsonpMapper() { + return mapper; + } - return withRequestOptions(fn.apply(builder).build()); + @Override + public Map headers() { + Map headers = new HashMap<>(); + requestOptions.headers().forEach(header -> { + headers.put(header.name(), header.value()); + }); + return headers; } @Override - public JsonpMapper jsonpMapper() { - return mapper; + public Map queryParameters() { + Map queryParameters = new HashMap<>(); + requestOptions.queryParameters().forEach(parameter -> { + queryParameters.put(parameter.name(), parameter.value()); + }); + return queryParameters; } @Override @@ -155,10 +159,11 @@ private org.elasticsearch.client.Request prepareLowLevelRequest( Map params = endpoint.queryParameters(request); org.elasticsearch.client.Request clientReq = new org.elasticsearch.client.Request(method, path); + org.elasticsearch.client.RequestOptions.Builder optBuilder = org.elasticsearch.client.RequestOptions.DEFAULT.toBuilder(); + headers().forEach(optBuilder::addHeader); + queryParameters().forEach(optBuilder::addParameter); clientReq.addParameters(params); - if (requestOptions != null) { - clientReq.setOptions(requestOptions); - } + clientReq.setOptions(optBuilder.build()); // Request-type specific parameters. if (request instanceof ElasticsearchCatRequest) { diff --git a/java-client/src/main/java/co/elastic/clients/util/NamedString.java b/java-client/src/main/java/co/elastic/clients/util/NamedString.java new file mode 100644 index 000000000..ca5ed5027 --- /dev/null +++ b/java-client/src/main/java/co/elastic/clients/util/NamedString.java @@ -0,0 +1,66 @@ +/* + * 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.util; + +import java.util.Objects; + +public class NamedString { + + private final String name; + + private final String value; + + public NamedString(String name, String value) { + this.name = name; + this.value = value; + } + + public String name() { + return this.name; + } + + public String value() { + return this.value; + } + + @Override + public boolean equals(Object other) { + if (this == other) return true; + if (!(other instanceof NamedString)) return false; + NamedString namedString = (NamedString) other; + return Objects.equals(name(), namedString.name()) && Objects.equals(value(), namedString.value()); + } + + @Override + public int hashCode() { + return Objects.hash(name(), value()); + } + + @Override + public String toString() { + if (name == null || value == null) { + return ""; + } + else { + return String.format("%s: %s", name(), value()); + } + } + +} diff --git a/java-client/src/main/java/co/elastic/clients/util/NamedValue.java b/java-client/src/main/java/co/elastic/clients/util/NamedValue.java deleted file mode 100644 index d83632c28..000000000 --- a/java-client/src/main/java/co/elastic/clients/util/NamedValue.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * 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.util; - -import co.elastic.clients.json.JsonpDeserializer; -import co.elastic.clients.json.JsonpMapper; -import co.elastic.clients.json.JsonpUtils; -import jakarta.json.stream.JsonParser; - -import java.util.EnumSet; -import java.util.function.Supplier; - -public class NamedValue { - - private final String name; - private final T value; - - public NamedValue(String name, T value) { - this.name = name; - this.value = value; - } - - public String name() { - return this.name; - } - - public T value() { - return this.value; - } - - public static JsonpDeserializer> deserializer(Supplier> valueParserBuilder) { - return new JsonpDeserializer>(EnumSet.of(JsonParser.Event.START_OBJECT)) { - @Override - public NamedValue deserialize(JsonParser parser, JsonpMapper mapper, JsonParser.Event event) { - - JsonpUtils.expectEvent(parser, JsonParser.Event.KEY_NAME, event); - String name = parser.getString(); - - T value = valueParserBuilder.get().deserialize(parser, mapper); - JsonpUtils.expectNextEvent(parser, JsonParser.Event.END_OBJECT); - - return new NamedValue<>(name, value); - } - }; - } -} diff --git a/java-client/src/test/java/co/elastic/clients/base/ClientMetadataTest.java b/java-client/src/test/java/co/elastic/clients/base/ClientMetadataTest.java new file mode 100644 index 000000000..9b1740790 --- /dev/null +++ b/java-client/src/test/java/co/elastic/clients/base/ClientMetadataTest.java @@ -0,0 +1,87 @@ +/* + * 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.base; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +public class ClientMetadataTest { + + @Test + public void testMetadataForLocalSystem() { + ClientMetadata metadata = ClientMetadata.forLocalSystem(); + // We can't check the actual content of this system + // metadata, as this varies... well, by system, so + // instead we simply check that it contains *some* data. + assertTrue(metadata.toString().length() > 0); + } + + @Test + public void testEmptyMetadata() { + ClientMetadata metadata = ClientMetadata.EMPTY; + // The string value of a null-valued header is always + // the empty string, by definition. + assertEquals("", metadata.toString()); + } + + @Test + public void testCustomMetadata() { + ClientMetadata metadata = new ClientMetadata.Builder() + .withClientVersion(Version.parse("12.3.4")) + .withJavaVersion(Version.parse("1.4.2")) + .withTransportVersion(Version.parse("6.7")) + .build(); + assertEquals("es=12.3.4,jv=1.4.2,t=6.7", metadata.toString()); + } + + @Test + public void testClientVersionIsMandatory() { + assertThrows(IllegalArgumentException.class, () -> { + new ClientMetadata.Builder() + .withJavaVersion(Version.parse("1.4.2")) + .withTransportVersion(Version.parse("6.7")) + .build(); + }); + } + + @Test + public void testJavaVersionIsMandatory() { + assertThrows(IllegalArgumentException.class, () -> { + new ClientMetadata.Builder() + .withClientVersion(Version.parse("12.3.4")) + .withTransportVersion(Version.parse("6.7")) + .build(); + }); + } + + @Test + public void testTransportVersionIsMandatory() { + assertThrows(IllegalArgumentException.class, () -> { + new ClientMetadata.Builder() + .withClientVersion(Version.parse("12.3.4")) + .withJavaVersion(Version.parse("1.4.2")) + .build(); + }); + } + +} diff --git a/java-client/src/test/java/co/elastic/clients/base/RequestOptionsTest.java b/java-client/src/test/java/co/elastic/clients/base/RequestOptionsTest.java new file mode 100644 index 000000000..bdbb31ea5 --- /dev/null +++ b/java-client/src/test/java/co/elastic/clients/base/RequestOptionsTest.java @@ -0,0 +1,172 @@ +/* + * 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.base; + +import org.junit.Test; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class RequestOptionsTest { + + @Test + public void testDefaultHeadersContainsClientMetadata() { + RequestOptions options = RequestOptions.DEFAULT; + List
clientMetadataHeaders = options.headers().stream().filter(header -> + header.name().equalsIgnoreCase("X-Elastic-Client-Meta")).collect(Collectors.toList()); + assertEquals(1, clientMetadataHeaders.size()); + Header clientMetadataHeader = clientMetadataHeaders.get(0); + String clientMetadataHeaderValue = clientMetadataHeader.value(); + assertTrue(clientMetadataHeaderValue.contains("es=")); + assertTrue(clientMetadataHeaderValue.contains("jv=")); + assertTrue(clientMetadataHeaderValue.contains("t=")); + } + + @Test + public void testCanDisableClientMetadata() { + RequestOptions options = RequestOptions.DEFAULT.toBuilder() + .withHeader(ClientMetadata.EMPTY.toHeader()) + .build(); + List
clientMetadataHeaders = options.headers().stream().filter(header -> + header.name().equalsIgnoreCase("X-Elastic-Client-Meta")).collect(Collectors.toList()); + assertEquals(0, clientMetadataHeaders.size()); + } + + @Test + public void testDisabledClientMetadataIsPropagatedThroughBuilder() { + RequestOptions options = RequestOptions.DEFAULT.toBuilder() + .withHeader(ClientMetadata.EMPTY.toHeader()) + .build(); + List
clientMetadataHeaders = options.toBuilder().headers().stream().filter(header -> + header.name().equalsIgnoreCase("X-Elastic-Client-Meta")).collect(Collectors.toList()); + assertTrue(clientMetadataHeaders.contains(Header.raw("X-Elastic-Client-Meta", null))); + } + + @Test + public void testCanReEnableClientMetadata() { + RequestOptions options = RequestOptions.DEFAULT.toBuilder() + .withHeader(ClientMetadata.EMPTY.toHeader()) + .withHeader(ClientMetadata.forLocalSystem().toHeader()) + .build(); + List
clientMetadataHeaders = options.headers().stream().filter(header -> + header.name().equalsIgnoreCase("X-Elastic-Client-Meta")).collect(Collectors.toList()); + assertEquals(1, clientMetadataHeaders.size()); + } + + @Test + public void testDefaultHeadersContainsUserAgent() { + RequestOptions options = RequestOptions.DEFAULT; + Collection
headers = options.headers(); + assertTrue(headers.contains(Header.raw("User-Agent", UserAgent.DEFAULT))); + } + + @Test + public void testCustomUserAgent() { + UserAgent userAgent = new UserAgent("MegaClient", "1.2.3"); + RequestOptions options = RequestOptions.DEFAULT.toBuilder() + .withHeader(userAgent.toHeader()) + .build(); + Collection
headers = options.headers(); + assertTrue(headers.contains(Header.raw("User-Agent", "MegaClient/1.2.3"))); + } + + @Test + public void testCustomUserAgentWithMetadata() { + UserAgent userAgent = new UserAgent("MegaClient", "1.2.3", + Collections.singletonMap("AmigaOS", "4.1")); + RequestOptions options = RequestOptions.DEFAULT.toBuilder() + .withHeader(userAgent.toHeader()) + .build(); + Collection
headers = options.headers(); + assertTrue(headers.contains(Header.raw("User-Agent", "MegaClient/1.2.3 (AmigaOS 4.1)"))); + } + + @Test + public void testCustomHeader() { + Header customHeader = Header.raw("X-Files", "Mulder, Scully"); + RequestOptions options = RequestOptions.DEFAULT.toBuilder() + .withHeader(customHeader) + .build(); + Collection
headers = options.headers(); + assertTrue(headers.contains(customHeader)); + } + + @Test + public void testOpaqueID() { + Header idHeader = new OpaqueID("ABC123").toHeader(); + RequestOptions options = RequestOptions.DEFAULT.toBuilder() + .withHeader(idHeader) + .build(); + Collection
headers = options.headers(); + assertTrue(headers.contains(idHeader)); + } + + @Test + public void testNullOpaqueIDShouldDisableHeader() { + Header idHeader = new OpaqueID(null).toHeader(); + RequestOptions options = RequestOptions.DEFAULT.toBuilder() + .withHeader(idHeader) + .build(); + List
idHeaders = options.headers().stream().filter(header -> + header.name().equalsIgnoreCase("X-Opaque-ID")).collect(Collectors.toList()); + assertEquals(0, idHeaders.size()); + } + + @Test + public void testQueryParameter() { + QueryParameter prettyPrint = QueryParameter.raw("format", "pretty"); + RequestOptions options = RequestOptions.DEFAULT.toBuilder() + .withQueryParameter(prettyPrint) + .build(); + List formatParameters = options.queryParameters().stream().filter(header -> + header.name().equals("format")).collect(Collectors.toList()); + assertEquals(1, formatParameters.size()); + assertTrue(formatParameters.contains(prettyPrint)); + } + + @Test + public void testNullQueryParameter() { + QueryParameter nullFormat = QueryParameter.raw("format", null); + RequestOptions options = RequestOptions.DEFAULT.toBuilder() + .withQueryParameter(nullFormat) + .build(); + List formatParameters = options.queryParameters().stream().filter(header -> + header.name().equals("format")).collect(Collectors.toList()); + assertEquals(0, formatParameters.size()); + } + + @Test + public void testBuilderContainsNullQueryParameter() { + QueryParameter nullFormat = QueryParameter.raw("format", null); + RequestOptions options = RequestOptions.DEFAULT.toBuilder() + .withQueryParameter(nullFormat) + .build(); + List formatParameters = options.toBuilder().queryParameters().stream().filter(header -> + header.name().equals("format")).collect(Collectors.toList()); + assertEquals(1, formatParameters.size()); + assertTrue(formatParameters.contains(nullFormat)); + } + +} diff --git a/java-client/src/test/java/co/elastic/clients/elasticsearch/end_to_end/RequestTest.java b/java-client/src/test/java/co/elastic/clients/elasticsearch/end_to_end/RequestTest.java index 8bc67db4c..6a26b0b85 100644 --- a/java-client/src/test/java/co/elastic/clients/elasticsearch/end_to_end/RequestTest.java +++ b/java-client/src/test/java/co/elastic/clients/elasticsearch/end_to_end/RequestTest.java @@ -20,17 +20,19 @@ package co.elastic.clients.elasticsearch.end_to_end; import co.elastic.clients.base.BooleanResponse; -import co.elastic.clients.elasticsearch._types.ElasticsearchException; -import co.elastic.clients.base.rest_client.RestClientTransport; +import co.elastic.clients.base.Header; +import co.elastic.clients.base.RequestOptions; import co.elastic.clients.base.Transport; +import co.elastic.clients.base.rest_client.RestClientTransport; import co.elastic.clients.elasticsearch.ElasticsearchAsyncClient; import co.elastic.clients.elasticsearch.ElasticsearchClient; +import co.elastic.clients.elasticsearch._types.ElasticsearchException; +import co.elastic.clients.elasticsearch.cat.NodesResponse; import co.elastic.clients.elasticsearch.core.BulkResponse; import co.elastic.clients.elasticsearch.core.GetResponse; import co.elastic.clients.elasticsearch.core.IndexResponse; import co.elastic.clients.elasticsearch.core.SearchResponse; import co.elastic.clients.elasticsearch.core.bulk.OperationType; -import co.elastic.clients.elasticsearch.cat.NodesResponse; import co.elastic.clients.elasticsearch.indices.CreateIndexResponse; import co.elastic.clients.elasticsearch.indices.GetIndexResponse; import co.elastic.clients.elasticsearch.indices.IndexState; @@ -40,7 +42,6 @@ import jakarta.json.Json; import jakarta.json.JsonValue; import org.apache.http.HttpHost; -import org.elasticsearch.client.RequestOptions; import org.elasticsearch.client.RestClient; import org.junit.AfterClass; import org.junit.Assert; @@ -159,8 +160,9 @@ public void testDataIngestion() throws Exception { // Search, adding some request options RequestOptions options = RequestOptions.DEFAULT.toBuilder() - .addHeader("x-super-header", "bar") - .build(); + .withHeader( + Header.raw("x-super-header", "bar")) + .build(); SearchResponse search = new ElasticsearchClient( ((RestClientTransport) client._transport()).withRequestOptions(options)