/*
* 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.lang.reflect.Method;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.WritableByteChannel;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.TimerTask;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.xlightweb.AbstractHttpConnection.IMultimodeExecutor;
import org.xlightweb.HttpUtils.CompletionHandlerInfo;
import org.xsocket.DataConverter;
import org.xsocket.Execution;
import org.xsocket.IDataSource;
import org.xsocket.MaxReadSizeExceededException;
import org.xsocket.connection.AbstractNonBlockingStream;
import org.xsocket.connection.ConnectionUtils;
import org.xsocket.connection.IWriteCompletionHandler;
import org.xsocket.connection.IConnection.FlushMode;
/**
* data source base implementation
*
* @author grro@xlightweb.org
*/
public abstract class NonBlockingBodyDataSource implements IDataSource, ReadableByteChannel, Closeable {
private static final Logger LOG = Logger.getLogger(NonBlockingBodyDataSource.class.getName());
@SuppressWarnings("unchecked")
private static final Map<Class, Integer> bodyDataHandlerExecutionModeCache = ConnectionUtils.newMapCache(25);
private final AtomicBoolean isDestroyed = new AtomicBoolean(false);
private final AtomicBoolean isComplete = new AtomicBoolean(false);
private final AtomicReference<IOException> exceptionRef = new AtomicReference<IOException>();
private final NonBlockingStream nonBlockingStream = new NonBlockingStream();
private final AtomicReference<ISink> sourceRef = new AtomicReference<ISink>(nonBlockingStream);
private final AtomicReference<IBodyDataHandler> bodyHandlerRef = new AtomicReference<IBodyDataHandler>(null);
private final ArrayList<IBodyCloseListener> closeListeners = new ArrayList<IBodyCloseListener>();
private final ArrayList<IBodyCompleteListener> completeListeners = new ArrayList<IBodyCompleteListener>();
private IBodyDestroyListener destroyListener = null;
private final IMultimodeExecutor executor;
// 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;
// statistics
private int dataReceived = 0;
NonBlockingBodyDataSource(String encoding, IMultimodeExecutor executor) {
nonBlockingStream.setEncoding(encoding);
this.executor = executor;
}
NonBlockingBodyDataSource(String encoding, IMultimodeExecutor executor, ByteBuffer[] data) throws IOException {
nonBlockingStream.setEncoding(encoding);
this.executor = executor;
if (data != null) {
append(data);
}
isComplete.set(true);
}
/**
* appends data
*
* @param isContentImmutable if the buffer is immutable
* @param data the data to append
*/
int append(ByteBuffer data) throws IOException {
int added = sourceRef.get().append(data);
dataReceived += added;
return added;
}
/**
* appends data
*
* @param isContentImmutable if the buffer is immutable
* @param data the data to append
*/
int append(ByteBuffer[] data) throws IOException {
int added = sourceRef.get().append(data);
dataReceived += added;
return added;
}
/**
* appends data
*
* @param isContentImmutable if the buffer is immutable
* @param data the data to append
*/
int append(ByteBuffer[] data, IWriteCompletionHandler completionHandler) throws IOException {
int added = 0;
ISink sink = sourceRef.get();
if (sink != null) {
if (completionHandler != null) {
added = sink.append(data, completionHandler, false);
} else {
added = sink.append(data);
}
dataReceived += added;
return added;
} else {
return 0;
}
}
final boolean isMoreInputDataExpected() {
// no, if data source is complete
if (isComplete.get()) {
return false;
}
// no, if data source is destroyed
if (isDestroyed.get()) {
return false;
}
// ... yes
return true;
}
void setComplete() throws IOException {
isComplete.set(true);
ISink sink = sourceRef.get();
sink.setComplete();
callCompleteListeners();
terminateWatchDog();
}
/**
* return true, if all body data has been received
*
* @return true, if all body data has been received
* @throws IOException if an error exists
*/
final boolean isCompleteReceived() throws IOException {
if (isComplete.get()) {
return true;
} else {
throwExceptionIfExist();
return false;
}
}
final boolean isComplete() {
return isComplete.get();
}
final int getSizeDataReceived() {
return dataReceived;
}
final boolean isDestroyed() {
return isDestroyed.get();
}
final void setException(IOException ioe) {
IOException oldException = exceptionRef.get();
if (oldException != null) {
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("[" + getId() + "] warning a exception alreday exits. ignore exception (old: " + oldException + ", new: " + ioe);
}
return;
}
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("[" + getId() + "] set exception " + ioe);
}
exceptionRef.set(ioe);
callBodyDataHandler(true);
destroy(ioe.toString());
}
/**
* destroys the data source
*/
public void destroy() {
destroy("user initiated");
}
/**
* destroys the data source
*/
void destroy(String reason) {
terminateWatchDog();
getExecutor().processNonthreaded(new DestroyTask(reason));
}
private final class DestroyTask implements Runnable {
private final String reason;
public DestroyTask(String reason) {
this.reason = reason;
}
public void run() {
performDestroy(reason);
}
}
private void performDestroy(String reason) {
if (!isDestroyed.getAndSet(true)) {
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("[" + getId() + "] destroying data source");
}
onDestroy(reason);
if (destroyListener != null) {
try {
destroyListener.onDestroyed();
} catch (IOException ioe) {
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("[" + getId() + "] error occured by calling destroy listener " + destroyListener + " " + ioe.toString());
}
}
}
sourceRef.get().destroy();
}
}
abstract void onDestroy(String reason);
/**
* returns the id
*
* @return the id
*/
abstract String getId();
/**
* adds a complete listener
*
* @param listener the complete listener
*/
public void addCompleteListener(IBodyCompleteListener listener) {
synchronized (completeListeners) {
completeListeners.add(listener);
}
if (isComplete.get()) {
callCompleteListener(listener);
}
}
@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) {
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("calling complete listener " + listener.getClass().getName() + "#" + listener.hashCode());
}
removeCompleteListener(listener);
callCompleteListener(listener);
}
}
}
private void callCompleteListener(final IBodyCompleteListener listener) {
Runnable task = new Runnable() {
public void run() {
try {
listener.onComplete();
} catch (IOException ioe) {
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("[" + getId() + "] Error occured by calling complete listener " + listener + " " + ioe.toString());
}
destroy(ioe.toString());
}
}
};
if (HttpUtils.isBodyCompleteListenerMutlithreaded(listener)) {
getExecutor().processMultithreaded(task);
} else {
getExecutor().processNonthreaded(task);
}
}
private boolean removeCompleteListener(IBodyCompleteListener listener) {
synchronized (completeListeners) {
return completeListeners.remove(listener);
}
}
final void setDestroyListener(IBodyDestroyListener destroyListener) {
assert (this.destroyListener == null);
this.destroyListener = destroyListener;
}
final void addCloseListener(IBodyCloseListener closeListener) {
synchronized (closeListeners) {
closeListeners.add(closeListener);
}
}
@SuppressWarnings("unchecked")
private void callCloseListener() {
ArrayList<IBodyCloseListener> closeListenersCopy = null;
synchronized (closeListeners) {
closeListenersCopy = (ArrayList<IBodyCloseListener>) closeListeners.clone();
}
for (IBodyCloseListener bodyCloseListener : closeListenersCopy) {
removeCloseListener(bodyCloseListener);
try {
bodyCloseListener.onClose();
} catch (IOException ioe) {
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("[" + getId() + "] error occured by calling " + bodyCloseListener + " " + ioe.toString());
}
}
}
}
/**
* remove a close listener
*
* @param closeListener a close listener
* @return true, if the listener is removed
*/
final boolean removeCloseListener(IBodyCloseListener closeListener) {
synchronized (closeListeners) {
return closeListeners.remove(closeListener);
}
}
/**
* set the body handler
*
* @param bodyDataHandler the body handler
*/
public final void setDataHandler(IBodyDataHandler bodyDataHandler) {
IBodyDataHandler bodyDataHandlerAdapter = newBodyDataHandlerAdapter(bodyDataHandler);
bodyHandlerRef.set(bodyDataHandlerAdapter);
callBodyDataHandler(true);
}
final void setSystemDataHandler(IBodyDataHandler bodyDataHandler) {
bodyHandlerRef.set(bodyDataHandler);
callBodyDataHandler(true);
}
final void callBodyDataHandler(boolean force) {
IBodyDataHandler bodyDataHandler = bodyHandlerRef.get();
if (bodyDataHandler != null) {
if ((getSize() != 0) || force) {
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("[" + getId() + "] calling body data handler " + bodyDataHandler.getClass().getName() + "#" + bodyDataHandler.hashCode());
}
bodyDataHandler.onData(this);
} else {
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("body data handler " + bodyDataHandler.getClass().getName() + "#" + bodyDataHandler.hashCode() +
" will not be called (size == 0)");
}
}
} else {
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("no body data handler assigned");
}
}
}
private int getSize() {
int size = nonBlockingStream.getSize();
if ((size == 0) && isComplete.get()) {
return -1;
}
return size;
}
private int getVersion() throws IOException {
return nonBlockingStream.getReadBufferVersion();
}
/**
* 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() {
IBodyDataHandler bodyDataHandler = bodyHandlerRef.get();
if (bodyDataHandler instanceof BodyDataHandlerAdapter) {
return ((BodyDataHandlerAdapter) bodyDataHandler).getDelegate();
} else {
return bodyDataHandler;
}
}
/**
* set the body data receive timeout
*
* @param bodyDataReceiveTimeoutMillis the timeout
*/
public final void setBodyDataReceiveTimeoutMillis(long bodyDataReceiveTimeoutMillis) {
if (bodyDataReceiveTimeoutMillis <= 0) {
if (!isComplete.get()) {
setException(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() || isDestroyed.get()) {
terminateWatchDog();
return;
}
long currentTimeMillis = System.currentTimeMillis();
if (currentTimeMillis > (lastTimeDataReceivedMillis + bodyDataReceiveTimeoutMillis) &&
currentTimeMillis > (creationTimeMillis + bodyDataReceiveTimeoutMillis)) {
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("[" + getId() + "] receive timeout reached. set exception");
}
if (!isComplete.get()) {
setException(new ReceiveTimeoutException());
}
destroy("receive timeout reached");
}
}
/**
* 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 final int available() throws ProtocolException, IOException {
// if an exception is pending -> throwing it
IOException ioe = exceptionRef.get();
if (ioe != null) {
// ClosedChannelException should not occur here. Anyway, handle it because available() should never throw a ClosedChannelException
if (!(ioe instanceof ClosedChannelException)) {
throw ioe;
}
}
// 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 {
// is destroyed?
if (isDestroyed.get()) {
close();
throw new ClosedChannelException();
} else {
return 0;
}
}
} else {
return available;
}
}
/**
* returns the current content size
*
* @return the current content size
*/
int size() {
int available = nonBlockingStream.getSize();
if ((available <= 0) && isComplete.get()) {
return -1;
} else {
return available;
}
}
/**
* closes the body data source
*
* @throws IOException if an exception occurs
*/
public final void close() throws IOException {
if (isOpen()) {
sourceRef.get().close();
try {
onClose();
callCloseListener(); // call close listener only in case of success closing
} catch (IOException ioe) {
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("[" + getId() + "] error occured by closing connection. destroying it " + ioe.toString());
}
setException(ioe);
}
}
}
abstract void onClose() throws IOException;
final void closeSilence() {
try {
close();
} catch (IOException ioe) {
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("[" + getId() + "] Error occured by closing connection " + ioe.toString());
}
}
}
/**
* returns true, if the body data source is open
*
* @return true, if the body data source is open
*/
public final boolean isOpen() {
return sourceRef.get().isOpen();
}
final void throwExceptionIfExist() throws IOException {
IOException ioe = exceptionRef.get();
if (ioe != null) {
throw ioe;
}
}
/**
* see {@link ReadableByteChannel#read(ByteBuffer)}
*/
public final int read(ByteBuffer buffer) throws 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);
}
}
}
boolean isForwarding() {
return (sourceRef.get() != nonBlockingStream);
}
void forwardTo(BodyDataSink bodyDataSink) throws IOException {
// reset the source
ForwardSink forwardSink = new ForwardSink(bodyDataSink);
sourceRef.set(forwardSink);
// after reseting the source, check if meanwhile the body is complete or data is received by previous sink
if (isComplete.get()) {
forwardSink.close();
} else {
forwardSink.appendThreaded(null, null);
}
}
private final class ForwardSink implements ISink {
private final BodyDataSink delegate;
private final AtomicInteger concurrentTasks = new AtomicInteger();
private final AtomicBoolean isClosed = new AtomicBoolean();
public ForwardSink(BodyDataSink delegate) throws IOException {
this.delegate = delegate;
delegate.setFlushmode(FlushMode.ASYNC);
delegate.setAutoflush(true);
}
final int appendThreaded(final ByteBuffer[] buffers, final IWriteCompletionHandler writeCompletionHandler) {
concurrentTasks.incrementAndGet();
int size = HttpUtils.computeRemaining(buffers);
Runnable task = new Runnable() {
public void run() {
try {
append(buffers, writeCompletionHandler, true);
} catch (ClosedChannelException ignore) {
} catch (IOException ioe) {
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("error occured by append null buffer " + ioe.toString());
}
} finally {
concurrentTasks.decrementAndGet();
}
}
};
getExecutor().processNonthreaded(task);
return size;
}
private void retrievePrevious() throws IOException {
int size = nonBlockingStream.getSize();
if (size > 0) {
ByteBuffer[] buffers = nonBlockingStream.readByteBufferByLength(size);
delegate.write(buffers);
} else if (size == -1) {
close();
}
}
public int append(ByteBuffer buffer) throws IOException {
if (concurrentTasks.get() > 0) {
return appendThreaded(new ByteBuffer[] { buffer }, null);
}
retrievePrevious();
delegate.getPendingWriteDataSize();
return delegate.write(buffer);
}
public int append(ByteBuffer[] buffers) throws IOException {
if (concurrentTasks.get() > 0) {
return appendThreaded(buffers, null);
}
retrievePrevious();
delegate.getPendingWriteDataSize();
return (int) delegate.write(buffers);
}
public int append(ByteBuffer[] buffers, IWriteCompletionHandler writeCompletionHandler, boolean force) throws IOException {
if ((force == false) && (concurrentTasks.get() > 0)) {
return appendThreaded(buffers, writeCompletionHandler);
}
retrievePrevious();
delegate.getPendingWriteDataSize();
int size = 0;
if (buffers != null) {
for(ByteBuffer buffer : buffers) {
size += buffer.remaining();
}
}
delegate.write(buffers, writeCompletionHandler);
return size;
}
public void setComplete() throws IOException {
if (concurrentTasks.get() > 0) {
concurrentTasks.incrementAndGet();
Runnable task = new Runnable() {
public void run() {
try {
performSetComplete();
} finally {
concurrentTasks.decrementAndGet();
}
}
};
getExecutor().processNonthreaded(task);
} else {
performSetComplete();
}
}
private void performSetComplete() {
try {
retrievePrevious();
delegate.close();
} catch (IOException ioe) {
LOG.fine("error occured by setting complete " + ioe.toString());
}
}
public void close() throws IOException {
if (!isClosed.getAndSet(true)) {
if (concurrentTasks.get() > 0) {
concurrentTasks.incrementAndGet();
Runnable task = new Runnable() {
public void run() {
try {
performClose();
} finally {
concurrentTasks.decrementAndGet();
}
}
};
getExecutor().processNonthreaded(task);
} else {
performClose();
}
}
}
private void performClose() {
try {
retrievePrevious();
delegate.close();
} catch (IOException ioe) {
LOG.fine("error occured by closing " + ioe.toString());
}
}
public boolean isOpen() {
return delegate.isOpen();
}
public void destroy() {
if (concurrentTasks.get() > 0) {
concurrentTasks.incrementAndGet();
Runnable task = new Runnable() {
public void run() {
try {
delegate.destroy();
} finally {
concurrentTasks.decrementAndGet();
}
}
};
getExecutor().processNonthreaded(task);
} else {
delegate.destroy();
}
}
}
/**
* 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 {
return transferTo((WritableByteChannel) dataSink, length);
}
/**
* 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 final long transferTo(WritableByteChannel target, int length) throws IOException, ClosedChannelException {
throwExceptionIfExist();
if (length > 0) {
long written = 0;
ByteBuffer[] buffers = readByteBufferByLength(length);
for (ByteBuffer buffer : buffers) {
while(buffer.hasRemaining()) {
written += target.write(buffer);
}
}
return written;
} else {
return 0;
}
}
/**
* suspend the (underlying connection of the) body data source
*
* @throws IOException if an error occurs
*/
abstract void suspend() throws IOException;
/**
* returns true, if the body data source is suspended
*
* @return true, if the body data source is suspended
*/
abstract boolean isSuspended();
/**
* resume the (underlying connection of the) body data source
*
* @throws IOException if an error occurs
*/
abstract void resume() throws IOException;
/**
* 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 final ByteBuffer[] readByteBufferByDelimiter(String delimiter, int maxLength) throws IOException,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 final ByteBuffer[] readByteBufferByLength(int length) throws IOException, BufferUnderflowException {
throwExceptionIfExist();
return nonBlockingStream.readByteBufferByLength(length);
}
/**
* returns the body encoding
*
* @return the body encoding
*/
String getEncoding() {
return nonBlockingStream.getEncoding();
}
private ByteBuffer readSingleByteBufferByLength(int length) throws IOException {
return DataConverter.toByteBuffer(readByteBufferByLength(length));
}
/**
* 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 final ByteBuffer[] readByteBufferByDelimiter(String delimiter) throws IOException {
return readByteBufferByDelimiter(delimiter, Integer.MAX_VALUE);
}
/**
* 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 final byte[] readBytesByDelimiter(String delimiter) throws IOException {
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 final byte[] readBytesByDelimiter(String delimiter, int maxLength) throws IOException, 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 final byte[] readBytesByLength(int length) throws IOException {
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 final String readStringByDelimiter(String delimiter) throws IOException, 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 final String readStringByDelimiter(String delimiter, int maxLength) throws IOException,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 final String readStringByLength(int length) throws IOException, BufferUnderflowException {
return DataConverter.toString(readByteBufferByLength(length), getEncoding());
}
/**
* 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 final double readDouble() throws IOException {
return readSingleByteBufferByLength(8).getDouble();
}
/**
* 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 final long readLong() throws IOException {
return readSingleByteBufferByLength(8).getLong();
}
/**
* 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 final int readInt() throws IOException {
return readSingleByteBufferByLength(4).getInt();
}
/**
* 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 final short readShort() throws IOException {
return DataConverter.toByteBuffer(readBytesByLength(2)).getShort();
}
/**
* 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 final byte readByte() throws IOException {
return DataConverter.toByteBuffer(readBytesByLength(1)).get();
}
/**
* Marks the read position in the connection. Subsequent calls to resetToReadMark() will attempt
* to reposition the connection to this point.
*
*/
public final 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 final boolean resetToReadMark() {
return nonBlockingStream.resetToReadMark();
}
/**
* remove the read mark
*/
public final 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 final 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 final int indexOf(String str, String encoding) throws IOException, MaxReadSizeExceededException {
throwExceptionIfExist();
return nonBlockingStream.indexOf(str, encoding);
}
/**
* 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();
}
/**
* returns body data receive timeout
*
* @return the body data receive timeout or <code>null</code>
*/
public long getBodyDataReceiveTimeoutMillis() {
return bodyDataReceiveTimeoutMillis;
}
/**
* copies the body content
*
* @return the copy
*/
final ByteBuffer[] copyContent() {
return nonBlockingStream.copyContent();
}
final IMultimodeExecutor getExecutor() {
return executor;
}
private BodyDataHandlerAdapter newBodyDataHandlerAdapter(IBodyDataHandler bodyDataHandler) {
if (bodyDataHandler == null) {
return null;
}
Integer executionMode = bodyDataHandlerExecutionModeCache.get(bodyDataHandler.getClass());
if (executionMode == null) {
executionMode = IBodyDataHandler.DEFAULT_EXECUTION_MODE;
Execution execution = bodyDataHandler.getClass().getAnnotation(Execution.class);
if (execution != null) {
executionMode = execution.value();
}
try {
Method meth = bodyDataHandler.getClass().getMethod("onData", new Class[] { NonBlockingBodyDataSource.class });
execution = meth.getAnnotation(Execution.class);
if (execution != null) {
executionMode = execution.value();
}
} catch (NoSuchMethodException nsme) {
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("shouldn't occure because body handler has to have such a method " + nsme.toString());
}
}
bodyDataHandlerExecutionModeCache.put(bodyDataHandler.getClass(), executionMode);
}
return new BodyDataHandlerAdapter(bodyDataHandler, executionMode);
}
/**
* {@inheritDoc}
*/
@Override
public String toString() {
try {
return nonBlockingStream.toString();
} catch (Exception e) {
return "[" + getId() + "] error occured by performing toString: " + DataConverter.toString(e);
}
}
String toDiagnosticString() {
StringBuilder sb = new StringBuilder();
sb.append("isDestroyed=" + isDestroyed.get());
sb.append(" isComplete=" + isComplete.get());
sb.append(" exception=" + exceptionRef.get());
sb.append(" nonblockingStream#isOpen=" + nonBlockingStream.isOpen());
sb.append(" nonblockingStream#isMoreInputDataExpected=" + nonBlockingStream.isMoreInputDataExpected());
sb.append(" nonblockingStream#Size=" + nonBlockingStream.getSize());
sb.append(" source=" + sourceRef.get());
return sb.toString();
}
private static interface ISink {
boolean isOpen();
void close() throws IOException;
void destroy();
int append(ByteBuffer data) throws IOException;
int append(ByteBuffer[] data) throws IOException;
int append(ByteBuffer[] data, IWriteCompletionHandler completionHandler, boolean force) throws IOException;
void setComplete() throws IOException;
}
private final class NonBlockingStream extends AbstractNonBlockingStream implements ISink {
public void destroy() {
drainReadQueue();
callBodyDataHandler(true);
}
public void setComplete() {
callBodyDataHandler(true);
}
@Override
protected boolean isDataWriteable() {
return false;
}
int getSize() {
return getReadQueueSize();
}
@Override
protected boolean isMoreInputDataExpected() {
return NonBlockingBodyDataSource.this.isMoreInputDataExpected();
}
public boolean isOpen() {
try {
return (super.available() != -1);
} catch (IOException ioe) { // BUG in xSocket: AbstractNonBlockingStream isOpen() method declares an exception which will never been thrown
return false;
}
}
public int append(ByteBuffer buffer) {
int size = 0;
if (buffer != null) {
size = buffer.remaining();
appendDataToReadBuffer(new ByteBuffer[] { buffer }, size);
}
callBodyDataHandler(false);
return size;
}
public int append(ByteBuffer[] buffer) {
int size = 0;
if (buffer != null) {
for (ByteBuffer byteBuffer : buffer) {
size += byteBuffer.remaining();
}
appendDataToReadBuffer(buffer, size);
}
callBodyDataHandler(false);
return size;
}
public int append(ByteBuffer[] buffers, IWriteCompletionHandler completionHandler, boolean force) {
int size = 0;
if (buffers != null) {
size += append(buffers);
}
if (completionHandler != null) {
new WriteCompletionHolder(completionHandler, executor, buffers).callOnWritten();
}
callBodyDataHandler(true);
return size;
}
ByteBuffer[] copyContent() {
return super.copyReadQueue();
}
@Override
public String toString() {
return printReadBuffer(NonBlockingBodyDataSource.this.getEncoding());
}
}
private final class BodyDataHandlerAdapter implements IBodyDataHandler {
private final IBodyDataHandler delegate;
private final int executionMode;
BodyDataHandlerAdapter(IBodyDataHandler bodyDataHandler, int executionMode) {
assert (bodyDataHandler != null);
this.delegate = bodyDataHandler;
this.executionMode = executionMode;
}
IBodyDataHandler getDelegate() {
return delegate;
}
public boolean onData(final NonBlockingBodyDataSource bodyDataSource) throws BufferUnderflowException {
Runnable task = new Runnable() {
public void run() {
performOnData(bodyDataSource);
}
};
if (executionMode == Execution.MULTITHREADED) {
bodyDataSource.getExecutor().processMultithreaded(task);
} else {
bodyDataSource.getExecutor().processNonthreaded(task);
}
return true;
}
private boolean performOnData(NonBlockingBodyDataSource bodyDataSource) {
try {
// get pre version
int preVersion = getVersion();
// perform call
boolean success = delegate.onData(bodyDataSource);
// get post version
int postVersion = getVersion();
// should call be repeated?
if (success && (preVersion != postVersion) && ((getSize() != 0))) {
// yes, initiate it
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("[" + getId() + "] re-initiate calling body data handler (read queue size=" + getSize());
}
callBodyDataHandler(false);
}
} catch (BufferUnderflowException bue) {
// swallow it
} catch (Exception e) {
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("[" + bodyDataSource.getId() + "] error occured by calling onData of " + delegate.getClass().getName() + "#" + delegate.hashCode() + " " + e.toString() + " destroying body data source");
}
bodyDataSource.destroy(e.toString());
}
return true;
}
}
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());
}
}
}
}
public static final class WriteCompletionHolder implements Runnable {
private final IWriteCompletionHandler handler;
private final CompletionHandlerInfo handlerInfo;
private final IMultimodeExecutor executor;
private final int size;
public WriteCompletionHolder(IWriteCompletionHandler handler, IMultimodeExecutor executor, ByteBuffer[] bufs) {
this.handler = handler;
this.executor = executor;
this.handlerInfo = HttpUtils.getCompletionHandlerInfo(handler);
this.size = computeSize(bufs);
}
private static int computeSize(ByteBuffer[] bufs) {
if (bufs == null) {
return 0;
}
int i = 0;
for (ByteBuffer byteBuffer : bufs) {
i += byteBuffer.remaining();
}
return i;
}
void performOnWritten(boolean isForceMultithreaded) {
executor.processMultithreaded(this);
}
public void run() {
callOnWritten();
}
private void callOnWritten() {
try {
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("data (size=" + size + " bytes) has been written. calling " + handler.getClass().getSimpleName() + "#" + handler.hashCode() + " onWritten method");
}
handler.onWritten(size);
} catch (IOException ioe) {
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("error occured by calling onWritten " + ioe.toString() + " closing connection");
}
performOnException(ioe);
}
}
void performOnException(final IOException ioe) {
if (handlerInfo.isOnExceptionMutlithreaded()) {
Runnable task = new Runnable() {
public void run() {
callOnException(ioe);
}
};
executor.processMultithreaded(task);
} else {
Runnable task = new Runnable() {
public void run() {
callOnException(ioe);
}
};
executor.processNonthreaded(task);
}
}
private void callOnException(IOException ioe) {
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("calling " + handler.getClass().getSimpleName() + "#" + handler.hashCode() + " onException with " + ioe.toString());
}
handler.onException(ioe);
}
}
}