diff --git a/autoconfigure/src/main/java/io/freefair/spring/okhttp/OkHttp3AutoConfiguration.java b/autoconfigure/src/main/java/io/freefair/spring/okhttp/OkHttp3AutoConfiguration.java index 78a01b4..b1a3172 100644 --- a/autoconfigure/src/main/java/io/freefair/spring/okhttp/OkHttp3AutoConfiguration.java +++ b/autoconfigure/src/main/java/io/freefair/spring/okhttp/OkHttp3AutoConfiguration.java @@ -57,10 +57,10 @@ public OkHttpClient okHttp3Client( eventListener.ifUnique(builder::eventListener); - builder.connectTimeout(okHttpProperties.getConnectTimeout().toMillis(), TimeUnit.MILLISECONDS); - builder.readTimeout(okHttpProperties.getReadTimeout().toMillis(), TimeUnit.MILLISECONDS); - builder.writeTimeout(okHttpProperties.getWriteTimeout().toMillis(), TimeUnit.MILLISECONDS); - builder.pingInterval(okHttpProperties.getPingInterval().toMillis(), TimeUnit.MILLISECONDS); + builder.connectTimeout(okHttpProperties.getConnectTimeout()); + builder.readTimeout(okHttpProperties.getReadTimeout()); + builder.writeTimeout(okHttpProperties.getWriteTimeout()); + builder.pingInterval(okHttpProperties.getPingInterval()); cookieJar.ifUnique(builder::cookieJar); diff --git a/autoconfigure/src/main/java/io/freefair/spring/okhttp/OkHttpRestClientAutoConfiguration.java b/autoconfigure/src/main/java/io/freefair/spring/okhttp/OkHttpRestClientAutoConfiguration.java index cec60c0..dac94e7 100644 --- a/autoconfigure/src/main/java/io/freefair/spring/okhttp/OkHttpRestClientAutoConfiguration.java +++ b/autoconfigure/src/main/java/io/freefair/spring/okhttp/OkHttpRestClientAutoConfiguration.java @@ -1,5 +1,6 @@ package io.freefair.spring.okhttp; +import io.freefair.spring.okhttp.client.OkHttpClientRequestFactory; import okhttp3.OkHttpClient; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; @@ -7,7 +8,6 @@ import org.springframework.boot.web.client.RestClientCustomizer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Conditional; -import org.springframework.http.client.OkHttp3ClientHttpRequestFactory; import org.springframework.web.client.RestClient; /** @@ -21,7 +21,7 @@ public class OkHttpRestClientAutoConfiguration { @Bean public RestClientCustomizer okHttpRestClientCustomizer(OkHttpClient okHttpClient) { - return restClientBuilder -> restClientBuilder.requestFactory(new OkHttp3ClientHttpRequestFactory(okHttpClient)); + return restClientBuilder -> restClientBuilder.requestFactory(new OkHttpClientRequestFactory(okHttpClient)); } } diff --git a/autoconfigure/src/main/java/io/freefair/spring/okhttp/OkHttpRestTemplateAutoConfiguration.java b/autoconfigure/src/main/java/io/freefair/spring/okhttp/OkHttpRestTemplateAutoConfiguration.java index a576021..eb8f3dc 100644 --- a/autoconfigure/src/main/java/io/freefair/spring/okhttp/OkHttpRestTemplateAutoConfiguration.java +++ b/autoconfigure/src/main/java/io/freefair/spring/okhttp/OkHttpRestTemplateAutoConfiguration.java @@ -1,5 +1,7 @@ package io.freefair.spring.okhttp; +import io.freefair.spring.okhttp.client.OkHttpClientRequestFactory; +import lombok.AllArgsConstructor; import okhttp3.OkHttpClient; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; @@ -7,14 +9,24 @@ import org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration; import org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration; import org.springframework.boot.autoconfigure.web.client.RestTemplateBuilderConfigurer; +import org.springframework.boot.ssl.SslBundle; +import org.springframework.boot.web.client.ClientHttpRequestFactorySettings; import org.springframework.boot.web.client.RestTemplateBuilder; import org.springframework.boot.web.client.RestTemplateCustomizer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Lazy; -import org.springframework.http.client.OkHttp3ClientHttpRequestFactory; +import org.springframework.http.client.ClientHttpRequestFactory; +import org.springframework.util.Assert; import org.springframework.web.client.RestTemplate; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; +import java.time.Duration; +import java.util.function.Function; + /** * @author Lars Grefer * @see RestTemplateAutoConfiguration @@ -30,8 +42,46 @@ public class OkHttpRestTemplateAutoConfiguration { public RestTemplateBuilder restTemplateBuilder(RestTemplateBuilderConfigurer restTemplateBuilderConfigurer, OkHttpClient okHttpClient) { RestTemplateBuilder builder = new RestTemplateBuilder(); - builder = builder.requestFactory(() -> new OkHttp3ClientHttpRequestFactory(okHttpClient)); + builder = builder.requestFactory(new RequestFactoryFunction(okHttpClient)); return restTemplateBuilderConfigurer.configure(builder); } + @AllArgsConstructor + static class RequestFactoryFunction implements Function { + + private OkHttpClient okHttpClient; + + @Override + public ClientHttpRequestFactory apply(ClientHttpRequestFactorySettings settings) { + + OkHttpClient.Builder builder = okHttpClient.newBuilder(); + + Duration connectTimeout = settings.connectTimeout(); + if (connectTimeout != null) { + builder.connectTimeout(connectTimeout); + } + + Duration readTimeout = settings.readTimeout(); + if (readTimeout != null) { + builder.readTimeout(readTimeout); + } + + SslBundle sslBundle = settings.sslBundle(); + if (sslBundle != null) { + Assert.state(!sslBundle.getOptions().isSpecified(), "SSL Options cannot be specified with OkHttp"); + + SSLContext sslContext = sslBundle.createSslContext(); + SSLSocketFactory socketFactory = sslContext.getSocketFactory(); + + TrustManager[] trustManagers = sslBundle.getManagers().getTrustManagers(); + Assert.state(trustManagers.length == 1, + "Trust material must be provided in the SSL bundle for OkHttp3ClientHttpRequestFactory"); + + builder.sslSocketFactory(socketFactory, (X509TrustManager) trustManagers[0]); + } + + return new OkHttpClientRequestFactory(builder.build()); + } + } + } diff --git a/autoconfigure/src/main/java/io/freefair/spring/okhttp/client/OkHttpClientRequest.java b/autoconfigure/src/main/java/io/freefair/spring/okhttp/client/OkHttpClientRequest.java new file mode 100644 index 0000000..4848e56 --- /dev/null +++ b/autoconfigure/src/main/java/io/freefair/spring/okhttp/client/OkHttpClientRequest.java @@ -0,0 +1,124 @@ +package io.freefair.spring.okhttp.client; + +import lombok.RequiredArgsConstructor; +import okhttp3.*; +import okio.Buffer; +import okio.ByteString; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.StreamingHttpOutputMessage; +import org.springframework.http.client.AbstractClientHttpRequest; +import org.springframework.http.client.ClientHttpRequest; +import org.springframework.http.client.ClientHttpResponse; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +import java.io.IOException; +import java.io.OutputStream; +import java.net.MalformedURLException; +import java.net.URI; + +/** + * OkHttp based {@link ClientHttpRequest} implementation. + * + * @author Lars Grefer + * @see OkHttpClientRequestFactory + */ +@RequiredArgsConstructor +class OkHttpClientRequest extends AbstractClientHttpRequest implements StreamingHttpOutputMessage { + + private final OkHttpClient okHttpClient; + + private final URI uri; + + private final HttpMethod method; + + + @Nullable + private Body streamingBody; + + @Nullable + private Buffer bufferBody; + + + @Override + public HttpMethod getMethod() { + return method; + } + + @Override + public URI getURI() { + return uri; + } + + @Override + public void setBody(Body body) { + Assert.notNull(body, "body must not be null"); + assertNotExecuted(); + Assert.state(bufferBody == null, "getBody has already been used."); + this.streamingBody = body; + } + + @Override + protected OutputStream getBodyInternal(HttpHeaders headers) { + Assert.state(this.streamingBody == null, "setBody has already been used."); + + if (bufferBody == null) { + bufferBody = new Buffer(); + } + + return bufferBody.outputStream(); + } + + @Override + protected ClientHttpResponse executeInternal(HttpHeaders headers) throws IOException { + + Request okHttpRequest = buildRequest(headers); + + Response okHttpResponse = this.okHttpClient.newCall(okHttpRequest).execute(); + + return new OkHttpClientResponse(okHttpResponse); + } + + private Request buildRequest(HttpHeaders headers) throws MalformedURLException { + + Request.Builder builder = new Request.Builder(); + + builder.url(uri.toURL()); + + MediaType contentType = null; + + String contentTypeHeader = headers.getFirst(HttpHeaders.CONTENT_TYPE); + if (StringUtils.hasText(contentTypeHeader)) { + contentType = MediaType.parse(contentTypeHeader); + } + + RequestBody body = null; + + if (bufferBody != null) { + ByteString bodyData = bufferBody.readByteString(); + if (headers.getContentLength() < 0) { + headers.setContentLength(bodyData.size()); + } + body = RequestBody.create(bodyData, contentType); + } else if (streamingBody != null) { + body = new StreamingBodyRequestBody(streamingBody, contentType, headers.getContentLength()); + } else if (okhttp3.internal.http.HttpMethod.requiresRequestBody(method.name())) { + body = RequestBody.create(new byte[0], contentType); + } + + builder.method(getMethod().name(), body); + + headers.forEach((name, values) -> { + for (String value : values) { + builder.addHeader(name, value); + } + }); + + return builder.build(); + + + } + +} diff --git a/autoconfigure/src/main/java/io/freefair/spring/okhttp/client/OkHttpClientRequestFactory.java b/autoconfigure/src/main/java/io/freefair/spring/okhttp/client/OkHttpClientRequestFactory.java new file mode 100644 index 0000000..144f895 --- /dev/null +++ b/autoconfigure/src/main/java/io/freefair/spring/okhttp/client/OkHttpClientRequestFactory.java @@ -0,0 +1,26 @@ +package io.freefair.spring.okhttp.client; + +import lombok.NonNull; +import okhttp3.OkHttpClient; +import org.springframework.http.HttpMethod; +import org.springframework.http.client.ClientHttpRequest; +import org.springframework.http.client.ClientHttpRequestFactory; + +import java.net.URI; + +/** + * OkHttp based {@link ClientHttpRequestFactory} implementation. + *

+ * Serves as replacement for the deprecated {@link org.springframework.http.client.OkHttp3ClientHttpRequestFactory}. + * + * @author Lars Grefer + */ +public record OkHttpClientRequestFactory(@NonNull OkHttpClient okHttpClient) implements ClientHttpRequestFactory { + + @Override + public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) { + return new OkHttpClientRequest(okHttpClient, uri, httpMethod); + } + + +} diff --git a/autoconfigure/src/main/java/io/freefair/spring/okhttp/client/OkHttpClientResponse.java b/autoconfigure/src/main/java/io/freefair/spring/okhttp/client/OkHttpClientResponse.java new file mode 100644 index 0000000..8e9d910 --- /dev/null +++ b/autoconfigure/src/main/java/io/freefair/spring/okhttp/client/OkHttpClientResponse.java @@ -0,0 +1,78 @@ +package io.freefair.spring.okhttp.client; + +import kotlin.Pair; +import lombok.RequiredArgsConstructor; +import okhttp3.Headers; +import okhttp3.Response; +import okhttp3.ResponseBody; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatusCode; +import org.springframework.http.client.ClientHttpResponse; + +import java.io.InputStream; + +/** + * OkHttp based {@link ClientHttpResponse} implementation. + * + * @author Lars Grefer + * @see OkHttpClientRequest + */ +@RequiredArgsConstructor +class OkHttpClientResponse implements ClientHttpResponse { + + private final Response okHttpResponse; + + private HttpHeaders springHeaders; + + @Override + public HttpStatusCode getStatusCode() { + return HttpStatusCode.valueOf(okHttpResponse.code()); + } + + @Override + public String getStatusText() { + return okHttpResponse.message(); + } + + @Override + public void close() { + ResponseBody body = okHttpResponse.body(); + if (body != null) { + body.close(); + } + } + + @Override + public InputStream getBody() { + ResponseBody body = okHttpResponse.body(); + if (body != null) { + return body.byteStream(); + } else { + return InputStream.nullInputStream(); + } + } + + @Override + public HttpHeaders getHeaders() { + if (springHeaders == null) { + springHeaders = convertHeaders(okHttpResponse.headers()); + } + + return springHeaders; + } + + /** + * Converts the given {@link Headers OkHttp Headers} to {@link HttpHeaders Spring Web HttpHeaders} + */ + static HttpHeaders convertHeaders(Headers okHttpHeaders) { + HttpHeaders springHeaders = new HttpHeaders(); + + for (Pair header : okHttpHeaders) { + springHeaders.add(header.getFirst(), header.getSecond()); + } + + return springHeaders; + } + + +} diff --git a/autoconfigure/src/main/java/io/freefair/spring/okhttp/client/StreamingBodyRequestBody.java b/autoconfigure/src/main/java/io/freefair/spring/okhttp/client/StreamingBodyRequestBody.java new file mode 100644 index 0000000..d68bbb4 --- /dev/null +++ b/autoconfigure/src/main/java/io/freefair/spring/okhttp/client/StreamingBodyRequestBody.java @@ -0,0 +1,49 @@ +package io.freefair.spring.okhttp.client; + +import lombok.RequiredArgsConstructor; +import okhttp3.MediaType; +import okhttp3.RequestBody; +import okio.BufferedSink; +import org.jetbrains.annotations.NotNull; +import org.springframework.http.StreamingHttpOutputMessage; +import org.springframework.lang.Nullable; + +import java.io.IOException; + +/** + * {@link StreamingHttpOutputMessage.Body} based {@link RequestBody} implementation. + * + * @author Lars Grefer + * @see OkHttpClientRequest + */ +@RequiredArgsConstructor +class StreamingBodyRequestBody extends RequestBody { + + private final StreamingHttpOutputMessage.Body streamingBody; + + private final MediaType contentType; + + @Nullable + private final long contentLength; + + @Nullable + @Override + public MediaType contentType() { + return contentType; + } + + @Override + public long contentLength() { + return contentLength; + } + + @Override + public void writeTo(@NotNull BufferedSink bufferedSink) throws IOException { + streamingBody.writeTo(bufferedSink.outputStream()); + } + + @Override + public boolean isOneShot() { + return true; + } +} diff --git a/autoconfigure/src/main/java/io/freefair/spring/okhttp/client/package-info.java b/autoconfigure/src/main/java/io/freefair/spring/okhttp/client/package-info.java new file mode 100644 index 0000000..adba992 --- /dev/null +++ b/autoconfigure/src/main/java/io/freefair/spring/okhttp/client/package-info.java @@ -0,0 +1,4 @@ +@NonNullApi +package io.freefair.spring.okhttp.client; + +import org.springframework.lang.NonNullApi; diff --git a/autoconfigure/src/test/java/io/freefair/spring/okhttp/OkHttpRestClientAutoConfigurationTest.java b/autoconfigure/src/test/java/io/freefair/spring/okhttp/OkHttpRestClientAutoConfigurationTest.java index 6cf2f57..9571a84 100644 --- a/autoconfigure/src/test/java/io/freefair/spring/okhttp/OkHttpRestClientAutoConfigurationTest.java +++ b/autoconfigure/src/test/java/io/freefair/spring/okhttp/OkHttpRestClientAutoConfigurationTest.java @@ -1,5 +1,6 @@ package io.freefair.spring.okhttp; +import io.freefair.spring.okhttp.client.OkHttpClientRequestFactory; import okhttp3.OkHttpClient; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -8,7 +9,6 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.client.AbstractClientHttpRequestFactoryWrapper; import org.springframework.http.client.ClientHttpRequestFactory; -import org.springframework.http.client.OkHttp3ClientHttpRequestFactory; import org.springframework.web.client.RestClient; import java.lang.reflect.Field; @@ -50,14 +50,11 @@ private OkHttpClient extractClient(RestClient restClient) throws NoSuchFieldExce requestFactory = (ClientHttpRequestFactory) field.get(requestFactory); } - assertThat(requestFactory).isInstanceOf(OkHttp3ClientHttpRequestFactory.class); + assertThat(requestFactory).isInstanceOf(OkHttpClientRequestFactory.class); - Field field = OkHttp3ClientHttpRequestFactory.class.getDeclaredField("client"); - field.setAccessible(true); - return (OkHttpClient) field.get(requestFactory); + return ((OkHttpClientRequestFactory)requestFactory).okHttpClient(); } - @SpringBootConfiguration @EnableAutoConfiguration public static class TestConfiguration { diff --git a/autoconfigure/src/test/java/io/freefair/spring/okhttp/OkHttpRestTemplateAutoConfigurationTest.java b/autoconfigure/src/test/java/io/freefair/spring/okhttp/OkHttpRestTemplateAutoConfigurationTest.java index d042e14..661e223 100644 --- a/autoconfigure/src/test/java/io/freefair/spring/okhttp/OkHttpRestTemplateAutoConfigurationTest.java +++ b/autoconfigure/src/test/java/io/freefair/spring/okhttp/OkHttpRestTemplateAutoConfigurationTest.java @@ -1,5 +1,6 @@ package io.freefair.spring.okhttp; +import io.freefair.spring.okhttp.client.OkHttpClientRequestFactory; import okhttp3.OkHttpClient; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -9,7 +10,6 @@ import org.springframework.boot.web.client.RestTemplateBuilder; import org.springframework.http.client.AbstractClientHttpRequestFactoryWrapper; import org.springframework.http.client.ClientHttpRequestFactory; -import org.springframework.http.client.OkHttp3ClientHttpRequestFactory; import org.springframework.web.client.RestTemplate; import java.lang.reflect.Field; @@ -48,11 +48,9 @@ private OkHttpClient extractClient(RestTemplate restTemplate) throws NoSuchField requestFactory = (ClientHttpRequestFactory) field.get(requestFactory); } - assertThat(requestFactory).isInstanceOf(OkHttp3ClientHttpRequestFactory.class); + assertThat(requestFactory).isInstanceOf(OkHttpClientRequestFactory.class); - Field field = OkHttp3ClientHttpRequestFactory.class.getDeclaredField("client"); - field.setAccessible(true); - return (OkHttpClient) field.get(requestFactory); + return ((OkHttpClientRequestFactory)requestFactory).okHttpClient(); } @SpringBootConfiguration diff --git a/autoconfigure/src/test/java/io/freefair/spring/okhttp/client/OkHttpClientRequestFactoryTest.java b/autoconfigure/src/test/java/io/freefair/spring/okhttp/client/OkHttpClientRequestFactoryTest.java new file mode 100644 index 0000000..6700913 --- /dev/null +++ b/autoconfigure/src/test/java/io/freefair/spring/okhttp/client/OkHttpClientRequestFactoryTest.java @@ -0,0 +1,58 @@ +package io.freefair.spring.okhttp.client; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.SpringBootConfiguration; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.web.client.RestTemplate; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest +class OkHttpClientRequestFactoryTest { + + RestTemplate restTemplate; + + @Autowired + RestTemplateBuilder restTemplateBuilder; + + @BeforeEach + void setUp() { + restTemplate = restTemplateBuilder.build(); + } + + @Test + void get() { + String response = restTemplate.getForObject("https://httpbin.org/get", String.class); + + assertThat(response).contains("okhttp"); + } + + @Test + void put() { + restTemplate.put("https://httpbin.org/put", "foo"); + } + + @Test + void post() { + String response = restTemplate.postForObject("https://httpbin.org/post", "foobar", String.class); + + assertThat(response).contains("foobar"); + } + + @Test + void post_empty() { + String response = restTemplate.postForObject("https://httpbin.org/post", null, String.class); + + assertThat(response).contains("headers"); + } + + @SpringBootConfiguration + @EnableAutoConfiguration + static class Config { + + } +} diff --git a/lombok.config b/lombok.config new file mode 100644 index 0000000..1eadded --- /dev/null +++ b/lombok.config @@ -0,0 +1,3 @@ +config.stopBubbling = true +lombok.addLombokGeneratedAnnotation = true +lombok.nonnull.exceptiontype=IllegalArgumentException \ No newline at end of file