Skip to content

Commit cfa0d07

Browse files
authored
Create client using javax.ws.rs.client.Client (OpenFeign#696)
1 parent ad136c3 commit cfa0d07

3 files changed

Lines changed: 279 additions & 0 deletions

File tree

jaxrs2/pom.xml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,5 +78,28 @@
7878
<type>test-jar</type>
7979
<scope>test</scope>
8080
</dependency>
81+
82+
<dependency>
83+
<groupId>org.glassfish.jersey.core</groupId>
84+
<artifactId>jersey-client</artifactId>
85+
<version>2.26</version>
86+
<scope>test</scope>
87+
</dependency>
88+
<dependency>
89+
<groupId>org.glassfish.jersey.inject</groupId>
90+
<artifactId>jersey-hk2</artifactId>
91+
<version>2.26</version>
92+
<scope>test</scope>
93+
</dependency>
94+
<dependency>
95+
<groupId>com.squareup.okhttp3</groupId>
96+
<artifactId>mockwebserver</artifactId>
97+
<scope>test</scope>
98+
</dependency>
99+
<dependency>
100+
<groupId>org.hamcrest</groupId>
101+
<artifactId>java-hamcrest</artifactId>
102+
<version>2.0.0.0</version>
103+
</dependency>
81104
</dependencies>
82105
</project>
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
/**
2+
* Copyright 2012-2018 The Feign Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5+
* in compliance with the License. You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software distributed under the License
10+
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11+
* or implied. See the License for the specific language governing permissions and limitations under
12+
* the License.
13+
*/
14+
package feign.jaxrs2;
15+
16+
import java.io.IOException;
17+
import java.io.InputStream;
18+
import java.nio.charset.Charset;
19+
import java.util.Collection;
20+
import java.util.Map;
21+
import java.util.Map.Entry;
22+
import java.util.concurrent.TimeUnit;
23+
import java.util.stream.Collectors;
24+
import javax.ws.rs.client.ClientBuilder;
25+
import javax.ws.rs.client.Entity;
26+
import javax.ws.rs.core.*;
27+
import feign.Client;
28+
import feign.Request.Options;
29+
30+
/**
31+
* This module directs Feign's http requests to javax.ws.rs.client.Client . Ex:
32+
*
33+
* <pre>
34+
* GitHub github =
35+
* Feign.builder().client(new JaxRSClient()).target(GitHub.class, "https://api.github.com");
36+
* </pre>
37+
*/
38+
public class JAXRSClient implements Client {
39+
40+
private final ClientBuilder clientBuilder;
41+
42+
public JAXRSClient() {
43+
this(ClientBuilder.newBuilder());
44+
}
45+
46+
public JAXRSClient(ClientBuilder clientBuilder) {
47+
this.clientBuilder = clientBuilder;
48+
}
49+
50+
@Override
51+
public feign.Response execute(feign.Request request, Options options) throws IOException {
52+
final Response response = clientBuilder
53+
.connectTimeout(options.connectTimeoutMillis(), TimeUnit.MILLISECONDS)
54+
.readTimeout(options.readTimeoutMillis(), TimeUnit.MILLISECONDS)
55+
.build()
56+
.target(request.url())
57+
.request()
58+
.headers(toMultivaluedMap(request.headers()))
59+
.method(request.method(), createRequestEntity(request));
60+
61+
return feign.Response.builder()
62+
.request(request)
63+
.body(response.readEntity(InputStream.class),
64+
integerHeader(response, HttpHeaders.CONTENT_LENGTH))
65+
.headers(toMap(response.getStringHeaders()))
66+
.status(response.getStatus())
67+
.reason(response.getStatusInfo().getReasonPhrase())
68+
.build();
69+
}
70+
71+
private Entity<byte[]> createRequestEntity(feign.Request request) {
72+
if (request.body() == null) {
73+
return null;
74+
}
75+
76+
return Entity.entity(
77+
request.body(),
78+
new Variant(mediaType(request.headers()), locale(request.headers()),
79+
encoding(request.charset())));
80+
}
81+
82+
private Integer integerHeader(Response response, String header) {
83+
final MultivaluedMap<String, String> headers = response.getStringHeaders();
84+
if (!headers.containsKey(header)) {
85+
return null;
86+
}
87+
88+
try {
89+
return new Integer(headers.getFirst(header));
90+
} catch (final NumberFormatException e) {
91+
// not a number or too big to fit Integer
92+
return null;
93+
}
94+
}
95+
96+
private String encoding(Charset charset) {
97+
if (charset == null)
98+
return null;
99+
100+
return charset.name();
101+
}
102+
103+
private String locale(Map<String, Collection<String>> headers) {
104+
if (!headers.containsKey(HttpHeaders.CONTENT_LANGUAGE))
105+
return null;
106+
107+
return headers.get(HttpHeaders.CONTENT_LANGUAGE).iterator().next();
108+
}
109+
110+
private MediaType mediaType(Map<String, Collection<String>> headers) {
111+
if (!headers.containsKey(HttpHeaders.CONTENT_TYPE))
112+
return null;
113+
114+
return MediaType.valueOf(headers.get(HttpHeaders.CONTENT_TYPE).iterator().next());
115+
}
116+
117+
private MultivaluedMap<String, Object> toMultivaluedMap(Map<String, Collection<String>> headers) {
118+
final MultivaluedHashMap<String, Object> mvHeaders = new MultivaluedHashMap<>();
119+
120+
headers.entrySet().forEach(entry -> entry.getValue().stream()
121+
.forEach(value -> mvHeaders.add(entry.getKey(), value)));
122+
123+
return mvHeaders;
124+
}
125+
126+
private Map<String, Collection<String>> toMap(MultivaluedMap<String, String> headers) {
127+
return headers.entrySet().stream()
128+
.collect(Collectors.toMap(
129+
Entry::getKey,
130+
Entry::getValue));
131+
}
132+
133+
}
134+
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
/**
2+
* Copyright 2012-2018 The Feign Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5+
* in compliance with the License. You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software distributed under the License
10+
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11+
* or implied. See the License for the specific language governing permissions and limitations under
12+
* the License.
13+
*/
14+
package feign.jaxrs2;
15+
16+
import feign.Feign.Builder;
17+
import feign.Headers;
18+
import feign.RequestLine;
19+
import feign.Response;
20+
import feign.Util;
21+
import feign.assertj.MockWebServerAssertions;
22+
import feign.client.AbstractClientTest;
23+
import feign.jaxrs2.JAXRSClient;
24+
import feign.Feign;
25+
import okhttp3.mockwebserver.MockResponse;
26+
import org.junit.Test;
27+
import java.io.ByteArrayInputStream;
28+
import java.io.IOException;
29+
import javax.ws.rs.ProcessingException;
30+
import static feign.Util.UTF_8;
31+
import static java.util.Arrays.asList;
32+
import static org.assertj.core.api.Assertions.assertThat;
33+
import static org.junit.Assert.assertEquals;
34+
import org.junit.Assume;
35+
36+
/** Tests client-specific behavior, such as ensuring Content-Length is sent when specified. */
37+
public class JAXRSClientTest extends AbstractClientTest {
38+
39+
@Override
40+
public Builder newBuilder() {
41+
return Feign.builder().client(new JAXRSClient());
42+
}
43+
44+
@Override
45+
public void testPatch() throws Exception {
46+
try {
47+
super.testPatch();
48+
} catch (final ProcessingException e) {
49+
Assume.assumeNoException("JaxRS client do not support PATCH requests", e);
50+
}
51+
}
52+
53+
@Override
54+
public void noResponseBodyForPut() {
55+
try {
56+
super.noResponseBodyForPut();
57+
} catch (final IllegalStateException e) {
58+
Assume.assumeNoException("JaxRS client do not support empty bodies on PUT", e);
59+
}
60+
}
61+
62+
@Test
63+
public void reasonPhraseIsOptional() throws IOException, InterruptedException {
64+
server.enqueue(new MockResponse().setStatus("HTTP/1.1 " + 200));
65+
66+
final TestInterface api = newBuilder()
67+
.target(TestInterface.class, "http://localhost:" + server.getPort());
68+
69+
final Response response = api.post("foo");
70+
71+
assertThat(response.status()).isEqualTo(200);
72+
// jaxrsclient is creating a reason when none is present
73+
// assertThat(response.reason()).isNullOrEmpty();
74+
}
75+
76+
@Test
77+
public void parsesRequestAndResponse() throws IOException, InterruptedException {
78+
server.enqueue(new MockResponse().setBody("foo").addHeader("Foo: Bar"));
79+
80+
final TestInterface api = newBuilder()
81+
.target(TestInterface.class, "http://localhost:" + server.getPort());
82+
83+
final Response response = api.post("foo");
84+
85+
assertThat(response.status()).isEqualTo(200);
86+
assertThat(response.reason()).isEqualTo("OK");
87+
assertThat(response.headers())
88+
.containsEntry("Content-Length", asList("3"))
89+
.containsEntry("Foo", asList("Bar"));
90+
assertThat(response.body().asInputStream())
91+
.hasContentEqualTo(new ByteArrayInputStream("foo".getBytes(UTF_8)));
92+
93+
MockWebServerAssertions.assertThat(server.takeRequest()).hasMethod("POST")
94+
.hasPath("/?foo=bar&foo=baz&qux=")
95+
.hasBody("foo");
96+
}
97+
98+
@Test
99+
public void testContentTypeWithoutCharset2() throws Exception {
100+
server.enqueue(new MockResponse()
101+
.setBody("AAAAAAAA"));
102+
final JaxRSClientTestInterface api = newBuilder()
103+
.target(JaxRSClientTestInterface.class, "http://localhost:" + server.getPort());
104+
105+
final Response response = api.getWithContentType();
106+
// Response length should not be null
107+
assertEquals("AAAAAAAA", Util.toString(response.body().asReader()));
108+
109+
MockWebServerAssertions.assertThat(server.takeRequest())
110+
.hasHeaders("Accept: text/plain", "Content-Type: text/plain") // Note: OkHttp adds content
111+
// length.
112+
.hasMethod("GET");
113+
}
114+
115+
116+
public interface JaxRSClientTestInterface {
117+
118+
@RequestLine("GET /")
119+
@Headers({"Accept: text/plain", "Content-Type: text/plain"})
120+
Response getWithContentType();
121+
}
122+
}

0 commit comments

Comments
 (0)