diff src/org/eclipse/jetty/http/HttpGenerator.java @ 802:3428c60d7cfc

replace jetty jars with source
author Franklin Schmidt <fschmidt@gmail.com>
date Wed, 07 Sep 2016 21:15:48 -0600
parents
children 8e9db0bbf4f9
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/eclipse/jetty/http/HttpGenerator.java	Wed Sep 07 21:15:48 2016 -0600
@@ -0,0 +1,1092 @@
+//
+//  ========================================================================
+//  Copyright (c) 1995-2014 Mort Bay Consulting Pty. Ltd.
+//  ------------------------------------------------------------------------
+//  All rights reserved. This program and the accompanying materials
+//  are made available under the terms of the Eclipse Public License v1.0
+//  and Apache License v2.0 which accompanies this distribution.
+//
+//      The Eclipse Public License is available at
+//      http://www.eclipse.org/legal/epl-v10.html
+//
+//      The Apache License v2.0 is available at
+//      http://www.opensource.org/licenses/apache2.0.php
+//
+//  You may elect to redistribute this code under either of these licenses.
+//  ========================================================================
+//
+
+package org.eclipse.jetty.http;
+
+import java.io.IOException;
+import java.io.InterruptedIOException;
+
+import org.eclipse.jetty.io.Buffer;
+import org.eclipse.jetty.io.BufferCache.CachedBuffer;
+import org.eclipse.jetty.io.BufferUtil;
+import org.eclipse.jetty.io.Buffers;
+import org.eclipse.jetty.io.ByteArrayBuffer;
+import org.eclipse.jetty.io.EndPoint;
+import org.eclipse.jetty.io.EofException;
+import org.eclipse.jetty.util.StringUtil;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+/* ------------------------------------------------------------ */
+/**
+ * HttpGenerator. Builds HTTP Messages.
+ *
+ *
+ *
+ */
+public class HttpGenerator extends AbstractGenerator
+{
+    private static final Logger LOG = Log.getLogger(HttpGenerator.class);
+
+    // Build cache of response lines for status
+    private static class Status
+    {
+        Buffer _reason;
+        Buffer _schemeCode;
+        Buffer _responseLine;
+    }
+    private static final Status[] __status = new Status[HttpStatus.MAX_CODE+1];
+    static
+    {
+        int versionLength=HttpVersions.HTTP_1_1_BUFFER.length();
+
+        for (int i=0;i<__status.length;i++)
+        {
+            HttpStatus.Code code = HttpStatus.getCode(i);
+            if (code==null)
+                continue;
+            String reason=code.getMessage();
+            byte[] bytes=new byte[versionLength+5+reason.length()+2];
+            HttpVersions.HTTP_1_1_BUFFER.peek(0,bytes, 0, versionLength);
+            bytes[versionLength+0]=' ';
+            bytes[versionLength+1]=(byte)('0'+i/100);
+            bytes[versionLength+2]=(byte)('0'+(i%100)/10);
+            bytes[versionLength+3]=(byte)('0'+(i%10));
+            bytes[versionLength+4]=' ';
+            for (int j=0;j<reason.length();j++)
+                bytes[versionLength+5+j]=(byte)reason.charAt(j);
+            bytes[versionLength+5+reason.length()]=HttpTokens.CARRIAGE_RETURN;
+            bytes[versionLength+6+reason.length()]=HttpTokens.LINE_FEED;
+
+            __status[i] = new Status();
+            __status[i]._reason=new ByteArrayBuffer(bytes,versionLength+5,bytes.length-versionLength-7,Buffer.IMMUTABLE);
+            __status[i]._schemeCode=new ByteArrayBuffer(bytes,0,versionLength+5,Buffer.IMMUTABLE);
+            __status[i]._responseLine=new ByteArrayBuffer(bytes,0,bytes.length,Buffer.IMMUTABLE);
+        }
+    }
+
+    /* ------------------------------------------------------------------------------- */
+    public static Buffer getReasonBuffer(int code)
+    {
+        Status status = code<__status.length?__status[code]:null;
+        if (status!=null)
+            return status._reason;
+        return null;
+    }
+
+
+    // common _content
+    private static final byte[] LAST_CHUNK =
+    { (byte) '0', (byte) '\015', (byte) '\012', (byte) '\015', (byte) '\012'};
+    private static final byte[] CONTENT_LENGTH_0 = StringUtil.getBytes("Content-Length: 0\015\012");
+    private static final byte[] CONNECTION_KEEP_ALIVE = StringUtil.getBytes("Connection: keep-alive\015\012");
+    private static final byte[] CONNECTION_CLOSE = StringUtil.getBytes("Connection: close\015\012");
+    private static final byte[] CONNECTION_ = StringUtil.getBytes("Connection: ");
+    private static final byte[] CRLF = StringUtil.getBytes("\015\012");
+    private static final byte[] TRANSFER_ENCODING_CHUNKED = StringUtil.getBytes("Transfer-Encoding: chunked\015\012");
+    private static byte[] SERVER = StringUtil.getBytes("Server: Jetty(7.0.x)\015\012");
+
+    // other statics
+    private static final int CHUNK_SPACE = 12;
+
+    public static void setServerVersion(String version)
+    {
+        SERVER=StringUtil.getBytes("Server: Jetty("+version+")\015\012");
+    }
+
+    // data
+    protected boolean _bypass = false; // True if _content buffer can be written directly to endp and bypass the content buffer
+    private boolean _needCRLF = false;
+    private boolean _needEOC = false;
+    private boolean _bufferChunked = false;
+
+
+    /* ------------------------------------------------------------------------------- */
+    /**
+     * Constructor.
+     *
+     * @param buffers buffer pool
+     * @param io the end point to use
+     */
+    public HttpGenerator(Buffers buffers, EndPoint io)
+    {
+        super(buffers,io);
+    }
+
+    /* ------------------------------------------------------------------------------- */
+    @Override
+    public void reset()
+    {
+        if (_persistent!=null && !_persistent && _endp!=null && !_endp.isOutputShutdown())
+        {
+            try
+            {
+                _endp.shutdownOutput();
+            }
+            catch(IOException e)
+            {
+                LOG.ignore(e);
+            }
+        }
+        super.reset();
+        if (_buffer!=null)
+            _buffer.clear();
+        if (_header!=null)
+            _header.clear();
+        if (_content!=null)
+            _content=null;
+        _bypass = false;
+        _needCRLF = false;
+        _needEOC = false;
+        _bufferChunked=false;
+        _method=null;
+        _uri=null;
+        _noContent=false;
+    }
+
+    /* ------------------------------------------------------------ */
+    /**
+     * Add content.
+     *
+     * @param content
+     * @param last
+     * @throws IllegalArgumentException if <code>content</code> is {@link Buffer#isImmutable immutable}.
+     * @throws IllegalStateException If the request is not expecting any more content,
+     *   or if the buffers are full and cannot be flushed.
+     * @throws IOException if there is a problem flushing the buffers.
+     */
+    public void addContent(Buffer content, boolean last) throws IOException
+    {
+        if (_noContent)
+            throw new IllegalStateException("NO CONTENT");
+
+        if (_last || _state==STATE_END)
+        {
+            LOG.warn("Ignoring extra content {}",content);
+            content.clear();
+            return;
+        }
+        _last = last;
+
+        // Handle any unfinished business?
+        if (_content!=null && _content.length()>0 || _bufferChunked)
+        {
+            if (_endp.isOutputShutdown())
+                throw new EofException();
+            flushBuffer();
+            if (_content != null && _content.length()>0)
+            {
+                if (_bufferChunked)
+                {
+                    Buffer nc=_buffers.getBuffer(_content.length()+CHUNK_SPACE+content.length());
+                    nc.put(_content);
+                    nc.put(HttpTokens.CRLF);
+                    BufferUtil.putHexInt(nc, content.length());
+                    nc.put(HttpTokens.CRLF);
+                    nc.put(content);
+                    content=nc;
+                }
+                else
+                {
+                    Buffer nc=_buffers.getBuffer(_content.length()+content.length());
+                    nc.put(_content);
+                    nc.put(content);
+                    content=nc;
+                }
+            }
+        }
+
+        _content = content;
+        _contentWritten += content.length();
+
+        // Handle the _content
+        if (_head)
+        {
+            content.clear();
+            _content=null;
+        }
+        else if (_endp != null && (_buffer==null || _buffer.length()==0) && _content.length() > 0 && (_last || isCommitted() && _content.length()>1024))
+        {
+            _bypass = true;
+        }
+        else if (!_bufferChunked)
+        {
+            // Yes - so we better check we have a buffer
+            if (_buffer == null)
+                _buffer = _buffers.getBuffer();
+
+            // Copy _content to buffer;
+            int len=_buffer.put(_content);
+            _content.skip(len);
+            if (_content.length() == 0)
+                _content = null;
+        }
+    }
+
+    /* ------------------------------------------------------------ */
+    /**
+     * send complete response.
+     *
+     * @param response
+     */
+    public void sendResponse(Buffer response) throws IOException
+    {
+        if (_noContent || _state!=STATE_HEADER || _content!=null && _content.length()>0 || _bufferChunked || _head )
+            throw new IllegalStateException();
+
+        _last = true;
+
+        _content = response;
+        _bypass = true;
+        _state = STATE_FLUSHING;
+
+        // TODO this is not exactly right, but should do.
+        _contentLength =_contentWritten = response.length();
+
+    }
+
+    /* ------------------------------------------------------------ */
+    /** Prepare buffer for unchecked writes.
+     * Prepare the generator buffer to receive unchecked writes
+     * @return the available space in the buffer.
+     * @throws IOException
+     */
+    @Override
+    public int prepareUncheckedAddContent() throws IOException
+    {
+        if (_noContent)
+            return -1;
+
+        if (_last || _state==STATE_END)
+            return -1;
+
+        // Handle any unfinished business?
+        Buffer content = _content;
+        if (content != null && content.length()>0 || _bufferChunked)
+        {
+            flushBuffer();
+            if (content != null && content.length()>0 || _bufferChunked)
+                throw new IllegalStateException("FULL");
+        }
+
+        // we better check we have a buffer
+        if (_buffer == null)
+            _buffer = _buffers.getBuffer();
+
+        _contentWritten-=_buffer.length();
+
+        // Handle the _content
+        if (_head)
+            return Integer.MAX_VALUE;
+
+        return _buffer.space()-(_contentLength == HttpTokens.CHUNKED_CONTENT?CHUNK_SPACE:0);
+    }
+
+    /* ------------------------------------------------------------ */
+    @Override
+    public boolean isBufferFull()
+    {
+        // Should we flush the buffers?
+        return super.isBufferFull() || _bufferChunked || _bypass  || (_contentLength == HttpTokens.CHUNKED_CONTENT && _buffer != null && _buffer.space() < CHUNK_SPACE);
+    }
+
+    /* ------------------------------------------------------------ */
+    public void send1xx(int code) throws IOException
+    {
+        if (_state != STATE_HEADER)
+            return;
+
+        if (code<100||code>199)
+            throw new IllegalArgumentException("!1xx");
+        Status status=__status[code];
+        if (status==null)
+            throw new IllegalArgumentException(code+"?");
+
+        // get a header buffer
+        if (_header == null)
+            _header = _buffers.getHeader();
+
+        _header.put(status._responseLine);
+        _header.put(HttpTokens.CRLF);
+
+        try
+        {
+            // nasty semi busy flush!
+            while(_header.length()>0)
+            {
+                int len = _endp.flush(_header);
+                if (len<0)
+                    throw new EofException();
+                if (len==0)
+                    Thread.sleep(100);
+            }
+        }
+        catch(InterruptedException e)
+        {
+            LOG.debug(e);
+            throw new InterruptedIOException(e.toString());
+        }
+    }
+
+    /* ------------------------------------------------------------ */
+    @Override
+    public boolean isRequest()
+    {
+        return _method!=null;
+    }
+
+    /* ------------------------------------------------------------ */
+    @Override
+    public boolean isResponse()
+    {
+        return _method==null;
+    }
+
+    /* ------------------------------------------------------------ */
+    @Override
+    public void completeHeader(HttpFields fields, boolean allContentAdded) throws IOException
+    {
+        if (_state != STATE_HEADER)
+            return;
+
+        // handle a reset
+        if (isResponse() && _status==0)
+            throw new EofException();
+
+        if (_last && !allContentAdded)
+            throw new IllegalStateException("last?");
+        _last = _last | allContentAdded;
+
+        // get a header buffer
+        if (_header == null)
+            _header = _buffers.getHeader();
+
+        boolean has_server = false;
+
+        try
+        {
+            if (isRequest())
+            {
+                _persistent=true;
+
+                if (_version == HttpVersions.HTTP_0_9_ORDINAL)
+                {
+                    _contentLength = HttpTokens.NO_CONTENT;
+                    _header.put(_method);
+                    _header.put((byte)' ');
+                    _header.put(_uri.getBytes("UTF-8")); // TODO check
+                    _header.put(HttpTokens.CRLF);
+                    _state = STATE_FLUSHING;
+                    _noContent=true;
+                    return;
+                }
+                else
+                {
+                    _header.put(_method);
+                    _header.put((byte)' ');
+                    _header.put(_uri.getBytes("UTF-8")); // TODO check
+                    _header.put((byte)' ');
+                    _header.put(_version==HttpVersions.HTTP_1_0_ORDINAL?HttpVersions.HTTP_1_0_BUFFER:HttpVersions.HTTP_1_1_BUFFER);
+                    _header.put(HttpTokens.CRLF);
+                }
+            }
+            else
+            {
+                // Responses
+                if (_version == HttpVersions.HTTP_0_9_ORDINAL)
+                {
+                    _persistent = false;
+                    _contentLength = HttpTokens.EOF_CONTENT;
+                    _state = STATE_CONTENT;
+                    return;
+                }
+                else
+                {
+                    if (_persistent==null)
+                        _persistent= (_version > HttpVersions.HTTP_1_0_ORDINAL);
+
+                    // add response line
+                    Status status = _status<__status.length?__status[_status]:null;
+
+                    if (status==null)
+                    {
+                        _header.put(HttpVersions.HTTP_1_1_BUFFER);
+                        _header.put((byte) ' ');
+                        _header.put((byte) ('0' + _status / 100));
+                        _header.put((byte) ('0' + (_status % 100) / 10));
+                        _header.put((byte) ('0' + (_status % 10)));
+                        _header.put((byte) ' ');
+                        if (_reason==null)
+                        {
+                            _header.put((byte) ('0' + _status / 100));
+                            _header.put((byte) ('0' + (_status % 100) / 10));
+                            _header.put((byte) ('0' + (_status % 10)));
+                        }
+                        else
+                            _header.put(_reason);
+                        _header.put(HttpTokens.CRLF);
+                    }
+                    else
+                    {
+                        if (_reason==null)
+                            _header.put(status._responseLine);
+                        else
+                        {
+                            _header.put(status._schemeCode);
+                            _header.put(_reason);
+                            _header.put(HttpTokens.CRLF);
+                        }
+                    }
+
+                    if (_status<200 && _status>=100 )
+                    {
+                        _noContent=true;
+                        _content=null;
+                        if (_buffer!=null)
+                            _buffer.clear();
+                        // end the header.
+
+                        if (_status!=101 )
+                        {
+                            _header.put(HttpTokens.CRLF);
+                            _state = STATE_CONTENT;
+                            return;
+                        }
+                    }
+                    else if (_status==204 || _status==304)
+                    {
+                        _noContent=true;
+                        _content=null;
+                        if (_buffer!=null)
+                            _buffer.clear();
+                    }
+                }
+            }
+
+            // Add headers
+            if (_status>=200 && _date!=null)
+            {
+                _header.put(HttpHeaders.DATE_BUFFER);
+                _header.put((byte)':');
+                _header.put((byte)' ');
+                _header.put(_date);
+                _header.put(CRLF);
+            }
+
+            // key field values
+            HttpFields.Field content_length = null;
+            HttpFields.Field transfer_encoding = null;
+            boolean keep_alive = false;
+            boolean close=false;
+            boolean content_type=false;
+            StringBuilder connection = null;
+
+            if (fields != null)
+            {
+                int s=fields.size();
+                for (int f=0;f<s;f++)
+                {
+                    HttpFields.Field field = fields.getField(f);
+                    if (field==null)
+                        continue;
+
+                    switch (field.getNameOrdinal())
+                    {
+                        case HttpHeaders.CONTENT_LENGTH_ORDINAL:
+                            content_length = field;
+                            _contentLength = field.getLongValue();
+
+                            if (_contentLength < _contentWritten || _last && _contentLength != _contentWritten)
+                                content_length = null;
+
+                            // write the field to the header buffer
+                            field.putTo(_header);
+                            break;
+
+                        case HttpHeaders.CONTENT_TYPE_ORDINAL:
+                            if (BufferUtil.isPrefix(MimeTypes.MULTIPART_BYTERANGES_BUFFER, field.getValueBuffer())) _contentLength = HttpTokens.SELF_DEFINING_CONTENT;
+
+                            // write the field to the header buffer
+                            content_type=true;
+                            field.putTo(_header);
+                            break;
+
+                        case HttpHeaders.TRANSFER_ENCODING_ORDINAL:
+                            if (_version == HttpVersions.HTTP_1_1_ORDINAL)
+                                transfer_encoding = field;
+                            // Do NOT add yet!
+                            break;
+
+                        case HttpHeaders.CONNECTION_ORDINAL:
+                            if (isRequest())
+                                field.putTo(_header);
+
+                            int connection_value = field.getValueOrdinal();
+                            switch (connection_value)
+                            {
+                                case -1:
+                                {
+                                    String[] values = field.getValue().split(",");
+                                    for  (int i=0;values!=null && i<values.length;i++)
+                                    {
+                                        CachedBuffer cb = HttpHeaderValues.CACHE.get(values[i].trim());
+
+                                        if (cb!=null)
+                                        {
+                                            switch(cb.getOrdinal())
+                                            {
+                                                case HttpHeaderValues.CLOSE_ORDINAL:
+                                                    close=true;
+                                                    if (isResponse())
+                                                        _persistent=false;
+                                                    keep_alive=false;
+                                                    if (!_persistent && isResponse() && _contentLength == HttpTokens.UNKNOWN_CONTENT)
+                                                        _contentLength = HttpTokens.EOF_CONTENT;
+                                                    break;
+
+                                                case HttpHeaderValues.KEEP_ALIVE_ORDINAL:
+                                                    if (_version == HttpVersions.HTTP_1_0_ORDINAL)
+                                                    {
+                                                        keep_alive = true;
+                                                        if (isResponse())
+                                                            _persistent = true;
+                                                    }
+                                                    break;
+
+                                                default:
+                                                    if (connection==null)
+                                                        connection=new StringBuilder();
+                                                    else
+                                                        connection.append(',');
+                                                    connection.append(values[i]);
+                                            }
+                                        }
+                                        else
+                                        {
+                                            if (connection==null)
+                                                connection=new StringBuilder();
+                                            else
+                                                connection.append(',');
+                                            connection.append(values[i]);
+                                        }
+                                    }
+
+                                    break;
+                                }
+                                case HttpHeaderValues.UPGRADE_ORDINAL:
+                                {
+                                    // special case for websocket connection ordering
+                                    if (isResponse())
+                                    {
+                                        field.putTo(_header);
+                                        continue;
+                                    }
+                                }
+                                case HttpHeaderValues.CLOSE_ORDINAL:
+                                {
+                                    close=true;
+                                    if (isResponse())
+                                        _persistent=false;
+                                    if (!_persistent && isResponse() && _contentLength == HttpTokens.UNKNOWN_CONTENT)
+                                        _contentLength = HttpTokens.EOF_CONTENT;
+                                    break;
+                                }
+                                case HttpHeaderValues.KEEP_ALIVE_ORDINAL:
+                                {
+                                    if (_version == HttpVersions.HTTP_1_0_ORDINAL)
+                                    {
+                                        keep_alive = true;
+                                        if (isResponse())
+                                            _persistent=true;
+                                    }
+                                    break;
+                                }
+                                default:
+                                {
+                                    if (connection==null)
+                                        connection=new StringBuilder();
+                                    else
+                                        connection.append(',');
+                                    connection.append(field.getValue());
+                                }
+                            }
+
+                            // Do NOT add yet!
+                            break;
+
+                        case HttpHeaders.SERVER_ORDINAL:
+                            if (getSendServerVersion())
+                            {
+                                has_server=true;
+                                field.putTo(_header);
+                            }
+                            break;
+
+                        default:
+                            // write the field to the header buffer
+                            field.putTo(_header);
+                    }
+                }
+            }
+
+            // Calculate how to end _content and connection, _content length and transfer encoding
+            // settings.
+            // From RFC 2616 4.4:
+            // 1. No body for 1xx, 204, 304 & HEAD response
+            // 2. Force _content-length?
+            // 3. If Transfer-Encoding!=identity && HTTP/1.1 && !HttpConnection==close then chunk
+            // 4. Content-Length
+            // 5. multipart/byteranges
+            // 6. close
+            switch ((int) _contentLength)
+            {
+                case HttpTokens.UNKNOWN_CONTENT:
+                    // It may be that we have no _content, or perhaps _content just has not been
+                    // written yet?
+
+                    // Response known not to have a body
+                    if (_contentWritten == 0 && isResponse() && (_status < 200 || _status == 204 || _status == 304))
+                        _contentLength = HttpTokens.NO_CONTENT;
+                    else if (_last)
+                    {
+                        // we have seen all the _content there is
+                        _contentLength = _contentWritten;
+                        if (content_length == null && (isResponse() || _contentLength>0 || content_type ) && !_noContent)
+                        {
+                            // known length but not actually set.
+                            _header.put(HttpHeaders.CONTENT_LENGTH_BUFFER);
+                            _header.put(HttpTokens.COLON);
+                            _header.put((byte) ' ');
+                            BufferUtil.putDecLong(_header, _contentLength);
+                            _header.put(HttpTokens.CRLF);
+                        }
+                    }
+                    else
+                    {
+                        // No idea, so we must assume that a body is coming
+                        _contentLength = (!_persistent || _version < HttpVersions.HTTP_1_1_ORDINAL ) ? HttpTokens.EOF_CONTENT : HttpTokens.CHUNKED_CONTENT;
+                        if (isRequest() && _contentLength==HttpTokens.EOF_CONTENT)
+                        {
+                            _contentLength=HttpTokens.NO_CONTENT;
+                            _noContent=true;
+                        }
+                    }
+                    break;
+
+                case HttpTokens.NO_CONTENT:
+                    if (content_length == null && isResponse() && _status >= 200 && _status != 204 && _status != 304)
+                        _header.put(CONTENT_LENGTH_0);
+                    break;
+
+                case HttpTokens.EOF_CONTENT:
+                    _persistent = isRequest();
+                    break;
+
+                case HttpTokens.CHUNKED_CONTENT:
+                    break;
+
+                default:
+                    // TODO - maybe allow forced chunking by setting te ???
+                    break;
+            }
+
+            // Add transfer_encoding if needed
+            if (_contentLength == HttpTokens.CHUNKED_CONTENT)
+            {
+                // try to use user supplied encoding as it may have other values.
+                if (transfer_encoding != null && HttpHeaderValues.CHUNKED_ORDINAL != transfer_encoding.getValueOrdinal())
+                {
+                    String c = transfer_encoding.getValue();
+                    if (c.endsWith(HttpHeaderValues.CHUNKED))
+                        transfer_encoding.putTo(_header);
+                    else
+                        throw new IllegalArgumentException("BAD TE");
+                }
+                else
+                    _header.put(TRANSFER_ENCODING_CHUNKED);
+            }
+
+            // Handle connection if need be
+            if (_contentLength==HttpTokens.EOF_CONTENT)
+            {
+                keep_alive=false;
+                _persistent=false;
+            }
+
+            if (isResponse())
+            {
+                if (!_persistent && (close || _version > HttpVersions.HTTP_1_0_ORDINAL))
+                {
+                    _header.put(CONNECTION_CLOSE);
+                    if (connection!=null)
+                    {
+                        _header.setPutIndex(_header.putIndex()-2);
+                        _header.put((byte)',');
+                        _header.put(connection.toString().getBytes());
+                        _header.put(CRLF);
+                    }
+                }
+                else if (keep_alive)
+                {
+                    _header.put(CONNECTION_KEEP_ALIVE);
+                    if (connection!=null)
+                    {
+                        _header.setPutIndex(_header.putIndex()-2);
+                        _header.put((byte)',');
+                        _header.put(connection.toString().getBytes());
+                        _header.put(CRLF);
+                    }
+                }
+                else if (connection!=null)
+                {
+                    _header.put(CONNECTION_);
+                    _header.put(connection.toString().getBytes());
+                    _header.put(CRLF);
+                }
+            }
+
+            if (!has_server && _status>199 && getSendServerVersion())
+                _header.put(SERVER);
+
+            // end the header.
+            _header.put(HttpTokens.CRLF);
+            _state = STATE_CONTENT;
+
+        }
+        catch(ArrayIndexOutOfBoundsException e)
+        {
+            throw new RuntimeException("Header>"+_header.capacity(),e);
+        }
+    }
+
+    /* ------------------------------------------------------------ */
+    /**
+     * Complete the message.
+     *
+     * @throws IOException
+     */
+    @Override
+    public void complete() throws IOException
+    {
+        if (_state == STATE_END)
+            return;
+
+        super.complete();
+
+        if (_state < STATE_FLUSHING)
+        {
+            _state = STATE_FLUSHING;
+            if (_contentLength == HttpTokens.CHUNKED_CONTENT)
+                _needEOC = true;
+        }
+
+        flushBuffer();
+    }
+
+    /* ------------------------------------------------------------ */
+    @Override
+    public int flushBuffer() throws IOException
+    {
+        try
+        {
+
+            if (_state == STATE_HEADER)
+                throw new IllegalStateException("State==HEADER");
+
+            prepareBuffers();
+
+            if (_endp == null)
+            {
+                if (_needCRLF && _buffer!=null)
+                    _buffer.put(HttpTokens.CRLF);
+                if (_needEOC && _buffer!=null && !_head)
+                    _buffer.put(LAST_CHUNK);
+                _needCRLF=false;
+                _needEOC=false;
+                return 0;
+            }
+
+            int total= 0;
+
+            int len = -1;
+            int to_flush = flushMask();
+            int last_flush;
+
+            do
+            {
+                last_flush=to_flush;
+                switch (to_flush)
+                {
+                    case 7:
+                        throw new IllegalStateException(); // should never happen!
+                    case 6:
+                        len = _endp.flush(_header, _buffer, null);
+                        break;
+                    case 5:
+                        len = _endp.flush(_header, _content, null);
+                        break;
+                    case 4:
+                        len = _endp.flush(_header);
+                        break;
+                    case 3:
+                        len = _endp.flush(_buffer, _content, null);
+                        break;
+                    case 2:
+                        len = _endp.flush(_buffer);
+                        break;
+                    case 1:
+                        len = _endp.flush(_content);
+                        break;
+                    case 0:
+                    {
+                        len=0;
+                        // Nothing more we can write now.
+                        if (_header != null)
+                            _header.clear();
+
+                        _bypass = false;
+                        _bufferChunked = false;
+
+                        if (_buffer != null)
+                        {
+                            _buffer.clear();
+                            if (_contentLength == HttpTokens.CHUNKED_CONTENT)
+                            {
+                                // reserve some space for the chunk header
+                                _buffer.setPutIndex(CHUNK_SPACE);
+                                _buffer.setGetIndex(CHUNK_SPACE);
+
+                                // Special case handling for small left over buffer from
+                                // an addContent that caused a buffer flush.
+                                if (_content != null && _content.length() < _buffer.space() && _state != STATE_FLUSHING)
+                                {
+                                    _buffer.put(_content);
+                                    _content.clear();
+                                    _content=null;
+                                }
+                            }
+                        }
+
+                        // Are we completely finished for now?
+                        if (!_needCRLF && !_needEOC && (_content==null || _content.length()==0))
+                        {
+                            if (_state == STATE_FLUSHING)
+                                _state = STATE_END;
+
+                            if (_state==STATE_END && _persistent != null && !_persistent && _status!=100 && _method==null)
+                                _endp.shutdownOutput();
+                        }
+                        else
+                            // Try to prepare more to write.
+                            prepareBuffers();
+                    }
+
+                }
+
+                if (len > 0)
+                    total+=len;
+
+                to_flush = flushMask();
+            }
+            // loop while progress is being made (OR we have prepared some buffers that might make progress)
+            while (len>0 || (to_flush!=0 && last_flush==0));
+
+            return total;
+        }
+        catch (IOException e)
+        {
+            LOG.ignore(e);
+            throw (e instanceof EofException) ? e:new EofException(e);
+        }
+    }
+
+    /* ------------------------------------------------------------ */
+    private int flushMask()
+    {
+        return  ((_header != null && _header.length() > 0)?4:0)
+        | ((_buffer != null && _buffer.length() > 0)?2:0)
+        | ((_bypass && _content != null && _content.length() > 0)?1:0);
+    }
+
+    /* ------------------------------------------------------------ */
+    private void prepareBuffers()
+    {
+        // if we are not flushing an existing chunk
+        if (!_bufferChunked)
+        {
+            // Refill buffer if possible
+            if (!_bypass && _content != null && _content.length() > 0 && _buffer != null && _buffer.space() > 0)
+            {
+                int len = _buffer.put(_content);
+                _content.skip(len);
+                if (_content.length() == 0)
+                    _content = null;
+            }
+
+            // Chunk buffer if need be
+            if (_contentLength == HttpTokens.CHUNKED_CONTENT)
+            {
+                if (_bypass && (_buffer==null||_buffer.length()==0) && _content!=null)
+                {
+                    // this is a bypass write
+                    int size = _content.length();
+                    _bufferChunked = true;
+
+                    if (_header == null)
+                        _header = _buffers.getHeader();
+
+                    // if we need CRLF add this to header
+                    if (_needCRLF)
+                    {
+                        if (_header.length() > 0) throw new IllegalStateException("EOC");
+                        _header.put(HttpTokens.CRLF);
+                        _needCRLF = false;
+                    }
+                    // Add the chunk size to the header
+                    BufferUtil.putHexInt(_header, size);
+                    _header.put(HttpTokens.CRLF);
+
+                    // Need a CRLF after the content
+                    _needCRLF=true;
+                }
+                else if (_buffer!=null)
+                {
+                    int size = _buffer.length();
+                    if (size > 0)
+                    {
+                        // Prepare a chunk!
+                        _bufferChunked = true;
+
+                        // Did we leave space at the start of the buffer.
+                        //noinspection ConstantConditions
+                        if (_buffer.getIndex() == CHUNK_SPACE)
+                        {
+                            // Oh yes, goodie! let's use it then!
+                            _buffer.poke(_buffer.getIndex() - 2, HttpTokens.CRLF, 0, 2);
+                            _buffer.setGetIndex(_buffer.getIndex() - 2);
+                            BufferUtil.prependHexInt(_buffer, size);
+
+                            if (_needCRLF)
+                            {
+                                _buffer.poke(_buffer.getIndex() - 2, HttpTokens.CRLF, 0, 2);
+                                _buffer.setGetIndex(_buffer.getIndex() - 2);
+                                _needCRLF = false;
+                            }
+                        }
+                        else
+                        {
+                            // No space so lets use a header buffer.
+                            if (_header == null)
+                                _header = _buffers.getHeader();
+
+                            if (_needCRLF)
+                            {
+                                if (_header.length() > 0) throw new IllegalStateException("EOC");
+                                _header.put(HttpTokens.CRLF);
+                                _needCRLF = false;
+                            }
+                            BufferUtil.putHexInt(_header, size);
+                            _header.put(HttpTokens.CRLF);
+                        }
+
+                        // Add end chunk trailer.
+                        if (_buffer.space() >= 2)
+                            _buffer.put(HttpTokens.CRLF);
+                        else
+                            _needCRLF = true;
+                    }
+                }
+
+                // If we need EOC and everything written
+                if (_needEOC && (_content == null || _content.length() == 0))
+                {
+                    if (_header == null && _buffer == null)
+                        _header = _buffers.getHeader();
+
+                    if (_needCRLF)
+                    {
+                        if (_buffer == null && _header != null && _header.space() >= HttpTokens.CRLF.length)
+                        {
+                            _header.put(HttpTokens.CRLF);
+                            _needCRLF = false;
+                        }
+                        else if (_buffer!=null && _buffer.space() >= HttpTokens.CRLF.length)
+                        {
+                            _buffer.put(HttpTokens.CRLF);
+                            _needCRLF = false;
+                        }
+                    }
+
+                    if (!_needCRLF && _needEOC)
+                    {
+                        if (_buffer == null && _header != null && _header.space() >= LAST_CHUNK.length)
+                        {
+                            if (!_head)
+                            {
+                                _header.put(LAST_CHUNK);
+                                _bufferChunked=true;
+                            }
+                            _needEOC = false;
+                        }
+                        else if (_buffer!=null && _buffer.space() >= LAST_CHUNK.length)
+                        {
+                            if (!_head)
+                            {
+                                _buffer.put(LAST_CHUNK);
+                                _bufferChunked=true;
+                            }
+                            _needEOC = false;
+                        }
+                    }
+                }
+            }
+        }
+
+        if (_content != null && _content.length() == 0)
+            _content = null;
+
+    }
+
+    public int getBytesBuffered()
+    {
+        return(_header==null?0:_header.length())+
+        (_buffer==null?0:_buffer.length())+
+        (_content==null?0:_content.length());
+    }
+
+    public boolean isEmpty()
+    {
+        return (_header==null||_header.length()==0) &&
+        (_buffer==null||_buffer.length()==0) &&
+        (_content==null||_content.length()==0);
+    }
+
+    @Override
+    public String toString()
+    {
+        Buffer header=_header;
+        Buffer buffer=_buffer;
+        Buffer content=_content;
+        return String.format("%s{s=%d,h=%d,b=%d,c=%d}",
+                getClass().getSimpleName(),
+                _state,
+                header == null ? -1 : header.length(),
+                buffer == null ? -1 : buffer.length(),
+                content == null ? -1 : content.length());
+    }
+}