Introduce OutputStream BodyInserter

This commit introduces a BodyInserter that inssert any bytes written to
an output stream to the body of an output message.

Closes gh-31184
This commit is contained in:
Arjen Poutsma
2023-09-11 11:00:11 +02:00
parent 913dc86e18
commit 59d123a18e
5 changed files with 634 additions and 1 deletions

View File

@@ -18,6 +18,7 @@ package org.springframework.core.io.buffer;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.URI;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousFileChannel;
@@ -34,11 +35,13 @@ import java.nio.file.StandardOpenOption;
import java.time.Duration;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executors;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.PooledByteBufAllocator;
import org.junit.jupiter.api.Test;
import org.mockito.stubbing.Answer;
import org.reactivestreams.Publisher;
import org.reactivestreams.Subscription;
import reactor.core.publisher.BaseSubscriber;
import reactor.core.publisher.Flux;
@@ -52,6 +55,8 @@ import org.springframework.core.io.Resource;
import org.springframework.core.testfixture.io.buffer.AbstractDataBufferAllocatingTests;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIOException;
import static org.assertj.core.api.Assertions.fail;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.BDDMockito.given;
@@ -543,6 +548,147 @@ class DataBufferUtilsTests extends AbstractDataBufferAllocatingTests {
assertThat(written).contains("foobar");
}
@ParameterizedDataBufferAllocatingTest
void outputStreamPublisher(DataBufferFactory bufferFactory) {
super.bufferFactory = bufferFactory;
byte[] foo = "foo".getBytes(StandardCharsets.UTF_8);
byte[] bar = "bar".getBytes(StandardCharsets.UTF_8);
byte[] baz = "baz".getBytes(StandardCharsets.UTF_8);
Publisher<DataBuffer> publisher = DataBufferUtils.outputStreamPublisher(outputStream -> {
try {
outputStream.write(foo);
outputStream.write(bar);
outputStream.write(baz);
}
catch (IOException ex) {
fail(ex.getMessage(), ex);
}
}, super.bufferFactory, Executors.newSingleThreadExecutor());
StepVerifier.create(publisher)
.consumeNextWith(stringConsumer("foobarbaz"))
.verifyComplete();
}
@ParameterizedDataBufferAllocatingTest
void outputStreamPublisherFlush(DataBufferFactory bufferFactory) {
super.bufferFactory = bufferFactory;
byte[] foo = "foo".getBytes(StandardCharsets.UTF_8);
byte[] bar = "bar".getBytes(StandardCharsets.UTF_8);
byte[] baz = "baz".getBytes(StandardCharsets.UTF_8);
Publisher<DataBuffer> publisher = DataBufferUtils.outputStreamPublisher(outputStream -> {
try {
outputStream.write(foo);
outputStream.flush();
outputStream.write(bar);
outputStream.flush();
outputStream.write(baz);
outputStream.flush();
}
catch (IOException ex) {
fail(ex.getMessage(), ex);
}
}, super.bufferFactory, Executors.newSingleThreadExecutor());
StepVerifier.create(publisher)
.consumeNextWith(stringConsumer("foo"))
.consumeNextWith(stringConsumer("bar"))
.consumeNextWith(stringConsumer("baz"))
.verifyComplete();
}
@ParameterizedDataBufferAllocatingTest
void outputStreamPublisherChunkSize(DataBufferFactory bufferFactory) {
super.bufferFactory = bufferFactory;
byte[] foo = "foo".getBytes(StandardCharsets.UTF_8);
byte[] bar = "bar".getBytes(StandardCharsets.UTF_8);
byte[] baz = "baz".getBytes(StandardCharsets.UTF_8);
Publisher<DataBuffer> publisher = DataBufferUtils.outputStreamPublisher(outputStream -> {
try {
outputStream.write(foo);
outputStream.write(bar);
outputStream.write(baz);
}
catch (IOException ex) {
fail(ex.getMessage(), ex);
}
}, super.bufferFactory, Executors.newSingleThreadExecutor(), 3);
StepVerifier.create(publisher)
.consumeNextWith(stringConsumer("foo"))
.consumeNextWith(stringConsumer("bar"))
.consumeNextWith(stringConsumer("baz"))
.verifyComplete();
}
@ParameterizedDataBufferAllocatingTest
void outputStreamPublisherCancel(DataBufferFactory bufferFactory) throws InterruptedException {
super.bufferFactory = bufferFactory;
byte[] foo = "foo".getBytes(StandardCharsets.UTF_8);
byte[] bar = "bar".getBytes(StandardCharsets.UTF_8);
CountDownLatch latch = new CountDownLatch(1);
Publisher<DataBuffer> publisher = DataBufferUtils.outputStreamPublisher(outputStream -> {
try {
assertThatIOException()
.isThrownBy(() -> {
outputStream.write(foo);
outputStream.flush();
outputStream.write(bar);
outputStream.flush();
})
.withMessage("Subscription has been terminated");
}
finally {
latch.countDown();
}
}, super.bufferFactory, Executors.newSingleThreadExecutor());
StepVerifier.create(publisher, 1)
.consumeNextWith(stringConsumer("foo"))
.thenCancel()
.verify();
latch.await();
}
@ParameterizedDataBufferAllocatingTest
void outputStreamPublisherClosed(DataBufferFactory bufferFactory) throws InterruptedException {
super.bufferFactory = bufferFactory;
CountDownLatch latch = new CountDownLatch(1);
Publisher<DataBuffer> publisher = DataBufferUtils.outputStreamPublisher(outputStream -> {
try {
OutputStreamWriter writer = new OutputStreamWriter(outputStream, StandardCharsets.UTF_8);
writer.write("foo");
writer.close();
assertThatIOException().isThrownBy(() -> writer.write("bar"))
.withMessage("Stream closed");
}
catch (IOException ex) {
fail(ex.getMessage(), ex);
}
finally {
latch.countDown();
}
}, super.bufferFactory, Executors.newSingleThreadExecutor());
StepVerifier.create(publisher)
.consumeNextWith(stringConsumer("foo"))
.verifyComplete();
latch.await();
}
@ParameterizedDataBufferAllocatingTest
void readAndWriteByteChannel(DataBufferFactory bufferFactory) throws Exception {
super.bufferFactory = bufferFactory;