Polishing external contribution

- Removed duplicate Client in types names.
- Removed buffering in favor of OutputStream to
  Flow.Publisher<ByteBuffer> bridge.
- Made request and types package private.
- Various other small improvements.

Closes gh-30478
This commit is contained in:
Arjen Poutsma 2023-06-28 12:22:36 +02:00
parent 2ca8dd2faa
commit 0033eb4ed6
9 changed files with 851 additions and 189 deletions

View File

@ -1,92 +0,0 @@
/*
* 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<String> 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<InputStream> 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);
}
}
});
}
}

View File

@ -1,55 +0,0 @@
/*
* 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
* <a href="https://docs.oracle.com/en/java/javase/17/docs/api/java.net.http/java/net/http/HttpClient.html">HttpClient</a> 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);
}
}

View File

@ -0,0 +1,134 @@
/*
* 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.io.UncheckedIOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.ByteBuffer;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.concurrent.Flow;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.lang.Nullable;
import org.springframework.util.StreamUtils;
/**
* {@link ClientHttpRequest} implementation based the Java {@link HttpClient}.
* Created via the {@link JdkClientHttpRequestFactory}.
*
* @author Marten Deinum
* @author Arjen Poutsma
* @since 6.1
*/
class JdkClientHttpRequest extends AbstractStreamingClientHttpRequest {
/*
* 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<String> DISALLOWED_HEADERS =
List.of("connection", "content-length", "expect", "host", "upgrade");
private final HttpClient httpClient;
private final HttpMethod method;
private final URI uri;
private final Executor executor;
public JdkClientHttpRequest(HttpClient httpClient, URI uri, HttpMethod method, Executor executor) {
this.httpClient = httpClient;
this.uri = uri;
this.method = method;
this.executor = executor;
}
@Override
public HttpMethod getMethod() {
return this.method;
}
@Override
public URI getURI() {
return this.uri;
}
@Override
protected ClientHttpResponse executeInternal(HttpHeaders headers, @Nullable Body body) throws IOException {
try {
HttpRequest request = buildRequest(headers, body);
HttpResponse<InputStream> response = this.httpClient.send(request, HttpResponse.BodyHandlers.ofInputStream());
return new JdkClientHttpResponse(response);
}
catch (UncheckedIOException ex) {
throw ex.getCause();
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
throw new IOException("Could not send request: " + ex.getMessage(), ex);
}
}
private HttpRequest buildRequest(HttpHeaders headers, @Nullable Body body) {
HttpRequest.Builder builder = HttpRequest.newBuilder()
.uri(this.uri);
headers.forEach((headerName, headerValues) -> {
if (!headerName.equalsIgnoreCase(HttpHeaders.CONTENT_LENGTH)) {
if (!DISALLOWED_HEADERS.contains(headerName.toLowerCase())) {
for (String headerValue : headerValues) {
builder.header(headerName, headerValue);
}
}
}
});
builder.method(this.method.name(), bodyPublisher(headers, body));
return builder.build();
}
private HttpRequest.BodyPublisher bodyPublisher(HttpHeaders headers, @Nullable Body body) {
if (body != null) {
Flow.Publisher<ByteBuffer> outputStreamPublisher = OutputStreamPublisher.create(
outputStream -> body.writeTo(StreamUtils.nonClosing(outputStream)),
this.executor);
long contentLength = headers.getContentLength();
if (contentLength != -1) {
return HttpRequest.BodyPublishers.fromPublisher(outputStreamPublisher, contentLength);
}
else {
return HttpRequest.BodyPublishers.fromPublisher(outputStreamPublisher);
}
}
else {
return HttpRequest.BodyPublishers.noBody();
}
}
}

View File

@ -0,0 +1,82 @@
/*
* 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 java.util.concurrent.Executor;
import org.springframework.core.task.SimpleAsyncTaskExecutor;
import org.springframework.http.HttpMethod;
import org.springframework.util.Assert;
/**
* {@link ClientHttpRequestFactory} implementation based on the Java
* {@link HttpClient}.
*
* @author Marten Deinum
* @author Arjen Poutsma
* @since 6.1
*/
public class JdkClientHttpRequestFactory implements ClientHttpRequestFactory {
private final HttpClient httpClient;
private final Executor executor;
/**
* Create a new instance of the {@code JdkClientHttpRequestFactory}
* with a default {@link HttpClient}.
*/
public JdkClientHttpRequestFactory() {
this(HttpClient.newHttpClient());
}
/**
* Create a new instance of the {@code JdkClientHttpRequestFactory} based on
* the given {@link HttpClient}.
* @param httpClient the client to base on
*/
public JdkClientHttpRequestFactory(HttpClient httpClient) {
Assert.notNull(httpClient, "HttpClient is required");
this.httpClient = httpClient;
this.executor = httpClient.executor().orElseGet(SimpleAsyncTaskExecutor::new);
}
/**
* Create a new instance of the {@code JdkClientHttpRequestFactory} based on
* the given {@link HttpClient} and {@link Executor}.
* @param httpClient the client to base on
* @param executor the executor to use for blocking write operations
*/
public JdkClientHttpRequestFactory(HttpClient httpClient, Executor executor) {
Assert.notNull(httpClient, "HttpClient is required");
Assert.notNull(executor, "Executor must not be null");
this.httpClient = httpClient;
this.executor = executor;
}
@Override
public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) throws IOException {
return new JdkClientHttpRequest(this.httpClient, uri, httpMethod, this.executor);
}
}

