// ======================================================================== // $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.browsermob.proxy.jetty.http; import org.apache.commons.logging.Log; import org.browsermob.proxy.jetty.log.LogFactory; import org.browsermob.proxy.jetty.util.*; import java.io.*; import java.util.ArrayList; /* ---------------------------------------------------------------- */ /** 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. */ }