From cfa0d07b65ac4d1f9dd682022068b4f4cb5da5e3 Mon Sep 17 00:00:00 2001 From: Marvin Froeder Date: Wed, 16 May 2018 17:06:52 +1200 Subject: [PATCH] Create client using javax.ws.rs.client.Client (#696) --- jaxrs2/pom.xml | 23 +++ .../main/java/feign/jaxrs2/JAXRSClient.java | 134 ++++++++++++++++++ .../java/feign/jaxrs2/JAXRSClientTest.java | 122 ++++++++++++++++ 3 files changed, 279 insertions(+) create mode 100644 jaxrs2/src/main/java/feign/jaxrs2/JAXRSClient.java create mode 100644 jaxrs2/src/test/java/feign/jaxrs2/JAXRSClientTest.java diff --git a/jaxrs2/pom.xml b/jaxrs2/pom.xml index 4f65eb80..c2002fc5 100644 --- a/jaxrs2/pom.xml +++ b/jaxrs2/pom.xml @@ -78,5 +78,28 @@ test-jar test + + + org.glassfish.jersey.core + jersey-client + 2.26 + test + + + org.glassfish.jersey.inject + jersey-hk2 + 2.26 + test + + + com.squareup.okhttp3 + mockwebserver + test + + + org.hamcrest + java-hamcrest + 2.0.0.0 + diff --git a/jaxrs2/src/main/java/feign/jaxrs2/JAXRSClient.java b/jaxrs2/src/main/java/feign/jaxrs2/JAXRSClient.java new file mode 100644 index 00000000..22b6022e --- /dev/null +++ b/jaxrs2/src/main/java/feign/jaxrs2/JAXRSClient.java @@ -0,0 +1,134 @@ +/** + * Copyright 2012-2018 The Feign Authors + * + * Licensed 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 feign.jaxrs2; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.Charset; +import java.util.Collection; +import java.util.Map; +import java.util.Map.Entry; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.Entity; +import javax.ws.rs.core.*; +import feign.Client; +import feign.Request.Options; + +/** + * This module directs Feign's http requests to javax.ws.rs.client.Client . Ex: + * + *
+ * GitHub github =
+ *     Feign.builder().client(new JaxRSClient()).target(GitHub.class, "https://api.github.com");
+ * 
+ */ +public class JAXRSClient implements Client { + + private final ClientBuilder clientBuilder; + + public JAXRSClient() { + this(ClientBuilder.newBuilder()); + } + + public JAXRSClient(ClientBuilder clientBuilder) { + this.clientBuilder = clientBuilder; + } + + @Override + public feign.Response execute(feign.Request request, Options options) throws IOException { + final Response response = clientBuilder + .connectTimeout(options.connectTimeoutMillis(), TimeUnit.MILLISECONDS) + .readTimeout(options.readTimeoutMillis(), TimeUnit.MILLISECONDS) + .build() + .target(request.url()) + .request() + .headers(toMultivaluedMap(request.headers())) + .method(request.method(), createRequestEntity(request)); + + return feign.Response.builder() + .request(request) + .body(response.readEntity(InputStream.class), + integerHeader(response, HttpHeaders.CONTENT_LENGTH)) + .headers(toMap(response.getStringHeaders())) + .status(response.getStatus()) + .reason(response.getStatusInfo().getReasonPhrase()) + .build(); + } + + private Entity createRequestEntity(feign.Request request) { + if (request.body() == null) { + return null; + } + + return Entity.entity( + request.body(), + new Variant(mediaType(request.headers()), locale(request.headers()), + encoding(request.charset()))); + } + + private Integer integerHeader(Response response, String header) { + final MultivaluedMap headers = response.getStringHeaders(); + if (!headers.containsKey(header)) { + return null; + } + + try { + return new Integer(headers.getFirst(header)); + } catch (final NumberFormatException e) { + // not a number or too big to fit Integer + return null; + } + } + + private String encoding(Charset charset) { + if (charset == null) + return null; + + return charset.name(); + } + + private String locale(Map> headers) { + if (!headers.containsKey(HttpHeaders.CONTENT_LANGUAGE)) + return null; + + return headers.get(HttpHeaders.CONTENT_LANGUAGE).iterator().next(); + } + + private MediaType mediaType(Map> headers) { + if (!headers.containsKey(HttpHeaders.CONTENT_TYPE)) + return null; + + return MediaType.valueOf(headers.get(HttpHeaders.CONTENT_TYPE).iterator().next()); + } + + private MultivaluedMap toMultivaluedMap(Map> headers) { + final MultivaluedHashMap mvHeaders = new MultivaluedHashMap<>(); + + headers.entrySet().forEach(entry -> entry.getValue().stream() + .forEach(value -> mvHeaders.add(entry.getKey(), value))); + + return mvHeaders; + } + + private Map> toMap(MultivaluedMap headers) { + return headers.entrySet().stream() + .collect(Collectors.toMap( + Entry::getKey, + Entry::getValue)); + } + +} + diff --git a/jaxrs2/src/test/java/feign/jaxrs2/JAXRSClientTest.java b/jaxrs2/src/test/java/feign/jaxrs2/JAXRSClientTest.java new file mode 100644 index 00000000..7ec10b87 --- /dev/null +++ b/jaxrs2/src/test/java/feign/jaxrs2/JAXRSClientTest.java @@ -0,0 +1,122 @@ +/** + * Copyright 2012-2018 The Feign Authors + * + * Licensed 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 feign.jaxrs2; + +import feign.Feign.Builder; +import feign.Headers; +import feign.RequestLine; +import feign.Response; +import feign.Util; +import feign.assertj.MockWebServerAssertions; +import feign.client.AbstractClientTest; +import feign.jaxrs2.JAXRSClient; +import feign.Feign; +import okhttp3.mockwebserver.MockResponse; +import org.junit.Test; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import javax.ws.rs.ProcessingException; +import static feign.Util.UTF_8; +import static java.util.Arrays.asList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertEquals; +import org.junit.Assume; + +/** Tests client-specific behavior, such as ensuring Content-Length is sent when specified. */ +public class JAXRSClientTest extends AbstractClientTest { + + @Override + public Builder newBuilder() { + return Feign.builder().client(new JAXRSClient()); + } + + @Override + public void testPatch() throws Exception { + try { + super.testPatch(); + } catch (final ProcessingException e) { + Assume.assumeNoException("JaxRS client do not support PATCH requests", e); + } + } + + @Override + public void noResponseBodyForPut() { + try { + super.noResponseBodyForPut(); + } catch (final IllegalStateException e) { + Assume.assumeNoException("JaxRS client do not support empty bodies on PUT", e); + } + } + + @Test + public void reasonPhraseIsOptional() throws IOException, InterruptedException { + server.enqueue(new MockResponse().setStatus("HTTP/1.1 " + 200)); + + final TestInterface api = newBuilder() + .target(TestInterface.class, "http://localhost:" + server.getPort()); + + final Response response = api.post("foo"); + + assertThat(response.status()).isEqualTo(200); + // jaxrsclient is creating a reason when none is present + // assertThat(response.reason()).isNullOrEmpty(); + } + + @Test + public void parsesRequestAndResponse() throws IOException, InterruptedException { + server.enqueue(new MockResponse().setBody("foo").addHeader("Foo: Bar")); + + final TestInterface api = newBuilder() + .target(TestInterface.class, "http://localhost:" + server.getPort()); + + final Response response = api.post("foo"); + + assertThat(response.status()).isEqualTo(200); + assertThat(response.reason()).isEqualTo("OK"); + assertThat(response.headers()) + .containsEntry("Content-Length", asList("3")) + .containsEntry("Foo", asList("Bar")); + assertThat(response.body().asInputStream()) + .hasContentEqualTo(new ByteArrayInputStream("foo".getBytes(UTF_8))); + + MockWebServerAssertions.assertThat(server.takeRequest()).hasMethod("POST") + .hasPath("/?foo=bar&foo=baz&qux=") + .hasBody("foo"); + } + + @Test + public void testContentTypeWithoutCharset2() throws Exception { + server.enqueue(new MockResponse() + .setBody("AAAAAAAA")); + final JaxRSClientTestInterface api = newBuilder() + .target(JaxRSClientTestInterface.class, "http://localhost:" + server.getPort()); + + final Response response = api.getWithContentType(); + // Response length should not be null + assertEquals("AAAAAAAA", Util.toString(response.body().asReader())); + + MockWebServerAssertions.assertThat(server.takeRequest()) + .hasHeaders("Accept: text/plain", "Content-Type: text/plain") // Note: OkHttp adds content + // length. + .hasMethod("GET"); + } + + + public interface JaxRSClientTestInterface { + + @RequestLine("GET /") + @Headers({"Accept: text/plain", "Content-Type: text/plain"}) + Response getWithContentType(); + } +}