package org.jboss.pitbull.internal.nio.http; import org.jboss.pitbull.ContentOutputStream; import org.jboss.pitbull.OrderedHeaders; import org.jboss.pitbull.PitbullChannel; import java.io.IOException; import java.io.InterruptedIOException; import java.io.UnsupportedEncodingException; import java.nio.ByteBuffer; import java.util.concurrent.TimeUnit; /** * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a> * @version $Revision: 1 $ */ public class BufferedContentOutputStream extends ContentOutputStream { static final byte[] CRLF = new byte[]{HttpRequestDecoder.CR, HttpRequestDecoder.LF}; static byte[] LAST_CHUNK; { try { LAST_CHUNK = "0\r\n\r\n".getBytes("ASCII"); } catch (UnsupportedEncodingException e) { throw new RuntimeException(e); } } public interface ContentMessage { OrderedHeaders getHeaders(); byte[] getMessageBytes() throws IOException; void prepareEmptyBody(); } protected PitbullChannel channel; protected ByteBuffer buffer; protected boolean initialFlush = true; protected int size; protected ContentMessage contentMessage; protected boolean closed; protected long timeout; public BufferedContentOutputStream(PitbullChannel channel, int bufferSize) { this.channel = channel; this.size = bufferSize; setBufferSize(size); } private void flushBuffer() throws IOException { if (initialFlush) { if (buffer.position() > 0) { initialFlush = false; contentMessage.getHeaders().removeHeader("Content-Length"); contentMessage.getHeaders().addHeader("Transfer-Encoding", "chunked"); writeContentMessage(); buffer.flip(); writeChunk(buffer); buffer.clear(); } else // if there is nothing to write from buffer, just keep the state in initial { initialFlush = true; } } else { if (buffer.position() > 0) { buffer.flip(); writeChunk(buffer); buffer.clear(); } } } private void writeChunk(ByteBuffer buf) throws IOException { int len = buf.remaining(); String hex = Integer.toHexString(len); StringBuilder builder = new StringBuilder(hex.length() + 2); builder.append(hex).append("\r\n"); writeMessage(ByteBuffer.wrap(builder.toString().getBytes("ASCII"))); writeMessage(buf); writeMessage(ByteBuffer.wrap(CRLF)); } private void writeContentMessage() throws IOException { byte[] bytes = contentMessage.getMessageBytes(); ByteBuffer tmp = ByteBuffer.wrap(bytes); writeMessage(tmp); } public void reset() throws IllegalStateException { if (initialFlush == false) throw new IllegalStateException("Buffer was flushed"); buffer.clear(); } public int getBufferSize() { return size; } public void setBufferSize(int size) throws IllegalStateException { if (size <= 0) { throw new IllegalArgumentException("Cannot set a buffer size that is less than zero."); } if (buffer != null && buffer.position() > 0) throw new IllegalStateException("Buffer has been written to, cannot reset size"); this.size = size; buffer = ByteBuffer.allocate(size); } public synchronized void write(int b) throws IOException { checkClosed(); if (buffer.hasRemaining()) { flushBuffer(); } buffer.put((byte) b); } public synchronized void write(byte b[], int off, int len) throws IOException { checkClosed(); if (len >= size) { flushBuffer(); ByteBuffer tmp = ByteBuffer.wrap(b, off, len); writeMessage(tmp); return; } if (len > buffer.remaining()) { flushBuffer(); } buffer.put(b, off, len); } protected void writeMessage(ByteBuffer tmp) throws IOException { final long timeout = this.timeout; int total = 0; if (timeout == 0L) { try { total = channel.writeBlocking(tmp); } catch (InterruptedIOException e) { e.bytesTransferred = tmp.position(); throw e; } } else { try { total = channel.writeBlocking(tmp, timeout, TimeUnit.MILLISECONDS); } catch (InterruptedIOException e) { e.bytesTransferred = tmp.position(); throw e; } } } public synchronized void flush() throws IOException { checkClosed(); flushBuffer(); } public synchronized void close() throws IOException { if (closed) return; closed = true; if (initialFlush) { initialFlush = false; if (buffer.position() > 0) // set content-length header and full content { buffer.flip(); contentMessage.getHeaders().removeHeader("Content-Length"); contentMessage.getHeaders().addHeader("Content-Length", Integer.toString(buffer.remaining())); writeContentMessage(); writeMessage(buffer); buffer.clear(); } else // we have nothing in buffer { contentMessage.prepareEmptyBody(); writeContentMessage(); } } else { if (buffer.position() > 0) { buffer.flip(); writeChunk(buffer); buffer.clear(); } writeMessage(ByteBuffer.wrap(LAST_CHUNK)); } } protected void checkClosed() throws IOException { if (closed) throw new IOException("Stream is closed."); } @Override public boolean isCommitted() { return !initialFlush; } }