/*
* 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.Flushable;
import java.io.IOException;
import java.nio.BufferOverflowException;
import java.nio.ByteBuffer;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.FileChannel;
import java.nio.channels.GatheringByteChannel;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.WritableByteChannel;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.xlightweb.AbstractHttpConnection.IMultimodeExecutor;
import org.xsocket.Execution;
import org.xsocket.IDataSink;
import org.xsocket.IDestroyable;
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 receiving the body data.
*
* @author grro@xlightweb.org
*
*/
public final class BodyDataSink implements IDataSink, IDestroyable, Flushable, Closeable, WritableByteChannel, GatheringByteChannel {
private static final Logger LOG = Logger.getLogger(BodyDataSink.class.getName());
private final NonBlockingStream nonBlockingStream = new NonBlockingStream();
private final ArrayList<IBodyCloseListener> closeListeners = new ArrayList<IBodyCloseListener>();
private final IMultimodeExecutor executor;
private final AbstractHttpConnection httpConnection;
private final String connectionId;
private IMessageWriter messageWriter;
private AtomicBoolean isOpen = new AtomicBoolean(true);
// write completion handler support
private final WriteCompletionManager writeCompletionManager;
private final Object writeCompletionGuard = new Object();
/**
* constructor
*
* @param httpConnection the underlying http connection
* @param executor the executor
* @param bodySerializer the body serializer
* @param characterEncoding the character encoding
* @throws IOException if an exception occurs
*/
BodyDataSink(AbstractHttpConnection httpConnection, IMultimodeExecutor executor, IMessageWriter bodySerializer, String characterEncoding) throws IOException {
this.httpConnection = httpConnection;
this.executor = executor;
this.messageWriter = bodySerializer;
if (httpConnection != null) {
this.httpConnection.setBodyDataSink(this);
connectionId = httpConnection.getId();
} else {
connectionId = "<unset>";
}
writeCompletionManager = new WriteCompletionManager(connectionId);
setFlushmode(FlushMode.SYNC);
setEncoding(characterEncoding);
}
/**
* return the id
* @return the id
*/
String getId() {
return connectionId;
}
/**
* returns the body writer
* @return the body writer
*/
IMessageWriter getMessageWriter() {
return messageWriter;
}
/**
* sets the body writer
* @param bodyWriter the body writer
*/
void setMessageWriter(IMessageWriter bodyWriter) {
this.messageWriter = bodyWriter;
}
/**
* adds a close listener
* @param closeListener the close listener
*/
void addCloseListener(IBodyCloseListener closeListener) {
synchronized (closeListeners) {
closeListeners.add(closeListener);
}
}
boolean isNetworkEndpoint() {
if (messageWriter == null) {
return false;
} else {
return messageWriter.isNetworkEndpoint();
}
}
/**
* returns the size of pending write data
*
* @return the size of pending write data
*/
int getPendingWriteDataSize() {
if (messageWriter == null) {
return 0;
} else {
return messageWriter.getPendingWriteDataSize();
}
}
/**
* remove a close listener
*
* @param closeListener a close listener
* @return true, if the listener is removed
*/
boolean removeCloseListener(IBodyCloseListener closeListener) {
synchronized (closeListeners) {
return closeListeners.remove(closeListener);
}
}
/**
* {@inheritDoc}
*/
public void flush() throws IOException {
nonBlockingStream.flush();
}
/**
* {@inheritDoc}
*/
public void close() throws IOException {
isOpen.set(false);
if (httpConnection != null) {
httpConnection.removeBodyDataSink(this);
}
try {
flush();
} catch (IOException ioe) {
throw ioe;
} catch (Exception e) {
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("[" + connectionId + "] error occured by flushing BodyDataSink " + e.toString());
}
throw new IOException(e.toString());
} finally {
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("[" + connectionId + "] closing body serializer " + messageWriter);
}
messageWriter.close();
callCloseListener();
}
}
/**
* writes a buffer array
*
* @param buffers the buffer array
* @param writeCompletionHandler the completion handler
* @throws IOException if an exception occurs
*/
public void write(ByteBuffer[] buffers, IWriteCompletionHandler writeCompletionHandler) throws IOException {
nonBlockingStream.write(buffers, writeCompletionHandler);
}
/**
* {@inheritDoc}
*/
public long transferFrom(FileChannel source) throws IOException, BufferOverflowException {
return nonBlockingStream.transferFrom(source);
}
/**
* transfer the available data from the data source
*
* @param source the data source
* @return the transfered size
*
* @throws IOException if an exception occurs
*/
public long transferFrom(NonBlockingBodyDataSource source) throws IOException {
return source.transferTo(this);
}
/**
* transfer the available data from the data source
*
* @param source the data source
* @param length the length to transfer
* @return the transfered size
*
* @throws IOException if an exception occurs
*/
public long transferFrom(NonBlockingBodyDataSource source, int length) throws IOException {
return source.transferTo(this, length);
}
/**
* transfer all data from the data source
*
* @param source the data source
* @return the transfered size
*
* @throws IOException if an exception occurs
*/
public long transferFrom(BlockingBodyDataSource source) throws IOException {
return source.transferTo(this);
}
/**
* transfer all data from the data source
*
* @param source the data source
* @param length the length to transfer
* @return the transfered size
*
* @throws IOException if an exception occurs
*/
public long transferFrom(BlockingBodyDataSource source, int length) throws IOException {
return source.transferTo(this);
}
/**
* {@inheritDoc}
*/
public long transferFrom(ReadableByteChannel source) throws IOException, BufferOverflowException {
return nonBlockingStream.transferFrom(source);
}
/**
* {@inheritDoc}
*/
public long transferFrom(ReadableByteChannel source, int chunkSize) throws IOException, BufferOverflowException {
return nonBlockingStream.transferFrom(source, chunkSize);
}
/**
* {@inheritDoc}
*/
public int write(byte b) throws IOException, BufferOverflowException {
return nonBlockingStream.write(b);
}
/**
* {@inheritDoc}
*/
public int write(byte... bytes) throws IOException, BufferOverflowException {
return nonBlockingStream.write(bytes);
}
/**
* {@inheritDoc}
*/
public int write(byte[] bytes, int offset, int length) throws IOException, BufferOverflowException {
return nonBlockingStream.write(bytes, offset, length);
}
/**
* {@inheritDoc}
*/
public int write(ByteBuffer buffer) throws IOException, BufferOverflowException {
return nonBlockingStream.write(buffer);
}
/**
* {@inheritDoc}
*/
public long write(ByteBuffer[] buffers) throws IOException, BufferOverflowException {
return nonBlockingStream.write(buffers);
}
/**
* {@inheritDoc}
*/
public long write(ByteBuffer[] srcs, int offset, int length) throws IOException {
return nonBlockingStream.write(srcs, offset, length);
}
/**
* {@inheritDoc}
*/
public long write(List<ByteBuffer> buffers) throws IOException, BufferOverflowException {
return write(buffers.toArray(new ByteBuffer[buffers.size()]));
}
/**
* {@inheritDoc}
*/
public int write(int i) throws IOException, BufferOverflowException {
return nonBlockingStream.write(i);
}
/**
* {@inheritDoc}
*/
public int write(short s) throws IOException, BufferOverflowException {
return nonBlockingStream.write(s);
}
/**
* {@inheritDoc}
*/
public int write(long l) throws IOException, BufferOverflowException {
return nonBlockingStream.write(l);
}
/**
* {@inheritDoc}
*/
public int write(double d) throws IOException, BufferOverflowException {
return nonBlockingStream.write(d);
}
/**
* {@inheritDoc}
*/
public int write(String message) throws IOException, BufferOverflowException {
return nonBlockingStream.write(message);
}
/**
* sets the default encoding
*
* @param defaultEncoding the default encoding
*/
public final void setEncoding(String defaultEncoding) {
nonBlockingStream.setEncoding(defaultEncoding);
}
/**
* gets the default encoding
*
* @return the default encoding
*/
public final String getEncoding() {
return nonBlockingStream.getEncoding();
}
/**
* see {@link IConnection#setFlushmode(FlushMode)}
*/
public void setFlushmode(FlushMode flushMode) {
nonBlockingStream.setFlushmode(flushMode);
}
/**
* see {@link IConnection#getFlushmode()}
*/
public final FlushMode getFlushmode() {
return nonBlockingStream.getFlushmode();
}
/**
* set autoflush. If autoflush is activated, each write call
* will cause a flush. <br><br>
*
* @param autoflush true if autoflush should be activated
*/
public final void setAutoflush(boolean autoflush) {
nonBlockingStream.setAutoflush(autoflush);
}
/**
* get autoflush
*
* @return true, if autoflush is activated
*/
public final boolean isAutoflush() {
return nonBlockingStream.isAutoflush();
}
/**
* Marks the write position in the connection.
*/
public final void markWritePosition() {
nonBlockingStream.markWritePosition();
}
/**
* Resets to the marked write position. If the connection has been marked,
* then attempt to reposition it at the mark.
*
* @return true, if reset was successful
*/
public final boolean resetToWriteMark() {
return nonBlockingStream.resetToWriteMark();
}
/**
* remove the write mark
*/
public final void removeWriteMark() {
nonBlockingStream.removeWriteMark();
}
/**
* Attaches the given object to this connection
*
* @param obj The object to be attached; may be null
* @return The previously-attached object, if any, otherwise null
*/
public final void setAttachment(Object obj) {
nonBlockingStream.setAttachment(obj);
}
/**
* Retrieves the current attachment.
*
* @return The object currently attached to this key, or null if there is no attachment
*/
public final Object getAttachment() {
return nonBlockingStream.getAttachment();
}
/**
* returns true if the data sink is open
*
* @return true if the data sink is open
*/
public boolean isOpen() {
return isOpen.get();
}
/**
* call back if the underlying connection is closed
*/
void onUnderlyingHttpConnectionClosed() {
if (isOpen.get()) {
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("[" + connectionId + "] underlying connection is closed. closing data source");
}
isOpen.set(false);
messageWriter.destroy();
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 CloseListenerCaller(listener);
if (HttpUtils.isBodyCloseListenerMutlithreaded(listener)) {
executor.processMultithreaded(task);
} else {
executor.processNonthreaded(task);
}
}
private static final class CloseListenerCaller implements Runnable {
private IBodyCloseListener listener = null;
public CloseListenerCaller(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());
}
}
}
}
/**
* destroys this data sink
*/
public void destroy() {
isOpen.set(false);
messageWriter.destroy();
callCloseListener();
}
/**
* {@inheritDoc}
*/
public String toString() {
return nonBlockingStream.toString();
}
private final class NonBlockingStream extends AbstractNonBlockingStream {
private boolean isContentImmutable = true;
public boolean isOpen() {
return BodyDataSink.this.isOpen();
}
public int write(ByteBuffer buffer) throws IOException, BufferOverflowException {
if (super.getFlushmode() == FlushMode.SYNC) {
isContentImmutable = false;
}
return super.write(buffer);
}
public long write(ByteBuffer[] buffers) throws IOException, BufferOverflowException {
if (super.getFlushmode() == FlushMode.SYNC) {
isContentImmutable = false;
}
return super.write(buffers);
}
public long write(ByteBuffer[] srcs, int offset, int length) throws IOException {
if (super.getFlushmode() == FlushMode.SYNC) {
isContentImmutable = false;
}
return super.write(srcs, offset, length);
}
void write(ByteBuffer[] buffers, IWriteCompletionHandler writeCompletionHandler) throws IOException {
synchronized (writeCompletionGuard) {
boolean isSuppressReuseBuffer = isSuppressReuseBufferWarning();
setSuppressReuseBufferWarning(true);
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("[" + connectionId + "] {" + buffers.hashCode() + "} Writing with completion handler. Register handler " + writeCompletionHandler);
}
writeCompletionManager.registerCompletionHandler(writeCompletionHandler, executor, buffers);
write(buffers);
setSuppressReuseBufferWarning(isSuppressReuseBuffer);
}
}
@Override
protected boolean isDataWriteable() {
return isOpen();
}
@Override
protected boolean isMoreInputDataExpected() {
return false;
}
public void flush() throws IOException {
super.removeWriteMark();
boolean isImmutable = isContentImmutable;
ByteBuffer[] dataToWrite = drainWriteQueue();
isContentImmutable = true;
IWriteCompletionHandler completionHandler = null;
if (!writeCompletionManager.isPendingCompletionConfirmationsEmtpy()) {
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("[" + connectionId + "] write completion handlers are registered. perform write with WriteCompletionHandlerAdapter");
}
completionHandler = new WriteCompletionHandlerAdapter(dataToWrite);
}
messageWriter.flush(dataToWrite, isImmutable, super.getFlushmode(), completionHandler);
}
@Execution(Execution.NONTHREADED)
private final class WriteCompletionHandlerAdapter implements IWriteCompletionHandler {
private final ByteBuffer[] dataToWrite;
public WriteCompletionHandlerAdapter(ByteBuffer[] dataToWrite) {
this.dataToWrite = dataToWrite;
}
public void onWritten(int written) throws IOException {
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("[" + connectionId + "] {" + dataToWrite.hashCode() + "} data (size=" + written + " bytes) has been written. notify registered WriteCompletionHandler (if exist)");
}
onDataWritten(dataToWrite);
}
public void onException(IOException ioe) {
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("[" + connectionId + "] {" + dataToWrite.hashCode() + "} " + ioe.toString() + " error has been occured by writing data. notify registered WriteCompletionHandler (if exist)");
}
onWriteException(ioe, dataToWrite);
}
}
private void onDataWritten(ByteBuffer[] data) {
writeCompletionManager.onWritten(data, false);
}
private void onWriteException(IOException ioException, ByteBuffer[] data) {
destroy();
writeCompletionManager.onWriteException(ioException, data);
}
/**
* {@inheritDoc}
*/
@Override
protected void onWriteDataInserted() throws IOException, ClosedChannelException {
if (super.isAutoflush()) {
try {
flush();
} catch (IOException ioe) {
throw ioe;
} catch (Exception e) {
IOException ioe = new IOException(e.getMessage());
ioe.setStackTrace(e.getStackTrace());
throw ioe;
}
}
}
public String toString() {
return printWriteBuffer(super.getEncoding());
}
}
}