view src/org/eclipse/jetty/http/HttpGenerator.java @ 867:4f5547d29192

minor
author Franklin Schmidt <fschmidt@gmail.com>
date Sun, 02 Oct 2016 21:06:18 -0600
parents 8e9db0bbf4f9
children fef4392f4905
line wrap: on
line source

//
//  ========================================================================
//  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.slf4j.Logger;
import org.slf4j.LoggerFactory;

/* ------------------------------------------------------------ */
/**
 * HttpGenerator. Builds HTTP Messages.
 *
 *
 *
 */
public class HttpGenerator extends AbstractGenerator
{
    private static final Logger LOG = LoggerFactory.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.trace("",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.trace("",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());
    }
}