View File

@ -18,81 +18,89 @@ package org.springframework.http.client;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.net.http.HttpClient;
import java.net.http.HttpResponse; import java.net.http.HttpResponse;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.HttpStatusCode; import org.springframework.http.HttpStatusCode;
import org.springframework.lang.Nullable; import org.springframework.util.CollectionUtils;
import org.springframework.util.LinkedCaseInsensitiveMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StreamUtils; import org.springframework.util.StreamUtils;
/** /**
* {@link ClientHttpResponse} implementation based on the Java {@code HttpClient}. * {@link ClientHttpResponse} implementation based on the Java {@link HttpClient}.
* *
* @author Marten Deinum * @author Marten Deinum
* @author Arjen Poutsma
* @since 6.1 * @since 6.1
*/ */
public class JdkClientClientHttpResponse implements ClientHttpResponse { class JdkClientHttpResponse implements ClientHttpResponse {
private final HttpResponse<InputStream> response; private final HttpResponse<InputStream> response;
@Nullable
private volatile HttpHeaders headers;
public JdkClientClientHttpResponse(HttpResponse<InputStream> response) { private final HttpHeaders headers;
private final InputStream body;
public JdkClientHttpResponse(HttpResponse<InputStream> response) {
this.response = response; this.response = response;
this.headers = adaptHeaders(response);
InputStream inputStream = response.body();
this.body = (inputStream != null) ? inputStream : InputStream.nullInputStream();
} }
private static HttpHeaders adaptHeaders(HttpResponse<?> response) {
Map<String, List<String>> rawHeaders = response.headers().map();
Map<String, List<String>> map = new LinkedCaseInsensitiveMap<>(rawHeaders.size(), Locale.ENGLISH);
MultiValueMap<String, String> multiValueMap = CollectionUtils.toMultiValueMap(map);
multiValueMap.putAll(rawHeaders);
return HttpHeaders.readOnlyHttpHeaders(multiValueMap);
}
@Override @Override
public HttpStatusCode getStatusCode() throws IOException { public HttpStatusCode getStatusCode() {
return HttpStatusCode.valueOf(this.response.statusCode()); return HttpStatusCode.valueOf(this.response.statusCode());
} }
@Override
@Deprecated
public int getRawStatusCode() {
return this.response.statusCode();
}
@Override @Override
public String getStatusText() { public String getStatusText() {
HttpStatus status = HttpStatus.resolve(this.response.statusCode()); // HttpResponse does not expose status text
return (status != null) ? status.getReasonPhrase() : ""; if (getStatusCode() instanceof HttpStatus status) {
} return status.getReasonPhrase();
}
@Override else {
public InputStream getBody() throws IOException { return "";
InputStream body = this.response.body(); }
return (body != null ? body : InputStream.nullInputStream());
} }
@Override @Override
public HttpHeaders getHeaders() { public HttpHeaders getHeaders() {
HttpHeaders headers = this.headers; return this.headers;
if (headers == null) { }
headers = new HttpHeaders();
for (String headerName : this.response.headers().map().keySet()) { @Override
for (String headerValue : this.response.headers().allValues(headerName)) { public InputStream getBody() throws IOException {
headers.add(headerName, headerValue); return this.body;
}
}
this.headers = headers;
}
return headers;
} }
@Override @Override
public void close() { public void close() {
InputStream body = this.response.body();
try { try {
try { try {
StreamUtils.drain(body); StreamUtils.drain(this.body);
} }
finally { finally {
body.close(); this.body.close();
} }
} }
catch (IOException ex) { catch (IOException ignored) {
// Ignore exception on close...
} }
} }
} }

