/************************************************************************ * Licensed under Public Domain (CC0) * * * * To the extent possible under law, the person who associated CC0 with * * this code has waived all copyright and related or neighboring * * rights to this code. * * * * You should have received a copy of the CC0 legalcode along with this * * work. If not, see .* ************************************************************************/ package org.reactivestreams.servlet; import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; import javax.servlet.AsyncContext; import javax.servlet.ReadListener; import javax.servlet.ServletInputStream; import java.io.IOException; import java.nio.ByteBuffer; import java.util.Objects; /** * Reactive streams publisher that represents a request. */ public class RequestPublisher implements Publisher { private static final int MAX_SEQUENTIAL_READS = 8; private final ServletInputStream inputStream; // TODO This should be cleared when Publisher is done to allow it to be GC:Ed private final int readBufferLimit; private final int sequentialReadLimit; private final NonBlockingMutexExecutor mutex = new NonBlockingMutexExecutor(); private Subscriber subscriber; private long demand = 0; private boolean running = true; public RequestPublisher(final AsyncContext context, final int readBufferLimit) throws IOException { this (context, readBufferLimit, MAX_SEQUENTIAL_READS); } public RequestPublisher(final AsyncContext context, final int readBufferLimit, final int sequentialReadLimit) throws IOException { this.inputStream = context.getRequest().getInputStream(); this.readBufferLimit = readBufferLimit; this.sequentialReadLimit = sequentialReadLimit; if (readBufferLimit <= 0) throw new IllegalArgumentException("readBufferLimit must be greater than 0"); if (sequentialReadLimit <= 0) throw new IllegalArgumentException("sequentialReadLimit must be greater than 0"); } /** * Handle the case where the subscriber cancels. * * It's not clear exactly what happens when you close a servlet input stream, so this doesn't close the input stream, * instead, it does nothing. To insert some custom behavior, such as closing the stream or consuming the remainder of * the body, override this callback. * * Note that consuming the remainder of the body shouldn't be necessary, servlet containers should consume the * remainder of the body when the response is committed. */ protected void handleCancel() { } private void maybeRead() { int readsLeft = sequentialReadLimit; ByteBuffer buffer = null; while (running && demand > 0 && readsLeft > 0 && inputStream.isReady()) { readsLeft -= 1; try { if (buffer == null) buffer = ByteBuffer.allocate(readBufferLimit); final int length = inputStream.read(buffer.array()); switch(length) { case -1: handleOnComplete(); break; case 0: break; default: buffer.limit(length); subscriber.onNext(buffer); buffer = null; demand -= 1; break; } } catch (IOException e) { handleError(e); } } // If data throughput is really high, and the demand is very high (generally not, but in the TCK there is some // Integer.MAX_VALUE demand) the loop above can starve all other tasks from running (including, for example, // cancel signals). So we limit the number of sequential reads that we do, and if we reach that limit, we resubmit // to do any further reads, but giving the opportunity for other tasks to execute. if (readsLeft == 0) { mutex.execute(this::maybeRead); } } private void handleError(final Throwable t) { if (running) { running = false; subscriber.onError(t); subscriber = null; } } private void handleOnComplete() { if (running) { running = false; subscriber.onComplete(); subscriber = null; } } @Override public void subscribe(final Subscriber subscriber) { Objects.requireNonNull(subscriber, "Subscriber passed to subscribe must not be null"); mutex.execute(() -> { if (this.subscriber == null) { this.subscriber = subscriber; inputStream.setReadListener(new Listener()); subscriber.onSubscribe(new RequestSubscription()); } else if (this.subscriber.equals(subscriber)) { handleError(new IllegalStateException("Attempted to subscribe this Subscriber more than once for the same Publisher")); } else { subscriber.onSubscribe(new Subscription() { @Override public void request(long n) { } @Override public void cancel() { } }); subscriber.onError(new IllegalStateException("This publisher only supports one subscriber")); } }); } private final class RequestSubscription implements Subscription { @Override public void request(final long n) { mutex.execute(() -> { if (running) { if (n <= 0) { handleError(new IllegalArgumentException("Reactive streams 3.9 spec violation: non-positive subscription request")); } else { final long old = demand; if (old < Long.MAX_VALUE) { demand = ((old + n) < 0) ? Long.MAX_VALUE : (old + n); // Overflow protection } if (old == 0) { maybeRead(); } } } }); } @Override public void cancel() { mutex.execute(() -> { if (running) { subscriber = null; running = false; handleCancel(); } }); } } private class Listener implements ReadListener { @Override public void onDataAvailable() throws IOException { mutex.execute(RequestPublisher.this::maybeRead); } @Override public void onAllDataRead() throws IOException { mutex.execute(RequestPublisher.this::handleOnComplete); } @Override public void onError(final Throwable t) { mutex.execute(() -> handleError(t)); } } }