diff --git a/spring-core/src/main/java/org/springframework/core/io/buffer/DataBufferUtils.java b/spring-core/src/main/java/org/springframework/core/io/buffer/DataBufferUtils.java
index 1fd2c1a8f3..e8de69083d 100644
--- a/spring-core/src/main/java/org/springframework/core/io/buffer/DataBufferUtils.java
+++ b/spring-core/src/main/java/org/springframework/core/io/buffer/DataBufferUtils.java
@@ -33,6 +33,7 @@ import java.nio.file.StandardOpenOption;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.Executor;
+import java.util.concurrent.Flow;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
@@ -457,23 +458,19 @@ public abstract class DataBufferUtils {
}
/**
- * Subscribes to given {@link Publisher} and returns subscription
- * as {@link InputStream} that allows reading all propagated {@link DataBuffer} messages via its imperative API.
- * Given the {@link InputStream} implementation buffers messages as per configuration.
- * The returned {@link InputStream} is considered terminated when the given {@link Publisher} signaled one of the
- * terminal signal ({@link Subscriber#onComplete() or {@link Subscriber#onError(Throwable)}})
- * and all the stored {@link DataBuffer} polled from the internal buffer.
- * The returned {@link InputStream} will call {@link Subscription#cancel()} and release all stored {@link DataBuffer}
- * when {@link InputStream#close()} is called.
- *
- * Note: The implementation of the returned {@link InputStream} disallow concurrent call on
- * any of the {@link InputStream#read} methods
- *
- * Note: {@link Subscription#request(long)} happens eagerly for the first time upon subscription
- * and then repeats every time {@code bufferSize - (bufferSize >> 2)} consumed.
- * @param publisher the source of {@link DataBuffer} which should be represented as an {@link InputStream}
- * @param demand the maximum number of buffers to request from the Publisher and buffer on an ongoing basis
- * @return an {@link InputStream} instance representing given {@link Publisher} messages
+ * Subscribe to given {@link Publisher} of {@code DataBuffer}s, and return an
+ * {@link InputStream} to consume the byte content with.
+ *
Byte buffers are stored in a queue. The {@code demand} constructor value
+ * determines the number of buffers requested initially. When storage falls
+ * below a {@code (demand - (demand >> 2))} limit, a request is made to refill
+ * the queue.
+ *
The {@code InputStream} terminates after an onError or onComplete signal,
+ * and stored buffers are read. If the {@code InputStream} is closed,
+ * the {@link Flow.Subscription} is cancelled, and stored buffers released.
+ * @param publisher the source of {@code DataBuffer}s
+ * @param demand the number of buffers to request initially, and buffer
+ * internally on an ongoing basis.
+ * @return an {@link InputStream} backed by the {@link Publisher}
*/
public static InputStream subscriberInputStream(Publisher publisher, int demand) {
Assert.notNull(publisher, "Publisher must not be null");
diff --git a/spring-core/src/main/java/org/springframework/core/io/buffer/SubscriberInputStream.java b/spring-core/src/main/java/org/springframework/core/io/buffer/SubscriberInputStream.java
index a4d9e465f4..4fc6d0eae7 100644
--- a/spring-core/src/main/java/org/springframework/core/io/buffer/SubscriberInputStream.java
+++ b/spring-core/src/main/java/org/springframework/core/io/buffer/SubscriberInputStream.java
@@ -22,12 +22,12 @@ import java.util.ConcurrentModificationException;
import java.util.Objects;
import java.util.Queue;
import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.Flow;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.LockSupport;
import java.util.concurrent.locks.ReentrantLock;
-import org.reactivestreams.Publisher;
import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;
import reactor.core.Exceptions;
@@ -36,7 +36,17 @@ import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
- * Bridges between {@link Publisher Publisher<DataBuffer>} and {@link InputStream}.
+ * An {@link InputStream} backed by {@link Flow.Subscriber Flow.Subscriber}
+ * receiving byte buffers from a {@link Flow.Publisher} source.
+ *
+ * Byte buffers are stored in a queue. The {@code demand} constructor value
+ * determines the number of buffers requested initially. When storage falls
+ * below a {@code (demand - (demand >> 2))} limit, a request is made to refill
+ * the queue.
+ *
+ *
The {@code InputStream} terminates after an onError or onComplete signal,
+ * and stored buffers are read. If the {@code InputStream} is closed,
+ * the {@link Flow.Subscription} is cancelled, and stored buffers released.
*
*
Note that this class has a near duplicate in
* {@link org.springframework.http.client.SubscriberInputStream}.
@@ -82,6 +92,11 @@ final class SubscriberInputStream extends InputStream implements Subscriber> 2));
diff --git a/spring-web/src/main/java/org/springframework/http/client/SubscriberInputStream.java b/spring-web/src/main/java/org/springframework/http/client/SubscriberInputStream.java
index c114c83584..ec3834d717 100644
--- a/spring-web/src/main/java/org/springframework/http/client/SubscriberInputStream.java
+++ b/spring-web/src/main/java/org/springframework/http/client/SubscriberInputStream.java
@@ -34,12 +34,21 @@ import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import reactor.core.Exceptions;
-import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
- * Bridges between {@link Flow.Publisher Flow.Publisher<T>} and {@link InputStream}.
+ * An {@link InputStream} backed by {@link Flow.Subscriber Flow.Subscriber}
+ * receiving byte buffers from a {@link Flow.Publisher} source.
+ *
+ * Byte buffers are stored in a queue. The {@code demand} constructor value
+ * determines the number of buffers requested initially. When storage falls
+ * below a {@code (demand - (demand >> 2))} limit, a request is made to refill
+ * the queue.
+ *
+ *
The {@code InputStream} terminates after an onError or onComplete signal,
+ * and stored buffers are read. If the {@code InputStream} is closed,
+ * the {@link Flow.Subscription} is cancelled, and stored buffers released.
*
*
Note that this class has a near duplicate in
* {@link org.springframework.core.io.buffer.SubscriberInputStream}.
@@ -94,7 +103,20 @@ final class SubscriberInputStream extends InputStream implements Flow.Subscri
private Throwable error;
- private SubscriberInputStream(Function mapper, Consumer onDiscardHandler, int demand) {
+ /**
+ * Create an instance.
+ * @param mapper function to transform byte buffers to {@code byte[]};
+ * the function should also release the byte buffer if necessary.
+ * @param onDiscardHandler a callback to release byte buffers if the
+ * {@link InputStream} is closed prematurely.
+ * @param demand the number of buffers to request initially, and buffer
+ * internally on an ongoing basis.
+ */
+ SubscriberInputStream(Function mapper, Consumer onDiscardHandler, int demand) {
+ Assert.notNull(mapper, "mapper must not be null");
+ Assert.notNull(onDiscardHandler, "onDiscardHandler must not be null");
+ Assert.isTrue(demand > 0, "demand must be greater than 0");
+
this.mapper = mapper;
this.onDiscardHandler = onDiscardHandler;
this.prefetch = demand;
@@ -104,38 +126,6 @@ final class SubscriberInputStream extends InputStream implements Flow.Subscri
}
- /**
- * Subscribes to given {@link Flow.Publisher} and returns subscription
- * as {@link InputStream} that allows reading all propagated {@link DataBuffer} messages via its imperative API.
- * Given the {@link InputStream} implementation buffers messages as per configuration.
- * The returned {@link InputStream} is considered terminated when the given {@link Flow.Publisher} signaled one of the
- * terminal signal ({@link Flow.Subscriber#onComplete() or {@link Flow.Subscriber#onError(Throwable)}})
- * and all the stored {@link DataBuffer} polled from the internal buffer.
- * The returned {@link InputStream} will call {@link Flow.Subscription#cancel()} and release all stored {@link DataBuffer}
- * when {@link InputStream#close()} is called.
- *
- * Note: The implementation of the returned {@link InputStream} disallow concurrent call on
- * any of the {@link InputStream#read} methods
- *
- * Note: {@link Flow.Subscription#request(long)} happens eagerly for the first time upon subscription
- * and then repeats every time {@code bufferSize - (bufferSize >> 2)} consumed.
- * @param publisher the source of {@link DataBuffer} which should be represented as an {@link InputStream}
- * @param mapper function to transform <T> element to {@code byte[]}. Note, <T> should be released during the mapping if needed.
- * @param onDiscardHandler <T> element consumer if returned {@link InputStream} is closed prematurely.
- * @param demand the maximum number of buffers to request from the Publisher and buffer on an ongoing basis
- * @return an {@link InputStream} instance representing given {@link Flow.Publisher} messages
- */
- public static InputStream subscribeTo(Flow.Publisher publisher, Function mapper, Consumer onDiscardHandler, int demand) {
- Assert.notNull(publisher, "Flow.Publisher must not be null");
- Assert.notNull(mapper, "mapper must not be null");
- Assert.notNull(onDiscardHandler, "onDiscardHandler must not be null");
- Assert.isTrue(demand > 0, "demand must be greater than 0");
-
- SubscriberInputStream iss = new SubscriberInputStream<>(mapper, onDiscardHandler, demand);
- publisher.subscribe(iss);
- return iss;
- }
-
@Override
public void onSubscribe(Flow.Subscription subscription) {
if (this.subscription != null) {
@@ -222,7 +212,7 @@ final class SubscriberInputStream extends InputStream implements Flow.Subscri
if (this.parkedThread != READY) {
Object old = this.parkedThread.getAndSet(READY);
if (old != READY) {
- LockSupport.unpark((Thread)old);
+ LockSupport.unpark((Thread) old);
}
}
}
diff --git a/spring-web/src/test/java/org/springframework/http/client/SubscriberInputStreamTests.java b/spring-web/src/test/java/org/springframework/http/client/SubscriberInputStreamTests.java
index 634885da00..1caa91d2bb 100644
--- a/spring-web/src/test/java/org/springframework/http/client/SubscriberInputStreamTests.java
+++ b/spring-web/src/test/java/org/springframework/http/client/SubscriberInputStreamTests.java
@@ -17,7 +17,6 @@
package org.springframework.http.client;
import java.io.IOException;
-import java.io.InputStream;
import java.io.OutputStreamWriter;
import java.util.ArrayList;
import java.util.List;
@@ -28,7 +27,6 @@ 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 java.nio.charset.StandardCharsets.UTF_8;
@@ -79,8 +77,8 @@ class SubscriberInputStreamTests {
},
this.byteMapper, this.executor, null);
- StepVerifier.create(toStringFlux(publisher))
- .assertNext(s -> assertThat(s).isEqualTo("foobarbaz"))
+ StepVerifier.create(FlowAdapters.toPublisher(publisher))
+ .assertNext(s -> assertThat(s).containsExactly("foobarbaz".getBytes(UTF_8)))
.verifyComplete();
}
@@ -98,17 +96,20 @@ class SubscriberInputStreamTests {
this.byteMapper, this.executor, null);
- try (InputStream is = SubscriberInputStream.subscribeTo(
- toStringPublisher(publisher), s -> s.getBytes(UTF_8), s -> {}, 1)) {
+ try (SubscriberInputStream is = new SubscriberInputStream<>(s -> s, s -> {}, 1)) {
+ publisher.subscribe(is);
byte[] chunk = new byte[3];
assertThat(is.read(chunk)).isEqualTo(3);
assertThat(chunk).containsExactly(FOO);
+
assertThat(is.read(chunk)).isEqualTo(3);
assertThat(chunk).containsExactly(BAR);
+
assertThat(is.read(chunk)).isEqualTo(3);
assertThat(chunk).containsExactly(BAZ);
+
assertThat(is.read(chunk)).isEqualTo(-1);
}
}
@@ -123,8 +124,8 @@ class SubscriberInputStreamTests {
},
this.byteMapper, this.executor, 2);
- try (InputStream is = SubscriberInputStream.subscribeTo(
- toStringPublisher(publisher), s -> s.getBytes(UTF_8), s -> {}, 1)) {
+ try (SubscriberInputStream is = new SubscriberInputStream<>(s -> s, s -> {}, 1)) {
+ publisher.subscribe(is);
StringBuilder stringBuilder = new StringBuilder();
byte[] chunk = new byte[3];
@@ -148,7 +149,7 @@ class SubscriberInputStreamTests {
}
@Test
- void cancel() throws InterruptedException {
+ void cancel() throws InterruptedException, IOException {
CountDownLatch latch = new CountDownLatch(1);
Flow.Publisher publisher = new OutputStreamPublisher<>(
@@ -165,27 +166,23 @@ class SubscriberInputStreamTests {
}, this.byteMapper, this.executor, null);
- List discarded = new ArrayList<>();
-
- try (InputStream is = SubscriberInputStream.subscribeTo(
- toStringPublisher(publisher), s -> s.getBytes(UTF_8), discarded::add, 1)) {
+ List discarded = new ArrayList<>();
+ try (SubscriberInputStream is = new SubscriberInputStream<>(s -> s, discarded::add, 1)) {
+ publisher.subscribe(is);
byte[] chunk = new byte[3];
assertThat(is.read(chunk)).isEqualTo(3);
assertThat(chunk).containsExactly(FOO);
}
- catch (IOException e) {
- throw new RuntimeException(e);
- }
latch.await();
- assertThat(discarded).containsExactly("bar");
+ assertThat(discarded).containsExactly("bar".getBytes(UTF_8));
}
@Test
- void closed() throws InterruptedException {
+ void closed() throws InterruptedException, IOException {
CountDownLatch latch = new CountDownLatch(1);
Flow.Publisher publisher = new OutputStreamPublisher<>(
@@ -198,18 +195,14 @@ class SubscriberInputStreamTests {
},
this.byteMapper, this.executor, null);
- try (InputStream is = SubscriberInputStream.subscribeTo(
- toStringPublisher(publisher), s -> s.getBytes(UTF_8), s -> {}, 1)) {
-
+ try (SubscriberInputStream is = new SubscriberInputStream<>(s -> s, s -> {}, 1)) {
+ publisher.subscribe(is);
byte[] chunk = new byte[3];
assertThat(is.read(chunk)).isEqualTo(3);
assertThat(chunk).containsExactly(FOO);
assertThat(is.read(chunk)).isEqualTo(-1);
}
- catch (IOException e) {
- throw new RuntimeException(e);
- }
latch.await();
}
@@ -234,19 +227,11 @@ class SubscriberInputStreamTests {
Throwable savedEx = null;
StringBuilder sb = new StringBuilder();
- try (InputStream is = SubscriberInputStream.subscribeTo(
- publisher, s -> { throw new NullPointerException("boom"); }, s -> {}, 1)) {
+ try (SubscriberInputStream is = new SubscriberInputStream<>(
+ s -> { throw new NullPointerException("boom"); }, s -> {}, 1)) {
- byte[] chunk = new byte[3];
-
- sb.append(new String(new byte[]{(byte)is.read()}, UTF_8));
- assertThat(is.read(chunk)).isEqualTo(3);
- sb.append(new String(chunk, UTF_8));
- assertThat(is.read(chunk)).isEqualTo(3);
- sb.append(new String(chunk, UTF_8));
- assertThat(is.read(chunk)).isEqualTo(2);
- sb.append(new String(chunk,0, 2, UTF_8));
- assertThat(is.read()).isEqualTo(-1);
+ publisher.subscribe(is);
+ sb.append(new String(new byte[] {(byte) is.read()}, UTF_8));
}
catch (Throwable ex) {
savedEx = ex;
@@ -258,12 +243,4 @@ class SubscriberInputStreamTests {
assertThat(savedEx).hasMessage("boom");
}
- private static Flow.Publisher toStringPublisher(Flow.Publisher publisher) {
- return FlowAdapters.toFlowPublisher(toStringFlux(publisher));
- }
-
- private static Flux toStringFlux(Flow.Publisher publisher) {
- return Flux.from(FlowAdapters.toPublisher(publisher)).map(bytes -> new String(bytes, UTF_8));
- }
-
}