Make OutputStreamPublisher more generic
This commit improves the OutputStreamPublisher so that it is capable of publishing other types that ByteBuffers.
This commit is contained in:
parent
d0a2820af4
commit
a6c5692586
|
|
@ -47,6 +47,8 @@ import org.springframework.util.StringUtils;
|
|||
*/
|
||||
class JdkClientHttpRequest extends AbstractStreamingClientHttpRequest {
|
||||
|
||||
private static final OutputStreamPublisher.ByteMapper<ByteBuffer> BYTE_MAPPER = new ByteBufferMapper();
|
||||
|
||||
private static final Set<String> DISALLOWED_HEADERS = disallowedHeaders();
|
||||
|
||||
/**
|
||||
|
|
@ -142,6 +144,7 @@ class JdkClientHttpRequest extends AbstractStreamingClientHttpRequest {
|
|||
if (body != null) {
|
||||
Flow.Publisher<ByteBuffer> outputStreamPublisher = OutputStreamPublisher.create(
|
||||
outputStream -> body.writeTo(StreamUtils.nonClosing(outputStream)),
|
||||
BYTE_MAPPER,
|
||||
this.executor);
|
||||
|
||||
long contentLength = headers.getContentLength();
|
||||
|
|
@ -157,4 +160,25 @@ class JdkClientHttpRequest extends AbstractStreamingClientHttpRequest {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
private static final class ByteBufferMapper implements OutputStreamPublisher.ByteMapper<ByteBuffer> {
|
||||
|
||||
@Override
|
||||
public ByteBuffer map(int b) {
|
||||
ByteBuffer byteBuffer = ByteBuffer.allocate(1);
|
||||
byteBuffer.put((byte) b);
|
||||
byteBuffer.flip();
|
||||
return byteBuffer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ByteBuffer map(byte[] b, int off, int len) {
|
||||
ByteBuffer byteBuffer = ByteBuffer.allocate(len);
|
||||
byteBuffer.put(b, off, len);
|
||||
byteBuffer.flip();
|
||||
return byteBuffer;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ 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;
|
||||
|
|
@ -32,37 +31,42 @@ import org.springframework.util.Assert;
|
|||
|
||||
/**
|
||||
* Bridges between {@link OutputStream} and
|
||||
* {@link Flow.Publisher Flow.Publisher<ByteBuffer>}.
|
||||
*
|
||||
* {@link Flow.Publisher Flow.Publisher<T>}.
|
||||
|
||||
* @author Oleh Dokuka
|
||||
* @author Arjen Poutsma
|
||||
* @since 6.1
|
||||
* @see #create(OutputStreamHandler, Executor)
|
||||
* @param <T> the published item type
|
||||
* @see #create(OutputStreamHandler, ByteMapper, Executor)
|
||||
*/
|
||||
final class OutputStreamPublisher implements Flow.Publisher<ByteBuffer> {
|
||||
final class OutputStreamPublisher<T> implements Flow.Publisher<T> {
|
||||
|
||||
private final OutputStreamHandler outputStreamHandler;
|
||||
|
||||
private final ByteMapper<T> byteMapper;
|
||||
|
||||
private final Executor executor;
|
||||
|
||||
|
||||
private OutputStreamPublisher(OutputStreamHandler outputStreamHandler, Executor executor) {
|
||||
private OutputStreamPublisher(OutputStreamHandler outputStreamHandler, ByteMapper<T> byteMapper, Executor executor) {
|
||||
this.outputStreamHandler = outputStreamHandler;
|
||||
this.byteMapper = byteMapper;
|
||||
this.executor = executor;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Creates a new {@code Publisher<ByteBuffer>} based on bytes written to a
|
||||
* {@code OutputStream}.
|
||||
* Creates a new {@code Publisher<T>} based on bytes written to a
|
||||
* {@code OutputStream}. The parameter {@code byteMapper} is used to map
|
||||
* from written bytes to the published type.
|
||||
* <ul>
|
||||
* <li>The parameter {@code outputStreamHandler} is invoked once per
|
||||
* subscription of the returned {@code Publisher}, when the first
|
||||
* {@code ByteBuffer} is
|
||||
* item 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}
|
||||
* {@linkplain Flow.Subscriber#onNext(Object) published} item
|
||||
* 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>
|
||||
|
|
@ -74,23 +78,28 @@ final class OutputStreamPublisher implements Flow.Publisher<ByteBuffer> {
|
|||
* be dispatched to the {@linkplain Flow.Subscriber#onError(Throwable) Subscriber}.
|
||||
* </ul>
|
||||
* @param outputStreamHandler invoked when the first buffer is requested
|
||||
* @param byteMapper maps written bytes to {@code T}
|
||||
* @param executor used to invoke the {@code outputStreamHandler}
|
||||
* @return a {@code Publisher<ByteBuffer>} based on bytes written by
|
||||
* {@code outputStreamHandler}
|
||||
* @param <T> the publisher type
|
||||
* @return a {@code Publisher<T>} based on bytes written by
|
||||
* {@code outputStreamHandler} mapped by {@code byteMapper}
|
||||
*/
|
||||
public static Flow.Publisher<ByteBuffer> create(OutputStreamHandler outputStreamHandler, Executor executor) {
|
||||
public static <T> Flow.Publisher<T> create(OutputStreamHandler outputStreamHandler, ByteMapper<T> byteMapper,
|
||||
Executor executor) {
|
||||
|
||||
Assert.notNull(outputStreamHandler, "OutputStreamHandler must not be null");
|
||||
Assert.notNull(byteMapper, "ByteMapper must not be null");
|
||||
Assert.notNull(executor, "Executor must not be null");
|
||||
|
||||
return new OutputStreamPublisher(outputStreamHandler, executor);
|
||||
return new OutputStreamPublisher<>(outputStreamHandler, byteMapper, executor);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void subscribe(Flow.Subscriber<? super ByteBuffer> subscriber) {
|
||||
public void subscribe(Flow.Subscriber<? super T> subscriber) {
|
||||
Objects.requireNonNull(subscriber, "Subscriber must not be null");
|
||||
|
||||
OutputStreamSubscription subscription = new OutputStreamSubscription(subscriber, this.outputStreamHandler);
|
||||
OutputStreamSubscription<T> subscription = new OutputStreamSubscription<>(subscriber, this.outputStreamHandler,
|
||||
this.byteMapper);
|
||||
subscriber.onSubscribe(subscription);
|
||||
this.executor.execute(subscription::invokeHandler);
|
||||
}
|
||||
|
|
@ -109,7 +118,8 @@ final class OutputStreamPublisher implements Flow.Publisher<ByteBuffer> {
|
|||
* <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
|
||||
* will be {@linkplain ByteMapper#map(byte[], int, int) mapped}
|
||||
* and {@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
|
||||
|
|
@ -128,14 +138,37 @@ final class OutputStreamPublisher implements Flow.Publisher<ByteBuffer> {
|
|||
}
|
||||
|
||||
|
||||
private static final class OutputStreamSubscription extends OutputStream implements Flow.Subscription {
|
||||
/**
|
||||
* Maps bytes written to in {@link OutputStreamHandler#handle(OutputStream)}
|
||||
* to published items.
|
||||
* @param <T> the type to map to
|
||||
*/
|
||||
public interface ByteMapper<T> {
|
||||
|
||||
/**
|
||||
* Maps a single byte to {@code T}.
|
||||
*/
|
||||
T map(int b);
|
||||
|
||||
/**
|
||||
* Maps a byte array to {@code T}.
|
||||
*/
|
||||
T map(byte[] b, int off, int len);
|
||||
|
||||
}
|
||||
|
||||
|
||||
private static final class OutputStreamSubscription<T> extends OutputStream implements Flow.Subscription {
|
||||
|
||||
static final Object READY = new Object();
|
||||
|
||||
private final Flow.Subscriber<? super ByteBuffer> actual;
|
||||
private final Flow.Subscriber<? super T> actual;
|
||||
|
||||
|
||||
private final OutputStreamHandler outputStreamHandler;
|
||||
|
||||
private final ByteMapper<T> byteMapper;
|
||||
|
||||
private final AtomicLong requested = new AtomicLong();
|
||||
|
||||
private final AtomicReference<Object> parkedThreadAtomic = new AtomicReference<>();
|
||||
|
|
@ -146,9 +179,10 @@ final class OutputStreamPublisher implements Flow.Publisher<ByteBuffer> {
|
|||
private long produced;
|
||||
|
||||
|
||||
public OutputStreamSubscription(Flow.Subscriber<? super ByteBuffer> actual,
|
||||
OutputStreamHandler outputStreamHandler) {
|
||||
public OutputStreamSubscription(Flow.Subscriber<? super T> actual, OutputStreamHandler outputStreamHandler,
|
||||
ByteMapper<T> byteMapper) {
|
||||
this.actual = actual;
|
||||
this.byteMapper = byteMapper;
|
||||
this.outputStreamHandler = outputStreamHandler;
|
||||
}
|
||||
|
||||
|
|
@ -156,11 +190,9 @@ final class OutputStreamPublisher implements Flow.Publisher<ByteBuffer> {
|
|||
public void write(int b) throws IOException {
|
||||
checkDemandAndAwaitIfNeeded();
|
||||
|
||||
ByteBuffer byteBuffer = ByteBuffer.allocate(1);
|
||||
byteBuffer.put((byte) b);
|
||||
byteBuffer.flip();
|
||||
T next = this.byteMapper.map(b);
|
||||
|
||||
this.actual.onNext(byteBuffer);
|
||||
this.actual.onNext(next);
|
||||
|
||||
this.produced++;
|
||||
}
|
||||
|
|
@ -174,11 +206,9 @@ final class OutputStreamPublisher implements Flow.Publisher<ByteBuffer> {
|
|||
public void write(byte[] b, int off, int len) throws IOException {
|
||||
checkDemandAndAwaitIfNeeded();
|
||||
|
||||
ByteBuffer byteBuffer = ByteBuffer.allocate(len);
|
||||
byteBuffer.put(b, off, len);
|
||||
byteBuffer.flip();
|
||||
T next = this.byteMapper.map(b, off, len);
|
||||
|
||||
this.actual.onNext(byteBuffer);
|
||||
this.actual.onNext(next);
|
||||
|
||||
this.produced++;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ 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;
|
||||
|
|
@ -41,15 +40,31 @@ class OutputStreamPublisherTests {
|
|||
|
||||
private final Executor executor = Executors.newSingleThreadExecutor();
|
||||
|
||||
private final OutputStreamPublisher.ByteMapper<byte[]> byteMapper =
|
||||
new OutputStreamPublisher.ByteMapper<>() {
|
||||
@Override
|
||||
public byte[] map(int b) {
|
||||
return new byte[]{(byte) b};
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] map(byte[] b, int off, int len) {
|
||||
byte[] result = new byte[len];
|
||||
System.arraycopy(b, off, result, 0, len);
|
||||
return result;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@Test
|
||||
void basic() {
|
||||
Flow.Publisher<ByteBuffer> flowPublisher = OutputStreamPublisher.create(outputStream -> {
|
||||
Flow.Publisher<byte[]> flowPublisher = OutputStreamPublisher.create(outputStream -> {
|
||||
try (Writer writer = new OutputStreamWriter(outputStream, StandardCharsets.UTF_8)) {
|
||||
writer.write("foo");
|
||||
writer.write("bar");
|
||||
writer.write("baz");
|
||||
}
|
||||
}, this.executor);
|
||||
}, this.byteMapper, this.executor);
|
||||
Flux<String> flux = toString(flowPublisher);
|
||||
|
||||
StepVerifier.create(flux)
|
||||
|
|
@ -59,7 +74,7 @@ class OutputStreamPublisherTests {
|
|||
|
||||
@Test
|
||||
void flush() {
|
||||
Flow.Publisher<ByteBuffer> flowPublisher = OutputStreamPublisher.create(outputStream -> {
|
||||
Flow.Publisher<byte[]> flowPublisher = OutputStreamPublisher.create(outputStream -> {
|
||||
try (Writer writer = new OutputStreamWriter(outputStream, StandardCharsets.UTF_8)) {
|
||||
writer.write("foo");
|
||||
writer.flush();
|
||||
|
|
@ -68,7 +83,7 @@ class OutputStreamPublisherTests {
|
|||
writer.write("baz");
|
||||
writer.flush();
|
||||
}
|
||||
}, this.executor);
|
||||
}, this.byteMapper, this.executor);
|
||||
Flux<String> flux = toString(flowPublisher);
|
||||
|
||||
StepVerifier.create(flux)
|
||||
|
|
@ -82,7 +97,7 @@ class OutputStreamPublisherTests {
|
|||
void cancel() throws InterruptedException {
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
|
||||
Flow.Publisher<ByteBuffer> flowPublisher = OutputStreamPublisher.create(outputStream -> {
|
||||
Flow.Publisher<byte[]> flowPublisher = OutputStreamPublisher.create(outputStream -> {
|
||||
try (Writer writer = new OutputStreamWriter(outputStream, StandardCharsets.UTF_8)) {
|
||||
assertThatIOException()
|
||||
.isThrownBy(() -> {
|
||||
|
|
@ -94,7 +109,7 @@ class OutputStreamPublisherTests {
|
|||
.withMessage("Subscription has been terminated");
|
||||
latch.countDown();
|
||||
}
|
||||
}, this.executor);
|
||||
}, this.byteMapper, this.executor);
|
||||
Flux<String> flux = toString(flowPublisher);
|
||||
|
||||
StepVerifier.create(flux, 1)
|
||||
|
|
@ -109,14 +124,14 @@ class OutputStreamPublisherTests {
|
|||
void closed() throws InterruptedException {
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
|
||||
Flow.Publisher<ByteBuffer> flowPublisher = OutputStreamPublisher.create(outputStream -> {
|
||||
Flow.Publisher<byte[]> 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);
|
||||
}, this.byteMapper, this.executor);
|
||||
Flux<String> flux = toString(flowPublisher);
|
||||
|
||||
StepVerifier.create(flux)
|
||||
|
|
@ -130,7 +145,7 @@ class OutputStreamPublisherTests {
|
|||
void negativeRequestN() throws InterruptedException {
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
|
||||
Flow.Publisher<ByteBuffer> flowPublisher = OutputStreamPublisher.create(outputStream -> {
|
||||
Flow.Publisher<byte[]> flowPublisher = OutputStreamPublisher.create(outputStream -> {
|
||||
try(Writer writer = new OutputStreamWriter(outputStream, StandardCharsets.UTF_8)) {
|
||||
writer.write("foo");
|
||||
writer.flush();
|
||||
|
|
@ -140,7 +155,7 @@ class OutputStreamPublisherTests {
|
|||
finally {
|
||||
latch.countDown();
|
||||
}
|
||||
}, this.executor);
|
||||
}, this.byteMapper, this.executor);
|
||||
Flow.Subscription[] subscriptions = new Flow.Subscription[1];
|
||||
Flux<String> flux = toString(a-> flowPublisher.subscribe(new Flow.Subscriber<>() {
|
||||
@Override
|
||||
|
|
@ -150,7 +165,7 @@ class OutputStreamPublisherTests {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void onNext(ByteBuffer item) {
|
||||
public void onNext(byte[] item) {
|
||||
a.onNext(item);
|
||||
}
|
||||
|
||||
|
|
@ -174,9 +189,9 @@ class OutputStreamPublisherTests {
|
|||
latch.await();
|
||||
}
|
||||
|
||||
private static Flux<String> toString(Flow.Publisher<ByteBuffer> flowPublisher) {
|
||||
private static Flux<String> toString(Flow.Publisher<byte[]> flowPublisher) {
|
||||
return Flux.from(FlowAdapters.toPublisher(flowPublisher))
|
||||
.map(bb -> StandardCharsets.UTF_8.decode(bb).toString());
|
||||
.map(bytes -> new String(bytes, StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue