Skip to content

Commit 0f5e5a9

Browse files
committed
Generic header mechanism using RequestOption interface
1 parent 2e38c86 commit 0f5e5a9

File tree

8 files changed

+242
-102
lines changed

8 files changed

+242
-102
lines changed
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
/*
2+
* Licensed to Elasticsearch B.V. under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch B.V. licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package co.elastic.clients.base;
21+
22+
import org.elasticsearch.client.RequestOptions;
23+
24+
import java.util.ArrayList;
25+
import java.util.List;
26+
import java.util.Objects;
27+
28+
import static java.util.Arrays.asList;
29+
import static java.util.Collections.unmodifiableList;
30+
31+
/**
32+
* HTTP header field, consisting of a string key ('field') plus one
33+
* or more values. This implements the {@link RequestOption} interface,
34+
* allowing it to be applied to a low-level request as required.
35+
*/
36+
public class Header implements RequestOption {
37+
38+
public static Header header(String field, String... values) {
39+
return new Header(field, values);
40+
}
41+
42+
static final String VALUE_DELIMITER = ", ";
43+
44+
private final String field;
45+
private final List<String> values;
46+
47+
Header(String field, String... values) {
48+
this.field = field;
49+
this.values = asList(values);
50+
}
51+
52+
public String field() {
53+
return field;
54+
}
55+
56+
public String value() {
57+
if (values == null || values.isEmpty()) {
58+
return null;
59+
} else if (values.size() == 1) {
60+
return values.get(0);
61+
} else {
62+
return String.join(VALUE_DELIMITER, values);
63+
}
64+
}
65+
66+
public List<String> values() {
67+
return values;
68+
}
69+
70+
@Override
71+
public boolean equals(Object other) {
72+
if (this == other) return true;
73+
if (!(other instanceof Header)) return false;
74+
Header header = (Header) other;
75+
return field.equalsIgnoreCase(header.field()) && Objects.equals(values(), header.values());
76+
}
77+
78+
@Override
79+
public int hashCode() {
80+
return Objects.hash(field(), values());
81+
}
82+
83+
public String toString() {
84+
return String.format("%s: %s", field(), value());
85+
}
86+
87+
@Override
88+
public void apply(final RequestOptions.Builder builder) {
89+
builder.addHeader(field(), value());
90+
}
91+
92+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
* Licensed to Elasticsearch B.V. under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch B.V. licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package co.elastic.clients.base;
21+
22+
import org.elasticsearch.client.RequestOptions;
23+
24+
/**
25+
* An option that can be applied to an HTTP request.
26+
*/
27+
public interface RequestOption {
28+
29+
/**
30+
* Apply the option to an existing builder object.
31+
*
32+
* @param builder builder to which to apply the option
33+
*/
34+
void apply(final RequestOptions.Builder builder);
35+
36+
}

java-client/src/main/java/co/elastic/clients/base/Transport.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323

2424
import java.io.Closeable;
2525
import java.io.IOException;
26+
import java.util.Map;
2627
import java.util.concurrent.CompletableFuture;
2728

2829
/**
@@ -42,5 +43,6 @@ <RequestT, ResponseT, ErrorT> CompletableFuture<ResponseT> performRequestAsync(
4243

4344
JsonpMapper jsonpMapper();
4445

45-
String userAgent();
46+
Map<String, Header> headers();
47+
4648
}

java-client/src/main/java/co/elastic/clients/base/UserAgent.java

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,10 @@
2626
import java.util.Properties;
2727

2828
/**
29-
* Models a user agent, consisting of a name, version,
29+
* Models a user agent header, consisting of a name, version,
3030
* and optional key-value metadata.
3131
*/
32-
public class UserAgent {
32+
public class UserAgent extends Header {
3333

3434
static final String DEFAULT_NAME = "elasticsearch-java";
3535

@@ -64,6 +64,7 @@ public class UserAgent {
6464
private final Map<String, String> metadata;
6565

6666
public UserAgent(String name, String version, Map<String, String> metadata) {
67+
super("User-Agent", buildValueString(name, version, metadata));
6768
this.name = name;
6869
this.version = version;
6970
this.metadata = metadata;
@@ -73,8 +74,19 @@ public UserAgent(String repoName, String version) {
7374
this(repoName, version, Collections.emptyMap());
7475
}
7576

76-
@Override
77-
public String toString() {
77+
public String name() {
78+
return name;
79+
}
80+
81+
public String version() {
82+
return version;
83+
}
84+
85+
public Map<String, String> metadata() {
86+
return metadata;
87+
}
88+
89+
private static String buildValueString(String name, String version, Map<String, String> metadata) {
7890
if (metadata.isEmpty()) {
7991
return String.format("%s/%s", name, version);
8092
}

java-client/src/main/java/co/elastic/clients/base/rest_client/RestClientTransport.java

Lines changed: 25 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,15 @@
2424
import co.elastic.clients.base.BooleanResponse;
2525
import co.elastic.clients.base.ElasticsearchCatRequest;
2626
import co.elastic.clients.base.Endpoint;
27+
import co.elastic.clients.base.Header;
28+
import co.elastic.clients.base.RequestOption;
2729
import co.elastic.clients.base.Transport;
2830
import co.elastic.clients.base.UserAgent;
2931
import co.elastic.clients.json.JsonpDeserializer;
3032
import co.elastic.clients.json.JsonpMapper;
3133
import co.elastic.clients.json.NdJsonpSerializable;
3234
import jakarta.json.stream.JsonGenerator;
3335
import jakarta.json.stream.JsonParser;
34-
import org.apache.http.Header;
3536
import org.apache.http.entity.ByteArrayEntity;
3637
import org.apache.http.entity.ContentType;
3738
import org.elasticsearch.client.Cancellable;
@@ -45,38 +46,35 @@
4546
import java.io.ByteArrayOutputStream;
4647
import java.io.IOException;
4748
import java.io.InputStream;
49+
import java.util.HashMap;
50+
import java.util.List;
4851
import java.util.Map;
4952
import java.util.concurrent.CompletableFuture;
50-
import java.util.function.Function;
53+
54+
import static co.elastic.clients.base.Header.header;
55+
import static java.util.Collections.singletonList;
5156

5257
public class RestClientTransport implements Transport {
5358

59+
private static final List<RequestOption> DEFAULT_REQUEST_OPTIONS = singletonList(UserAgent.DEFAULT);
60+
5461
private final RestClient restClient;
5562
private final JsonpMapper mapper;
5663
private final RequestOptions requestOptions;
5764

58-
public RestClientTransport(RestClient restClient, JsonpMapper mapper, @Nullable RequestOptions options,
59-
@Nullable UserAgent userAgent) {
60-
this.restClient = restClient;
61-
this.mapper = mapper;
62-
RequestOptions baseOptions = options == null ? RequestOptions.DEFAULT : options;
63-
String manualUserAgent = findUserAgentIn(baseOptions);
64-
if (manualUserAgent == null && userAgent == null) {
65-
this.requestOptions = baseOptions.toBuilder().addHeader("User-Agent", UserAgent.DEFAULT.toString()).build();
66-
}
67-
else if (manualUserAgent == null) {
68-
this.requestOptions = baseOptions.toBuilder().addHeader("User-Agent", userAgent.toString()).build();
69-
}
70-
else if (userAgent == null) {
71-
this.requestOptions = baseOptions;
72-
}
73-
else {
74-
throw new IllegalArgumentException("Multiple user agents specified");
65+
private static void applyRequestOptions(final RequestOptions.Builder builder, Iterable<RequestOption> options) {
66+
if (options != null) {
67+
options.forEach(option -> option.apply(builder));
7568
}
7669
}
7770

78-
public RestClientTransport(RestClient restClient, JsonpMapper mapper, @Nullable RequestOptions options) {
79-
this(restClient, mapper, options, null);
71+
public RestClientTransport(RestClient restClient, JsonpMapper mapper, @Nullable Iterable<RequestOption> options) {
72+
this.restClient = restClient;
73+
this.mapper = mapper;
74+
final RequestOptions.Builder builder = RequestOptions.DEFAULT.toBuilder();
75+
applyRequestOptions(builder, DEFAULT_REQUEST_OPTIONS);
76+
applyRequestOptions(builder, options);
77+
this.requestOptions = builder.build();
8078
}
8179

8280
public RestClientTransport(RestClient restClient, JsonpMapper mapper) {
@@ -86,59 +84,28 @@ public RestClientTransport(RestClient restClient, JsonpMapper mapper) {
8684
/**
8785
* Creates a new {@link #RestClientTransport} with specific request options.
8886
*/
89-
public RestClientTransport withRequestOptions(@Nullable RequestOptions options) {
87+
public RestClientTransport withRequestOptions(@Nullable Iterable<RequestOption> options) {
9088
return new RestClientTransport(this.restClient, this.mapper, options);
9189
}
9290

93-
/**
94-
* Creates a new {@link #RestClientTransport} with specific request options, inheriting existing options.
95-
*
96-
* @param fn a function taking an options builder initialized with the current request options, or initialized
97-
* with default values.
98-
*/
99-
public RestClientTransport withRequestOptions(Function<RequestOptions.Builder, RequestOptions.Builder> fn) {
100-
RequestOptions.Builder builder = requestOptions == null ?
101-
RequestOptions.DEFAULT.toBuilder() :
102-
requestOptions.toBuilder();
103-
104-
return withRequestOptions(fn.apply(builder).build());
105-
}
106-
107-
public RestClientTransport withUserAgent(UserAgent userAgent) {
108-
return new RestClientTransport(this.restClient, this.mapper, this.requestOptions, userAgent);
109-
}
110-
11191
@Override
11292
public JsonpMapper jsonpMapper() {
11393
return mapper;
11494
}
11595

116-
/**
117-
* Find and return the first header value which is keyed under any
118-
* case-insensitive variant of "User-Agent".
119-
*
120-
* @return user agent string
121-
*/
12296
@Override
123-
public String userAgent() {
124-
return findUserAgentIn(requestOptions);
97+
public Map<String, Header> headers() {
98+
Map<String, Header> headers = new HashMap<>();
99+
requestOptions.getHeaders().forEach(header ->
100+
headers.put(header.getName(), header(header.getName(), header.getValue())));
101+
return headers;
125102
}
126103

127104
@Override
128105
public void close() throws IOException {
129106
this.restClient.close();
130107
}
131108

132-
private static String findUserAgentIn(RequestOptions options) {
133-
// TODO: move this into RequestOptions?
134-
for (Header header : options.getHeaders()) {
135-
if (header.getName().equalsIgnoreCase("User-Agent")) {
136-
return header.getValue();
137-
}
138-
}
139-
return null;
140-
}
141-
142109
public <RequestT, ResponseT, ErrorT> ResponseT performRequest(
143110
RequestT request,
144111
Endpoint<RequestT, ResponseT, ErrorT> endpoint
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/*
2+
* Licensed to Elasticsearch B.V. under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch B.V. licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package co.elastic.clients.base;
21+
22+
import org.junit.Test;
23+
24+
import static co.elastic.clients.base.Header.header;
25+
import static org.junit.Assert.assertEquals;
26+
import static org.junit.Assert.assertNull;
27+
28+
public class HeaderTest {
29+
30+
@Test
31+
public void testSimple() {
32+
Header header = header("Content-Type", "application/json");
33+
assertEquals("Content-Type", header.field());
34+
assertEquals("application/json", header.value());
35+
}
36+
37+
@Test
38+
public void testMulti() {
39+
Header header = header("Cache-Control", "no-cache", "no-store");
40+
assertEquals("Cache-Control", header.field());
41+
assertEquals("no-cache, no-store", header.value());
42+
}
43+
44+
@Test
45+
public void testEmpty() {
46+
Header header = header("Content-Length");
47+
assertEquals("Content-Length", header.field());
48+
assertNull(header.value());
49+
}
50+
51+
}

0 commit comments

Comments
 (0)