/*
* Copyright (c) xlightweb.org, 2008 - 2010. 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.IOException;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.zip.GZIPOutputStream;
import org.xsocket.DataConverter;
import org.xsocket.connection.IWriteCompletionHandler;
import org.xsocket.connection.IConnection.FlushMode;
/**
* I/O resource capable of receiving the body data.
*
* @author grro@xlightweb.org
*
*/
abstract class AbstractNetworkBodyDataSink extends BodyDataSinkImplBase {
private static final Logger LOG = Logger.getLogger(AbstractNetworkBodyDataSink.class.getName());
private final AbstractHttpConnection httpConnection;
private CompressingOutputStream cos = null;
/**
* constructor
*
* @param header the header
* @param httpConnection the http connection
*/
public AbstractNetworkBodyDataSink(IHttpMessageHeader header, AbstractHttpConnection httpConnection) throws IOException {
super(header, httpConnection.getExecutor());
this.httpConnection = httpConnection;
this.httpConnection.setNetworkBodyDataSink(this);
httpConnection.getUnderlyingTcpConnection().setFlushmode(FlushMode.ASYNC);
httpConnection.getUnderlyingTcpConnection().setAutoflush(false);
}
void onDisconnect() {
httpConnection.setNetworkBodyDataSink(null);
// data sink should not be open
if (isOpen()) {
destroy();
}
}
@Override
final int onWriteData(ByteBuffer[] dataToWrite, IWriteCompletionHandler completionHandler) throws IOException {
if (cos == null) {
return onWriteNetworkData(dataToWrite, completionHandler);
} else {
return cos.write(dataToWrite, completionHandler);
}
}
abstract int onWriteNetworkData(ByteBuffer[] dataToWrite, IWriteCompletionHandler completionHandler) throws IOException;
/**
* {@inheritDoc}
*/
@Override
protected boolean isNetworkendpoint() {
return true;
}
protected final AbstractHttpConnection getHttpConnection() {
return httpConnection;
}
@Override
void setAutocompressThreshold(int autocompressThreshold) {
if ((cos == null) && (autocompressThreshold != Integer.MAX_VALUE)) {
if ((((HttpMessageHeader) getHeader()).getAttribute(AbstractNetworkBodyDataSource.AUTOUNCOPMRESSED_ATTR_KEY) != null) &&
((Boolean) ((HttpMessageHeader)getHeader()).getAttribute(AbstractNetworkBodyDataSource.AUTOUNCOPMRESSED_ATTR_KEY) == true)) {
getHeader().removeHeader(AbstractNetworkBodyDataSource.UNCOMPRESSED_KEY);
autocompressThreshold = 0;
}
cos = new CompressingOutputStream(autocompressThreshold);
if (autocompressThreshold == 0) {
getHeader().setHeader("Content-Encoding", "gzip");
}
}
}
/**
* returns the http connection
* @return the http connection
*/
final AbstractHttpConnection getConnection() {
return httpConnection;
}
public final String getId() {
return httpConnection.getId();
}
@Override
final void onClose() throws IOException {
if (cos != null) {
cos.close();
}
httpConnection.removeNetworkBodyDataSink(this);
try {
performClose();
httpConnection.onMessageWritten();
} catch(IOException ioe) {
if (isIgnoreWriteError()) {
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("[" + getId() + "] error occured by closing connection. ignoring it (isIgnoreWriteError=true) " + ioe.toString());
}
} else {
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("[" + getId() + "] error occured by closing connection. destroying it " + ioe.toString());
}
destroy();
throw ioe;
}
}
}
abstract void performClose() throws IOException;
/**
* {@inheritDoc}
*/
@Override
void onDestroy(String reason) {
httpConnection.removeNetworkBodyDataSink(this);
if (isOpen()) {
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("[" + getId() + "] destroying connection");
}
try {
performDestroy();
} catch (IOException ioe) {
if (LOG.isLoggable(Level.FINE)) {
LOG.fine(ioe.toString());
}
}
}
httpConnection.destroy(reason);
}
abstract void performDestroy() throws IOException;
/**
* {@inheritDoc}
*/
public int getPendingWriteDataSize() {
return httpConnection.getUnderlyingTcpConnection().getPendingWriteDataSize();
}
/**
*
* {@inheritDoc}
*/
@Override
public String toString() {
if (isOpen()) {
return getClass().getName() + "#" + hashCode();
} else {
return getClass().getName() + "#" + hashCode() + " closed";
}
}
private final class CompressingOutputStream {
private int compressThresholdByte;
private boolean isCompressActivated = true;
private boolean isCompressing = false;
private ByteBuffer[] buffer = new ByteBuffer[0];
private GZIPOutputStream gos = null;
private BufferOutputStream bos = null;
private int sizePlainData = 0;
private int sizeNetworkData = 0;
public CompressingOutputStream(int compressThresholdByte) {
this.compressThresholdByte = compressThresholdByte;
}
public int write(ByteBuffer[] dataToWrite, IWriteCompletionHandler completionHandler) throws IOException {
int length = 0;
if (dataToWrite != null) {
length = HttpUtils.computeRemaining(dataToWrite);
sizePlainData += length;
}
if (!isCompressActivated || (length == 0)) {
// deactivate compressing
if (!isCompressing && isCompressActivated && (compressThresholdByte != 0)) {
isCompressActivated = false;
}
sizeNetworkData += length;
return onWriteNetworkData(dataToWrite, completionHandler);
} else {
if (completionHandler != null) {
completionHandler = new CompletionHandlerAdapter(completionHandler, length);
}
if (isCompressing) {
if (gos == null) {
if (length > 1024) {
bos = new BufferOutputStream(length);
} else {
bos = new BufferOutputStream(1024);
}
gos = new GZIPOutputStream(bos);
}
byte[] plain = DataConverter.toBytes(dataToWrite);
gos.write(plain);
gos.flush();
ByteBuffer buffer = bos.drainBuffer();
writeCompressedToNetwork(buffer, completionHandler);
} else {
if ((HttpUtils.computeRemaining(buffer) + HttpUtils.computeRemaining(dataToWrite)) > compressThresholdByte) {
isCompressing = true;
getHeader().setHeader("Content-Encoding", "gzip");
ByteBuffer[] buf = buffer;
buffer = null;
return write(HttpUtils.merge(buf, dataToWrite), completionHandler);
} else {
buffer = HttpUtils.merge(HttpUtils.copy(buffer), dataToWrite);
if (completionHandler != null) {
completionHandler.onWritten(length);
}
}
}
return length;
}
}
private int writeCompressedToNetwork(ByteBuffer buffer, IWriteCompletionHandler completionHandler) throws IOException {
if (buffer == null) {
if (completionHandler != null) {
completionHandler.onWritten(0);
}
return 0;
} else {
sizeNetworkData += buffer.remaining();
if (completionHandler == null) {
buffer = HttpUtils.copy(buffer);
}
return onWriteNetworkData(new ByteBuffer[] { buffer }, completionHandler);
}
}
public void close() throws IOException {
if (isCompressing) {
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("closing gzip stream");
}
gos.close();
ByteBuffer buffer = bos.drainBuffer();
writeCompressedToNetwork(buffer, null);
} else {
ByteBuffer[] buf = buffer;
buffer = null;
onWriteNetworkData(buf, null);
}
if (LOG.isLoggable(Level.FINE)) {
int ratio = 100 - ((sizeNetworkData * 100) / sizePlainData);
LOG.fine(sizeNetworkData + " data written to network (plain size " + sizePlainData + ", compression=" + ratio + "%)");
}
}
}
private static final class CompletionHandlerAdapter implements IWriteCompletionHandler {
private final int size;
private final IWriteCompletionHandler completionHandler;
public CompletionHandlerAdapter(IWriteCompletionHandler completionHandler, int size) {
this.completionHandler = completionHandler;
this.size = size;
}
public void onWritten(int written) throws IOException {
try {
completionHandler.onWritten(size);
} catch (IOException ioe) {
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("error occured by calling onWritten on " + completionHandler + " " + ioe.toString());
}
}
}
public void onException(IOException ioe) {
completionHandler.onException(ioe);
}
}
private static final class BufferOutputStream extends OutputStream {
private final int bufferSize;
private boolean isSafeUse = false;
private byte[] buffer;
private int pos;
public BufferOutputStream(int bufferSize) {
this.bufferSize = bufferSize;
buffer = new byte[bufferSize];
pos = 0;
}
public ByteBuffer drainBuffer() {
if (pos > 0) {
ByteBuffer buf = DataConverter.toByteBuffer(buffer, 0, pos);
pos = 0;
if (!isSafeUse) {
buffer = new byte[bufferSize];
}
return buf;
} else {
return null;
}
}
@Override
public void write(int b) throws IOException {
write(new byte[] { (byte) b }); // should never be called
}
@Override
public void write(byte[] b, int off, int len) throws IOException {
append(b, off, len);
}
@Override
public void write(byte[] b) throws IOException {
append(b);
}
private void append(byte[] b) {
append(b, 0, b.length);
}
private void append(byte[] b, int off, int len) {
int remainingSize = buffer.length - pos;
if (remainingSize < len) {
incBuffer(len - remainingSize);
}
System.arraycopy(b, off, buffer, pos, len);
pos += len;
}
private void incBuffer(int incSize) {
byte[] newBuffer = new byte[buffer.length + incSize];
System.arraycopy(buffer, 0, newBuffer, 0, pos);
buffer = newBuffer;
}
}
}