/* * ResponseBuffer.java February 2007 * * Copyright (C) 2001, Niall Gallagher <niallg@users.sf.net> * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or * implied. See the License for the specific language governing * permissions and limitations under the License. */ package org.simpleframework.http.core; import java.io.IOException; import java.io.OutputStream; import java.nio.ByteBuffer; import java.nio.channels.WritableByteChannel; import org.simpleframework.http.Response; import org.simpleframework.http.message.Entity; import org.simpleframework.transport.Channel; /** * The <code>ResponseBuffer</code> object is an output stream that can * buffer bytes written up to a given size. This is used if a buffer * is requested for the response output. Such a mechanism allows the * response to be written without committing the response. Also it * enables content that has been written to be reset, by simply * clearing the response buffer. If the response buffer overflows * then the response is committed. * * @author Niall Gallagher * * @see org.simpleframework.http.core.ResponseEncoder */ class ResponseBuffer extends OutputStream implements WritableByteChannel { /** * This is the transfer object used to transfer the response. */ private ResponseEncoder encoder; /** * This is the buffer used to accumulate the response bytes. */ private byte[] buffer; /** * This is used to determine if the accumulate was flushed. */ private boolean flushed; /** * This is used to determine if the accumulator was closed. */ private boolean closed; /** * This counts the number of bytes that have been accumulated. */ private int count; /** * Constructor for the <code>ResponseBuffer</code> object. This will * create a buffering output stream which will flush data to the * underlying transport provided with the entity. All I/O events * are reported to the monitor so the server can process other * requests within the pipeline when the current one is finished. * * @param observer this is used to notify of response completion * @param response this is the response header for this buffer * @param support this is used to determine the response semantics * @param entity this is used to acquire the underlying transport */ public ResponseBuffer(BodyObserver observer, Response response, Conversation support, Entity entity) { this(observer, response, support, entity.getChannel()); } /** * Constructor for the <code>ResponseBuffer</code> object. This will * create a buffering output stream which will flush data to the * underlying transport provided with the channel. All I/O events * are reported to the monitor so the server can process other * requests within the pipeline when the current one is finished. * * @param observer this is used to notify of response completion * @param response this is the response header for this buffer * @param support this is used to determine the response semantics * @param channel this is the channel used to write the data to */ public ResponseBuffer(BodyObserver observer, Response response, Conversation support, Channel channel) { this.encoder = new ResponseEncoder(observer, response, support, channel); this.buffer = new byte[] {}; } /** * This is used to determine if the accumulator is still open. If * the accumulator is still open then data can still be written to * it and this transmitted to the client. When the accumulator is * closed the data is committed and this can not be used. * * @return this returns true if the accumulator object is open */ public boolean isOpen() { return !closed; } /** * This is used to reset the buffer so that it can be written to * again. If the accumulator has already been flushed then the * stream can not be reset. Resetting the stream is typically * done if there is an error in writing the response and an error * message is generated to replaced the partial response. */ public void reset() throws IOException { if(flushed) { throw new IOException("Response has been flushed"); } count = 0; } /** * This is used to write the provided octet to the buffer. If the * buffer is full it will be flushed and the octet is appended to * the start of the buffer. If however the buffer is zero length * then this will write directly to the underlying transport. * * @param octet this is the octet that is to be written */ public void write(int octet) throws IOException { byte value = (byte) octet; if(closed) { throw new IOException("Response has been transferred"); } write(new byte[] { value }); } /** * This is used to write the provided array to the buffer. If the * buffer is full it will be flushed and the array is appended to * the start of the buffer. If however the buffer is zero length * then this will write directly to the underlying transport. * * @param array this is the array of bytes to send to the client * @param off this is the offset within the array to send from * @param size this is the number of bytes that are to be sent */ public void write(byte[] array, int off, int size) throws IOException { ByteBuffer buffer = ByteBuffer.wrap(array, off, size); if(size > 0) { write(buffer); } } /** * This is used to write the provided buffer to the buffer. If the * buffer is full it will be flushed and the buffer is appended to * the start of the buffer. If however the buffer is zero length * then this will write directly to the underlying transport. * * @param source this is the byte buffer to send to the client * * @return this returns the number of bytes that have been sent */ public int write(ByteBuffer source) throws IOException { int mark = source.position(); int size = source.limit(); if(mark > size) { throw new ResponseException("Buffer position greater than limit"); } return write(source, 0, size - mark); } /** * This is used to write the provided buffer to the buffer. If the * buffer is full it will be flushed and the buffer is appended to * the start of the buffer. If however the buffer is zero length * then this will write directly to the underlying transport. * * @param source this is the byte buffer to send to the client * @param off this is the offset within the array to send from * @param size this is the number of bytes that are to be sent * * @return this returns the number of bytes that have been sent */ public int write(ByteBuffer source, int off, int size) throws IOException { if(closed) { throw new IOException("Response has been transferred"); } int mark = source.position(); int limit = source.limit(); if(limit - mark < size) { // not enough data size = limit - mark; // reduce expectation } if(count + size > buffer.length) { flush(false); } if(size > buffer.length){ encoder.write(source); } else { source.get(buffer, count, size); count += size; } return size; } /** * This is used to expand the capacity of the internal buffer. If * there is already content that has been appended to the buffer * this will copy that data to the newly created buffer. This * will not decrease the size of the buffer if it is larger than * the requested capacity. * * @param capacity this is the capacity to expand the buffer to */ public void expand(int capacity) throws IOException { if(buffer.length < capacity) { int size = buffer.length * 2; int resize = Math.max(capacity, size); byte[] temp = new byte[resize]; System.arraycopy(buffer, 0, temp, 0, count); buffer = temp; } } /** * This is used to flush the contents of the buffer to the * underlying transport. Once the accumulator is flushed the HTTP * headers are written such that the semantics of the connection * match the protocol version and the existing response headers. */ public void flush() throws IOException { flush(true); } /** * This is used to flush the contents of the buffer to the * underlying transport. Once the accumulator is flushed the HTTP * headers are written such that the semantics of the connection * match the protocol version and the existing response headers. * * @param flush indicates whether the transport should be flushed */ private void flush(boolean flush) throws IOException { if(!flushed) { encoder.start(); } if(count > 0) { encoder.write(buffer, 0, count); } if(flush) { encoder.flush(); } flushed = true; count = 0; } /** * This will flush the buffer to the underlying transport and * close the stream. Once the accumulator is flushed the HTTP * headers are written such that the semantics of the connection * match the protocol version and the existing response headers. * Closing this stream does not mean the connection is closed. */ public void close() throws IOException { if(!closed) { commit(); } flushed = true; closed = true; } /** * This will close the underlying transfer object which will * notify the server kernel that the next request is read to be * processed. If the accumulator is unflushed then this will set * a Content-Length header such that it matches the number of * bytes that are buffered within the internal buffer. */ private void commit() throws IOException { if(!flushed) { encoder.start(count); } if(count > 0) { encoder.write(buffer, 0, count); } encoder.close(); } }