/*
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
*
* Copyright (c) 2015 Oracle and/or its affiliates. All rights reserved.
*
* The contents of this file are subject to the terms of either the GNU
* General Public License Version 2 only ("GPL") or the Common Development
* and Distribution License("CDDL") (collectively, the "License"). You
* may not use this file except in compliance with the License. You can
* obtain a copy of the License at
* http://glassfish.java.net/public/CDDL+GPL_1_1.html
* or packager/legal/LICENSE.txt. See the License for the specific
* language governing permissions and limitations under the License.
*
* When distributing the software, include this License Header Notice in each
* file and include the License file at packager/legal/LICENSE.txt.
*
* GPL Classpath Exception:
* Oracle designates this particular file as subject to the "Classpath"
* exception as provided by Oracle in the GPL Version 2 section of the License
* file that accompanied this code.
*
* Modifications:
* If applicable, add the following below the License Header, with the fields
* enclosed by brackets [] replaced by your own identifying information:
* "Portions Copyright [year] [name of copyright owner]"
*
* Contributor(s):
* If you wish your version of this file to be governed by only the CDDL or
* only the GPL Version 2, indicate your decision by adding "[Contributor]
* elects to include this software in this distribution under the [CDDL or GPL
* Version 2] license." If you don't indicate a single choice of license, a
* recipient has the option to distribute your version of this file under
* either the CDDL, the GPL Version 2 or to extend the choice of license to
* its licensees as provided above. However, if you add GPL Version 2 code
* and therefore, elected the GPL Version 2 license, then the option applies
* only if the new code is made subject to such option by the copyright
* holder.
*/
package org.glassfish.jersey.jdk.connector;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Deque;
import java.util.LinkedList;
import java.util.concurrent.ExecutorService;
import org.glassfish.jersey.internal.util.collection.ByteBufferInputStream;
/**
* TODO Some of the operations added for async. support (e.g.) can be also supported in sync. mode
* <p/>
* Body stream that can operate either synchronously or asynchronously. See {@link BodyInputStream} for details.
*
* @author Petr Janouch (petr.janouch at oracle.com)
*/
class AsynchronousBodyInputStream extends BodyInputStream {
// marker of the end of data stream
private static final ByteBuffer EOF = ByteBuffer.wrap(new byte[] {});
// marker of an error in the data stream
private static final ByteBuffer ERROR = ByteBuffer.wrap(new byte[] {});
// mode this stream operates in
private Mode mode = Mode.UNDECIDED;
private ReadListener readListener = null;
// read listener is not called always when data become available
// it must be called for the first time or after isReady returned false
private boolean callReadListener = false;
// exception stored until we come to ERROR marker in the input stream
private Throwable t = null;
// marker that the stream does not admit more data/errors/stream-end notifications
private boolean closedForInput = false;
// by default readListener is invoked on IO/worker threads
// this might deadlock the entire connector if a blocking operations are used inside the listener implementations
// the readListener will be invoked using this executor if present
private ExecutorService listenerExecutor = null;
// a listener used internally by the connector
private StateChangeLister stateChangeLister;
// if in synchronous mode, this stream delegates to synchronousStream
private ByteBufferInputStream synchronousStream = null;
// data to be read
private Deque<ByteBuffer> data = new LinkedList<>();
synchronized void setListenerExecutor(ExecutorService listenerExecutor) {
assertAsynchronousOperation();
this.listenerExecutor = listenerExecutor;
commitToMode();
}
@Override
public synchronized boolean isReady() {
assertAsynchronousOperation();
// return false if this stream has not been initialised
if (mode == Mode.UNDECIDED) {
return false;
}
ByteBuffer headBuffer = data.peek();
boolean ready = true;
if (headBuffer == null) {
ready = false;
}
if (headBuffer == ERROR) {
ready = false;
callOnError(t);
}
if (headBuffer == EOF) {
ready = false;
callOnAllDataRead();
}
if (!ready) {
// returning false automatically enables listener
callReadListener = true;
}
return ready;
}
@Override
public synchronized void setReadListener(ReadListener readListener) {
if (this.readListener != null) {
throw new IllegalStateException(LocalizationMessages.READ_LISTENER_SET_ONLY_ONCE());
}
// make sure we are not already in synchronous mode
assertAsynchronousOperation();
this.readListener = readListener;
commitToMode();
// if there is an error or EOF at the head of the data queue, isReady will handle it
if (isReady()) {
callDataAvailable();
}
}
@Override
public int read() throws IOException {
commitToMode();
if (mode == Mode.SYNCHRONOUS) {
return synchronousStream.read();
}
validateState();
return doRead();
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
commitToMode();
if (mode == Mode.SYNCHRONOUS) {
return synchronousStream.read(b, off, len);
}
// some validation borrowed from InputStream
if (b == null) {
throw new NullPointerException();
} else if (off < 0 || len < 0 || len > b.length - off) {
throw new IndexOutOfBoundsException();
} else if (len == 0) {
return 0;
}
validateState();
for (int i = 0; i < len; i++) {
if (!hasDataToRead()) {
return i;
}
b[off + i] = doRead();
}
// if we are here we were able to fill the entire buffer
return len;
}
private synchronized byte doRead() {
// if we are here we passed all the validation, so there must be something to read
ByteBuffer headBuffer = data.peek();
byte b = headBuffer.get();
if (!headBuffer.hasRemaining()) {
// remove empty buffer
data.poll();
}
return b;
}
@Override
public int available() throws IOException {
commitToMode();
// TODO this could be also supported in async mode
assertSynchronousOperation();
return synchronousStream.available();
}
@Override
public long skip(long n) throws IOException {
commitToMode();
// TODO this could be also supported in async mode
assertSynchronousOperation();
return synchronousStream.skip(n);
}
@Override
public int tryRead() throws IOException {
commitToMode();
assertSynchronousOperation();
return synchronousStream.tryRead();
}
@Override
public int tryRead(byte[] b) throws IOException {
commitToMode();
assertSynchronousOperation();
return synchronousStream.tryRead(b);
}
@Override
public int tryRead(byte[] b, int off, int len) throws IOException {
commitToMode();
assertSynchronousOperation();
return synchronousStream.tryRead(b, off, len);
}
synchronized void notifyDataAvailable(ByteBuffer availableData) {
assertClosedForInput();
if (!availableData.hasRemaining()) {
return;
}
if (mode == Mode.SYNCHRONOUS) {
try {
synchronousStream.put(availableData);
} catch (InterruptedException e) {
synchronousStream.closeQueue(e);
}
return;
}
data.add(availableData);
if (readListener != null && callReadListener) {
callDataAvailable();
}
}
@Override
public void close() throws IOException {
if (mode == Mode.SYNCHRONOUS) {
synchronousStream.close();
}
}
synchronized void notifyError(Throwable t) {
assertClosedForInput();
if (stateChangeLister != null) {
stateChangeLister.onError(t);
}
closedForInput = true;
if (mode == Mode.SYNCHRONOUS) {
synchronousStream.closeQueue(t);
return;
}
// we store the error and put a marker in the stream, so that the user can read all data that
// were successfully received up to the error.
this.t = t;
data.add(ERROR);
if (mode == Mode.ASYNCHRONOUS && callReadListener) {
callOnError(t);
}
}
synchronized void notifyAllDataRead() {
assertClosedForInput();
if (stateChangeLister != null) {
stateChangeLister.onAllDataRead();
}
if (mode == Mode.SYNCHRONOUS) {
synchronousStream.closeQueue();
return;
}
data.add(EOF);
if (mode == Mode.ASYNCHRONOUS && callReadListener) {
callOnAllDataRead();
}
}
private synchronized void commitToMode() {
// return if the mode has already been committed
if (mode != Mode.UNDECIDED) {
return;
}
// go asynchronous, if the user has made any move suggesting asynchronous mode
if (readListener != null || listenerExecutor != null) {
mode = Mode.ASYNCHRONOUS;
return;
}
// go synchronous, if the user has not made any move suggesting asynchronous mode
mode = Mode.SYNCHRONOUS;
synchronousStream = new ByteBufferInputStream();
// move all buffered data to synchronous stream
for (ByteBuffer b : data) {
if (b == EOF) {
synchronousStream.closeQueue();
} else if (b == ERROR) {
synchronousStream.closeQueue(t);
} else {
try {
synchronousStream.put(b);
} catch (InterruptedException e) {
synchronousStream.closeQueue(e);
}
}
}
}
private void assertAsynchronousOperation() {
if (mode == Mode.SYNCHRONOUS) {
throw new UnsupportedOperationException(LocalizationMessages.ASYNC_OPERATION_NOT_SUPPORTED());
}
}
private void assertSynchronousOperation() {
if (mode == Mode.ASYNCHRONOUS) {
throw new UnsupportedOperationException(LocalizationMessages.SYNC_OPERATION_NOT_SUPPORTED());
}
}
private void validateState() {
if (mode == Mode.ASYNCHRONOUS && !hasDataToRead()) {
throw new IllegalStateException(LocalizationMessages.WRITE_WHEN_NOT_READY());
}
}
private void assertClosedForInput() {
if (closedForInput) {
throw new IllegalStateException(LocalizationMessages.STREAM_CLOSED_FOR_INPUT());
}
}
private boolean hasDataToRead() {
ByteBuffer headBuffer = data.peek();
if (headBuffer == null || headBuffer == EOF || headBuffer == ERROR || !headBuffer.hasRemaining()) {
return false;
}
return true;
}
private void callDataAvailable() {
callReadListener = false;
if (listenerExecutor == null) {
try {
readListener.onDataAvailable();
} catch (IOException e) {
readListener.onError(e);
}
} else {
listenerExecutor.submit(() -> {
try {
readListener.onDataAvailable();
} catch (IOException e) {
readListener.onError(e);
}
});
}
}
private void callOnError(final Throwable t) {
if (listenerExecutor == null) {
readListener.onError(t);
} else {
listenerExecutor.submit(() -> readListener.onError(t));
}
}
private void callOnAllDataRead() {
if (listenerExecutor == null) {
try {
readListener.onAllDataRead();
} catch (IOException e) {
readListener.onError(e);
}
} else {
listenerExecutor.submit(() -> {
try {
readListener.onAllDataRead();
} catch (IOException e) {
readListener.onError(e);
}
});
}
}
synchronized void setStateChangeLister(StateChangeLister stateChangeLister) {
this.stateChangeLister = stateChangeLister;
if (!data.isEmpty() && data.getLast() == EOF) {
stateChangeLister.onAllDataRead();
}
if (!data.isEmpty() && data.getLast() == ERROR) {
stateChangeLister.onError(t);
}
}
private enum Mode {
SYNCHRONOUS,
ASYNCHRONOUS,
UNDECIDED
}
/**
* Internal listener, so that the connection pool knows when the body has been read,
* so it can reuse/close the connection.
*/
interface StateChangeLister {
void onError(Throwable t);
void onAllDataRead();
}
}