View File

@ -0,0 +1,400 @@
/*
* Copyright 2002-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.BufferedOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.util.Objects;
import java.util.concurrent.Executor;
import java.util.concurrent.Flow;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.LockSupport;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* Bridges between {@link OutputStream} and
* {@link Flow.Publisher Flow.Publisher&lt;ByteBuffer&gt;}.
*
* @author Oleh Dokuka
* @author Arjen Poutsma
* @since 6.1
* @see #create(OutputStreamHandler, Executor)
*/
final class OutputStreamPublisher implements Flow.Publisher<ByteBuffer> {
private final OutputStreamHandler outputStreamHandler;
private final Executor executor;
private OutputStreamPublisher(OutputStreamHandler outputStreamHandler, Executor executor) {
this.outputStreamHandler = outputStreamHandler;
this.executor = executor;
}
/**
* Creates a new {@code Publisher<ByteBuffer>} based on bytes written to a
* {@code OutputStream}.
* <ul>
* <li>The parameter {@code outputStreamHandler} is invoked once per
* subscription of the returned {@code Publisher}, when the first
* {@code ByteBuffer} is
* {@linkplain Flow.Subscription#request(long) requested}.</li>
* <li>Each {@link OutputStream#write(byte[], int, int) OutputStream.write()}
* invocation that {@code outputStreamHandler} makes will result in a
* {@linkplain Flow.Subscriber#onNext(Object) published} {@code ByteBuffer}
* if there is {@linkplain Flow.Subscription#request(long) demand}.</li>
* <li>If there is <em>no demand</em>, {@code OutputStream.write()} will block
* until there is.</li>
* <li>If the subscription is {@linkplain Flow.Subscription#cancel() cancelled},
* {@code OutputStream.write()} will throw a {@code IOException}.</li>
* <li>{@linkplain OutputStream#close() Closing} the {@code OutputStream}
* will result in a {@linkplain Flow.Subscriber#onComplete() complete} signal.</li>
* <li>Any {@code IOException}s thrown from {@code outputStreamHandler} will
* be dispatched to the {@linkplain Flow.Subscriber#onError(Throwable) Subscriber}.
* </ul>
* @param outputStreamHandler invoked when the first buffer is requested
* @param executor used to invoke the {@code outputStreamHandler}
* @return a {@code Publisher<ByteBuffer>} based on bytes written by
* {@code outputStreamHandler}
*/
public static Flow.Publisher<ByteBuffer> create(OutputStreamHandler outputStreamHandler, Executor executor) {
Assert.notNull(outputStreamHandler, "OutputStreamHandler must not be null");
Assert.notNull(executor, "Executor must not be null");
return new OutputStreamPublisher(outputStreamHandler, executor);
}
@Override
public void subscribe(Flow.Subscriber<? super ByteBuffer> subscriber) {
Objects.requireNonNull(subscriber, "Subscriber must not be null");
OutputStreamSubscription subscription = new OutputStreamSubscription(subscriber, this.outputStreamHandler);
subscriber.onSubscribe(subscription);
this.executor.execute(subscription::invokeHandler);
}
/**
* Defines the contract for handling the {@code OutputStream} provided by
* the {@code OutputStreamPublisher}.
*/
@FunctionalInterface
public interface OutputStreamHandler {
/**
* Use the given stream for writing.
* <ul>
* <li>If the linked subscription has
* {@linkplain Flow.Subscription#request(long) demand}, any
* {@linkplain OutputStream#write(byte[], int, int) written} bytes
* will be {@linkplain Flow.Subscriber#onNext(Object) published} to the
* {@link Flow.Subscriber Subscriber}.</li>
* <li>If there is no demand, any
* {@link OutputStream#write(byte[], int, int) write()} invocations will
* block until there is demand.</li>
* <li>If the linked subscription is
* {@linkplain Flow.Subscription#cancel() cancelled},
* {@link OutputStream#write(byte[], int, int) write()} invocations will
* result in a {@code IOException}.</li>
* </ul>
* @param outputStream the stream to write to
* @throws IOException any thrown I/O errors will be dispatched to the
* {@linkplain Flow.Subscriber#onError(Throwable) Subscriber}
*/
void handle(OutputStream outputStream) throws IOException;
}
private static final class OutputStreamSubscription extends OutputStream implements Flow.Subscription {
static final Object READY = new Object();
private final Flow.Subscriber<? super ByteBuffer> actual;
private final OutputStreamHandler outputStreamHandler;
private final AtomicLong requested = new AtomicLong();
private final AtomicReference<Object> parkedThreadAtomic = new AtomicReference<>();
@Nullable
private volatile Throwable error;
private long produced;
public OutputStreamSubscription(Flow.Subscriber<? super ByteBuffer> actual,
OutputStreamHandler outputStreamHandler) {
this.actual = actual;
this.outputStreamHandler = outputStreamHandler;
}
@Override
public void write(int b) throws IOException {
checkDemandAndAwaitIfNeeded();
ByteBuffer byteBuffer = ByteBuffer.allocate(1);
byteBuffer.put((byte) b);
byteBuffer.flip();
this.actual.onNext(byteBuffer);
this.produced++;
}
@Override
public void write(byte[] b) throws IOException {
write(b, 0, b.length);
}
@Override
public void write(byte[] b, int off, int len) throws IOException {
checkDemandAndAwaitIfNeeded();
ByteBuffer byteBuffer = ByteBuffer.allocate(len);
byteBuffer.put(b, off, len);
byteBuffer.flip();
this.actual.onNext(byteBuffer);
this.produced++;
}
private void checkDemandAndAwaitIfNeeded() throws IOException {
long r = this.requested.get();
if (isTerminated(r) || isCancelled(r)) {
throw new IOException("Subscription has been terminated");
}
long p = this.produced;
if (p == r) {
if (p > 0) {
r = tryProduce(p);
this.produced = 0;
}
while (true) {
if (isTerminated(r) || isCancelled(r)) {
throw new IOException("Subscription has been terminated");
}
if (r != 0) {
return;
}
await();
r = this.requested.get();
}
}
}
private void invokeHandler() {
// assume sync write within try-with-resource block
// use BufferedOutputStream, so that written bytes are buffered
// before publishing as byte buffer
try (OutputStream outputStream = new BufferedOutputStream(this)) {
this.outputStreamHandler.handle(outputStream);
}
catch (IOException ex) {
long previousState = tryTerminate();
if (isCancelled(previousState)) {
return;
}
if (isTerminated(previousState)) {
// failure due to illegal requestN
this.actual.onError(this.error);
return;
}
this.actual.onError(ex);
return;
}
long previousState = tryTerminate();
if (isCancelled(previousState)) {
return;
}
if (isTerminated(previousState)) {
// failure due to illegal requestN
this.actual.onError(this.error);
return;
}
this.actual.onComplete();
}
@Override
public void request(long n) {
if (n <= 0) {
this.error = new IllegalArgumentException("request should be a positive number");
long previousState = tryTerminate();
if (isTerminated(previousState) || isCancelled(previousState)) {
return;
}
if (previousState > 0) {
// error should eventually be observed and propagated
return;
}
// resume parked thread, so it can observe error and propagate it
resume();
return;
}
if (addCap(n) == 0) {
// resume parked thread so it can continue the work
resume();
}
}
@Override
public void cancel() {
long previousState = tryCancel();
if (isCancelled(previousState) || previousState > 0) {
return;
}
// resume parked thread, so it can be unblocked and close all the resources
resume();
}
private void await() {
Thread toUnpark = Thread.currentThread();
while (true) {
Object current = this.parkedThreadAtomic.get();
if (current == READY) {
break;
}
if (current != null && current != toUnpark) {
throw new IllegalStateException("Only one (Virtual)Thread can await!");
}
if (this.parkedThreadAtomic.compareAndSet(null, toUnpark)) {
LockSupport.park();
// we don't just break here because park() can wake up spuriously
// if we got a proper resume, get() == READY and the loop will quit above
}
}
// clear the resume indicator so that the next await call will park without a resume()
this.parkedThreadAtomic.lazySet(null);
}
private void resume() {
if (this.parkedThreadAtomic.get() != READY) {
Object old = this.parkedThreadAtomic.getAndSet(READY);
if (old != READY) {
LockSupport.unpark((Thread)old);
}
}
}
private long tryCancel() {
while (true) {
long r = this.requested.get();
if (isCancelled(r)) {
return r;
}
if (this.requested.compareAndSet(r, Long.MIN_VALUE)) {
return r;
}
}
}
private long tryTerminate() {
while (true) {
long r = this.requested.get();
if (isCancelled(r) || isTerminated(r)) {
return r;
}
if (this.requested.compareAndSet(r, Long.MIN_VALUE | Long.MAX_VALUE)) {
return r;
}
}
}
private long tryProduce(long n) {
while (true) {
long current = this.requested.get();
if (isTerminated(current) || isCancelled(current)) {
return current;
}
if (current == Long.MAX_VALUE) {
return Long.MAX_VALUE;
}
long update = current - n;
if (update < 0L) {
update = 0L;
}
if (this.requested.compareAndSet(current, update)) {
return update;
}
}
}
private long addCap(long n) {
while (true) {
long r = this.requested.get();
if (isTerminated(r) || isCancelled(r) || r == Long.MAX_VALUE) {
return r;
}
long u = addCap(r, n);
if (this.requested.compareAndSet(r, u)) {
return r;
}
}
}
private static boolean isTerminated(long state) {
return state == (Long.MIN_VALUE | Long.MAX_VALUE);
}
private static boolean isCancelled(long state) {
return state == Long.MIN_VALUE;
}
private static long addCap(long a, long b) {
long res = a + b;
if (res < 0L) {
return Long.MAX_VALUE;
}
return res;
}
}
}

