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)