/*
* Copyright (c) xlightweb.org, 2008 - 2009. All rights reserved.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*
* Please refer to the LGPL license at: http://www.gnu.org/copyleft/lesser.txt
* The latest copy of this software may be found on http://www.xlightweb.org/
*/
package org.xlightweb;
import java.io.Closeable;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.lang.ref.WeakReference;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.FileChannel;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.WritableByteChannel;
import java.util.ArrayList;
import java.util.List;
import java.util.TimerTask;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.xlightweb.AbstractHttpConnection.IMultimodeExecutor;
import org.xsocket.DataConverter;
import org.xsocket.IDataSource;
import org.xsocket.MaxReadSizeExceededException;
import org.xsocket.connection.AbstractNonBlockingStream;
import org.xsocket.connection.IConnection;
import org.xsocket.connection.IWriteCompletionHandler;
import org.xsocket.connection.IConnection.FlushMode;
/**
*
* I/O resource capable of providing body data in a non blocking way.
* Read operations returns immediately
*
* @author grro@xlightweb.org
*/
public final class NonBlockingBodyDataSource implements IDataSource, ReadableByteChannel, Closeable {
private static final Logger LOG = Logger.getLogger(NonBlockingBodyDataSource.class.getName());
// max write buffer size
private static int maxWriteBufferSize = AbstractHttpConnection.getMaxWriteBufferSize();
// body type
private final BodyType bodyType;
// delegee classes
private final NonBlockingStream nonBlockingStream = new NonBlockingStream();
private final HandlerCaller handlerCaller = new HandlerCaller();
// close flag
private final AtomicBoolean isOpen = new AtomicBoolean(true);
private final AtomicBoolean isDestroyed = new AtomicBoolean(false);
private final AtomicBoolean isUnderlyingConnectionOpen = new AtomicBoolean(true);
// life cycle management
private static final long MIN_WATCHDOG_PERIOD_MILLIS = 10 * 1000;
public static final long DEFAULT_RECEIVE_TIMEOUT_MILLIS = Long.MAX_VALUE;
private long bodyDataReceiveTimeoutMillis = DEFAULT_RECEIVE_TIMEOUT_MILLIS;
private long creationTimeMillis = 0;
private long lastTimeDataReceivedMillis = System.currentTimeMillis();
private TimeoutWatchDogTask watchDogTask;
private boolean isDestroyConnectionAfterReceived = false;
private boolean isCloseConnectionAfterReceived = false;
private AtomicBoolean isOnDisconnectCalled = new AtomicBoolean(false);
// the underlying connection
private final AbstractHttpConnection httpConnection;
// listener management
private final ArrayList<IBodyCloseListener> closeListeners = new ArrayList<IBodyCloseListener>();
private final ArrayList<IBodyCompleteListener> completeListeners = new ArrayList<IBodyCompleteListener>();
private final AtomicBoolean isComplete = new AtomicBoolean(false);
// handler management
private final AtomicReference<IBodyDataSourceDisconnectHandler> disconnectHandler = new AtomicReference<IBodyDataSourceDisconnectHandler>(null);
private final AtomicReference<IBodyDataHandler> handler = new AtomicReference<IBodyDataHandler>(null);
private final AtomicBoolean isMultithreaded = new AtomicBoolean(true);
private boolean isSystem = false;
// suspend support
private final AtomicBoolean isSuspended = new AtomicBoolean(false);
// exception support
private final AtomicReference<IExceptionHandler> exceptionHandler = new AtomicReference<IExceptionHandler>(null);
private final AtomicReference<IOException> exceptionHolder = new AtomicReference<IOException>();
// callback execution support
private final IMultimodeExecutor executor;
// write completion support
private final AtomicBoolean isCompletionSupportActivated = new AtomicBoolean(false);
private final WriteCompletionManager writeCompletionManager = new WriteCompletionManager(getId());
/**
* constructor
*
* @param bodyType the body type
* @param encoding the encoding
*/
NonBlockingBodyDataSource(BodyType bodyType, String encoding) {
this.bodyType = bodyType;
httpConnection = null;
executor = new DefaultMultimodeExecutor();
nonBlockingStream.setEncoding(encoding);
}
/**
* constructor
*
* @param bodyType the body type
* @param encoding the encoding
* @param httpConnection the underlying httpConnection
* @param executor the executor
*/
NonBlockingBodyDataSource(BodyType bodyType, String encoding, AbstractHttpConnection httpConnection, IMultimodeExecutor executor) {
this.bodyType = bodyType;
this.executor = executor;
nonBlockingStream.setEncoding(encoding);
this.httpConnection = httpConnection;
}
/**
* constructor
*
* @param bodyType the body type
* @param body the body
* @param encoding the encoding
*/
NonBlockingBodyDataSource(BodyType bodyType, String body, String encoding) {
this(bodyType, new ByteBuffer[] { DataConverter.toByteBuffer(body, encoding) }, encoding);
}
/**
* constructor
*
* @param bodyType the body type
* @param body the body
* @param encoding the encoding
*/
NonBlockingBodyDataSource(BodyType bodyType, byte[] body, String encoding) {
this(bodyType, new ByteBuffer[] { ByteBuffer.wrap(body) }, encoding);
}
/**
* constructor
*
* @param bodyType the body type
* @param body the body
* @param encoding the encoding
*/
NonBlockingBodyDataSource(BodyType bodyType, ByteBuffer[] body, String encoding) {
this.bodyType = bodyType;
httpConnection = null;
executor = new DefaultMultimodeExecutor();
if (encoding != null) {
nonBlockingStream.setEncoding(encoding);
}
nonBlockingStream.append(body, null);
isComplete.set(true);
}
/**
* constructor
*
* @param bodyType the body type
* @param bodyDatasource the body data source
* @param encoding the encoding
*/
NonBlockingBodyDataSource(BodyType bodyType, ReadableByteChannel bodyDatasource, String encoding) throws IOException {
this(bodyType, bodyDatasource, 8192, encoding);
}
/**
* constructor
*
* @param bodyDatasource the body data source
* @param encoding the encoding
*/
NonBlockingBodyDataSource(FileChannel bodyDatasource, String encoding) throws IOException {
this(BodyType.IN_MEMORY, bodyDatasource, (int) bodyDatasource.size(), encoding);
}
/**
* constructor
*
* @param bodyDatasource the body data source
* @param encoding the encoding
*/
private NonBlockingBodyDataSource(BodyType bodyType, ReadableByteChannel bodyDatasource, int chunkSize, String encoding) throws IOException {
this.bodyType = bodyType;
httpConnection = null;
executor = new DefaultMultimodeExecutor();
setEncoding(encoding);
List<ByteBuffer> buffers = new ArrayList<ByteBuffer>();
int read = 0;
do {
ByteBuffer transferBuffer = ByteBuffer.allocate(chunkSize);
read = bodyDatasource.read(transferBuffer);
if (read > 0) {
if (transferBuffer.remaining() == 0) {
transferBuffer.flip();
buffers.add(transferBuffer);
} else {
transferBuffer.flip();
buffers.add(transferBuffer.slice());
}
}
} while (read > 0);
nonBlockingStream.append(buffers.toArray(new ByteBuffer[buffers.size()]), null);
isComplete.set(true);
}
void setEncoding(String encoding) {
nonBlockingStream.setEncoding(encoding);
}
/**
* returns the id
*
* @return the id
*/
String getId() {
if (httpConnection != null) {
return httpConnection.getId();
} else {
return Integer.toString(hashCode());
}
}
/**
* returns the assigned http connection
*
* @return the assigned http connection
*/
AbstractHttpConnection getConnection() {
return httpConnection;
}
/**
* copies the body content
*
* @return the copy
*/
ByteBuffer[] copyContent() {
return nonBlockingStream.copyContent();
}
/**
* destroys the data source
*/
void destroy() {
isDestroyed.set(true);
if (httpConnection != null) {
httpConnection.destroy();
}
}
/**
* initiates that the assigned connection will be destroyed, if the body is received completely
*
* @param isDestroyConnectionAfterReceived true, if the connection should be destroyed
*/
void setDestroyAfterReceived(boolean isDestroyConnectionAfterReceived) {
this.isDestroyConnectionAfterReceived = isDestroyConnectionAfterReceived;
if (isComplete.get()) {
handleReceivingFinished();
}
}
/**
* initiates that the assigned connection will be destroyed, if the body is received completely
*
* @param isCloseConnectionAfterReceived true, if the connection should be closed
*/
void setCloseAfterReceived(boolean isCloseConnectionAfterReceived) {
this.isCloseConnectionAfterReceived = isCloseConnectionAfterReceived;
if (isComplete.get()) {
handleReceivingFinished();
}
}
/**
* suspend the (underlying connection of the) body data source
*
* @throws IOException if an error occurs
*/
private void suspend() throws IOException {
if ((httpConnection != null) && !httpConnection.isReceivingSuspended()) {
boolean suspended = isSuspended.getAndSet(true);
// is not already suspended
if (!suspended) {
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("suspend receiving data");
}
isSuspended.set(true);
Runnable suspendTask = new Runnable() {
public void run() {
try {
httpConnection.suspendReceiving();
} catch (IOException ioe) {
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("error occured by resuming " + NonBlockingBodyDataSource.this + " " + ioe.toString());
}
}
}
};
executor.processNonthreaded(suspendTask);
}
}
}
/**
* resume the (underlying connection of the) body data source
*
* @throws IOException if an error occurs
*/
private void resume() throws IOException {
if (httpConnection != null) {
boolean suspended = isSuspended.getAndSet(false);
// is suspended?
if (suspended) {
Runnable resumeTask = new Runnable() {
public void run() {
try {
if (httpConnection.isReceivingSuspended()) {
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("resume receiving data");
}
httpConnection.resumeReceiving();
callBodyHandler(true, true);
}
} catch (IOException ioe) {
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("error occured by resuming " + NonBlockingBodyDataSource.this + " " + ioe.toString());
}
}
}
};
executor.processNonthreaded(resumeTask);
}
}
}
/**
* on disconnect call back
*/
void onDisconnect() {
// is disconnect already called?
if (isOnDisconnectCalled.getAndSet(true)) {
return;
}
if (LOG.isLoggable(Level.FINE)) {
if (!isComplete.get()) {
LOG.fine("protocol error occured (connection is closed, but more data is expected). May be connection has been closed by peer?");
}
}
isUnderlyingConnectionOpen.set(false);
callBodyHandler(true, false);
IBodyDataSourceDisconnectHandler dh = disconnectHandler.get();
if (dh != null) {
Runnable task = new OnDisconnectCaller(dh);
processNonthreaded(task);
}
}
private static final class OnDisconnectCaller implements Runnable {
private IBodyDataSourceDisconnectHandler dh = null;
public OnDisconnectCaller(IBodyDataSourceDisconnectHandler dh) {
this.dh = dh;
}
public void run() {
dh.onDisconnect();
}
};
/**
* returns the body type
*
* @return the body type
*/
BodyType getBodyType() {
return bodyType;
}
/**
* returns body data receive timeout
*
* @return the body data receive timeout or <code>null</code>
*/
public long getBodyDataReceiveTimeoutMillis() {
return bodyDataReceiveTimeoutMillis;
}
/**
* set the body data receive timeout
*
* @param bodyDataReceiveTimeoutMillis the timeout
*/
public void setBodyDataReceiveTimeoutMillis(long bodyDataReceiveTimeoutMillis) {
if (bodyDataReceiveTimeoutMillis <= 0) {
if (!isComplete.get()) {
setIOException(new ReceiveTimeoutException(bodyDataReceiveTimeoutMillis));
}
return;
}
creationTimeMillis = System.currentTimeMillis();
if (this.bodyDataReceiveTimeoutMillis != bodyDataReceiveTimeoutMillis) {
this.bodyDataReceiveTimeoutMillis = bodyDataReceiveTimeoutMillis;
if (bodyDataReceiveTimeoutMillis == Long.MAX_VALUE) {
terminateWatchDog();
} else{
long watchdogPeriod = 100;
if (bodyDataReceiveTimeoutMillis > 1000) {
watchdogPeriod = bodyDataReceiveTimeoutMillis / 10;
}
if (watchdogPeriod > MIN_WATCHDOG_PERIOD_MILLIS) {
watchdogPeriod = MIN_WATCHDOG_PERIOD_MILLIS;
}
updateWatchDog(watchdogPeriod);
}
}
}
private synchronized void updateWatchDog(long watchDogPeriod) {
terminateWatchDog();
watchDogTask = new TimeoutWatchDogTask(this);
AbstractHttpConnection.schedule(watchDogTask, watchDogPeriod, watchDogPeriod);
}
private synchronized void terminateWatchDog() {
if (watchDogTask != null) {
watchDogTask.cancel();
watchDogTask = null;
}
}
private void checkTimeouts() {
if (isComplete.get()) {
terminateWatchDog();
return;
}
long currentTimeMillis = System.currentTimeMillis();
if (currentTimeMillis > (lastTimeDataReceivedMillis + bodyDataReceiveTimeoutMillis) &&
currentTimeMillis > (creationTimeMillis + bodyDataReceiveTimeoutMillis)) {
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("receive timeout reached. set exception");
}
if (!isComplete.get()) {
setIOException(new ReceiveTimeoutException());
}
destroy();
}
}
/**
* sets the exception handler<
*
* @param eh the exception handler<
*/
void setExceptionHandler(IExceptionHandler eh) {
exceptionHandler.set(eh);
}
/**
* sets a exception
*
* @param ioe the exception
*/
void setIOException(IOException ioe) {
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("error occured " + ioe.toString());
}
if (exceptionHolder.get() == null) {
IExceptionHandler eh = exceptionHandler.get();
if (eh != null) {
eh.onException(ioe);
}
exceptionHolder.set(ioe);
}
closeUnclean();
}
private void throwExceptionIfExist() throws IOException {
if (exceptionHolder.get() != null) {
IOException ex = exceptionHolder.get();
exceptionHolder.set(null);
throw ex;
}
}
/**
* returns the body encoding
*
* @return the body encoding
*/
String getEncoding() {
return nonBlockingStream.getEncoding();
}
/**
* returns true, if the body data source is open
*
* @return true, if the body data source is open
*/
public boolean isOpen() {
return isOpen.get();
}
/**
* closes the body data source
*
* @throws IOException if an exception occurs
*/
public void close() throws IOException {
close(true);
}
private void close(boolean isSetComplete) throws IOException {
if (!isComplete.get() && (httpConnection != null)) {
httpConnection.setPersistent(false);
}
if (isSetComplete) {
isComplete.set(true);
}
terminateWatchDog();
nonBlockingStream.close();
callBodyHandler(true, false);
handleReceivingFinished();
callCloseListener();
}
@SuppressWarnings("unchecked")
private void callCloseListener() {
ArrayList<IBodyCloseListener> closeListenersCopy = null;
synchronized (closeListeners) {
closeListenersCopy = (ArrayList<IBodyCloseListener>) closeListeners.clone();
}
for (IBodyCloseListener bodyCloseListener : closeListenersCopy) {
removeCloseListener(bodyCloseListener);
callCloseListener(bodyCloseListener);
}
}
private void callCloseListener(IBodyCloseListener listener) {
Runnable task = new BodyCloselistenerCaller(listener);
if (HttpUtils.isBodyCloseListenerMutlithreaded(listener)) {
executor.processMultithreaded(task);
} else {
executor.processNonthreaded(task);
}
}
private static final class BodyCloselistenerCaller implements Runnable {
private IBodyCloseListener listener = null;
public BodyCloselistenerCaller(IBodyCloseListener listener) {
this.listener = listener;
}
public void run() {
try {
listener.onClose();
} catch (IOException ioe) {
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("Error occured by calling close listener " + listener + " " + ioe.toString());
}
}
}
}
private int getSize() throws IOException {
return nonBlockingStream.available();
}
/**
* returns the available bytes
*
* @return the number of available bytes, possibly zero, or -1 if the channel has reached end-of-stream
*
* @throws ProtocolException if a protocol error occurs
* @throws IOException if some other exception occurs
*/
public int available() throws ProtocolException, IOException {
// if an exception is pending -> throwing it
if (exceptionHolder.get() != null) {
IOException ex = exceptionHolder.get();
exceptionHolder.set(null);
// ClosedChannelException should not occur here. Anyway, handle it because available() should never throw a ClosedChannelException
if (!(ex instanceof ClosedChannelException)) {
throw ex;
}
}
// retrieve the available data size
int available = nonBlockingStream.getSize();
// non data available?
if ((available == 0)) {
// if body is complete return -1 to signal end-of-stream
if (isComplete.get()) {
return -1;
} else {
// if underlying connection is open return 0 because more data can be received
if (isUnderlyingConnectionOpen.get()) {
return 0;
// if underlying connection is closed throwing a ClosedChannelException, because
// the body is not complete
} else {
isOpen.set(false);
throw new ClosedChannelException();
}
}
} else {
return available;
}
}
/**
* returns the current content size
*
* @return the current content size
* @throws IOException if an exception occurs
*/
int size() throws IOException {
int available = nonBlockingStream.getSize();
if ((available <= 0) && isComplete.get()) {
return -1;
} else {
return available;
}
}
private int getVersion() throws IOException {
return nonBlockingStream.getReadBufferVersion();
}
/**
* get the version of read buffer. The version number increases, if
* the read buffer queue has been modified
*
* @return the read buffer version
* @throws IOException if an exception occurs
*/
public int getReadBufferVersion() throws IOException {
throwExceptionIfExist();
return nonBlockingStream.getReadBufferVersion();
}
/**
* Marks the read position in the connection. Subsequent calls to resetToReadMark() will attempt
* to reposition the connection to this point.
*
*/
public void markReadPosition() {
nonBlockingStream.markReadPosition();
}
/**
* Resets to the marked read position. If the connection has been marked,
* then attempt to reposition it at the mark.
*
* @return true, if reset was successful
*/
public boolean resetToReadMark() {
return nonBlockingStream.resetToReadMark();
}
/**
* remove the read mark
*/
public void removeReadMark() {
nonBlockingStream.removeReadMark();
}
/**
* Returns the index of the first occurrence of the given string.
*
* @param str any string
* @return if the string argument occurs as a substring within this object, then
* the index of the first character of the first such substring is returned;
* if it does not occur as a substring, -1 is returned.
* @throws IOException If some other I/O error occurs
*/
public int indexOf(String str) throws IOException {
throwExceptionIfExist();
return nonBlockingStream.indexOf(str);
}
/**
* Returns the index of the first occurrence of the given string.
*
* @param str any string
* @param encoding the encoding to use
* @return if the string argument occurs as a substring within this object, then
* the index of the first character of the first such substring is returned;
* if it does not occur as a substring, -1 is returned.
* @throws IOException If some other I/O error occurs
*/
public int indexOf(String str, String encoding) throws IOException, MaxReadSizeExceededException {
throwExceptionIfExist();
return nonBlockingStream.indexOf(str, encoding);
}
/**
* read a byte
*
* @return the byte value
* @throws IOException If some other I/O error occurs
* @throws BufferUnderflowException if not enough data is available
*/
public byte readByte() throws ProtocolException, IOException, BufferUnderflowException {
throwExceptionIfExist();
return nonBlockingStream.readByte();
}
/**
* read a short value
*
* @return the short value
* @throws IOException If some other I/O error occurs
* @throws BufferUnderflowException if not enough data is available
*/
public short readShort() throws ProtocolException, IOException, BufferUnderflowException {
throwExceptionIfExist();
return nonBlockingStream.readShort();
}
/**
* read an int
*
* @return the int value
* @throws IOException If some other I/O error occurs
* @throws BufferUnderflowException if not enough data is available
*/
public int readInt() throws ProtocolException, IOException, BufferUnderflowException {
throwExceptionIfExist();
return nonBlockingStream.readInt();
}
/**
* read a long
*
* @return the long value
* @throws IOException If some other I/O error occurs
* @throws BufferUnderflowException if not enough data is available
*/
public long readLong() throws IOException, BufferUnderflowException {
throwExceptionIfExist();
return nonBlockingStream.readLong();
}
/**
* read a double
*
* @return the double value
* @throws IOException If some other I/O error occurs
* @throws BufferUnderflowException if not enough data is available
*/
public double readDouble() throws ProtocolException, IOException, BufferUnderflowException {
throwExceptionIfExist();
return nonBlockingStream.readDouble();
}
/**
* see {@link ReadableByteChannel#read(ByteBuffer)}
*/
public int read(ByteBuffer buffer) throws ProtocolException, IOException {
throwExceptionIfExist();
int size = buffer.remaining();
int available = available();
if (available == -1) {
close();
return -1;
}
if (available == 0) {
return 0;
}
if (available > 0) {
if (available < size) {
size = available;
}
if (size > 0) {
ByteBuffer[] bufs = readByteBufferByLength(size);
copyBuffers(bufs, buffer);
}
}
return size;
}
private void copyBuffers(ByteBuffer[] source, ByteBuffer target) {
for (ByteBuffer buf : source) {
if (buf.hasRemaining()) {
target.put(buf);
}
}
}
/**
* read a ByteBuffer by using a delimiter. The default encoding will be used to decode the delimiter
* To avoid memory leaks the {@link IReadWriteableConnection#readByteBufferByDelimiter(String, int)} method is generally preferable
* <br>
* For performance reasons, the ByteBuffer readByteBuffer method is
* generally preferable to get bytes
*
* @param delimiter the delimiter
* @return the ByteBuffer
* @throws IOException If some other I/O error occurs
* @throws BufferUnderflowException if not enough data is available
*/
public ByteBuffer[] readByteBufferByDelimiter(String delimiter) throws ProtocolException, IOException, BufferUnderflowException {
return readByteBufferByDelimiter(delimiter, Integer.MAX_VALUE);
}
/**
* read a ByteBuffer by using a delimiter
*
* For performance reasons, the ByteBuffer readByteBuffer method is
* generally preferable to get bytes
*
* @param delimiter the delimiter
* @param maxLength the max length of bytes that should be read. If the limit is exceeded a MaxReadSizeExceededException will been thrown
* @return the ByteBuffer
* @throws MaxReadSizeExceededException If the max read length has been exceeded and the delimiter hasn�t been found
* @throws IOException If some other I/O error occurs
* @throws BufferUnderflowException if not enough data is available
*/
public ByteBuffer[] readByteBufferByDelimiter(String delimiter, int maxLength) throws ProtocolException, IOException, BufferUnderflowException, MaxReadSizeExceededException {
throwExceptionIfExist();
return nonBlockingStream.readByteBufferByDelimiter(delimiter, maxLength);
}
/**
* read a ByteBuffer
*
* @param length the length could be negative, in this case a empty array will be returned
* @return the ByteBuffer
* @throws IOException If some other I/O error occurs
* @throws BufferUnderflowException if not enough data is available
*/
public ByteBuffer[] readByteBufferByLength(int length) throws IOException, BufferUnderflowException {
throwExceptionIfExist();
ByteBuffer[] bufs = nonBlockingStream.readByteBufferByLength(length);
if (LOG.isLoggable(Level.FINE)) {
int size = 0;
for (ByteBuffer buffer : bufs) {
size += buffer.remaining();
}
LOG.fine(size + " data read (remaining " + nonBlockingStream.getSize() + ")");
}
return bufs;
}
/**
* read a byte array by using a delimiter
*
* For performance reasons, the ByteBuffer readByteBuffer method is
* generally preferable to get bytes
*
* @param delimiter the delimiter
* @return the read bytes
* @throws IOException If some other I/O error occurs
* @throws BufferUnderflowException if not enough data is available
*/
public byte[] readBytesByDelimiter(String delimiter) throws ProtocolException, IOException, BufferUnderflowException {
return DataConverter.toBytes(readByteBufferByDelimiter(delimiter));
}
/**
* read a byte array by using a delimiter
*
* For performance reasons, the ByteBuffer readByteBuffer method is
* generally preferable to get bytes
*
* @param delimiter the delimiter
* @param maxLength the max length of bytes that should be read. If the limit is exceeded a MaxReadSizeExceededException will been thrown
* @return the read bytes
* @throws MaxReadSizeExceededException If the max read length has been exceeded and the delimiter hasn�t been found
* @throws IOException If some other I/O error occurs
* @throws BufferUnderflowException if not enough data is available
*/
public byte[] readBytesByDelimiter(String delimiter, int maxLength) throws ProtocolException, IOException, BufferUnderflowException, MaxReadSizeExceededException {
return DataConverter.toBytes(readByteBufferByDelimiter(delimiter, maxLength));
}
/**
* read bytes by using a length definition
*
* @param length the amount of bytes to read
* @return the read bytes
* @throws IOException If some other I/O error occurs
* @throws IllegalArgumentException, if the length parameter is negative
* @throws BufferUnderflowException if not enough data is available
*/
public byte[] readBytesByLength(int length) throws ProtocolException, IOException, BufferUnderflowException {
return DataConverter.toBytes(readByteBufferByLength(length));
}
/**
* read a string by using a delimiter
*
* @param delimiter the delimiter
* @return the string
* @throws IOException If some other I/O error occurs
* @throws UnsupportedEncodingException if the default encoding is not supported
* @throws BufferUnderflowException if not enough data is available
*/
public String readStringByDelimiter(String delimiter) throws ProtocolException, IOException, BufferUnderflowException, UnsupportedEncodingException {
return DataConverter.toString(readByteBufferByDelimiter(delimiter), getEncoding());
}
/**
* read a string by using a delimiter
*
* @param delimiter the delimiter
* @param maxLength the max length of bytes that should be read. If the limit is exceeded a MaxReadSizeExceededException will been thrown
* @return the string
* @throws MaxReadSizeExceededException If the max read length has been exceeded and the delimiter hasn�t been found
* @throws IOException If some other I/O error occurs
* @throws UnsupportedEncodingException If the given encoding is not supported
* @throws BufferUnderflowException if not enough data is available
*/
public String readStringByDelimiter(String delimiter, int maxLength) throws ProtocolException, IOException, BufferUnderflowException, UnsupportedEncodingException, MaxReadSizeExceededException {
return DataConverter.toString(readByteBufferByDelimiter(delimiter, maxLength), getEncoding());
}
/**
* read a string by using a length definition
*
* @param length the amount of bytes to read
* @return the string
* @throws IOException If some other I/O error occurs
* @throws UnsupportedEncodingException if the given encoding is not supported
* @throws IllegalArgumentException, if the length parameter is negative
* @throws BufferUnderflowException if not enough data is available
*/
public String readStringByLength(int length) throws ProtocolException, IOException, BufferUnderflowException, UnsupportedEncodingException {
return DataConverter.toString(readByteBufferByLength(length), getEncoding());
}
/**
* transfer the data of the this source channel to the given data sink
*
* @param dataSink the data sink
* @param length the size to transfer
*
* @return the number of transfered bytes
* @throws ClosedChannelException If either this channel or the target channel is closed
* @throws IOException If some other I/O error occurs
*/
public long transferTo(WritableByteChannel dataSink, int length) throws ProtocolException, IOException, ClosedChannelException {
throwExceptionIfExist();
if (length > 0) {
long written = 0;
ByteBuffer[] buffers = readByteBufferByLength(length);
for (ByteBuffer buffer : buffers) {
while(buffer.hasRemaining()) {
written += dataSink.write(buffer);
}
}
return written;
} else {
return 0;
}
}
/**
* transfer the available data of the this source channel to the given data sink
*
* @param dataSink the data sink
*
* @return the number of transfered bytes
* @throws ClosedChannelException If either this channel or the target channel is closed
* @throws IOException If some other I/O error occurs
*/
public long transferTo(BodyDataSink dataSink) throws ProtocolException, IOException, ClosedChannelException {
return transferTo(dataSink, available());
}
/**
* transfer the data of the this source channel to the given data sink
*
* @param dataSink the data sink
* @param length the size to transfer
*
* @return the number of transfered bytes
* @throws ClosedChannelException If either this channel or the target channel is closed
* @throws IOException If some other I/O error occurs
*/
public long transferTo(final BodyDataSink dataSink, final int length) throws ProtocolException, IOException, ClosedChannelException {
throwExceptionIfExist();
long written = 0;
if (length > 0) {
boolean savedAutoflush = dataSink.isAutoflush();
dataSink.setAutoflush(true);
// completion handler adapter is only required in async mode
if (dataSink.getFlushmode() == FlushMode.ASYNC) {
final ByteBuffer[] bufs = nonBlockingStream.readByteBufferByLength(length);
IWriteCompletionHandler adapter = new IWriteCompletionHandler() {
public void onWritten(int length) throws IOException {
flowControlOnWritten(dataSink);
writeCompletionManager.onWritten(bufs, true);
}
public void onException(IOException ioe) {
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("error occured by writing " + ioe.toString());
}
flowControlOnWritten(dataSink);
writeCompletionManager.onWriteException(ioe, bufs);
destroy();
}
};
flowControlOnWrite(dataSink, adapter);
dataSink.write(bufs, adapter);
written = length;
} else {
written = transferTo((WritableByteChannel) dataSink, length);
}
dataSink.setAutoflush(savedAutoflush);
}
return written;
}
private void flowControlOnWrite(BodyDataSink dataSink, IWriteCompletionHandler adapter) throws IOException {
// suspend only if data sink is network endpoint
if (dataSink.isNetworkEndpoint()) {
if ((maxWriteBufferSize != Integer.MAX_VALUE) && (dataSink.getPendingWriteDataSize() > maxWriteBufferSize)) {
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("pending write buffer " + dataSink.getPendingWriteDataSize() + " is larger than max size " + maxWriteBufferSize + ". suspend receving");
}
suspend();
}
}
}
private void flowControlOnWritten(BodyDataSink dataSink) {
if ((maxWriteBufferSize != Integer.MAX_VALUE) && (dataSink.getPendingWriteDataSize() > maxWriteBufferSize)) {
return;
} else {
try {
resume();
} catch (IOException e) {
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("error occured by resume receving " + this + " " + e.toString());
}
}
}
}
/**
* adds a complete listener
*
* @param listener the complete listener
*/
public void addCompleteListener(IBodyCompleteListener listener) {
synchronized (completeListeners) {
completeListeners.add(listener);
}
if (isComplete.get()) {
callCompleteListener(listener);
}
}
private boolean removeCompleteListener(IBodyCompleteListener listener) {
synchronized (completeListeners) {
return completeListeners.remove(listener);
}
}
/**
* adds a close listener
*
* @param closeListener the close listener
*/
void addCloseListener(IBodyCloseListener closeListener) {
synchronized (closeListeners) {
closeListeners.add(closeListener);
}
}
/**
* removes a close listener
*
* @param closeListener the close listener
* @return true, if the listener is removed
*/
boolean removeCloseListener(IBodyCloseListener closeListener) {
synchronized (closeListeners) {
return closeListeners.remove(closeListener);
}
}
/**
* replaces the complete listener
*
* @param listener the complete listener
* @return the old listeners
*/
@SuppressWarnings("unchecked")
List<IBodyCompleteListener> replaceCompleteListener(IBodyCompleteListener listener) {
List<IBodyCompleteListener> oldCompleteListeners = null;
synchronized (completeListeners) {
oldCompleteListeners = (List<IBodyCompleteListener>) completeListeners.clone();
completeListeners.clear();
completeListeners.add(listener);
}
return oldCompleteListeners;
}
/**
* set complete flag
*
* @param isComplete the complete flag
*/
void setComplete(boolean isComplete) {
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("[" + getId() + "] complete message received");
}
if (isComplete) {
// terminate watch dog
terminateWatchDog();
// set complete flag
this.isComplete.set(isComplete);
// call body handler
callBodyHandler(true, false);
// notify listeners
callCompleteListeners();
handleReceivingFinished();
}
}
private void handleReceivingFinished() {
if ((isDestroyConnectionAfterReceived) && (httpConnection != null)) {
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("[" + getId() + "] auto destroying connection");
}
httpConnection.destroy();
}
if ((isCloseConnectionAfterReceived) && (httpConnection != null)) {
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("[" + getId() + "] auto closing connection");
}
httpConnection.closeSilence();
}
}
@SuppressWarnings("unchecked")
private void callCompleteListeners() {
List<IBodyCompleteListener> completeListenersCopy = null;
synchronized (completeListeners) {
if (!completeListeners.isEmpty()) {
completeListenersCopy = (List<IBodyCompleteListener>) completeListeners.clone();
}
}
if (completeListenersCopy != null) {
for (IBodyCompleteListener listener : completeListenersCopy) {
removeCompleteListener(listener);
callCompleteListener(listener);
}
}
}
private void callCompleteListener(IBodyCompleteListener listener) {
if (LOG.isLoggable(Level.FINER)) {
LOG.finer("call complete listener " + listener);
}
Runnable task = new CompleteListenerCaller(listener);
if (HttpUtils.isBodyCompleteListenerMutlithreaded(listener)) {
executor.processMultithreaded(task);
} else {
executor.processNonthreaded(task);
}
}
private static final class CompleteListenerCaller implements Runnable {
private IBodyCompleteListener listener = null;
public CompleteListenerCaller(IBodyCompleteListener listener) {
this.listener = listener;
}
public void run() {
try {
listener.onComplete();
} catch (IOException ioe) {
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("Error occured by calling complete listener " + listener + " " + ioe.toString());
}
}
}
}
/**
* processes the task multi threaded
*
* @param task the task
*/
void processMultithreaded(Runnable task) {
executor.processMultithreaded(task);
}
/**
* processes the task non threaded
*
* @param task the task
*/
void processNonthreaded(Runnable task) {
executor.processNonthreaded(task);
}
/**
* return true, if all body data has been received
*
* @return true, if all body data has been received
* @throws IOException if an exception occurs
*/
boolean isComplete() throws IOException {
throwExceptionIfExist();
return isComplete.get();
}
/**
* set a disconect handler
*
* @param dh the disconnect handler
*/
void setDisconnectHandler(IBodyDataSourceDisconnectHandler dh) {
disconnectHandler.set(dh);
}
/**
* sets a system data handler
*
* @param bodyHandler the handler
* @throws IOException if an exception occurs
*/
void setSystemDataHandler(IBodyDataHandler bodyHandler) throws IOException {
isSystem = true;
setDataHandler(bodyHandler);
}
/**
* returns the body data handler or <code>null</code> if no data handler is assigned
*
* @return the body data handler or <code>null</code> if no data handler is assigned
*/
public IBodyDataHandler getDataHandler() {
return handler.get();
}
/**
* set the body handler
*
* @param bodyHandler the body handler
*/
public void setDataHandler(IBodyDataHandler bodyHandler) {
if (bodyHandler == null) {
handler.set(null);
} else {
isMultithreaded.set(HttpUtils.isMutlithreaded(bodyHandler));
handler.set(bodyHandler);
callBodyHandler(false, false);
}
}
/**
* replace the body handler
*
* @param bodyHandler the body handler
* @return the old body date handler
*/
IBodyDataHandler replaceDataHandler(IBodyDataHandler bodyHandler) {
if (bodyHandler == null) {
return handler.getAndSet(null);
} else {
isMultithreaded.set(HttpUtils.isMutlithreaded(bodyHandler));
IBodyDataHandler bdh = handler.getAndSet(bodyHandler);
callBodyHandler(false, false);
return bdh;
}
}
/**
* appends data
*
* @param isContentImmutable if the buffer is immutable
* @param data the data to append
*/
void append(boolean isContentImmutable, ByteBuffer data) throws IOException {
preAppend();
if (!isContentImmutable) {
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("buffer to append is mutable. coping it");
}
data = HttpUtils.copy(data);
}
if (data != null) {
nonBlockingStream.append(data);
}
callBodyHandler(false, false);
}
/**
* appends data
*
* @param isContentImmutable if the buffer is immutable
* @param data the data to append
* @param completionHandler the completion handler
*/
void append(boolean isContentImmutable, ByteBuffer[] data, IWriteCompletionHandler completionHandler) throws IOException {
preAppend();
if (!isContentImmutable) {
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("buffer to append is mutable. coping it");
}
data = HttpUtils.copy(data);
}
if (data != null) {
nonBlockingStream.append(data, completionHandler);
}
callBodyHandler(false, false);
}
private void preAppend() throws IOException {
lastTimeDataReceivedMillis = System.currentTimeMillis();
if (!isOpen.get() || isDestroyed.get()) {
throw new ClosedChannelException();
}
}
private void callBodyHandler(boolean forceCall, boolean forceMultithreaded) {
if (handler.get() != null) {
handlerCaller.setForceCall(forceCall);
if ((httpConnection == null) || isSystem) {
performCallLoop(forceCall);
} else {
if (forceMultithreaded || isMultithreaded.get()) {
executor.processMultithreaded(handlerCaller);
} else {
executor.processNonthreaded(handlerCaller);
}
}
}
}
private void performCallLoop(boolean forceCall) {
try {
while ((getSize() != 0) || forceCall) {
int version = getVersion();
boolean success = call();
if (!success) {
return;
}
if (version == getVersion()) {
return;
}
}
} catch (BufferUnderflowException bue) {
// swallow it
} catch (IOException ioe) {
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("error occured by performing handler call " + ioe.toString());
}
}
}
private boolean call() {
try {
IBodyDataHandler bdh = handler.get();
if (bdh != null) {
return bdh.onData(NonBlockingBodyDataSource.this);
} else {
return false;
}
} catch (BufferUnderflowException ignore) {
} catch (RuntimeException re) {
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("closing data source because an error has been occured by handling data by bodyHandler. " + handler + " Reason: " + re.toString());
}
closeUnclean();
throw re;
}
return false;
}
/**
* returns the writer transfer chunk size
*
* @return the writer transfer chunk size
*/
int getWriteTransferChunkeSize() {
return nonBlockingStream.getWriteTransferChunkeSize();
}
private void closeUnclean() {
try {
close(false);
} catch (IOException ioe) {
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("Error occured by closing data source " + this + " " + ioe.toString());
}
}
}
boolean isMoreInputDataExpected() {
if (isComplete.get()) {
return false;
}
if (!isUnderlyingConnectionOpen.get()) {
return false;
}
return true;
}
/**
* {@inheritDoc}
*/
@Override
public String toString() {
try {
return nonBlockingStream.toString();
} catch (Exception e) {
return "error occured by performing toString: " + DataConverter.toString(e);
}
}
/**
* the handler caller
*
*/
final class HandlerCaller implements Runnable {
private boolean forceCall = false;
private void setForceCall(boolean forceCall) {
this.forceCall = forceCall;
}
/**
* {@inheritDoc}
*/
public void run() {
performCallLoop(forceCall);
}
}
private final class NonBlockingStream extends AbstractNonBlockingStream {
private void append(ByteBuffer buffer) {
appendDataToReadBuffer(new ByteBuffer[] { buffer }, buffer.remaining());
}
private void append(ByteBuffer[] buffers, IWriteCompletionHandler completionHandler) {
if (completionHandler != null) {
isCompletionSupportActivated.set(true);
writeCompletionManager.registerCompletionHandler(completionHandler, executor, buffers);
}
int size = 0;
for (ByteBuffer byteBuffer : buffers) {
size += byteBuffer.remaining();
}
appendDataToReadBuffer(buffers, size);
}
int getSize() {
return getReadQueueSize();
}
ByteBuffer[] copyContent() {
return super.copyReadQueue();
}
@Override
protected ByteBuffer[] onRead(ByteBuffer[] readBufs) throws IOException {
if ((NonBlockingBodyDataSource.this.getSize() == 0) && (NonBlockingBodyDataSource.this.isComplete.get())) {
try {
NonBlockingBodyDataSource.this.close();
} catch (IOException ioe) {
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("error occured by closing body data source " + ioe.toString());
}
}
}
return readBufs;
}
@Override
public void close() throws IOException {
super.close();
if (isOpen.get()) {
terminateWatchDog();
}
isOpen.set(false);
}
@Override
protected boolean isDataWriteable() {
return isOpen.get();
}
@Override
protected boolean isMoreInputDataExpected() {
return NonBlockingBodyDataSource.this.isMoreInputDataExpected();
}
public boolean isOpen() {
return NonBlockingBodyDataSource.this.isOpen();
}
@Override
protected int getWriteTransferChunkeSize() {
if (httpConnection != null) {
try {
return (Integer) httpConnection.getOption(IConnection.SO_SNDBUF);
} catch (IOException ignore) { }
}
return super.getWriteTransferChunkeSize();
}
@Override
public String toString() {
return printReadBuffer(NonBlockingBodyDataSource.this.getEncoding());
}
}
private static final class TimeoutWatchDogTask extends TimerTask {
private WeakReference<NonBlockingBodyDataSource> dataSourceRef = null;
public TimeoutWatchDogTask(NonBlockingBodyDataSource dataSource) {
dataSourceRef = new WeakReference<NonBlockingBodyDataSource>(dataSource);
}
@Override
public void run() {
try {
NonBlockingBodyDataSource dataSource = dataSourceRef.get();
if (dataSource == null) {
this.cancel();
} else {
dataSource.checkTimeouts();
}
} catch (Exception e) {
// eat and log exception
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("error occured by checking timeouts " + e.toString());
}
}
}
}
private static class DefaultMultimodeExecutor implements IMultimodeExecutor {
private static final Executor defaultExecutor = Executors.newCachedThreadPool();
public void processMultithreaded(Runnable task) {
defaultExecutor.execute(task);
}
public void processNonthreaded(Runnable task) {
task.run();
}
}
/**
* exception handler
* @author grro
*
*/
static interface IExceptionHandler {
/**
* call back
*
* @param ioe the exception
*/
void onException(IOException ioe);
}
/**
* disconnect handler
*
* @author grro
*/
static interface IBodyDataSourceDisconnectHandler {
/**
* call back
*/
void onDisconnect();
}
}