view src/org/eclipse/jetty/http/HttpGenerator.java @ 1071:b4ba8a4d5a16

remove JBuffer.space()
author Franklin Schmidt <fschmidt@gmail.com>
date Thu, 10 Nov 2016 02:13:28 -0700
parents a44fc6b53757
children 6b7ff30bb990
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 java.util.Arrays;

import org.eclipse.jetty.io.JBuffer;
import org.eclipse.jetty.io.BufferUtil;
import org.eclipse.jetty.io.Buffers;
import org.eclipse.jetty.io.EndPoint;
import org.eclipse.jetty.io.EofException;
import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.server.Server;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/* ------------------------------------------------------------ */
/**
 * HttpGenerator. Builds HTTP Messages.
 *
 *
 *
 */
public final class HttpGenerator
{
	private static final Logger LOG = LoggerFactory.getLogger(HttpGenerator.class);

	// Build cache of response lines for status
	private static class Status
	{
		byte[] _schemeCode;
		byte[] _responseLine;
	}
	private static final Status[] __status = new Status[HttpStatus.MAX_CODE+1];
	static
	{
		final int versionLength=HttpVersions.HTTP_1_1_BYTES.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);
			System.arraycopy(HttpVersions.HTTP_1_1_BYTES,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]._schemeCode = Arrays.copyOf(bytes,versionLength+5);
			__status[i]._responseLine = bytes;
		}
	}


	// 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("+Server.version+")\015\012");

	// other statics
	private static final int CHUNK_SPACE = 12;

	// data
	private 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;


	public void shutdown() {
		if (_persistent!=null && !_persistent && !_endp.isOutputShutdown())
		{
			try
			{
				_endp.shutdownOutput();
			}
			catch(IOException e)
			{
				LOG.trace("",e);
			}
		}
	}

	/* ------------------------------------------------------------ */
	/**
	 * Add content.
	 *
	 * @param content
	 * @param last
	 * @throws IllegalArgumentException if <code>content</code> is {@link JBuffer#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(JBuffer 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.hasRemaining() || _bufferChunked)
		{
			if (_endp.isOutputShutdown())
				throw new EofException();
			flushBuffer();
			if (_content != null && _content.hasRemaining())
			{
				if (_bufferChunked)
				{
					JBuffer nc = _buffers.getBuffer(_content.remaining()+CHUNK_SPACE+content.remaining());
					nc.putQ(_content);
					nc.putQ(HttpTokens.CRLF);
					BufferUtil.putHexInt(nc, content.remaining());
					nc.putQ(HttpTokens.CRLF);
					nc.putQ(content);
					nc.flip();
					content = nc;
				}
				else
				{
					JBuffer nc = _buffers.getBuffer(_content.remaining()+content.remaining());
					nc.putQ(_content);
					nc.putQ(content);
					nc.flip();
					content = nc;
				}
			}
		}

		_content = content;
		_contentWritten += content.remaining();

		// Handle the _content
		if (_head)
		{
//			content.clear();
			_content = null;
		}
		else if ((_buffer.position()==0) && _content.hasRemaining() && (_last || isCommitted() && _content.remaining()>1024))
		{
			_bypass = true;
		}
		else if (!_bufferChunked)
		{
//System.out.println("qqqqqqqqqqqqqqqqqqq c");
			// Copy _content to buffer;
			_buffer.putQ(_content);
			if (!_content.hasRemaining())
				_content = null;
		}
	}

	/* ------------------------------------------------------------ */
	/** Prepare buffer for unchecked writes.
	 * Prepare the generator buffer to receive unchecked writes
	 * @return the available space in the buffer.
	 * @throws IOException
	 */
	public void prepareUncheckedAddContent() throws IOException
	{
		if (_noContent)
//			return -1;
			throw new RuntimeException("_noContent");

		if (_last || _state==STATE_END)
//			return -1;
			throw new RuntimeException("_last");

		// Handle any unfinished business?
		if (_content != null && _content.hasRemaining() || _bufferChunked)
		{
			flushBuffer();
			if (_content != null && _content.hasRemaining() || _bufferChunked)
				throw new IllegalStateException("FULL");
		}

		_contentWritten -= _buffer.position();
	}

	public boolean isBufferFull()
	{
		// Should we flush the buffers?
		return isBufferFull2() || _bufferChunked || _bypass /* || (_contentLength == HttpTokens.CHUNKED_CONTENT && _buffer.remaining() < 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+"?");

		_header.putQ(status._responseLine);
		_header.putQ(HttpTokens.CRLF);

		try
		{
			// nasty semi busy flush!
			_header.flip();
			while(_header.remaining()>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());
		}
	}

	public void completeHeader(HttpFields fields, boolean allContentAdded) throws IOException
	{
		if (_state != STATE_HEADER)
			return;

		// handle a reset
		if (_status==0)
			throw new EofException();

		if (_last && !allContentAdded)
			throw new IllegalStateException("last?");
		_last = _last | allContentAdded;

		boolean has_server = false;

		try
		{
			// 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.putQ(HttpVersions.HTTP_1_1_BYTES);
					_header.putQ((byte) ' ');
					_header.putQ((byte) ('0' + _status / 100));
					_header.putQ((byte) ('0' + (_status % 100) / 10));
					_header.putQ((byte) ('0' + (_status % 10)));
					_header.putQ((byte) ' ');
					if (_reason==null)
					{
						_header.putQ((byte) ('0' + _status / 100));
						_header.putQ((byte) ('0' + (_status % 100) / 10));
						_header.putQ((byte) ('0' + (_status % 10)));
					}
					else
						_header.putQ(_reason);
					_header.putQ(HttpTokens.CRLF);
				}
				else
				{
					if (_reason==null)
						_header.putQ(status._responseLine);
					else
					{
						_header.putQ(status._schemeCode);
						_header.putQ(_reason);
						_header.putQ(HttpTokens.CRLF);
					}
				}

				if (_status<200 && _status>=100 )
				{
					_noContent = true;
					_content = null;
					_buffer.clear();
					// end the header.

					if (_status!=101 )
					{
						_header.putQ(HttpTokens.CRLF);
						_state = STATE_CONTENT;
						return;
					}
				}
				else if (_status==204 || _status==304)
				{
					_noContent = true;
					_content = null;
					_buffer.clear();
				}
			}

			// 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()))
							if (field.getValue().startsWith(MimeTypes.MULTIPART_BYTERANGES))
								_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:
							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++)
									{
										int ord = HttpHeaderValues.CACHE.getOrdinal(values[i].trim());

										if (ord != -1)
										{
											switch(ord)
											{
												case HttpHeaderValues.CLOSE_ORDINAL:
													close=true;
													_persistent = false;
													keep_alive=false;
													if (!_persistent && _contentLength == HttpTokens.UNKNOWN_CONTENT)
														_contentLength = HttpTokens.EOF_CONTENT;
													break;

												case HttpHeaderValues.KEEP_ALIVE_ORDINAL:
													if (_version == HttpVersions.HTTP_1_0_ORDINAL)
													{
														keep_alive = true;
														_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
									field.putTo(_header);
									continue;
								}
								case HttpHeaderValues.CLOSE_ORDINAL:
								{
									close=true;
									_persistent=false;
									if (!_persistent && _contentLength == HttpTokens.UNKNOWN_CONTENT)
										_contentLength = HttpTokens.EOF_CONTENT;
									break;
								}
								case HttpHeaderValues.KEEP_ALIVE_ORDINAL:
								{
									if (_version == HttpVersions.HTTP_1_0_ORDINAL)
									{
										keep_alive = true;
										_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:
							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 && (_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 && !_noContent)
						{
							// known length but not actually set.
							_header.putQ(HttpHeaders.CONTENT_LENGTH_BYTES);
							_header.putQ(HttpTokens.COLON);
							_header.putQ((byte) ' ');
							BufferUtil.putDecLong(_header, _contentLength);
							_header.putQ(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;
					}
					break;

				case HttpTokens.NO_CONTENT:
					if (content_length == null && _status >= 200 && _status != 204 && _status != 304)
						_header.putQ(CONTENT_LENGTH_0);
					break;

				case HttpTokens.EOF_CONTENT:
					_persistent = false;
					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.putQ(TRANSFER_ENCODING_CHUNKED);
			}

			// Handle connection if need be
			if (_contentLength==HttpTokens.EOF_CONTENT)
			{
				keep_alive=false;
				_persistent=false;
			}

			if (!_persistent && (close || _version > HttpVersions.HTTP_1_0_ORDINAL))
			{
				_header.putQ(CONNECTION_CLOSE);
				if (connection!=null)
				{
					_header.position(_header.position()-2);
					_header.putQ((byte)',');
					_header.putQ(connection.toString().getBytes());
					_header.putQ(CRLF);
				}
			}
			else if (keep_alive)
			{
				_header.putQ(CONNECTION_KEEP_ALIVE);
				if (connection!=null)
				{
					_header.position(_header.position()-2);
					_header.putQ((byte)',');
					_header.putQ(connection.toString().getBytes());
					_header.putQ(CRLF);
				}
			}
			else if (connection!=null)
			{
				_header.putQ(CONNECTION_);
				_header.putQ(connection.toString().getBytes());
				_header.putQ(CRLF);
			}

			if (!has_server && _status>199)
				_header.putQ(SERVER);

			// end the header.
			_header.putQ(HttpTokens.CRLF);
			_state = STATE_CONTENT;

		}
		catch(ArrayIndexOutOfBoundsException e)
		{
			throw new RuntimeException("Header>"+_header.remaining(),e);
		}
	}

	/* ------------------------------------------------------------ */
	/**
	 * Complete the message.
	 *
	 * @throws IOException
	 */
	public void complete() throws IOException
	{
		if (_state == STATE_END)
			return;

		complete2();

		if (_state < STATE_FLUSHING)
		{
			_state = STATE_FLUSHING;
			if (_contentLength == HttpTokens.CHUNKED_CONTENT)
				_needEOC = true;
		}

		flushBuffer();
	}

	public int flushBuffer() throws IOException
	{
		try
		{

			if (_state == STATE_HEADER)
				throw new IllegalStateException("State==HEADER");

			prepareBuffers();

			int total= 0;

			int len = -1;
			int to_flush = flushMask();
			int last_flush;

			do
			{
				last_flush = to_flush;
				switch (to_flush)
				{
//qqq
					case 7:
						throw new IllegalStateException(); // should never happen!
					case 6:
						_header.flip();
						_buffer.flip();
						len = _endp.flush(_header, _buffer, null);
						_header.compact();
						_buffer.compact();
						break;
					case 5:
						_header.flip();
						len = _endp.flush(_header, _content, null);
						_header.compact();
						break;
					case 4:
						_header.flip();
						len = _endp.flush(_header);
						_header.compact();
						break;
					case 3:
						_buffer.flip();
						len = _endp.flush(_buffer, _content, null);
						_buffer.compact();
						break;
					case 2:
						_buffer.flip();
						len = _endp.flush(_buffer);
						_buffer.compact();
						break;
					case 1:
						len = _endp.flush(_content);
						break;
					case 0:
					{
						len = 0;
						// Nothing more we can write now.
						_header.clear();

						_bypass = false;
						_bufferChunked = false;

						_buffer.clear();  // ?
						if (_contentLength == HttpTokens.CHUNKED_CONTENT)
						{
							// Special case handling for small left over buffer from
							// an addContent that caused a buffer flush.
							if (_content != null && _content.remaining() < _buffer.remaining() && _state != STATE_FLUSHING)
							{
								_buffer.putQ(_content);
								_content = null;
							}
						}

						// Are we completely finished for now?
						if (!_needCRLF && !_needEOC && (_content==null || !_content.hasRemaining()))
						{
							if (_state == STATE_FLUSHING)
								_state = STATE_END;

							if (_state==STATE_END && _persistent != null && !_persistent && _status!=100)
								_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.position() > 0)?4:0)
		| ((_buffer.position() > 0)?2:0)
		| ((_bypass && _content != null && _content.hasRemaining())?1:0);
	}

	private void prepareBuffers()
	{
		// if we are not flushing an existing chunk
		if (!_bufferChunked)
		{
			// Refill buffer if possible
			if (!_bypass && _content != null && _content.hasRemaining() && _buffer.hasRemaining())
			{
				_buffer.putQ(_content);
				if (!_content.hasRemaining())
					_content = null;
			}

			// Chunk buffer if need be
			if (_contentLength == HttpTokens.CHUNKED_CONTENT)
			{
				if (_bypass && _buffer.position()==0 && _content!=null)
				{
					// this is a bypass write
					int size = _content.remaining();
					_bufferChunked = true;

					// if we need CRLF add this to header
					if (_needCRLF)
					{
						if (_header.position() > 0) throw new IllegalStateException("EOC");
						_header.putQ(HttpTokens.CRLF);
						_needCRLF = false;
					}
					// Add the chunk size to the header
					BufferUtil.putHexInt(_header, size);
					_header.putQ(HttpTokens.CRLF);

					// Need a CRLF after the content
					_needCRLF = true;
				}
				else
				{
					int size = _buffer.position();
					if (size > 0)
					{
						// Prepare a chunk!
						_bufferChunked = true;

						if (_needCRLF)
						{
							if (_header.position() > 0) throw new IllegalStateException("EOC");
							_header.putQ(HttpTokens.CRLF);
							_needCRLF = false;
						}
						BufferUtil.putHexInt(_header, size);
						_header.putQ(HttpTokens.CRLF);

						// Add end chunk trailer.
						if (_buffer.remaining() >= 2)
							_buffer.putQ(HttpTokens.CRLF);
						else
							_needCRLF = true;
					}
				}

				// If we need EOC and everything written
				if (_needEOC && (_content == null || !_content.hasRemaining()))
				{
					if (_needCRLF && _buffer.remaining() >= HttpTokens.CRLF.length) {
						_buffer.putQ(HttpTokens.CRLF);
						_needCRLF = false;
					}

					if (!_needCRLF && _needEOC && _buffer.remaining() >= LAST_CHUNK.length) {
						if (!_head)
						{
							_buffer.putQ(LAST_CHUNK);
							_bufferChunked = true;
						}
						_needEOC = false;
					}
				}
			}
		}

		if (_content != null && !_content.hasRemaining())
			_content = null;

	}

	@Override
	public String toString()
	{
		JBuffer header = _header;
		JBuffer buffer = _buffer;
		JBuffer content = _content;
		return String.format("%s{s=%d,h=%d,b=%d,c=%d}",
				getClass().getSimpleName(),
				_state,
				header == null ? -1 : header.position(),
				buffer == null ? -1 : buffer.position(),
				content == null ? -1 : content.remaining());
	}












	// AbstractGenerator

	public static final boolean LAST=true;
	public static final boolean MORE=false;

	// states
	private final static int STATE_HEADER = 0;
	private final static int STATE_CONTENT = 2;
	private final static int STATE_FLUSHING = 3;
	private final static int STATE_END = 4;

	// data

	private final Buffers _buffers; // source of buffers
	private final EndPoint _endp;

	private int _state = STATE_HEADER;

	private int _status = 0;
	private int _version = HttpVersions.HTTP_1_1_ORDINAL;
	private JBuffer _reason;

	private long _contentWritten = 0;
	private long _contentLength = HttpTokens.UNKNOWN_CONTENT;
	private boolean _last = false;
	private boolean _head = false;
	private boolean _noContent = false;
	private Boolean _persistent = null;

	private final JBuffer _header; // JBuffer for HTTP header (and maybe small _content)
	private final JBuffer _buffer; // JBuffer for copy of passed _content
	private JBuffer _content; // JBuffer passed to addContent


	public HttpGenerator(Buffers buffers, EndPoint io)
	{
		this._buffers = buffers;
		this._endp = io;
		_header = _buffers.getHeader();
		_buffer = _buffers.getBuffer();
	}

	public final boolean isOpen()
	{
		return _endp.isOpen();
	}

	public final void resetBuffer()
	{
		if(_state>=STATE_FLUSHING)
			throw new IllegalStateException("Flushed");

		_last = false;
		_persistent = null;
		_contentWritten = 0;
		_contentLength = HttpTokens.UNKNOWN_CONTENT;
		_content = null;
		_buffer.clear();
	}

	/* ------------------------------------------------------------ */
	/**
	 * @return Returns the contentBufferSize.
	 */
	public final int getContentBufferSize()
	{
		return _buffer.capacity();
	}

	public final JBuffer getUncheckedBuffer()
	{
		return _buffer;
	}

	public final boolean isComplete()
	{
		return _state == STATE_END;
	}

	public final boolean isIdle()
	{
		return _state == STATE_HEADER && _status==0;
	}

	public final boolean isCommitted()
	{
		return _state != STATE_HEADER;
	}

	public final void setContentLength(long value)
	{
		if (value<0)
			_contentLength = HttpTokens.UNKNOWN_CONTENT;
		else
			_contentLength = value;
	}

	public final 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 final boolean isPersistent()
	{
		return _persistent!=null ? _persistent.booleanValue() : _version>HttpVersions.HTTP_1_0_ORDINAL;
	}

	public final void setPersistent(boolean persistent)
	{
		_persistent = 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 final void setVersion(int version)
	{
		if (_state != STATE_HEADER)
			throw new IllegalStateException("STATE!=START "+_state);
		_version = version;
	}

	/* ------------------------------------------------------------ */
	/**
	 * @param status The status code to send.
	 * @param reason the status message to send.
	 */
	public final void setResponse(int status, String reason)
	{
		if (_state != STATE_HEADER) throw new IllegalStateException("STATE!=START");
		_status = status;
		if (reason!=null)
		{
			int len = reason.length();

			// TODO don't hard code
			if (len>1024)
				len=1024;
			_reason = BufferUtil.newBuffer(len);
			for (int i=0;i<len;i++)
			{
				char ch = reason.charAt(i);
				if (ch!='\r'&&ch!='\n')
					_reason.putQ((byte)ch);
				else
					_reason.putQ((byte)' ');
			}
			_reason.flip();
		}
	}

	public final void completeUncheckedAddContent()
	{
		_contentWritten += _buffer.position();
		if (_head)
			_buffer.clear();
	}

	private boolean isBufferFull2()
	{
		return !_buffer.hasRemaining() || _content!=null && _content.remaining()>0;
	}

	public final boolean isWritten()
	{
		return _contentWritten>0;
	}

	public final boolean isAllContentWritten()
	{
		return _contentLength>=0 && _contentWritten>=_contentLength;
	}


	private void complete2() 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);
			_persistent = false;
		}
	}


	public final void flush(long maxIdleTime) throws IOException
	{
		// block until everything is flushed
		long now = System.currentTimeMillis();
		long end = now+maxIdleTime;
		JBuffer content = _content;
		JBuffer buffer = _buffer;
		if (content!=null && content.remaining()>0 || buffer.position()>0 || isBufferFull())
		{
			flushBuffer();

			while (now<end && (content!=null && content.remaining()>0 || buffer.position()>0) && _endp.isOpen()&& !_endp.isOutputShutdown())
			{
				blockForOutput(end-now);
				now = System.currentTimeMillis();
			}
		}
	}

	/* ------------------------------------------------------------ */
	/**
	 * 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 The error code
	 * @param reason The error reason
	 * @param content Contents of the error page
	 * @param close True if the connection should be closed
	 * @throws IOException if there is a problem flushing the response
	 */
	public final void sendError(int code, String reason, String content, boolean close) throws IOException
	{
		if (close)
			_persistent=false;
		if (isCommitted())
		{
			LOG.debug("sendError on committed: {} {}",code,reason);
		}
		else
		{
			LOG.debug("sendError: {} {}",code,reason);
			setResponse(code, reason);
			if (content != null)
			{
				completeHeader(null, false);
				addContent(BufferUtil.wrap(content), LAST);
			}
			else if (code>=400)
			{
				completeHeader(null, false);
				addContent(BufferUtil.wrap("Error: "+(reason==null?(""+code):reason)), LAST);
			}
			else
			{
				completeHeader(null, true);
			}
			complete();
		}
	}

	public final long getContentWritten()
	{
		return _contentWritten;
	}


	public final void blockForOutput(long maxIdleTime) throws IOException
	{
		if (_endp.isBlocking())
		{
			try
			{
				flushBuffer();
			}
			catch(IOException e)
			{
				_endp.close();
				throw e;
			}
		}
		else
		{
			if (!_endp.blockWritable(maxIdleTime))
			{
				_endp.close();
				throw new EofException("timeout");
			}

			flushBuffer();
		}
	}

}