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..8cc3bba30 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 @@ -41,4 +41,6 @@ CompletableFuture performRequestAsync( ); JsonpMapper jsonpMapper(); + + String userAgent(); } 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..f26ebc070 --- /dev/null +++ b/java-client/src/main/java/co/elastic/clients/base/UserAgent.java @@ -0,0 +1,95 @@ +/* + * 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.Collections; +import java.util.Map; +import java.util.Properties; + +/** + * Models a user agent, consisting of a name, version, + * and optional key-value metadata. + */ +public class UserAgent { + + static final String DEFAULT_NAME = "elasticsearch-java"; + + // The client version is loaded from the 'version.properties' file + static final String DEFAULT_VERSION; + static { + InputStream in = UserAgent.class.getResourceAsStream("/co.elastic.clients.elasticsearch/version.properties"); + if (in != null) { + Properties prop = new Properties(); + String version; + try { + prop.load(in); + version = prop.getProperty("version", "?"); + } catch (IOException e) { + // Unable to read properties file + version = "?"; + } + DEFAULT_VERSION = version; + } + else { + // Unable to locate properties file + DEFAULT_VERSION = "?"; + } + // TODO: log error if DEFAULT_VERSION now equals "?" + } + + // Default user agent, constructed from default repo name and version + 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()); + } + + @Override + public String toString() { + if (metadata.isEmpty()) { + return String.format("%s/%s", name, 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, metadataString); + } + } + +} 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 8d15fec38..bd22c6ff9 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 @@ -25,11 +25,13 @@ import co.elastic.clients.base.ElasticsearchCatRequest; import co.elastic.clients.base.Endpoint; import co.elastic.clients.base.Transport; +import co.elastic.clients.base.UserAgent; import co.elastic.clients.json.JsonpDeserializer; import co.elastic.clients.json.JsonpMapper; import co.elastic.clients.json.NdJsonpSerializable; import jakarta.json.stream.JsonGenerator; import jakarta.json.stream.JsonParser; +import org.apache.http.Header; import org.apache.http.entity.ByteArrayEntity; import org.apache.http.entity.ContentType; import org.elasticsearch.client.Cancellable; @@ -51,12 +53,30 @@ 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) { + public RestClientTransport(RestClient restClient, JsonpMapper mapper, @Nullable RequestOptions options, + @Nullable UserAgent userAgent) { this.restClient = restClient; this.mapper = mapper; - this.requestOptions = options; + RequestOptions baseOptions = options == null ? RequestOptions.DEFAULT : options; + String manualUserAgent = findUserAgentIn(baseOptions); + if (manualUserAgent == null && userAgent == null) { + this.requestOptions = baseOptions.toBuilder().addHeader("User-Agent", UserAgent.DEFAULT.toString()).build(); + } + else if (manualUserAgent == null) { + this.requestOptions = baseOptions.toBuilder().addHeader("User-Agent", userAgent.toString()).build(); + } + else if (userAgent == null) { + this.requestOptions = baseOptions; + } + else { + throw new IllegalArgumentException("Multiple user agents specified"); + } + } + + public RestClientTransport(RestClient restClient, JsonpMapper mapper, @Nullable RequestOptions options) { + this(restClient, mapper, options, null); } public RestClientTransport(RestClient restClient, JsonpMapper mapper) { @@ -84,16 +104,41 @@ public RestClientTransport withRequestOptions(Function ResponseT performRequest( RequestT request, Endpoint endpoint @@ -154,9 +199,7 @@ private org.elasticsearch.client.Request prepareLowLevelRequest( org.elasticsearch.client.Request clientReq = new org.elasticsearch.client.Request(method, path); clientReq.addParameters(params); - if (requestOptions != null) { - clientReq.setOptions(requestOptions); - } + clientReq.setOptions(requestOptions); // Request-type specific parameters. if (request instanceof ElasticsearchCatRequest) { diff --git a/java-client/src/test/java/co/elastic/clients/base/UserAgentTest.java b/java-client/src/test/java/co/elastic/clients/base/UserAgentTest.java new file mode 100644 index 000000000..1875052d9 --- /dev/null +++ b/java-client/src/test/java/co/elastic/clients/base/UserAgentTest.java @@ -0,0 +1,89 @@ +/* + * 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.base.rest_client.RestClientTransport; +import org.apache.http.HttpHost; +import org.elasticsearch.client.RequestOptions; +import org.elasticsearch.client.RestClient; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.io.IOException; + +import static co.elastic.clients.base.UserAgent.DEFAULT_NAME; +import static co.elastic.clients.base.UserAgent.DEFAULT_VERSION; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; + +public class UserAgentTest { + + private static final String DEFAULT_USER_AGENT = String.format("%s/%s", DEFAULT_NAME, DEFAULT_VERSION); + + private static final String CUSTOM_NAME = "ÜberClient"; + private static final String CUSTOM_VERSION = "1.0.13"; + private static final String CUSTOM_USER_AGENT = String.format("%s/%s", CUSTOM_NAME, CUSTOM_VERSION); + + private static final String CUSTOM_NAME_2 = "MegaClient"; + private static final String CUSTOM_VERSION_2 = "6.7.8"; + + public static RestClient restClient; + + @BeforeClass + public static void setUp() { + restClient = RestClient.builder(new HttpHost("localhost", 9200)).build(); + } + + @AfterClass + public static void tearDown() throws IOException { + restClient.close(); + } + + @Test + public void testDefaultUserAgent() throws Exception { + Transport transport = new RestClientTransport(restClient, null); + assertEquals(DEFAULT_USER_AGENT, transport.userAgent()); + } + + @Test + public void testCustomUserAgent() throws Exception { + Transport transport = new RestClientTransport(restClient, null, null, + new UserAgent(CUSTOM_NAME, CUSTOM_VERSION)); + assertEquals(CUSTOM_USER_AGENT, transport.userAgent()); + } + + @Test + public void testManualUserAgent() throws Exception { + Transport transport = new RestClientTransport(restClient, null, + RequestOptions.DEFAULT.toBuilder().addHeader("User-Agent", CUSTOM_USER_AGENT).build()); + assertEquals(CUSTOM_USER_AGENT, transport.userAgent()); + } + + @Test + public void testMultipleUserAgentsThrowsException() throws Exception { + Exception exception = assertThrows(IllegalArgumentException.class, () -> { + Transport transport = new RestClientTransport(restClient, null, + RequestOptions.DEFAULT.toBuilder().addHeader("User-Agent", CUSTOM_USER_AGENT).build(), + new UserAgent(CUSTOM_NAME_2, CUSTOM_VERSION_2)); + }); + } + +}