Skip to content

Commit f1cfe7a

Browse files
committed
InputStreamSubscriber instantiation and Javadoc
See gh-31677
1 parent dfaf7a0 commit f1cfe7a

File tree

4 files changed

+78
-99
lines changed

4 files changed

+78
-99
lines changed

spring-core/src/main/java/org/springframework/core/io/buffer/DataBufferUtils.java

+14-17
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import java.util.Set;
3434
import java.util.concurrent.Callable;
3535
import java.util.concurrent.Executor;
36+
import java.util.concurrent.Flow;
3637
import java.util.concurrent.atomic.AtomicBoolean;
3738
import java.util.concurrent.atomic.AtomicLong;
3839
import java.util.concurrent.atomic.AtomicReference;
@@ -457,23 +458,19 @@ public static Publisher<DataBuffer> outputStreamPublisher(
457458
}
458459

459460
/**
460-
* Subscribes to given {@link Publisher} and returns subscription
461-
* as {@link InputStream} that allows reading all propagated {@link DataBuffer} messages via its imperative API.
462-
* Given the {@link InputStream} implementation buffers messages as per configuration.
463-
* The returned {@link InputStream} is considered terminated when the given {@link Publisher} signaled one of the
464-
* terminal signal ({@link Subscriber#onComplete() or {@link Subscriber#onError(Throwable)}})
465-
* and all the stored {@link DataBuffer} polled from the internal buffer.
466-
* The returned {@link InputStream} will call {@link Subscription#cancel()} and release all stored {@link DataBuffer}
467-
* when {@link InputStream#close()} is called.
468-
* <p>
469-
* Note: The implementation of the returned {@link InputStream} disallow concurrent call on
470-
* any of the {@link InputStream#read} methods
471-
* <p>
472-
* Note: {@link Subscription#request(long)} happens eagerly for the first time upon subscription
473-
* and then repeats every time {@code bufferSize - (bufferSize >> 2)} consumed.
474-
* @param publisher the source of {@link DataBuffer} which should be represented as an {@link InputStream}
475-
* @param demand the maximum number of buffers to request from the Publisher and buffer on an ongoing basis
476-
* @return an {@link InputStream} instance representing given {@link Publisher} messages
461+
* Subscribe to given {@link Publisher} of {@code DataBuffer}s, and return an
462+
* {@link InputStream} to consume the byte content with.
463+
* <p>Byte buffers are stored in a queue. The {@code demand} constructor value
464+
* determines the number of buffers requested initially. When storage falls
465+
* below a {@code (demand - (demand >> 2))} limit, a request is made to refill
466+
* the queue.
467+
* <p>The {@code InputStream} terminates after an onError or onComplete signal,
468+
* and stored buffers are read. If the {@code InputStream} is closed,
469+
* the {@link Flow.Subscription} is cancelled, and stored buffers released.
470+
* @param publisher the source of {@code DataBuffer}s
471+
* @param demand the number of buffers to request initially, and buffer
472+
* internally on an ongoing basis.
473+
* @return an {@link InputStream} backed by the {@link Publisher}
477474
*/
478475
public static <T extends DataBuffer> InputStream subscriberInputStream(Publisher<T> publisher, int demand) {
479476
Assert.notNull(publisher, "Publisher must not be null");

spring-core/src/main/java/org/springframework/core/io/buffer/SubscriberInputStream.java

+17-2
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,12 @@
2222
import java.util.Objects;
2323
import java.util.Queue;
2424
import java.util.concurrent.ArrayBlockingQueue;
25+
import java.util.concurrent.Flow;
2526
import java.util.concurrent.atomic.AtomicInteger;
2627
import java.util.concurrent.atomic.AtomicReference;
2728
import java.util.concurrent.locks.LockSupport;
2829
import java.util.concurrent.locks.ReentrantLock;
2930

30-
import org.reactivestreams.Publisher;
3131
import org.reactivestreams.Subscriber;
3232
import org.reactivestreams.Subscription;
3333
import reactor.core.Exceptions;
@@ -36,7 +36,17 @@
3636
import org.springframework.util.Assert;
3737

3838
/**
39-
* Bridges between {@link Publisher Publisher&lt;DataBuffer&gt;} and {@link InputStream}.
39+
* An {@link InputStream} backed by {@link Flow.Subscriber Flow.Subscriber}
40+
* receiving byte buffers from a {@link Flow.Publisher} source.
41+
*
42+
* <p>Byte buffers are stored in a queue. The {@code demand} constructor value
43+
* determines the number of buffers requested initially. When storage falls
44+
* below a {@code (demand - (demand >> 2))} limit, a request is made to refill
45+
* the queue.
46+
*
47+
* <p>The {@code InputStream} terminates after an onError or onComplete signal,
48+
* and stored buffers are read. If the {@code InputStream} is closed,
49+
* the {@link Flow.Subscription} is cancelled, and stored buffers released.
4050
*
4151
* <p>Note that this class has a near duplicate in
4252
* {@link org.springframework.http.client.SubscriberInputStream}.
@@ -82,6 +92,11 @@ final class SubscriberInputStream extends InputStream implements Subscriber<Data
8292
private Throwable error;
8393

8494

95+
/**
96+
* Create an instance.
97+
* @param demand the number of buffers to request initially, and buffer
98+
* internally on an ongoing basis.
99+
*/
85100
SubscriberInputStream(int demand) {
86101
this.prefetch = demand;
87102
this.limit = (demand == Integer.MAX_VALUE ? Integer.MAX_VALUE : demand - (demand >> 2));

spring-web/src/main/java/org/springframework/http/client/SubscriberInputStream.java

+26-36
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,21 @@
3434
import org.apache.commons.logging.LogFactory;
3535
import reactor.core.Exceptions;
3636

37-
import org.springframework.core.io.buffer.DataBuffer;
3837
import org.springframework.lang.Nullable;
3938
import org.springframework.util.Assert;
4039

4140
/**
42-
* Bridges between {@link Flow.Publisher Flow.Publisher&lt;T&gt;} and {@link InputStream}.
41+
* An {@link InputStream} backed by {@link Flow.Subscriber Flow.Subscriber}
42+
* receiving byte buffers from a {@link Flow.Publisher} source.
43+
*
44+
* <p>Byte buffers are stored in a queue. The {@code demand} constructor value
45+
* determines the number of buffers requested initially. When storage falls
46+
* below a {@code (demand - (demand >> 2))} limit, a request is made to refill
47+
* the queue.
48+
*
49+
* <p>The {@code InputStream} terminates after an onError or onComplete signal,
50+
* and stored buffers are read. If the {@code InputStream} is closed,
51+
* the {@link Flow.Subscription} is cancelled, and stored buffers released.
4352
*
4453
* <p>Note that this class has a near duplicate in
4554
* {@link org.springframework.core.io.buffer.SubscriberInputStream}.
@@ -94,7 +103,20 @@ final class SubscriberInputStream<T> extends InputStream implements Flow.Subscri
94103
private Throwable error;
95104

96105

97-
private SubscriberInputStream(Function<T, byte[]> mapper, Consumer<T> onDiscardHandler, int demand) {
106+
/**
107+
* Create an instance.
108+
* @param mapper function to transform byte buffers to {@code byte[]};
109+
* the function should also release the byte buffer if necessary.
110+
* @param onDiscardHandler a callback to release byte buffers if the
111+
* {@link InputStream} is closed prematurely.
112+
* @param demand the number of buffers to request initially, and buffer
113+
* internally on an ongoing basis.
114+
*/
115+
SubscriberInputStream(Function<T, byte[]> mapper, Consumer<T> onDiscardHandler, int demand) {
116+
Assert.notNull(mapper, "mapper must not be null");
117+
Assert.notNull(onDiscardHandler, "onDiscardHandler must not be null");
118+
Assert.isTrue(demand > 0, "demand must be greater than 0");
119+
98120
this.mapper = mapper;
99121
this.onDiscardHandler = onDiscardHandler;
100122
this.prefetch = demand;
@@ -104,38 +126,6 @@ private SubscriberInputStream(Function<T, byte[]> mapper, Consumer<T> onDiscardH
104126
}
105127

106128

107-
/**
108-
* Subscribes to given {@link Flow.Publisher} and returns subscription
109-
* as {@link InputStream} that allows reading all propagated {@link DataBuffer} messages via its imperative API.
110-
* Given the {@link InputStream} implementation buffers messages as per configuration.
111-
* The returned {@link InputStream} is considered terminated when the given {@link Flow.Publisher} signaled one of the
112-
* terminal signal ({@link Flow.Subscriber#onComplete() or {@link Flow.Subscriber#onError(Throwable)}})
113-
* and all the stored {@link DataBuffer} polled from the internal buffer.
114-
* The returned {@link InputStream} will call {@link Flow.Subscription#cancel()} and release all stored {@link DataBuffer}
115-
* when {@link InputStream#close()} is called.
116-
* <p>
117-
* Note: The implementation of the returned {@link InputStream} disallow concurrent call on
118-
* any of the {@link InputStream#read} methods
119-
* <p>
120-
* Note: {@link Flow.Subscription#request(long)} happens eagerly for the first time upon subscription
121-
* and then repeats every time {@code bufferSize - (bufferSize >> 2)} consumed.
122-
* @param publisher the source of {@link DataBuffer} which should be represented as an {@link InputStream}
123-
* @param mapper function to transform &lt;T&gt; element to {@code byte[]}. Note, &lt;T&gt; should be released during the mapping if needed.
124-
* @param onDiscardHandler &lt;T&gt; element consumer if returned {@link InputStream} is closed prematurely.
125-
* @param demand the maximum number of buffers to request from the Publisher and buffer on an ongoing basis
126-
* @return an {@link InputStream} instance representing given {@link Flow.Publisher} messages
127-
*/
128-
public static <T> InputStream subscribeTo(Flow.Publisher<T> publisher, Function<T, byte[]> mapper, Consumer<T> onDiscardHandler, int demand) {
129-
Assert.notNull(publisher, "Flow.Publisher must not be null");
130-
Assert.notNull(mapper, "mapper must not be null");
131-
Assert.notNull(onDiscardHandler, "onDiscardHandler must not be null");
132-
Assert.isTrue(demand > 0, "demand must be greater than 0");
133-
134-
SubscriberInputStream<T> iss = new SubscriberInputStream<>(mapper, onDiscardHandler, demand);
135-
publisher.subscribe(iss);
136-
return iss;
137-
}
138-
139129
@Override
140130
public void onSubscribe(Flow.Subscription subscription) {
141131
if (this.subscription != null) {
@@ -222,7 +212,7 @@ private void resume() {
222212
if (this.parkedThread != READY) {
223213
Object old = this.parkedThread.getAndSet(READY);
224214
if (old != READY) {
225-
LockSupport.unpark((Thread)old);
215+
LockSupport.unpark((Thread) old);
226216
}
227217
}
228218
}

spring-web/src/test/java/org/springframework/http/client/SubscriberInputStreamTests.java

+21-44
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
package org.springframework.http.client;
1818

1919
import java.io.IOException;
20-
import java.io.InputStream;
2120
import java.io.OutputStreamWriter;
2221
import java.util.ArrayList;
2322
import java.util.List;
@@ -28,7 +27,6 @@
2827

2928
import org.junit.jupiter.api.Test;
3029
import org.reactivestreams.FlowAdapters;
31-
import reactor.core.publisher.Flux;
3230
import reactor.test.StepVerifier;
3331

3432
import static java.nio.charset.StandardCharsets.UTF_8;
@@ -79,8 +77,8 @@ void basic() {
7977
},
8078
this.byteMapper, this.executor, null);
8179

82-
StepVerifier.create(toStringFlux(publisher))
83-
.assertNext(s -> assertThat(s).isEqualTo("foobarbaz"))
80+
StepVerifier.create(FlowAdapters.toPublisher(publisher))
81+
.assertNext(s -> assertThat(s).containsExactly("foobarbaz".getBytes(UTF_8)))
8482
.verifyComplete();
8583
}
8684

@@ -98,17 +96,20 @@ void flush() throws IOException {
9896
this.byteMapper, this.executor, null);
9997

10098

101-
try (InputStream is = SubscriberInputStream.subscribeTo(
102-
toStringPublisher(publisher), s -> s.getBytes(UTF_8), s -> {}, 1)) {
99+
try (SubscriberInputStream<byte[]> is = new SubscriberInputStream<>(s -> s, s -> {}, 1)) {
100+
publisher.subscribe(is);
103101

104102
byte[] chunk = new byte[3];
105103

106104
assertThat(is.read(chunk)).isEqualTo(3);
107105
assertThat(chunk).containsExactly(FOO);
106+
108107
assertThat(is.read(chunk)).isEqualTo(3);
109108
assertThat(chunk).containsExactly(BAR);
109+
110110
assertThat(is.read(chunk)).isEqualTo(3);
111111
assertThat(chunk).containsExactly(BAZ);
112+
112113
assertThat(is.read(chunk)).isEqualTo(-1);
113114
}
114115
}
@@ -123,8 +124,8 @@ void chunkSize() {
123124
},
124125
this.byteMapper, this.executor, 2);
125126

126-
try (InputStream is = SubscriberInputStream.subscribeTo(
127-
toStringPublisher(publisher), s -> s.getBytes(UTF_8), s -> {}, 1)) {
127+
try (SubscriberInputStream<byte[]> is = new SubscriberInputStream<>(s -> s, s -> {}, 1)) {
128+
publisher.subscribe(is);
128129

129130
StringBuilder stringBuilder = new StringBuilder();
130131
byte[] chunk = new byte[3];
@@ -148,7 +149,7 @@ void chunkSize() {
148149
}
149150

150151
@Test
151-
void cancel() throws InterruptedException {
152+
void cancel() throws InterruptedException, IOException {
152153
CountDownLatch latch = new CountDownLatch(1);
153154

154155
Flow.Publisher<byte[]> publisher = new OutputStreamPublisher<>(
@@ -165,27 +166,23 @@ void cancel() throws InterruptedException {
165166

166167
}, this.byteMapper, this.executor, null);
167168

168-
List<String> discarded = new ArrayList<>();
169-
170-
try (InputStream is = SubscriberInputStream.subscribeTo(
171-
toStringPublisher(publisher), s -> s.getBytes(UTF_8), discarded::add, 1)) {
169+
List<byte[]> discarded = new ArrayList<>();
172170

171+
try (SubscriberInputStream<byte[]> is = new SubscriberInputStream<>(s -> s, discarded::add, 1)) {
172+
publisher.subscribe(is);
173173
byte[] chunk = new byte[3];
174174

175175
assertThat(is.read(chunk)).isEqualTo(3);
176176
assertThat(chunk).containsExactly(FOO);
177177
}
178-
catch (IOException e) {
179-
throw new RuntimeException(e);
180-
}
181178

182179
latch.await();
183180

184-
assertThat(discarded).containsExactly("bar");
181+
assertThat(discarded).containsExactly("bar".getBytes(UTF_8));
185182
}
186183

187184
@Test
188-
void closed() throws InterruptedException {
185+
void closed() throws InterruptedException, IOException {
189186
CountDownLatch latch = new CountDownLatch(1);
190187

191188
Flow.Publisher<byte[]> publisher = new OutputStreamPublisher<>(
@@ -198,18 +195,14 @@ void closed() throws InterruptedException {
198195
},
199196
this.byteMapper, this.executor, null);
200197

201-
try (InputStream is = SubscriberInputStream.subscribeTo(
202-
toStringPublisher(publisher), s -> s.getBytes(UTF_8), s -> {}, 1)) {
203-
198+
try (SubscriberInputStream<byte[]> is = new SubscriberInputStream<>(s -> s, s -> {}, 1)) {
199+
publisher.subscribe(is);
204200
byte[] chunk = new byte[3];
205201

206202
assertThat(is.read(chunk)).isEqualTo(3);
207203
assertThat(chunk).containsExactly(FOO);
208204
assertThat(is.read(chunk)).isEqualTo(-1);
209205
}
210-
catch (IOException e) {
211-
throw new RuntimeException(e);
212-
}
213206

214207
latch.await();
215208
}
@@ -234,19 +227,11 @@ void mapperThrowsException() throws InterruptedException {
234227
Throwable savedEx = null;
235228

236229
StringBuilder sb = new StringBuilder();
237-
try (InputStream is = SubscriberInputStream.subscribeTo(
238-
publisher, s -> { throw new NullPointerException("boom"); }, s -> {}, 1)) {
230+
try (SubscriberInputStream<byte[]> is = new SubscriberInputStream<>(
231+
s -> { throw new NullPointerException("boom"); }, s -> {}, 1)) {
239232

240-
byte[] chunk = new byte[3];
241-
242-
sb.append(new String(new byte[]{(byte)is.read()}, UTF_8));
243-
assertThat(is.read(chunk)).isEqualTo(3);
244-
sb.append(new String(chunk, UTF_8));
245-
assertThat(is.read(chunk)).isEqualTo(3);
246-
sb.append(new String(chunk, UTF_8));
247-
assertThat(is.read(chunk)).isEqualTo(2);
248-
sb.append(new String(chunk,0, 2, UTF_8));
249-
assertThat(is.read()).isEqualTo(-1);
233+
publisher.subscribe(is);
234+
sb.append(new String(new byte[] {(byte) is.read()}, UTF_8));
250235
}
251236
catch (Throwable ex) {
252237
savedEx = ex;
@@ -258,12 +243,4 @@ void mapperThrowsException() throws InterruptedException {
258243
assertThat(savedEx).hasMessage("boom");
259244
}
260245

261-
private static Flow.Publisher<String> toStringPublisher(Flow.Publisher<byte[]> publisher) {
262-
return FlowAdapters.toFlowPublisher(toStringFlux(publisher));
263-
}
264-
265-
private static Flux<String> toStringFlux(Flow.Publisher<byte[]> publisher) {
266-
return Flux.from(FlowAdapters.toPublisher(publisher)).map(bytes -> new String(bytes, UTF_8));
267-
}
268-
269246
}

0 commit comments

Comments
 (0)