/************************************************************************ * 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 java.io.IOException; import java.nio.ByteBuffer; import java.util.Objects; import java.util.concurrent.Executor; import java.util.concurrent.atomic.AtomicBoolean; import javax.servlet.AsyncContext; import javax.servlet.AsyncEvent; import javax.servlet.AsyncListener; import javax.servlet.ServletOutputStream; import javax.servlet.WriteListener; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import io.reactivex.disposables.Disposable; /** * Reactive streams subscriber that represents a response. * * This subscriber only requests one element at a time, and only if the attached {@link ServletOutputStream} is ready to * receive data in a non blocking way. It may be necessary to insert a buffer between this and and a publisher to * achieve high through puts. * * Errors from the {@link org.reactivestreams.Publisher} result in the {@link AsyncContext} being completed, after that * {@link #onPublisherError} is invoked - this can be overridden to insert behaviour such as logging or clean up. * * Errors from the {@link ServletOutputStream} result in the subscription being cancelled, followed by * {@link #onOutputStreamError} being invoked - this can be overridden to insert behaviour such as logging or clean up. */ public class ResponseSubscriber implements Subscriber, Disposable { private final AsyncContext context; private final ServletOutputStream outputStream; private final Executor mutex = new NonBlockingMutexExecutor(); private Subscription subscription; private State state = State.IDLE; private final static Logger logger = LoggerFactory.getLogger(ResponseSubscriber.class); private final AtomicBoolean isDisposed = new AtomicBoolean(false); public ResponseSubscriber(final AsyncContext context) throws IOException { this.context = context; this.outputStream = context.getResponse().getOutputStream(); } // public void cancel() { // if(this.subscription!=null) { // this.subscription.cancel(); // } // } /** * Invoked when a downstream error occurs, ie, when an error writing to the servlet * output stream occurs. * * Override to insert error handling for downstream writing errors, such as logging. * * By default this does nothing. * * This method will be invoked at most once. * * @param t The error that occurred. */ protected void onOutputStreamError(final Throwable t) { logger.error("Error: ", t); dispose(); try { this.outputStream.close(); this.context.complete(); } catch (IOException e) { logger.error("Error: ", e); } } /** * Invoked when an upstream error occurs, ie, when an error is received from the publisher. * * Override to insert error handling for downstream writing errors, such as logging. * * By default this does nothing. * * @param t The error that occurred. */ protected void onPublisherError(final Throwable t) { logger.error("Error publisher: ", t); try { this.outputStream.close(); this.context.complete(); } catch (IOException e) { logger.error("Error: ", e); } } @Override public void onSubscribe(final Subscription subscription) { Objects.requireNonNull(subscription, "Subscription passed to onSubscribe must not be null"); mutex.execute(() -> { if (this.subscription == null) { this.subscription = subscription; outputStream.setWriteListener(new SubscriberWriteListener()); context.addListener(new SubscriberAsyncListener()); maybeRequest(); } else { subscription.cancel(); } }); } @Override public void onNext(final ByteBuffer item) { Objects.requireNonNull(item, "Element passed to onNext must not be null"); mutex.execute(() -> { switch (state) { case DEMANDING: state = State.IDLE; try { if (item.hasArray()) { outputStream.write(item.array(), item.arrayOffset(), item.remaining()); } else { byte[] array = new byte[item.remaining()]; item.get(array); outputStream.write(array); } // Jetty requires isReady to be invoked before invoking flush if (outputStream.isReady()) { outputStream.flush(); } maybeRequest(); } catch (IOException e) { streamError(e); } break; case IDLE: // Should not happen throw new IllegalStateException("onNext with no demand"); case FINISHED: // Ignore break; } }); } private void maybeRequest() { if (outputStream.isReady() && state != State.DEMANDING) { state = State.DEMANDING; subscription.request(1); } } private void streamError(final Throwable t) { switch (state) { case IDLE: case DEMANDING: state = State.FINISHED; subscription.cancel(); onOutputStreamError(t); break; case FINISHED: // Already finished, nothing to do. break; } } @Override public void onError(final Throwable throwable) { Objects.requireNonNull(throwable, "Exception passed to onError must not be null"); mutex.execute(() -> { switch (state) { case IDLE: case DEMANDING: state = State.FINISHED; onPublisherError(throwable); context.complete(); break; case FINISHED: // Already finished, nothing to do. break; } }); } @Override public void onComplete() { mutex.execute(() -> { switch (state) { case IDLE: case DEMANDING: state = State.FINISHED; context.complete(); break; case FINISHED: // Already finished, nothing to do. break; } }); } private class SubscriberWriteListener implements WriteListener { @Override public void onWritePossible() throws IOException { mutex.execute(() -> { switch (state) { case IDLE: state = State.DEMANDING; subscription.request(1); break; default: // Nothing to do break; } }); } @Override public void onError(Throwable t) { mutex.execute(() -> streamError(t)); } } private void requestComplete() { switch (state) { case IDLE: case DEMANDING: state = State.FINISHED; subscription.cancel(); break; case FINISHED: // Already finished, nothing to do. break; } } private final class SubscriberAsyncListener implements AsyncListener { @Override public void onComplete(AsyncEvent event) throws IOException { mutex.execute(ResponseSubscriber.this::requestComplete); } @Override public void onTimeout(AsyncEvent event) throws IOException { mutex.execute(ResponseSubscriber.this::requestComplete); } @Override public void onError(AsyncEvent event) throws IOException { mutex.execute(() -> streamError(event.getThrowable())); } @Override public void onStartAsync(AsyncEvent event) throws IOException { } } private enum State { IDLE, DEMANDING, FINISHED } @Override public void dispose() { if(this.subscription!=null) { this.isDisposed.set(true); this.subscription.cancel(); } } @Override public boolean isDisposed() { return isDisposed.get(); } }