diff --git a/spring-web/src/main/java/org/springframework/http/client/JdkClientClientHttpRequest.java b/spring-web/src/main/java/org/springframework/http/client/JdkClientClientHttpRequest.java new file mode 100644 index 0000000000..89b3b42968 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/client/JdkClientClientHttpRequest.java @@ -0,0 +1,92 @@ +/* + * Copyright 2023-2023 the original author or 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 + * + * https://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 org.springframework.http.client; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.List; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; + +/** + * {@link ClientHttpRequest} implementation based on the Java {@code HttpClient}. + * + * @author Marten Deinum + * @since 6.1 + */ +public class JdkClientClientHttpRequest extends AbstractBufferingClientHttpRequest { + + /* + * The JDK HttpRequest doesn't allow all headers to be set. The named headers are taken from the default + * implementation for HttpRequest. + */ + private static final List DISALLOWED_HEADERS = + List.of("connection", "content-length", "expect", "host", "upgrade"); + + private final HttpClient client; + private final URI uri; + private final HttpMethod method; + public JdkClientClientHttpRequest(HttpClient client, URI uri, HttpMethod method) { + this.client = client; + this.uri = uri; + this.method = method; + } + + @Override + public HttpMethod getMethod() { + return this.method; + } + + @Override + public URI getURI() { + return this.uri; + } + + @Override + protected ClientHttpResponse executeInternal(HttpHeaders headers, byte[] content) throws IOException { + + HttpRequest.Builder builder = HttpRequest.newBuilder(this.uri) + .method(getMethod().name(), HttpRequest.BodyPublishers.ofByteArray(content)); + + addHeaders(headers, builder); + HttpRequest request = builder.build(); + HttpResponse response; + try { + response = this.client.send(request, HttpResponse.BodyHandlers.ofInputStream()); + } catch (InterruptedException ex) + { + Thread.currentThread().interrupt(); + throw new IllegalStateException("Request interupted.", ex); + } + return new JdkClientClientHttpResponse(response); + } + + private static void addHeaders(HttpHeaders headers, HttpRequest.Builder builder) { + headers.forEach((headerName, headerValues) -> { + if (!DISALLOWED_HEADERS.contains(headerName.toLowerCase())) { + for (String headerValue : headerValues) { + builder.header(headerName, headerValue); + } + } + }); + } +} diff --git a/spring-web/src/main/java/org/springframework/http/client/JdkClientClientHttpRequestFactory.java b/spring-web/src/main/java/org/springframework/http/client/JdkClientClientHttpRequestFactory.java new file mode 100644 index 0000000000..02c2269d80 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/client/JdkClientClientHttpRequestFactory.java @@ -0,0 +1,55 @@ +/* + * Copyright 2023-2023 the original author or 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 + * + * https://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 org.springframework.http.client; + +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; + +import org.springframework.http.HttpMethod; + + +/** + * {@link ClientHttpRequestFactory} implementation that uses a + * HttpClient to create requests. + * + * @author Marten Deinum + * @since 6.1 + */ +public class JdkClientClientHttpRequestFactory implements ClientHttpRequestFactory { + + private HttpClient client; + + private final boolean defaultClient; + + + public JdkClientClientHttpRequestFactory() { + this.client = HttpClient.newHttpClient(); + this.defaultClient = true; + } + + public JdkClientClientHttpRequestFactory(HttpClient client) { + this.client = client; + this.defaultClient = false; + } + + @Override + public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) throws IOException { + return new JdkClientClientHttpRequest(this.client, uri, httpMethod); + } + +} diff --git a/spring-web/src/main/java/org/springframework/http/client/JdkClientClientHttpResponse.java b/spring-web/src/main/java/org/springframework/http/client/JdkClientClientHttpResponse.java new file mode 100644 index 0000000000..770f79fb16 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/client/JdkClientClientHttpResponse.java @@ -0,0 +1,98 @@ +/* + * Copyright 2023-2023 the original author or 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 + * + * https://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 org.springframework.http.client; + +import java.io.IOException; +import java.io.InputStream; +import java.net.http.HttpResponse; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.HttpStatusCode; +import org.springframework.lang.Nullable; +import org.springframework.util.StreamUtils; + +/** + * {@link ClientHttpResponse} implementation based on the Java {@code HttpClient}. + * + * @author Marten Deinum + * @since 6.1 + */ +public class JdkClientClientHttpResponse implements ClientHttpResponse { + + private final HttpResponse response; + @Nullable + private volatile HttpHeaders headers; + + public JdkClientClientHttpResponse(HttpResponse response) { + this.response = response; + } + + @Override + public HttpStatusCode getStatusCode() throws IOException { + return HttpStatusCode.valueOf(this.response.statusCode()); + } + + @Override + @Deprecated + public int getRawStatusCode() { + return this.response.statusCode(); + } + + @Override + public String getStatusText() { + HttpStatus status = HttpStatus.resolve(this.response.statusCode()); + return (status != null) ? status.getReasonPhrase() : ""; + } + + @Override + public InputStream getBody() throws IOException { + InputStream body = this.response.body(); + return (body != null ? body : InputStream.nullInputStream()); + } + + @Override + public HttpHeaders getHeaders() { + HttpHeaders headers = this.headers; + if (headers == null) { + headers = new HttpHeaders(); + for (String headerName : this.response.headers().map().keySet()) { + for (String headerValue : this.response.headers().allValues(headerName)) { + headers.add(headerName, headerValue); + } + } + this.headers = headers; + } + return headers; + } + + @Override + public void close() { + InputStream body = this.response.body(); + try { + try { + StreamUtils.drain(body); + } + finally { + body.close(); + } + } + catch (IOException ex) { + // Ignore exception on close... + } + } +} diff --git a/spring-web/src/test/java/org/springframework/http/client/JdkClientClientHttpRequestFactoryTests.java b/spring-web/src/test/java/org/springframework/http/client/JdkClientClientHttpRequestFactoryTests.java new file mode 100644 index 0000000000..c4ee18a8ad --- /dev/null +++ b/spring-web/src/test/java/org/springframework/http/client/JdkClientClientHttpRequestFactoryTests.java @@ -0,0 +1,40 @@ +/* + * Copyright 2023-2023 the original author or 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 + * + * https://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 org.springframework.http.client; + +import org.junit.jupiter.api.Test; + +import org.springframework.http.HttpMethod; + +/** + * @author Marten Deinum + */ +public class JdkClientClientHttpRequestFactoryTests extends AbstractHttpRequestFactoryTests { + + @Override + protected ClientHttpRequestFactory createRequestFactory() { + return new JdkClientClientHttpRequestFactory(); + } + + @Override + @Test + public void httpMethods() throws Exception { + super.httpMethods(); + assertHttpMethod("patch", HttpMethod.PATCH); + } + +}