View File

@ -23,11 +23,11 @@ import org.springframework.http.HttpMethod;
/** /**
* @author Marten Deinum * @author Marten Deinum
*/ */
public class JdkClientClientHttpRequestFactoryTests extends AbstractHttpRequestFactoryTests { public class JdkClientHttpRequestFactoryTests extends AbstractHttpRequestFactoryTests {
@Override @Override
protected ClientHttpRequestFactory createRequestFactory() { protected ClientHttpRequestFactory createRequestFactory() {
return new JdkClientClientHttpRequestFactory(); return new JdkClientHttpRequestFactory();
} }
@Override @Override

View File

@ -0,0 +1,182 @@
/*
* Copyright 2002-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.OutputStreamWriter;
import java.io.Writer;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.Flow;
import org.junit.jupiter.api.Test;
import org.reactivestreams.FlowAdapters;
import reactor.core.publisher.Flux;
import reactor.test.StepVerifier;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIOException;
/**
* @author Arjen Poutsma
* @author Oleh Dokuka
*/
class OutputStreamPublisherTests {
private final Executor executor = Executors.newSingleThreadExecutor();
@Test
void basic() {
Flow.Publisher<ByteBuffer> flowPublisher = OutputStreamPublisher.create(outputStream -> {
try (Writer writer = new OutputStreamWriter(outputStream, StandardCharsets.UTF_8)) {
writer.write("foo");
writer.write("bar");
writer.write("baz");
}
}, this.executor);
Flux<String> flux = toString(flowPublisher);
StepVerifier.create(flux)
.assertNext(s -> assertThat(s).isEqualTo("foobarbaz"))
.verifyComplete();
}
@Test
void flush() {
Flow.Publisher<ByteBuffer> flowPublisher = OutputStreamPublisher.create(outputStream -> {
try (Writer writer = new OutputStreamWriter(outputStream, StandardCharsets.UTF_8)) {
writer.write("foo");
writer.flush();
writer.write("bar");
writer.flush();
writer.write("baz");
writer.flush();
}
}, this.executor);
Flux<String> flux = toString(flowPublisher);
StepVerifier.create(flux)
.assertNext(s -> assertThat(s).isEqualTo("foo"))
.assertNext(s -> assertThat(s).isEqualTo("bar"))
.assertNext(s -> assertThat(s).isEqualTo("baz"))
.verifyComplete();
}
@Test
void cancel() throws InterruptedException {
CountDownLatch latch = new CountDownLatch(1);
Flow.Publisher<ByteBuffer> flowPublisher = OutputStreamPublisher.create(outputStream -> {
try (Writer writer = new OutputStreamWriter(outputStream, StandardCharsets.UTF_8)) {
assertThatIOException()
.isThrownBy(() -> {
writer.write("foo");
writer.flush();
writer.write("bar");
writer.flush();
})
.withMessage("Subscription has been terminated");
latch.countDown();
}
}, this.executor);
Flux<String> flux = toString(flowPublisher);
StepVerifier.create(flux, 1)
.assertNext(s -> assertThat(s).isEqualTo("foo"))
.thenCancel()
.verify();
latch.await();
}
@Test
void closed() throws InterruptedException {
CountDownLatch latch = new CountDownLatch(1);
Flow.Publisher<ByteBuffer> flowPublisher = OutputStreamPublisher.create(outputStream -> {
Writer writer = new OutputStreamWriter(outputStream, StandardCharsets.UTF_8);
writer.write("foo");
writer.close();
assertThatIOException().isThrownBy(() -> writer.write("bar"))
.withMessage("Stream closed");
latch.countDown();
}, this.executor);
Flux<String> flux = toString(flowPublisher);
StepVerifier.create(flux)
.assertNext(s -> assertThat(s).isEqualTo("foo"))
.verifyComplete();
latch.await();
}
@Test
void negativeRequestN() throws InterruptedException {
CountDownLatch latch = new CountDownLatch(1);
Flow.Publisher<ByteBuffer> flowPublisher = OutputStreamPublisher.create(outputStream -> {
try(Writer writer = new OutputStreamWriter(outputStream, StandardCharsets.UTF_8)) {
writer.write("foo");
writer.flush();
writer.write("foo");
writer.flush();
}
finally {
latch.countDown();
}
}, this.executor);
Flow.Subscription[] subscriptions = new Flow.Subscription[1];
Flux<String> flux = toString(a-> flowPublisher.subscribe(new Flow.Subscriber<>() {
@Override
public void onSubscribe(Flow.Subscription subscription) {
subscriptions[0] = subscription;
a.onSubscribe(subscription);
}
@Override
public void onNext(ByteBuffer item) {
a.onNext(item);
}
@Override
public void onError(Throwable throwable) {
a.onError(throwable);
}
@Override
public void onComplete() {
a.onComplete();
}
}));
StepVerifier.create(flux, 1)
.assertNext(s -> assertThat(s).isEqualTo("foo"))
.then(() -> subscriptions[0].request(-1))
.expectErrorMessage("request should be a positive number")
.verify();
latch.await();
}
private static Flux<String> toString(Flow.Publisher<ByteBuffer> flowPublisher) {
return Flux.from(FlowAdapters.toPublisher(flowPublisher))
.map(bb -> StandardCharsets.UTF_8.decode(bb).toString());
}
}

View File

@ -48,6 +48,7 @@ import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.http.client.ClientHttpRequestFactory; import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.http.client.JdkClientHttpRequestFactory;
import org.springframework.http.client.JettyClientHttpRequestFactory; import org.springframework.http.client.JettyClientHttpRequestFactory;
import org.springframework.http.client.OkHttp3ClientHttpRequestFactory; import org.springframework.http.client.OkHttp3ClientHttpRequestFactory;
import org.springframework.http.client.SimpleClientHttpRequestFactory; import org.springframework.http.client.SimpleClientHttpRequestFactory;
@ -91,10 +92,11 @@ class RestTemplateIntegrationTests extends AbstractMockWebServerTests {
static Stream<Named<ClientHttpRequestFactory>> clientHttpRequestFactories() { static Stream<Named<ClientHttpRequestFactory>> clientHttpRequestFactories() {
return Stream.of( return Stream.of(
named("JDK", new SimpleClientHttpRequestFactory()), named("JDK HttpURLConnection", new SimpleClientHttpRequestFactory()),
named("HttpComponents", new HttpComponentsClientHttpRequestFactory()), named("HttpComponents", new HttpComponentsClientHttpRequestFactory()),
named("OkHttp", new OkHttp3ClientHttpRequestFactory()), named("OkHttp", new OkHttp3ClientHttpRequestFactory()),
named("Jetty", new JettyClientHttpRequestFactory()) named("Jetty", new JettyClientHttpRequestFactory()),
named("JDK HttpClient", new JdkClientHttpRequestFactory())
); );
} }
@ -225,7 +227,7 @@ class RestTemplateIntegrationTests extends AbstractMockWebServerTests {
@ParameterizedRestTemplateTest @ParameterizedRestTemplateTest
void patchForObject(ClientHttpRequestFactory clientHttpRequestFactory) throws Exception { void patchForObject(ClientHttpRequestFactory clientHttpRequestFactory) throws Exception {
assumeFalse(clientHttpRequestFactory instanceof SimpleClientHttpRequestFactory, assumeFalse(clientHttpRequestFactory instanceof SimpleClientHttpRequestFactory,
"JDK client does not support the PATCH method"); "HttpURLConnection does not support the PATCH method");
setUpClient(clientHttpRequestFactory); setUpClient(clientHttpRequestFactory);
@ -254,6 +256,7 @@ class RestTemplateIntegrationTests extends AbstractMockWebServerTests {
template.execute(baseUrl + "/status/badrequest", HttpMethod.GET, null, null)) template.execute(baseUrl + "/status/badrequest", HttpMethod.GET, null, null))
.satisfies(ex -> { .satisfies(ex -> {
assertThat(ex.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); assertThat(ex.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
assumeFalse(clientHttpRequestFactory instanceof JdkClientHttpRequestFactory, "JDK HttpClient does not expose status text");
assertThat(ex.getMessage()).isEqualTo("400 Client Error: [no body]"); assertThat(ex.getMessage()).isEqualTo("400 Client Error: [no body]");
}); });
} }