//======================================================================== //$Id: HttpGenerator.java,v 1.7 2005/11/25 21:17:12 gregwilkins Exp $ //Copyright 2004-2005 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.mortbay.jetty; import java.io.IOException; import java.io.OutputStreamWriter; import java.io.Writer; import java.lang.reflect.Field; import java.lang.reflect.Modifier; import javax.servlet.ServletOutputStream; import javax.servlet.WriteListener; import javax.servlet.http.HttpServletResponse; import org.mortbay.io.Buffer; import org.mortbay.io.Buffers; import org.mortbay.io.ByteArrayBuffer; import org.mortbay.io.EndPoint; import org.mortbay.io.View; import org.mortbay.log.Log; import org.mortbay.util.ByteArrayOutputStream2; import org.mortbay.util.StringUtil; import org.mortbay.util.TypeUtil; /* ------------------------------------------------------------ */ /** * Abstract Generator. Builds HTTP Messages. * * Currently this class uses a system parameter "jetty.direct.writers" to control * two optional writer to byte conversions. buffer.writers=true will probably be * faster, but will consume more memory. This option is just for testing and tuning. * * @author gregw * */ public abstract class AbstractGenerator implements Generator { // states public final static int STATE_HEADER = 0; public final static int STATE_CONTENT = 2; public final static int STATE_FLUSHING = 3; public final static int STATE_END = 4; private static final byte[] NO_BYTES = {}; private static int MAX_OUTPUT_CHARS = 512; private static Buffer[] __reasons = new Buffer[505]; static { Field[] fields = HttpServletResponse.class.getDeclaredFields(); for (int i=0;i<fields.length;i++) { if ((fields[i].getModifiers()&Modifier.STATIC)!=0 && fields[i].getName().startsWith("SC_")) { try { int code = fields[i].getInt(null); if (code<__reasons.length) __reasons[code]=new ByteArrayBuffer(fields[i].getName().substring(3)); } catch(IllegalAccessException e) {} } } } protected static Buffer getReasonBuffer(int code) { Buffer reason=(code<__reasons.length)?__reasons[code]:null; return reason==null?null:reason; } public static String getReason(int code) { Buffer reason=(code<__reasons.length)?__reasons[code]:null; return reason==null?TypeUtil.toString(code):reason.toString(); } // data protected int _state = STATE_HEADER; protected int _status = 0; protected int _version = HttpVersions.HTTP_1_1_ORDINAL; protected Buffer _reason; protected Buffer _method; protected String _uri; protected long _contentWritten = 0; protected long _contentLength = HttpTokens.UNKNOWN_CONTENT; protected boolean _last = false; protected boolean _head = false; protected boolean _noContent = false; protected boolean _close = false; protected Buffers _buffers; // source of buffers protected EndPoint _endp; protected int _headerBufferSize; protected int _contentBufferSize; protected Buffer _header; // Buffer for HTTP header (and maybe small _content) protected Buffer _buffer; // Buffer for copy of passed _content protected Buffer _content; // Buffer passed to addContent private boolean _sendServerVersion; /* ------------------------------------------------------------------------------- */ /** * Constructor. * * @param buffers buffer pool * @param headerBufferSize Size of the buffer to allocate for HTTP header * @param contentBufferSize Size of the buffer to allocate for HTTP content */ public AbstractGenerator(Buffers buffers, EndPoint io, int headerBufferSize, int contentBufferSize) { this._buffers = buffers; this._endp = io; _headerBufferSize=headerBufferSize; _contentBufferSize=contentBufferSize; } /* ------------------------------------------------------------------------------- */ public void reset(boolean returnBuffers) { _state = STATE_HEADER; _status = 0; _version = HttpVersions.HTTP_1_1_ORDINAL; _reason = null; _last = false; _head = false; _noContent=false; _close = false; _contentWritten = 0; _contentLength = HttpTokens.UNKNOWN_CONTENT; synchronized(this) { if (returnBuffers) { if (_header != null) _buffers.returnBuffer(_header); _header = null; if (_buffer != null) _buffers.returnBuffer(_buffer); _buffer = null; } else { if (_header != null) _header.clear(); if (_buffer != null) { _buffers.returnBuffer(_buffer); _buffer = null; } } } _content = null; _method=null; } /* ------------------------------------------------------------------------------- */ public void resetBuffer() { if(_state>=STATE_FLUSHING) throw new IllegalStateException("Flushed"); _last = false; _close = false; _contentWritten = 0; _contentLength = HttpTokens.UNKNOWN_CONTENT; _content=null; if (_buffer!=null) _buffer.clear(); } /* ------------------------------------------------------------ */ /** * @return Returns the contentBufferSize. */ public int getContentBufferSize() { return _contentBufferSize; } /* ------------------------------------------------------------ */ /** * @param contentBufferSize The contentBufferSize to set. */ public void increaseContentBufferSize(int contentBufferSize) { if (contentBufferSize > _contentBufferSize) { _contentBufferSize = contentBufferSize; if (_buffer != null) { Buffer nb = _buffers.getBuffer(_contentBufferSize); nb.put(_buffer); _buffers.returnBuffer(_buffer); _buffer = nb; } } } /* ------------------------------------------------------------ */ public Buffer getUncheckedBuffer() { return _buffer; } /* ------------------------------------------------------------ */ public boolean getSendServerVersion () { return _sendServerVersion; } /* ------------------------------------------------------------ */ public void setSendServerVersion (boolean sendServerVersion) { _sendServerVersion = sendServerVersion; } /* ------------------------------------------------------------ */ public int getState() { return _state; } /* ------------------------------------------------------------ */ public boolean isState(int state) { return _state == state; } /* ------------------------------------------------------------ */ public boolean isComplete() { return _state == STATE_END; } /* ------------------------------------------------------------ */ public boolean isIdle() { return _state == STATE_HEADER && _method==null && _status==0; } /* ------------------------------------------------------------ */ public boolean isCommitted() { return _state != STATE_HEADER; } /* ------------------------------------------------------------ */ /** * @return Returns the head. */ public boolean isHead() { return _head; } /* ------------------------------------------------------------ */ public void setContentLength(long value) { if (value<0) _contentLength=HttpTokens.UNKNOWN_CONTENT; else _contentLength=value; } /* ------------------------------------------------------------ */ /** * @param head The head to set. */ public void setHead(boolean head) { _head = head; } /* ------------------------------------------------------------ */ /** * @return <code>false</code> if the connection should be closed after a request has been read, * <code>true</code> if it should be used for additional requests. */ public boolean isPersistent() { return !_close; } /* ------------------------------------------------------------ */ public void setPersistent(boolean persistent) { _close=!persistent; } /* ------------------------------------------------------------ */ /** * @param version The version of the client the response is being sent to (NB. Not the version * in the response, which is the version of the server). */ public void setVersion(int version) { if (_state != STATE_HEADER) throw new IllegalStateException("STATE!=START"); _version = version; if (_version==HttpVersions.HTTP_0_9_ORDINAL && _method!=null) _noContent=true; } /* ------------------------------------------------------------ */ public int getVersion() { return _version; } /* ------------------------------------------------------------ */ /** */ public void setRequest(String method, String uri) { if (method==null || HttpMethods.GET.equals(method) ) _method=HttpMethods.GET_BUFFER; else _method=HttpMethods.CACHE.lookup(method); _uri=uri; if (_version==HttpVersions.HTTP_0_9_ORDINAL) _noContent=true; } /* ------------------------------------------------------------ */ /** * @param status The status code to send. * @param reason the status message to send. */ public void setResponse(int status, String reason) { if (_state != STATE_HEADER) throw new IllegalStateException("STATE!=START"); _status = status; if (reason!=null) { int len=reason.length(); if (len>_headerBufferSize/2) len=_headerBufferSize/2; _reason=new ByteArrayBuffer(len); for (int i=0;i<len;i++) { char ch = reason.charAt(i); if (ch!='\r'&&ch!='\n') _reason.put((byte)ch); else _reason.put((byte)' '); } } } /* ------------------------------------------------------------ */ /** Prepare buffer for unchecked writes. * Prepare the generator buffer to receive unchecked writes * @return the available space in the buffer. * @throws IOException */ protected abstract int prepareUncheckedAddContent() throws IOException; /* ------------------------------------------------------------ */ void uncheckedAddContent(int b) { _buffer.put((byte)b); } /* ------------------------------------------------------------ */ void completeUncheckedAddContent() { if (_noContent) { if(_buffer!=null) _buffer.clear(); return; } else { _contentWritten+=_buffer.length(); if (_head) _buffer.clear(); } } /* ------------------------------------------------------------ */ public boolean isBufferFull() { if (_buffer != null && _buffer.space()==0) { if (_buffer.length()==0 && !_buffer.isImmutable()) _buffer.compact(); return _buffer.space()==0; } return _content!=null && _content.length()>0; } /* ------------------------------------------------------------ */ public boolean isContentWritten() { return _contentLength>=0 && _contentWritten>=_contentLength; } /* ------------------------------------------------------------ */ public abstract void completeHeader(HttpFields fields, boolean allContentAdded) throws IOException; /* ------------------------------------------------------------ */ /** * Complete the message. * * @throws IOException */ public void complete() throws IOException { if (_state == STATE_HEADER) { throw new IllegalStateException("State==HEADER"); } if (_contentLength >= 0 && _contentLength != _contentWritten && !_head) { if (Log.isDebugEnabled()) Log.debug("ContentLength written=="+_contentWritten+" != contentLength=="+_contentLength); _close = true; } } /* ------------------------------------------------------------ */ public abstract long flush() throws IOException; /* ------------------------------------------------------------ */ /** * Utility method to send an error response. If the builder is not committed, this call is * equivalent to a setResponse, addcontent and complete call. * * @param code * @param reason * @param content * @param close * @throws IOException */ public void sendError(int code, String reason, String content, boolean close) throws IOException { if (!isCommitted()) { setResponse(code, reason); _close = close; completeHeader(null, false); if (content != null) addContent(new View(new ByteArrayBuffer(content)), Generator.LAST); complete(); } } /* ------------------------------------------------------------ */ /** * @return Returns the contentWritten. */ public long getContentWritten() { return _contentWritten; } /* ------------------------------------------------------------ */ /* ------------------------------------------------------------ */ /* ------------------------------------------------------------ */ /* ------------------------------------------------------------ */ /** Output. * * <p> * Implements {@link javax.servlet.ServletOutputStream} from the {@link javax.servlet} package. * </p> * A {@link ServletOutputStream} implementation that writes content * to a {@link AbstractGenerator}. The class is designed to be reused * and can be reopened after a close. */ public static class Output extends ServletOutputStream { protected AbstractGenerator _generator; protected long _maxIdleTime; protected ByteArrayBuffer _buf = new ByteArrayBuffer(NO_BYTES); protected boolean _closed; // These are held here for reuse by Writer String _characterEncoding; Writer _converter; char[] _chars; ByteArrayOutputStream2 _bytes; /* ------------------------------------------------------------ */ public Output(AbstractGenerator generator, long maxIdleTime) { _generator=generator; _maxIdleTime=maxIdleTime; } /* ------------------------------------------------------------ */ /* * @see java.io.OutputStream#close() */ public void close() throws IOException { _closed=true; } /* ------------------------------------------------------------ */ void blockForOutput() throws IOException { if (_generator._endp.isBlocking()) { try { flush(); } catch(IOException e) { _generator._endp.close(); throw e; } } else { if (!_generator._endp.blockWritable(_maxIdleTime)) { _generator._endp.close(); throw new EofException("timeout"); } _generator.flush(); } } /* ------------------------------------------------------------ */ void reopen() { _closed=false; } /* ------------------------------------------------------------ */ public void flush() throws IOException { // block until everything is flushed Buffer content = _generator._content; Buffer buffer = _generator._buffer; if (content!=null && content.length()>0 || buffer!=null && buffer.length()>0 || _generator.isBufferFull()) { _generator.flush(); while ((content!=null && content.length()>0 ||buffer!=null && buffer.length()>0) && _generator._endp.isOpen()) blockForOutput(); } } /* ------------------------------------------------------------ */ public void write(byte[] b, int off, int len) throws IOException { _buf.wrap(b, off, len); write(_buf); _buf.wrap(NO_BYTES); } /* ------------------------------------------------------------ */ /* * @see java.io.OutputStream#write(byte[]) */ public void write(byte[] b) throws IOException { _buf.wrap(b); write(_buf); _buf.wrap(NO_BYTES); } /* ------------------------------------------------------------ */ /* * @see java.io.OutputStream#write(int) */ public void write(int b) throws IOException { if (_closed) throw new IOException("Closed"); if (!_generator._endp.isOpen()) throw new EofException(); // Block until we can add _content. while (_generator.isBufferFull()) { blockForOutput(); if (_closed) throw new IOException("Closed"); if (!_generator._endp.isOpen()) throw new EofException(); } // Add the _content if (_generator.addContent((byte)b)) // Buffers are full so flush. flush(); if (_generator.isContentWritten()) { flush(); close(); } } /* ------------------------------------------------------------ */ private void write(Buffer buffer) throws IOException { if (_closed) throw new IOException("Closed"); if (!_generator._endp.isOpen()) throw new EofException(); // Block until we can add _content. while (_generator.isBufferFull()) { blockForOutput(); if (_closed) throw new IOException("Closed"); if (!_generator._endp.isOpen()) throw new EofException(); } // Add the _content _generator.addContent(buffer, Generator.MORE); // Have to flush and complete headers? if (_generator.isBufferFull()) flush(); if (_generator.isContentWritten()) { flush(); close(); } // Block until our buffer is free while (buffer.length() > 0 && _generator._endp.isOpen()) blockForOutput(); } /* ------------------------------------------------------------ */ /* * @see javax.servlet.ServletOutputStream#print(java.lang.String) */ public void print(String s) throws IOException { write(s.getBytes()); } @Override public boolean isReady() { ;return false; } @Override public void setWriteListener(WriteListener listener) { ; } } /* ------------------------------------------------------------ */ /* ------------------------------------------------------------ */ /* ------------------------------------------------------------ */ /** OutputWriter. * A writer that can wrap a {@link Output} stream and provide * character encodings. * * The UTF-8 encoding is done by this class and no additional * buffers or Writers are used. * The UTF-8 code was inspired by http://javolution.org */ public static class OutputWriter extends Writer { private static final int WRITE_CONV = 0; private static final int WRITE_ISO1 = 1; private static final int WRITE_UTF8 = 2; Output _out; AbstractGenerator _generator; int _writeMode; int _surrogate; /* ------------------------------------------------------------ */ public OutputWriter(Output out) { _out=out; _generator=_out._generator; } /* ------------------------------------------------------------ */ public void setCharacterEncoding(String encoding) { if (encoding == null || StringUtil.__ISO_8859_1.equalsIgnoreCase(encoding)) { _writeMode = WRITE_ISO1; } else if (StringUtil.__UTF8.equalsIgnoreCase(encoding)) { _writeMode = WRITE_UTF8; } else { _writeMode = WRITE_CONV; if (_out._characterEncoding == null || !_out._characterEncoding.equalsIgnoreCase(encoding)) _out._converter = null; // Set lazily in getConverter() } _out._characterEncoding = encoding; if (_out._bytes==null) _out._bytes = new ByteArrayOutputStream2(MAX_OUTPUT_CHARS); } /* ------------------------------------------------------------ */ public void close() throws IOException { _out.close(); } /* ------------------------------------------------------------ */ public void flush() throws IOException { _out.flush(); } /* ------------------------------------------------------------ */ public void write (String s,int offset, int length) throws IOException { while (length > MAX_OUTPUT_CHARS) { write(s, offset, MAX_OUTPUT_CHARS); offset += MAX_OUTPUT_CHARS; length -= MAX_OUTPUT_CHARS; } if (_out._chars==null) { _out._chars = new char[MAX_OUTPUT_CHARS]; } char[] chars = _out._chars; s.getChars(offset, offset + length, chars, 0); write(chars, 0, length); } /* ------------------------------------------------------------ */ public void write (char[] s,int offset, int length) throws IOException { Output out = _out; while (length > 0) { out._bytes.reset(); int chars = length>MAX_OUTPUT_CHARS?MAX_OUTPUT_CHARS:length; switch (_writeMode) { case WRITE_CONV: { Writer converter=getConverter(); converter.write(s, offset, chars); converter.flush(); } break; case WRITE_ISO1: { byte[] buffer=out._bytes.getBuf(); int bytes=out._bytes.getCount(); if (chars>buffer.length-bytes) chars=buffer.length-bytes; for (int i = 0; i < chars; i++) { int c = s[offset+i]; buffer[bytes++]=(byte)(c<256?c:'?'); // ISO-1 and UTF-8 match for 0 - 255 } if (bytes>=0) out._bytes.setCount(bytes); break; } case WRITE_UTF8: { byte[] buffer=out._bytes.getBuf(); int bytes=out._bytes.getCount(); if (bytes+chars>buffer.length) chars=buffer.length-bytes; for (int i = 0; i < chars; i++) { int code = s[offset+i]; if ((code & 0xffffff80) == 0) { // 1b if (bytes+1>buffer.length) { chars=i; break; } buffer[bytes++]=(byte)(code); } else { if((code&0xfffff800)==0) { // 2b if (bytes+2>buffer.length) { chars=i; break; } buffer[bytes++]=(byte)(0xc0|(code>>6)); buffer[bytes++]=(byte)(0x80|(code&0x3f)); } else if((code&0xffff0000)==0) { // 3b if (bytes+3>buffer.length) { chars=i; break; } buffer[bytes++]=(byte)(0xe0|(code>>12)); buffer[bytes++]=(byte)(0x80|((code>>6)&0x3f)); buffer[bytes++]=(byte)(0x80|(code&0x3f)); } else if((code&0xff200000)==0) { // 4b if (bytes+4>buffer.length) { chars=i; break; } buffer[bytes++]=(byte)(0xf0|(code>>18)); buffer[bytes++]=(byte)(0x80|((code>>12)&0x3f)); buffer[bytes++]=(byte)(0x80|((code>>6)&0x3f)); buffer[bytes++]=(byte)(0x80|(code&0x3f)); } else if((code&0xf4000000)==0) { // 5b if (bytes+5>buffer.length) { chars=i; break; } buffer[bytes++]=(byte)(0xf8|(code>>24)); buffer[bytes++]=(byte)(0x80|((code>>18)&0x3f)); buffer[bytes++]=(byte)(0x80|((code>>12)&0x3f)); buffer[bytes++]=(byte)(0x80|((code>>6)&0x3f)); buffer[bytes++]=(byte)(0x80|(code&0x3f)); } else if((code&0x80000000)==0) { // 6b if (bytes+6>buffer.length) { chars=i; break; } buffer[bytes++]=(byte)(0xfc|(code>>30)); buffer[bytes++]=(byte)(0x80|((code>>24)&0x3f)); buffer[bytes++]=(byte)(0x80|((code>>18)&0x3f)); buffer[bytes++]=(byte)(0x80|((code>>12)&0x3f)); buffer[bytes++]=(byte)(0x80|((code>>6)&0x3f)); buffer[bytes++]=(byte)(0x80|(code&0x3f)); } else { buffer[bytes++]=(byte)('?'); } if (bytes==buffer.length) { chars=i+1; break; } } } out._bytes.setCount(bytes); break; } default: throw new IllegalStateException(); } out._bytes.writeTo(out); length-=chars; offset+=chars; } } /* ------------------------------------------------------------ */ private Writer getConverter() throws IOException { if (_out._converter == null) _out._converter = new OutputStreamWriter(_out._bytes, _out._characterEncoding); return _out._converter; } } }