| // ======================================================================== |
| // $Id: HttpOutputStream.java,v 1.28 2006/10/08 14:13:05 gregwilkins Exp $ |
| // Copyright 199-2004 Mort Bay Consulting Pty. Ltd. |
| // ------------------------------------------------------------------------ |
| // 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.openqa.jetty.http; |
| |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.OutputStream; |
| import java.io.OutputStreamWriter; |
| import java.io.Writer; |
| import java.util.ArrayList; |
| |
| import org.apache.commons.logging.Log; |
| import org.openqa.jetty.log.LogFactory; |
| import org.openqa.jetty.util.ByteArrayPool; |
| import org.openqa.jetty.util.IO; |
| import org.openqa.jetty.util.LogSupport; |
| import org.openqa.jetty.util.OutputObserver; |
| import org.openqa.jetty.util.StringUtil; |
| |
| |
| /* ---------------------------------------------------------------- */ |
| /** HTTP Http OutputStream. |
| * Acts as a BufferedOutputStream until setChunking() is called. |
| * Once chunking is enabled, the raw stream is chunk encoded as per RFC2616. |
| * |
| * Implements the following HTTP and Servlet features: <UL> |
| * <LI>Filters for content and transfer encodings. |
| * <LI>Allows output to be reset if not committed (buffer never flushed). |
| * <LI>Notification of significant output events for filter triggering, |
| * header flushing, etc. |
| * </UL> |
| * |
| * This class is not synchronized and should be synchronized |
| * explicitly if an instance is used by multiple threads. |
| * |
| * @version $Id: HttpOutputStream.java,v 1.28 2006/10/08 14:13:05 gregwilkins Exp $ |
| * @author Greg Wilkins |
| */ |
| public class HttpOutputStream extends OutputStream |
| implements OutputObserver, HttpMessage.HeaderWriter |
| { |
| private static Log log = LogFactory.getLog(HttpOutputStream.class); |
| |
| /* ------------------------------------------------------------ */ |
| final static int __BUFFER_SIZE=4096; |
| final static int __FIRST_RESERVE=512; |
| |
| public final static Class[] __filterArg = {java.io.OutputStream.class}; |
| |
| /* ------------------------------------------------------------ */ |
| private OutputStream _out; |
| private OutputStream _realOut; |
| private BufferedOutputStream _bufferedOut; |
| private boolean _written; |
| private ArrayList _observers; |
| private int _bufferSize; |
| private int _headerReserve; |
| private HttpWriter _iso8859writer; |
| private HttpWriter _utf8writer; |
| private HttpWriter _asciiwriter; |
| private boolean _nulled; |
| private boolean _closing=false; |
| private int _contentLength=-1; |
| private int _bytes; |
| private boolean _disableFlush; |
| |
| /* ------------------------------------------------------------ */ |
| /** Constructor. |
| * @param outputStream The outputStream to buffer or chunk to. |
| */ |
| public HttpOutputStream(OutputStream outputStream) |
| { |
| this (outputStream,__BUFFER_SIZE,__FIRST_RESERVE); |
| } |
| |
| /* ------------------------------------------------------------ */ |
| /** Constructor. |
| * @param outputStream The outputStream to buffer or chunk to. |
| */ |
| public HttpOutputStream(OutputStream outputStream, int bufferSize) |
| { |
| this (outputStream,bufferSize,__FIRST_RESERVE); |
| } |
| |
| /* ------------------------------------------------------------ */ |
| /** Constructor. |
| * @param outputStream The outputStream to buffer or chunk to. |
| */ |
| public HttpOutputStream(OutputStream outputStream, |
| int bufferSize, |
| int headerReserve) |
| { |
| _written=false; |
| _bufferSize=bufferSize; |
| _headerReserve=headerReserve; |
| _realOut=outputStream; |
| _out=_realOut; |
| } |
| |
| /* ------------------------------------------------------------ */ |
| public void setContentLength(int length) |
| { |
| if (length>=0 && length<_bytes) |
| throw new IllegalStateException(); |
| _contentLength=length; |
| } |
| |
| /* ------------------------------------------------------------ */ |
| public void setBufferedOutputStream(BufferedOutputStream bos) |
| { |
| _bufferedOut=bos; |
| _bufferedOut.setCommitObserver(this); |
| if (_out!=null && _out!=_realOut) |
| _out=_bufferedOut; |
| } |
| |
| /* ------------------------------------------------------------ */ |
| /** Get the backing output stream. |
| * A stream without filters or chunking is returned. |
| * @return Raw OutputStream. |
| */ |
| public OutputStream getOutputStream() |
| { |
| return _realOut; |
| } |
| |
| /* ------------------------------------------------------------ */ |
| /** Get the buffered output stream. |
| */ |
| public OutputStream getBufferedOutputStream() |
| { |
| return _out; |
| } |
| |
| /* ------------------------------------------------------------ */ |
| /** Has any data been written to the stream. |
| * @return True if write has been called. |
| */ |
| public boolean isWritten() |
| { |
| return _written; |
| } |
| |
| /* ------------------------------------------------------------ */ |
| /** Get the output buffer capacity. |
| * @return Buffer capacity in bytes. |
| */ |
| public int getBufferSize() |
| { |
| return _bufferSize; |
| } |
| |
| /* ------------------------------------------------------------ */ |
| /** Set the output buffer size. |
| * Note that this is the minimal buffer size and that installed |
| * filters may perform their own buffering and are likely to change |
| * the size of the output. Also the pre and post reserve buffers may be |
| * allocated within the buffer for headers and chunking. |
| * @param size Minimum buffer size in bytes |
| * @exception IllegalStateException If output has been written. |
| */ |
| public void setBufferSize(int size) |
| throws IllegalStateException |
| { |
| if (size<=_bufferSize) |
| return; |
| |
| if (_bufferedOut!=null && _bufferedOut.size()>0) |
| throw new IllegalStateException("Not Reset"); |
| |
| try |
| { |
| _bufferSize=size; |
| if (_bufferedOut!=null) |
| { |
| boolean fixed=_bufferedOut.isFixed(); |
| _bufferedOut.setFixed(false); |
| _bufferedOut.ensureSize(size); |
| _bufferedOut.setFixed(fixed); |
| } |
| |
| } |
| catch (IOException e){log.warn(LogSupport.EXCEPTION,e);} |
| } |
| |
| /* ------------------------------------------------------------ */ |
| public int getBytesWritten() |
| { |
| return _bytes; |
| } |
| |
| /* ------------------------------------------------------------ */ |
| /** Reset Buffered output. |
| * If no data has been committed, the buffer output is discarded and |
| * the filters may be reinitialized. |
| * @exception IllegalStateException |
| */ |
| public void resetBuffer() |
| throws IllegalStateException |
| { |
| // Shutdown filters without observation |
| |
| if (_out!=null && _out!=_realOut) |
| { |
| ArrayList save_observers=_observers; |
| _observers=null; |
| _nulled=true; |
| try |
| { |
| // discard current buffer and set it to output |
| if (_bufferedOut!=null) |
| { |
| _bufferedOut.resetStream(); |
| if (_bufferedOut instanceof ChunkingOutputStream) |
| ((ChunkingOutputStream)_bufferedOut).setChunking(false); |
| } |
| } |
| catch(Exception e) |
| { |
| LogSupport.ignore(log,e); |
| } |
| finally |
| { |
| _observers=save_observers; |
| } |
| } |
| _contentLength=-1; |
| _nulled=false; |
| _bytes=0; |
| _written=false; |
| _out=_realOut; |
| try |
| { |
| notify(OutputObserver.__RESET_BUFFER); |
| } |
| catch(IOException e) |
| { |
| LogSupport.ignore(log,e); |
| } |
| } |
| |
| /* ------------------------------------------------------------ */ |
| /** Add an Output Observer. |
| * Output Observers get notified of significant events on the |
| * output stream. Observers are called in the reverse order they |
| * were added. |
| * They are removed when the stream is closed. |
| * @param observer The observer. |
| */ |
| public void addObserver(OutputObserver observer) |
| { |
| if (_observers==null) |
| _observers=new ArrayList(4); |
| _observers.add(observer); |
| _observers.add(null); |
| } |
| |
| /* ------------------------------------------------------------ */ |
| /** Add an Output Observer. |
| * Output Observers get notified of significant events on the |
| * output stream. Observers are called in the reverse order they |
| * were added. |
| * They are removed when the stream is closed. |
| * @param observer The observer. |
| * @param data Data to be passed wit notify calls. |
| */ |
| public void addObserver(OutputObserver observer, Object data) |
| { |
| if (_observers==null) |
| _observers=new ArrayList(4); |
| _observers.add(observer); |
| _observers.add(data); |
| } |
| |
| /* ------------------------------------------------------------ */ |
| /** Reset the observers. |
| */ |
| public void resetObservers() |
| { |
| _observers=null; |
| } |
| |
| /* ------------------------------------------------------------ */ |
| /** Null the output. |
| * All output written is discarded until the stream is reset. Used |
| * for HEAD requests. |
| */ |
| public void nullOutput() |
| throws IOException |
| { |
| _nulled=true; |
| } |
| |
| /* ------------------------------------------------------------ */ |
| /** is the output Nulled? |
| */ |
| public boolean isNullOutput() |
| throws IOException |
| { |
| return _nulled; |
| } |
| |
| /* ------------------------------------------------------------ */ |
| /** Set chunking mode. |
| */ |
| public void setChunking() |
| { |
| checkOutput(); |
| if (_bufferedOut instanceof ChunkingOutputStream) |
| ((ChunkingOutputStream)_bufferedOut).setChunking(true); |
| else |
| throw new IllegalStateException(_bufferedOut.getClass().toString()); |
| } |
| |
| /* ------------------------------------------------------------ */ |
| /** Get chunking mode |
| */ |
| public boolean isChunking() |
| { |
| return (_bufferedOut instanceof ChunkingOutputStream) && |
| ((ChunkingOutputStream)_bufferedOut).isChunking(); |
| } |
| |
| /* ------------------------------------------------------------ */ |
| /** Reset the stream. |
| * Turn disable all filters. |
| * @exception IllegalStateException The stream cannot be |
| * reset if chunking is enabled. |
| */ |
| public void resetStream() |
| throws IOException, IllegalStateException |
| { |
| if (isChunking()) |
| close(); |
| |
| _out=null; |
| _nulled=true; |
| if (_bufferedOut!=null) |
| { |
| _bufferedOut.resetStream(); |
| if (_bufferedOut instanceof ChunkingOutputStream) |
| ((ChunkingOutputStream)_bufferedOut).setChunking(false); |
| } |
| if (_iso8859writer!=null) |
| _iso8859writer.flush(); |
| if (_utf8writer!=null) |
| _utf8writer.flush(); |
| if (_asciiwriter!=null) |
| _asciiwriter.flush(); |
| |
| _bytes=0; |
| _written=false; |
| _out=_realOut; |
| _closing=false; |
| _contentLength=-1; |
| _nulled=false; |
| |
| if (_observers!=null) |
| _observers.clear(); |
| } |
| |
| /* ------------------------------------------------------------ */ |
| public void destroy() |
| { |
| if (_bufferedOut!=null) |
| _bufferedOut.destroy(); |
| _bufferedOut=null; |
| if (_iso8859writer!=null) |
| _iso8859writer.destroy(); |
| _iso8859writer=null; |
| if (_utf8writer!=null) |
| _utf8writer.destroy(); |
| _utf8writer=null; |
| if (_asciiwriter!=null) |
| _asciiwriter.destroy(); |
| _asciiwriter=null; |
| } |
| |
| |
| /* ------------------------------------------------------------ */ |
| public void writeHeader(HttpMessage httpMessage) |
| throws IOException |
| { |
| checkOutput(); |
| _bufferedOut.writeHeader(httpMessage); |
| } |
| |
| /* ------------------------------------------------------------ */ |
| public void write(int b) throws IOException |
| { |
| prepareOutput(1); |
| if (!_nulled) |
| _out.write(b); |
| if (_bytes==_contentLength) |
| flush(); |
| } |
| |
| /* ------------------------------------------------------------ */ |
| public void write(byte b[]) throws IOException |
| { |
| write(b,0,b.length); |
| } |
| |
| /* ------------------------------------------------------------ */ |
| public void write(byte b[], int off, int len) |
| throws IOException |
| { |
| len=prepareOutput(len); |
| if (!_nulled) |
| _out.write(b,off,len); |
| if (_bytes==_contentLength) |
| flush(); |
| } |
| |
| /* ------------------------------------------------------------ */ |
| protected void checkOutput() |
| { |
| if (_out==_realOut) |
| { |
| if (_bufferedOut==null) |
| { |
| _bufferedOut=new ChunkingOutputStream(_realOut, |
| _bufferSize, |
| _headerReserve, |
| false); |
| _bufferedOut.setCommitObserver(this); |
| _bufferedOut.setBypassBuffer(true); |
| _bufferedOut.setFixed(true); |
| } |
| _out=_bufferedOut; |
| } |
| } |
| |
| /* ------------------------------------------------------------ */ |
| protected int prepareOutput(int length) |
| throws IOException |
| { |
| if (_out==null) |
| throw new IOException("closed"); |
| checkOutput(); |
| if (!_written) |
| { |
| _written=true; |
| notify(OutputObserver.__FIRST_WRITE); |
| } |
| |
| if (_contentLength>=0) |
| { |
| if (_bytes+length>=_contentLength) |
| { |
| length=_contentLength-_bytes; |
| if (length==0) |
| _nulled=true; |
| } |
| } |
| _bytes+=length; |
| return length; |
| } |
| |
| /* ------------------------------------------------------------ */ |
| public void flush() |
| throws IOException |
| { |
| if (!_disableFlush && _out!=null && !_closing) |
| _out.flush(); |
| } |
| |
| /* ------------------------------------------------------------ */ |
| /** Close the stream. |
| * @exception IOException |
| */ |
| public boolean isClosed() |
| throws IOException |
| { |
| return _out==null; |
| } |
| |
| /* ------------------------------------------------------------ */ |
| /** Close the stream. |
| * @exception IOException |
| */ |
| public void close() |
| throws IOException |
| { |
| // Are we already closed? |
| if (_out==null) |
| return; |
| _closing=true; |
| // Close |
| try { |
| notify(OutputObserver.__CLOSING); |
| |
| OutputStream out =_out; |
| _out=null; |
| |
| if (out!=_bufferedOut) |
| out.close(); |
| else |
| _bufferedOut.close(); |
| |
| notify(OutputObserver.__CLOSED); |
| } |
| catch (IOException e) |
| { |
| LogSupport.ignore(log,e); |
| } |
| } |
| |
| /* ------------------------------------------------------------ */ |
| /** Output Notification. |
| * Called by the internal Buffered Output and the event is passed on to |
| * this streams observers. |
| */ |
| public void outputNotify(OutputStream out, int action, Object ignoredData) |
| throws IOException |
| { |
| notify(action); |
| } |
| |
| /* ------------------------------------------------------------ */ |
| /* Notify observers of action. |
| * @see OutputObserver |
| * @param action the action. |
| */ |
| private void notify(int action) |
| throws IOException |
| { |
| if (_observers!=null) |
| { |
| for (int i=_observers.size();i-->0;) |
| { |
| Object data=_observers.get(i--); |
| ((OutputObserver)_observers.get(i)).outputNotify(this,action,data); |
| } |
| } |
| } |
| |
| /* ------------------------------------------------------------ */ |
| public void write(InputStream in, int len) |
| throws IOException |
| { |
| IO.copy(in,this,len); |
| } |
| |
| /* ------------------------------------------------------------ */ |
| private Writer getISO8859Writer() |
| throws IOException |
| { |
| if (_iso8859writer==null) |
| _iso8859writer=new HttpWriter(StringUtil.__ISO_8859_1, |
| getBufferSize()); |
| return _iso8859writer; |
| } |
| |
| /* ------------------------------------------------------------ */ |
| private Writer getUTF8Writer() |
| throws IOException |
| { |
| if (_utf8writer==null) |
| _utf8writer=new HttpWriter("UTF-8",getBufferSize()); |
| return _utf8writer; |
| } |
| |
| /* ------------------------------------------------------------ */ |
| private Writer getASCIIWriter() |
| throws IOException |
| { |
| if (_asciiwriter==null) |
| _asciiwriter=new HttpWriter("US-ASCII",getBufferSize()); |
| return _asciiwriter; |
| } |
| |
| /* ------------------------------------------------------------ */ |
| public Writer getWriter(String encoding) |
| throws IOException |
| { |
| if (encoding==null || |
| StringUtil.__ISO_8859_1.equalsIgnoreCase(encoding) || |
| "ISO8859_1".equalsIgnoreCase(encoding)) |
| return getISO8859Writer(); |
| |
| if ("UTF-8".equalsIgnoreCase(encoding) || |
| "UTF8".equalsIgnoreCase(encoding)) |
| return getUTF8Writer(); |
| |
| if ("US-ASCII".equalsIgnoreCase(encoding)) |
| return getASCIIWriter(); |
| |
| return new OutputStreamWriter(this,encoding); |
| } |
| |
| /* ------------------------------------------------------------ */ |
| public String toString() |
| { |
| return super.toString() + |
| "\nout="+_out+ |
| "\nrealOut="+_realOut+ |
| "\nbufferedOut="+_bufferedOut; |
| } |
| |
| /* ------------------------------------------------------------ */ |
| /* ------------------------------------------------------------ */ |
| private class HttpWriter extends Writer |
| { |
| private OutputStreamWriter _writer=null; |
| private boolean _writting=false; |
| private byte[] _buf; |
| private String _encoding; |
| |
| /* -------------------------------------------------------- */ |
| HttpWriter(String encoding,int bufferSize) |
| { |
| _buf = ByteArrayPool.getByteArray(bufferSize); |
| _encoding=encoding; |
| } |
| |
| /* -------------------------------------------------------- */ |
| public Object getLock() |
| { |
| return lock; |
| } |
| |
| /* -------------------------------------------------------- */ |
| public void write(char c) |
| throws IOException |
| { |
| HttpOutputStream.this.prepareOutput(1); |
| if (!_nulled) |
| { |
| if (_writting) |
| _writer.write(c); |
| else if (c>=0&&c<=0x7f) |
| HttpOutputStream.this.write((int)c); |
| else |
| { |
| char[] ca ={c}; |
| writeEncoded(ca,0,1); |
| } |
| |
| if (_bytes==_contentLength) |
| flush(); |
| } |
| } |
| |
| /* ------------------------------------------------------------ */ |
| public void write(char[] ca) |
| throws IOException |
| { |
| this.write(ca,0,ca.length); |
| } |
| |
| /* ------------------------------------------------------------ */ |
| public void write(char[] ca,int offset, int len) |
| throws IOException |
| { |
| if (_writting) |
| _writer.write(ca,offset,len); |
| else |
| { |
| int s=0; |
| for (int i=0;i<len;i++) |
| { |
| char c=ca[offset+i]; |
| if (c>=0&&c<=0x7f) |
| { |
| _buf[s++]=(byte)c; |
| if (s==_buf.length) |
| { |
| s=HttpOutputStream.this.prepareOutput(s); |
| if (!_nulled) |
| HttpOutputStream.this._out.write(_buf,0,s); |
| s=0; |
| } |
| } |
| else |
| { |
| if (s>0) |
| { |
| s=HttpOutputStream.this.prepareOutput(s); |
| if (!_nulled) |
| HttpOutputStream.this._out.write(_buf,0,s); |
| s=0; |
| } |
| writeEncoded(ca,offset+i,len-i); |
| break; |
| } |
| } |
| |
| if (s>0) |
| { |
| s=HttpOutputStream.this.prepareOutput(s); |
| if (!_nulled) |
| HttpOutputStream.this._out.write(_buf,0,s); |
| s=0; |
| } |
| } |
| |
| if (!_nulled && _bytes==_contentLength) |
| flush(); |
| } |
| |
| /* ------------------------------------------------------------ */ |
| public void write(String s) |
| throws IOException |
| { |
| this.write(s,0,s.length()); |
| } |
| |
| /* ------------------------------------------------------------ */ |
| public void write(String str,int offset, int len) |
| throws IOException |
| { |
| if (_writting) |
| _writer.write(str,offset,len); |
| else |
| { |
| int s=0; |
| for (int i=0;i<len;i++) |
| { |
| char c=str.charAt(offset+i); |
| if (c>=0&&c<=0x7f) |
| { |
| _buf[s++]=(byte)c; |
| if (s==_buf.length) |
| { |
| s=HttpOutputStream.this.prepareOutput(s); |
| if (!_nulled) |
| HttpOutputStream.this._out.write(_buf,0,s); |
| s=0; |
| } |
| } |
| else |
| { |
| if (s>0) |
| { |
| s=HttpOutputStream.this.prepareOutput(s); |
| if (!_nulled) |
| HttpOutputStream.this._out.write(_buf,0,s); |
| s=0; |
| } |
| char[] chars = str.toCharArray(); |
| writeEncoded(chars,offset+i,len-i); |
| break; |
| } |
| } |
| if (s>0) |
| { |
| s=HttpOutputStream.this.prepareOutput(s); |
| if (!_nulled) |
| HttpOutputStream.this._out.write(_buf,0,s); |
| s=0; |
| } |
| } |
| |
| if (_bytes==_contentLength) |
| flush(); |
| } |
| |
| /* ------------------------------------------------------------ */ |
| private void writeEncoded(char[] ca,int offset, int length) |
| throws IOException |
| { |
| _writting=true; |
| if (_writer==null) |
| _writer = new OutputStreamWriter(HttpOutputStream.this,_encoding); |
| |
| try |
| { |
| HttpOutputStream.this._disableFlush=true; |
| _writer.write(ca,offset,length); |
| if (HttpOutputStream.this._contentLength>=0) |
| _writer.flush(); |
| } |
| finally |
| { |
| HttpOutputStream.this._disableFlush=false; |
| } |
| } |
| |
| /* ------------------------------------------------------------ */ |
| public void flush() |
| throws IOException |
| { |
| if (_writting) |
| _writer.flush(); |
| else |
| HttpOutputStream.this.flush(); |
| _writting=false; |
| } |
| |
| /* ------------------------------------------------------------ */ |
| public void close() |
| throws IOException |
| { |
| _closing=true; |
| if (_writting) |
| _writer.flush(); |
| HttpOutputStream.this.close(); |
| _writting=false; |
| } |
| |
| /* ------------------------------------------------------------ */ |
| public void destroy() |
| { |
| ByteArrayPool.returnByteArray(_buf); |
| _buf=null; |
| _writer=null; |
| _encoding=null; |
| } |
| } |
| /** |
| * @return Returns the disableFlush. |
| */ |
| } |