From a4bee5b12d3153f8347eb14b050b7bd3bee7cad2 Mon Sep 17 00:00:00 2001 From: Greg Wilkins Date: Fri, 2 Mar 2012 11:39:42 +1100 Subject: [PATCH] jetty-9 work in progress on new HttpGenerator --- .../org/eclipse/jetty/http/HttpFields.java | 11 +- .../org/eclipse/jetty/http/HttpGenerator.java | 1345 ++++++++--------- .../eclipse/jetty/http/HttpGeneratorTest.java | 66 +- .../eclipse/jetty/io/nio/SslConnection.java | 20 +- .../eclipse/jetty/io/nio/BufferUtilTest.java | 169 +++ .../io/nio/SelectChannelEndPointTest.java | 6 +- .../org/eclipse/jetty/util/BufferUtil.java | 105 +- 7 files changed, 936 insertions(+), 786 deletions(-) create mode 100644 jetty-io/src/test/java/org/eclipse/jetty/io/nio/BufferUtilTest.java diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpFields.java b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpFields.java index f09f272d290..9850370a4eb 100644 --- a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpFields.java +++ b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpFields.java @@ -24,6 +24,7 @@ import java.util.Date; import java.util.Enumeration; import java.util.GregorianCalendar; import java.util.HashMap; +import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Map; @@ -49,7 +50,7 @@ import org.eclipse.jetty.util.log.Logger; * * */ -public class HttpFields +public class HttpFields implements Iterable { private static final Logger LOG = Log.getLogger(HttpFields.class); @@ -362,7 +363,13 @@ public class HttpFields { return _fields.get(i); } - + + /* ------------------------------------------------------------ */ + public Iterator iterator() + { + return _fields.iterator(); + } + /* ------------------------------------------------------------ */ public Field getField(HttpHeader header) { diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpGenerator.java b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpGenerator.java index 4b859652b66..bd3623bafd0 100644 --- a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpGenerator.java +++ b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpGenerator.java @@ -15,6 +15,7 @@ package org.eclipse.jetty.http; import java.io.IOException; import java.io.InterruptedIOException; +import java.nio.BufferOverflowException; import java.nio.ByteBuffer; import javax.swing.text.View; @@ -81,29 +82,30 @@ public class HttpGenerator // states - enum State { HEADER, CONTENT, FLUSHING, END }; + enum State { START, CHUNKING, STREAMING, COMPLETING, END }; + enum Result { NEED_COMMIT,NEED_CHUNK,NEED_BUFFER,FLUSH,FLUSH_CONTENT,NEED_COMPLETE,OK,SHUTDOWN_OUT}; public static final byte[] NO_BYTES = {}; // data - protected State _state = State.HEADER; + private State _state = State.START; - protected int _status = 0; - protected HttpVersion _version = HttpVersion.HTTP_1_1; - protected byte[] _reason; - protected byte[] _method; - protected byte[] _uri; + private int _status = 0; + private HttpVersion _version = HttpVersion.HTTP_1_1; + private byte[] _reason; + private byte[] _method; + private byte[] _uri; - protected long _contentWritten = 0; - protected long _contentLength = HttpTokens.UNKNOWN_CONTENT; - protected boolean _last = false; - protected boolean _head = false; - protected boolean _noContent = false; - protected Boolean _persistent = null; + private int _largeContent=4096; + private long _contentPrepared = 0; + private long _contentLength = HttpTokens.UNKNOWN_CONTENT; + private boolean _head = false; + private boolean _noContent = false; + private Boolean _persistent = null; - protected ByteBuffer _date; + private ByteBuffer _date; private boolean _sendServerVersion; @@ -130,50 +132,32 @@ public class HttpGenerator } // 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; + private boolean _sentEOC = false; - - - + /* ------------------------------------------------------------------------------- */ public void reset() { - _state = State.HEADER; + _state = State.START; _status = 0; _version = HttpVersion.HTTP_1_1; _reason = null; - _last = false; _head = false; _noContent=false; _persistent = null; - _contentWritten = 0; + _contentPrepared = 0; _contentLength = HttpTokens.UNKNOWN_CONTENT; _date = null; _method=null; _needCRLF = false; - _needEOC = false; - _bufferChunked=false; + _sentEOC = false; _uri=null; _noContent=false; } - - /* ------------------------------------------------------------------------------- */ - public void resetBuffer() - { - if(_state.ordinal()>=State.FLUSHING.ordinal()) - throw new IllegalStateException("Flushed"); - - _last = false; - _persistent=null; - _contentWritten = 0; - _contentLength = HttpTokens.UNKNOWN_CONTENT; - } /* ------------------------------------------------------------ */ public boolean getSendServerVersion () @@ -208,13 +192,25 @@ public class HttpGenerator /* ------------------------------------------------------------ */ public boolean isIdle() { - return _state == State.HEADER && _method==null && _status==0; + return _state == State.START && _method==null && _status==0; } /* ------------------------------------------------------------ */ public boolean isCommitted() { - return _state != State.HEADER; + return _state != State.START; + } + + /* ------------------------------------------------------------ */ + public int getLargeContent() + { + return _largeContent; + } + + /* ------------------------------------------------------------ */ + public void setLargeContent(int largeContent) + { + _largeContent = largeContent; } /* ------------------------------------------------------------ */ @@ -269,7 +265,7 @@ public class HttpGenerator */ public void setVersion(HttpVersion version) { - if (_state != State.HEADER) + if (_state != State.START) throw new IllegalStateException("STATE!=START "+_state); _version = version; if (_version==HttpVersion.HTTP_0_9 && _method!=null) @@ -296,7 +292,7 @@ public class HttpGenerator */ public void setRequest(String method, String uri) { - if (_state != State.HEADER) + if (_state != State.START) throw new IllegalStateException("STATE!=START "+_state); _method=StringUtil.getBytes(method); _uri=StringUtil.getUtf8Bytes(uri); @@ -307,7 +303,7 @@ public class HttpGenerator */ public void setRequest(HttpMethod method, String uri,HttpVersion version) { - if (_state != State.HEADER) + if (_state != State.START) throw new IllegalStateException("STATE!=START "+_state); _method=method.toBytes(); _uri=StringUtil.getUtf8Bytes(uri); @@ -322,7 +318,8 @@ public class HttpGenerator */ public void setResponse(int status, String reason) { - if (_state != State.HEADER) throw new IllegalStateException("STATE!=START"); + if (_state != State.START) + throw new IllegalStateException("STATE!=START"); _method=null; _status = status; if (reason==null) @@ -341,272 +338,22 @@ public class HttpGenerator /* ------------------------------------------------------------ */ public boolean isWritten() { - return _contentWritten>0; + return _contentPrepared>0; } /* ------------------------------------------------------------ */ public boolean isAllContentWritten() { - return _contentLength>=0 && _contentWritten>=_contentLength; + return _contentLength>=0 && _contentPrepared>=_contentLength; } + /* ------------------------------------------------------------ */ - /** - * Complete the message. - * - * @throws IOException - */ - public void complete() throws IOException - { - if (_state == State.END) - return; - - 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; - } - - if (_state.ordinal() < State.FLUSHING.ordinal()) - { - _state = State.FLUSHING; - if (_contentLength == HttpTokens.CHUNKED_CONTENT) - _needEOC = true; - } - } - - - /* ------------------------------------------------------------ */ - /** - * 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 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(new View(BufferUtil.allocate(content)), Generator.LAST); - } - else - { - completeHeader(null, true); - } - complete(); - } - } - - /* ------------------------------------------------------------ */ - /** - * @return Returns the contentWritten. - */ public long getContentWritten() { - return _contentWritten; + return _contentPrepared; } - - - /* ------------------------------------------------------------------------------- */ - public static byte[] getReasonBuffer(int code) - { - Status status = code<__status.length?__status[code]:null; - if (status!=null) - return status._reason; - return null; - } - - - /* ------------------------------------------------------------ */ - /** - * Add content. - * - * @param content - * @param last - * @throws IllegalArgumentException if content is {@link ByteBuffer#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(ByteBuffer 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()) - { - ByteBuffer nc=_buffers.getBuffer(_content.remaining()+content.remaining()); - nc.put(_content); - nc.put(content); - content=nc; - } - } - - _content = content; - _contentWritten += content.remaining(); - - // Handle the _content - if (_head) - { - content.clear(); - _content=null; - } - else if (_endp != null && (_buffer==null || _buffer.remaining()==0) && _content.remaining() > 0 && (_last || isCommitted() && _content.remaining()>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.remaining() == 0) - _content = null; - } - } - - /* ------------------------------------------------------------ */ - /** - * send complete response. - * - * @param response - */ - public void sendResponse(ByteBuffer response) throws IOException - { - if (_noContent || _state!=State.HEADER || _content!=null && _content.hasRemaining() || _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(); - - } - - /* ------------------------------------------------------------ */ - /** - * Add content. - * - * @param b byte - * @return true if the buffers are full - * @throws IOException - */ - public boolean addContent(byte b) throws IOException - { - if (_noContent) - throw new IllegalStateException("NO CONTENT"); - - if (_last || _state==State.END) - { - LOG.warn("Ignoring extra content {}",Byte.valueOf(b)); - return false; - } - - // Handle any unfinished business? - if (_content != null && _content.hasRemaining() || _bufferChunked) - { - flushBuffer(); - if (_content != null && _content.hasRemaining() || _bufferChunked) - throw new IllegalStateException("FULL"); - } - - _contentWritten++; - - // Handle the _content - if (_head) - return false; - - // we better check we have a buffer - if (_buffer == null) - _buffer = _buffers.getBuffer(); - - // Copy _content to buffer; - _buffer.put(b); - - return _buffer.space()<=(_contentLength == HttpTokens.CHUNKED_CONTENT?CHUNK_SPACE:0); - } - - /* ------------------------------------------------------------ */ - 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.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 boolean isRequest() { @@ -619,109 +366,115 @@ public class HttpGenerator return _method==null; } + /* ------------------------------------------------------------ */ - public void completeHeader(ByteBuffer buffer, HttpFields fields, boolean allContentAdded) throws IOException + public boolean needsHeader() { - if (_state != State.HEADER) - return; + return _state==State.START || _contentLength==HttpTokens.CHUNKED_CONTENT; + } - // handle a reset - if (isResponse() && _status==0) - throw new EofException(); - - if (_last && !allContentAdded) - throw new IllegalStateException("last?"); - _last = _last | allContentAdded; - - - if (isRequest() && _version==HttpVersion.HTTP_0_9) - _noContent=true; + + /* ------------------------------------------------------------ */ + public Result commit(HttpFields fields,ByteBuffer header,ByteBuffer buffer, ByteBuffer content, boolean last) + throws IOException + { - boolean has_server = false; + if (header==null) + throw new IllegalArgumentException("!header"); - try + if (isResponse() && _status==0) + throw new EofException(); // TODO ??? + + + if (_state == State.START) { - if (isRequest()) - { - _persistent=true; + if (isRequest() && _version==HttpVersion.HTTP_0_9) + _noContent=true; - if (_version == HttpVersion.HTTP_0_9) - { - _contentLength = HttpTokens.NO_CONTENT; - buffer.put(_method); - buffer.put((byte)' '); - buffer.put(_uri); - buffer.put(HttpTokens.CRLF); - _state = State.FLUSHING; - _noContent=true; - return; - } - else - { - buffer.put(_method); - buffer.put((byte)' '); - buffer.put(_uri); - buffer.put((byte)' '); - buffer.put((_version==HttpVersion.HTTP_1_0?HttpVersion.HTTP_1_0:HttpVersion.HTTP_1_1).toBytes()); - buffer.put(HttpTokens.CRLF); - } - } - else + boolean has_server = false; + int pos=header.position(); + try { - // Responses - if (_version == HttpVersion.HTTP_0_9) + BufferUtil.flipToFill(header); + if (isRequest()) { - _persistent = false; - _contentLength = HttpTokens.EOF_CONTENT; - _state = State.CONTENT; - return; + _persistent=true; + + if (_version == HttpVersion.HTTP_0_9) + { + _contentLength = HttpTokens.NO_CONTENT; + header.put(_method); + header.put((byte)' '); + header.put(_uri); + header.put(HttpTokens.CRLF); + _state = State.END; + _noContent=true; + return Result.FLUSH; + } + + header.put(_method); + header.put((byte)' '); + header.put(_uri); + header.put((byte)' '); + header.put((_version==HttpVersion.HTTP_1_0?HttpVersion.HTTP_1_0:HttpVersion.HTTP_1_1).toBytes()); + header.put(HttpTokens.CRLF); } else { + // Responses + if (_version == HttpVersion.HTTP_0_9) + { + _persistent = false; + _contentLength = HttpTokens.EOF_CONTENT; + _state = State.STREAMING; + return prepareContent(null,buffer,content); + } + + // Are we persistent by default? if (_persistent==null) - _persistent= (_version.ordinal() > HttpVersion.HTTP_1_0.ordinal()); + _persistent=(_version.ordinal() > HttpVersion.HTTP_1_0.ordinal()); - // add response line + // Look for prepared response line Status status = _status<__status.length?__status[_status]:null; - - if (status==null) - { - buffer.put(HTTP_1_1_SPACE); - buffer.put((byte) ('0' + _status / 100)); - buffer.put((byte) ('0' + (_status % 100) / 10)); - buffer.put((byte) ('0' + (_status % 10))); - buffer.put((byte) ' '); - if (_reason==null) - { - buffer.put((byte) ('0' + _status / 100)); - buffer.put((byte) ('0' + (_status % 100) / 10)); - buffer.put((byte) ('0' + (_status % 10))); - } - else - buffer.put(_reason); - buffer.put(HttpTokens.CRLF); - } - else + if (status!=null) { if (_reason==null) - buffer.put(status._responseLine); + header.put(status._responseLine); else { - buffer.put(status._schemeCode); - buffer.put(_reason); - buffer.put(HttpTokens.CRLF); + header.put(status._schemeCode); + header.put(_reason); + header.put(HttpTokens.CRLF); } } + else // generate response line + { + header.put(HTTP_1_1_SPACE); + 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); + } - if (_status<200 && _status>=100 ) + // Handle 1xx + if (_status>=100 && _status<200 ) { _noContent=true; if (_status!=101 ) { - buffer.put(HttpTokens.CRLF); - _state = State.CONTENT; - return; + header.put(HttpTokens.CRLF); + _state = State.START; + return Result.FLUSH; } } else if (_status==204 || _status==304) @@ -729,443 +482,529 @@ public class HttpGenerator _noContent=true; } } - } - // Add headers - if (_status>=200 && _date!=null) - { - buffer.put(HttpHeader.DATE.toBytesColonSpace()); - buffer.put(_date); - buffer.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=200 && _date!=null) { - HttpFields.Field field = fields.getField(f); - if (field==null) - continue; + header.put(HttpHeader.DATE.toBytesColonSpace()); + header.put(_date); + header.put(CRLF); + } - HttpHeader header = HttpHeader.CACHE.get(field.getName()); - HttpHeaderValue value = null; - - switch (header==null?HttpHeader.UNKNOWN:header) - { - case CONTENT_LENGTH: - content_length = field; - _contentLength = field.getLongValue(); + // default field values + HttpFields.Field transfer_encoding=null; + boolean keep_alive=false; + boolean close=false; + boolean content_type=false; + boolean content_length=false; + StringBuilder connection = null; - if (_contentLength < _contentWritten || _last && _contentLength != _contentWritten) - content_length = null; + // Generate fields + if (fields != null) + { + for (HttpFields.Field field : fields) + { + HttpHeader name = HttpHeader.CACHE.get(field.getName()); - // write the field to the header buffer - field.putTo(buffer); - break; - - case CONTENT_TYPE: - if (field.getValue().startsWith(MimeTypes.Type.MULTIPART_BYTERANGES.toString())) - _contentLength = HttpTokens.SELF_DEFINING_CONTENT; - - // write the field to the header buffer - content_type=true; - field.putTo(buffer); - break; - - case TRANSFER_ENCODING: - if (_version == HttpVersion.HTTP_1_1) - transfer_encoding = field; - // Do NOT add yet! - break; - - case CONNECTION: - if (isRequest()) - field.putTo(buffer); - - value = HttpHeaderValue.CACHE.get(field.getValue()); - - switch (value==null?HttpHeaderValue.UNKNOWN:value) + switch (name==null?HttpHeader.UNKNOWN:name) + { + case CONTENT_LENGTH: { - case UPGRADE: + long length = field.getLongValue(); + if (length>=0) { - // special case for websocket connection ordering - if (isResponse()) + if (length < _contentPrepared || last && length != _contentPrepared) + LOG.warn("Incorrect ContentLength ignored ",new Throwable()); + else { - field.putTo(buffer); - continue; + // write the field to the header + field.putTo(header); + _contentLength=length; + content_length=true; } } - case CLOSE: + break; + } + + case CONTENT_TYPE: + { + if (field.getValue().startsWith(MimeTypes.Type.MULTIPART_BYTERANGES.toString())) + _contentLength = HttpTokens.SELF_DEFINING_CONTENT; + + // write the field to the header + content_type=true; + field.putTo(header); + break; + } + + case TRANSFER_ENCODING: + { + if (_version == HttpVersion.HTTP_1_1) + transfer_encoding = field; + // Do NOT add yet! + break; + } + + case CONNECTION: + { + if (isRequest()) + field.putTo(header); + + // Lookup and/or split connection value field + HttpHeaderValue[] values = new HttpHeaderValue[]{HttpHeaderValue.CACHE.get(field.getValue())}; + String[] split = null; + + if (values[0]==null) { - close=true; - if (isResponse()) - _persistent=false; - if (!_persistent && isResponse() && _contentLength == HttpTokens.UNKNOWN_CONTENT) - _contentLength = HttpTokens.EOF_CONTENT; - break; - } - case KEEP_ALIVE: - { - if (_version == HttpVersion.HTTP_1_0) + split = field.getValue().split("\\s*,\\s*"); + if (split.length>0) { - keep_alive = true; - if (isResponse()) - _persistent=true; + values=new HttpHeaderValue[split.length]; + for (int i=0;i0 || content_type ) && !_noContent) - { - // known length but not actually set. - buffer.put(HttpHeader.CONTENT_LENGTH.toBytes()); - buffer.put(HttpTokens.COLON); - buffer.put((byte) ' '); - BufferUtil.putDecLong(buffer, _contentLength); - buffer.put(HttpTokens.CRLF); - } - } - else - { - // No idea, so we must assume that a body is coming - _contentLength = (!_persistent || _version.ordinal() < HttpVersion.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) - buffer.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 && !HttpHeaderValue.CHUNKED.toString().equalsIgnoreCase(transfer_encoding.getValue())) + // 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) { - String c = transfer_encoding.getValue(); - if (c.endsWith(HttpHeaderValue.CHUNKED.toString())) - transfer_encoding.putTo(buffer); + 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 (_contentPrepared == 0 && isResponse() && (_status < 200 || _status == 204 || _status == 304)) + _contentLength = HttpTokens.NO_CONTENT; + else if (last) + { + // we have seen all the _content there is + _contentLength = _contentPrepared+BufferUtil.remaining(content); + if (!content_length && (isResponse() || _contentLength>0 || content_type ) && !_noContent) + { + // known length but not actually set. + header.put(HttpHeader.CONTENT_LENGTH.toBytesColonSpace()); + BufferUtil.putDecLong(header, _contentLength); + header.put(HttpTokens.CRLF); + } + } + else + { + // No idea, so we must assume that a body is coming + _contentLength = (!_persistent || _version.ordinal() < HttpVersion.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 && 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 && !HttpHeaderValue.CHUNKED.toString().equalsIgnoreCase(transfer_encoding.getValue())) + { + String c = transfer_encoding.getValue(); + if (c.endsWith(HttpHeaderValue.CHUNKED.toString())) + transfer_encoding.putTo(header); + else + throw new IllegalArgumentException("BAD TE"); + } else - throw new IllegalArgumentException("BAD TE"); + 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.ordinal() > HttpVersion.HTTP_1_0.ordinal())) + { + if (connection==null) + header.put(CONNECTION_CLOSE); + else + { + header.put(CONNECTION_CLOSE,0,CONNECTION_CLOSE.length-2); + header.put((byte)','); + header.put(StringUtil.getBytes(connection.toString())); + header.put(CRLF); + } + } + else if (keep_alive) + { + if (connection==null) + header.put(CONNECTION_KEEP_ALIVE); + else + { + header.put(CONNECTION_KEEP_ALIVE,0,CONNECTION_CLOSE.length-2); + header.put((byte)','); + header.put(StringUtil.getBytes(connection.toString())); + header.put(CRLF); + } + } + else if (connection!=null) + { + header.put(CONNECTION_); + header.put(StringUtil.getBytes(connection.toString())); + header.put(CRLF); + } + } + + if (!has_server && _status>199 && getSendServerVersion()) + header.put(SERVER); + + // end the header. + header.put(HttpTokens.CRLF); + _state = _contentLength==HttpTokens.CHUNKED_CONTENT?State.CHUNKING:State.STREAMING; + + } + catch(BufferOverflowException e) + { + throw new RuntimeException("Header>"+header.capacity(),e); + } + finally + { + BufferUtil.flipToFlush(header,pos); + } + } + + // Handle the content + if (BufferUtil.hasContent(content)) + { + // Can we do a direct flush + if (BufferUtil.isEmpty(buffer) && content.remaining()>_largeContent) + { + _contentPrepared+=content.remaining(); + if (_state==State.CHUNKING) + prepareChunk(header,content.remaining()); + return Result.FLUSH_CONTENT; + } + + // we copy content to buffer + // if we don't have one, we need one + if (buffer==null) + return Result.NEED_BUFFER; + + _contentPrepared+=BufferUtil.put(content,buffer); + + if (_state==State.CHUNKING) + prepareChunk(header,buffer.remaining()); + } + + if (BufferUtil.hasContent(buffer)) + { + if (last && BufferUtil.isEmpty(content) || _contentLength>0&&_contentLength==_contentPrepared) + return Result.NEED_COMPLETE; + + return Result.FLUSH; + } + return Result.OK; + } + + /* ------------------------------------------------------------ */ + public Result prepareContent(ByteBuffer chunk, ByteBuffer buffer, ByteBuffer content) + { + switch (_state) + { + case START: + // Can we do a direct flush + if (BufferUtil.isEmpty(buffer) && content.remaining()>_largeContent) + return Result.NEED_COMMIT; + + // we copy content to buffer + // if we don't have one, we need one + if (buffer==null) + return Result.NEED_BUFFER; + + // are we limited by content length? + if (_contentLength>0) + { + _contentPrepared+=BufferUtil.put(content,buffer,_contentLength-_contentPrepared); + if (_contentPrepared==_contentLength) + return Result.NEED_COMMIT; } else - buffer.put(TRANSFER_ENCODING_CHUNKED); - } + _contentPrepared+=BufferUtil.put(content,buffer); - // Handle connection if need be - if (_contentLength==HttpTokens.EOF_CONTENT) - { - keep_alive=false; - _persistent=false; - } + // are we full? + if (BufferUtil.isAtCapacity(buffer)) + return Result.NEED_COMMIT; - if (isResponse()) - { - if (!_persistent && (close || _version.ordinal() > HttpVersion.HTTP_1_0.ordinal())) + return Result.OK; + + case STREAMING: + // Can we do a direct flush + if (BufferUtil.isEmpty(buffer) && content.remaining()>_largeContent) { - if (connection==null) - buffer.put(CONNECTION_CLOSE); - else + if (_contentLength>0) { - buffer.put(CONNECTION_CLOSE,0,CONNECTION_CLOSE.length-2); - buffer.put((byte)','); - buffer.put(connection.toString().getBytes()); - buffer.put(CRLF); + long total=_contentPrepared+content.remaining(); + if (total>_contentLength) + throw new IllegalStateException(); + if (total==_contentLength) + return Result.NEED_COMPLETE; } + _contentPrepared+=content.remaining(); + return Result.FLUSH_CONTENT; } - else if (keep_alive) + + // we copy content to buffer + // if we don't have one, we need one + if (buffer==null) + return Result.NEED_BUFFER; + + // are we limited by content length? + if (_contentLength>0) { - if (connection==null) - buffer.put(CONNECTION_KEEP_ALIVE); - else - { - buffer.put(CONNECTION_KEEP_ALIVE,0,CONNECTION_CLOSE.length-2); - buffer.put((byte)','); - buffer.put(connection.toString().getBytes()); - buffer.put(CRLF); - } + _contentPrepared+=BufferUtil.put(content,buffer,_contentLength-_contentPrepared); + if (_contentPrepared==_contentLength) + return Result.NEED_COMPLETE; } - else if (connection!=null) + else + _contentPrepared+=BufferUtil.put(content,buffer); + + // are we full? + if (BufferUtil.isAtCapacity(buffer)) + return Result.FLUSH; + + return Result.OK; + + + case CHUNKING: + { + if (chunk==null) + return Result.NEED_CHUNK; + + // Can we do a direct flush + if (BufferUtil.isEmpty(buffer) && content.remaining()>_largeContent) { - buffer.put(CONNECTION_); - buffer.put(connection.toString().getBytes()); - buffer.put(CRLF); + _contentPrepared+=content.remaining(); + BufferUtil.clear(chunk); + prepareChunk(chunk,content.remaining()); + return Result.FLUSH_CONTENT; } + // we copy content to buffer + // if we don't have one, we need one + if (buffer==null) + return Result.NEED_BUFFER; + + _contentPrepared+=BufferUtil.put(content,buffer); + + // are we full? + if (BufferUtil.isAtCapacity(buffer)) + { + BufferUtil.clear(chunk); + prepareChunk(chunk,buffer.remaining()); + return Result.FLUSH; + } + + return Result.OK; } - - if (!has_server && _status>199 && getSendServerVersion()) - buffer.put(SERVER); - - // end the header. - buffer.put(HttpTokens.CRLF); - _state = State.CONTENT; - - } - catch(ArrayIndexOutOfBoundsException e) - { - throw new RuntimeException("Header>"+buffer.capacity(),e); - } + default: + throw new IllegalStateException(); + } } + /* ------------------------------------------------------------ */ - private void prepareBuffers() + private void prepareChunk(ByteBuffer chunk, int remaining) { - // if we are not flushing an existing chunk - if (!_bufferChunked) - { - // Refill buffer if possible - if (!_bypass && _content != null && _content.remaining() > 0 && _buffer != null && _buffer.space() > 0) - { - int len = _buffer.put(_content); - _content.skip(len); - if (_content.remaining() == 0) - _content = null; - } - - // Chunk buffer if need be - if (_contentLength == HttpTokens.CHUNKED_CONTENT) - { - if (_bypass && (_buffer==null||_buffer.remaining()==0) && _content!=null) - { - // this is a bypass write - int size = _content.remaining(); - _bufferChunked = true; - - if (_header == null) - _header = _buffers.getHeader(); - - // if we need CRLF add this to header - if (_needCRLF) - { - if (_header.remaining() > 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.remaining(); - 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.remaining() > 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.remaining() == 0)) - { - if (_needCRLF) - { - if (_buffer == null && _header.space() >= 2) - { - _header.put(HttpTokens.CRLF); - _needCRLF = false; - } - else if (_buffer!=null && _buffer.space() >= 2) - { - _buffer.put(HttpTokens.CRLF); - _needCRLF = false; - } - } - - if (!_needCRLF && _needEOC) - { - if (_buffer == 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.remaining() == 0) - _content = null; + // if we need CRLF add this to header + if (_needCRLF) + BufferUtil.putCRLF(chunk); + + // Add the chunk size to the header + BufferUtil.putHexInt(chunk, remaining); + BufferUtil.putCRLF(chunk); + // Need a CRLF after the content + _needCRLF=remaining>0; } + /* ------------------------------------------------------------ */ + /** + * Complete the message. + * + * @throws IOException + */ + public Result flush(ByteBuffer chunk, ByteBuffer buffer) throws IOException + { + switch(_state) + { + case START: + return Result.NEED_COMMIT; + case CHUNKING: + if (chunk==null) + return Result.NEED_CHUNK; + + if (BufferUtil.hasContent(buffer)) + { + BufferUtil.clear(chunk); + prepareChunk(chunk,buffer.remaining()); + } + } + return Result.FLUSH; + } + + /* ------------------------------------------------------------ */ + /** + * Complete the message. + * + * @throws IOException + */ + public Result complete(ByteBuffer chunk, ByteBuffer buffer) throws IOException + { + if (_state == State.END) + return Result.OK; + + switch(_state) + { + case START: + return Result.NEED_COMMIT; + + case CHUNKING: + if (chunk==null) + return Result.NEED_CHUNK; + + if (BufferUtil.hasContent(buffer)) + { + _state=State.COMPLETING; + BufferUtil.clear(chunk); + prepareChunk(chunk,buffer.remaining()); + return Result.FLUSH; + } + + if (!_sentEOC) + { + _state=State.END; + prepareChunk(chunk,0); + _sentEOC=true; + return Result.FLUSH; + } + return Result.OK; + + case STREAMING: + if (BufferUtil.hasContent(buffer)) + { + _state=State.COMPLETING; + return Result.FLUSH; + } + _state=State.END; + return Result.OK; + } + return Result.OK; + } + + /* ------------------------------------------------------------------------------- */ + public static byte[] getReasonBuffer(int code) + { + Status status = code<__status.length?__status[code]:null; + if (status!=null) + return status._reason; + return null; + } + + @Override public String toString() { diff --git a/jetty-http/src/test/java/org/eclipse/jetty/http/HttpGeneratorTest.java b/jetty-http/src/test/java/org/eclipse/jetty/http/HttpGeneratorTest.java index 792d75109e2..ac7ace20327 100644 --- a/jetty-http/src/test/java/org/eclipse/jetty/http/HttpGeneratorTest.java +++ b/jetty-http/src/test/java/org/eclipse/jetty/http/HttpGeneratorTest.java @@ -43,40 +43,82 @@ public class HttpGeneratorTest { ByteBuffer header=BufferUtil.allocate(8096); HttpFields fields = new HttpFields(); - HttpGenerator hg = new HttpGenerator(); + HttpGenerator gen = new HttpGenerator(); fields.add("Host","something"); fields.add("User-Agent","test"); - hg.setRequest(HttpMethod.GET,"/index.html",HttpVersion.HTTP_1_1); - hg.completeHeader(header,fields,true); - hg.complete(); - + gen.setRequest(HttpMethod.GET,"/index.html",HttpVersion.HTTP_1_1); + + HttpGenerator.Result + result=gen.complete(null,null); + assertEquals(HttpGenerator.Result.NEED_COMMIT,result); + + result=gen.commit(fields,header,null,null,true); String out = BufferUtil.toString(header); + BufferUtil.clear(header); + assertEquals(HttpGenerator.Result.NEED_COMPLETE,result); + result=gen.complete(null,null); + assertEquals(HttpGenerator.Result.OK,result); + assertTrue(out.indexOf("GET /index.html HTTP/1.1")==0); assertTrue(out.indexOf("Content-Length")==-1); - } + + assertEquals(HttpGenerator.State.END,gen.getState()); + assertEquals(0,gen.getContentWritten()); } @Test - public void testRequestWithContent() throws Exception + public void testRequestWithSmallContent() throws Exception { ByteBuffer header=BufferUtil.allocate(8096); + ByteBuffer buffer=BufferUtil.allocate(8096); + ByteBuffer content=BufferUtil.toBuffer("Hello World"); HttpFields fields = new HttpFields(); - HttpGenerator hg = new HttpGenerator(); + HttpGenerator gen = new HttpGenerator(); - hg.setRequest("GET","/index.html"); + gen.setVersion(HttpVersion.HTTP_1_1); + gen.setRequest("POST","/index.html"); fields.add("Host","something"); fields.add("User-Agent","test"); - hg.setVersion(HttpVersion.HTTP_1_1); - hg.completeHeader(header,fields,true); - hg.complete(); + HttpGenerator.Result + + result=gen.prepareContent(null,null,content); + assertEquals(HttpGenerator.Result.NEED_BUFFER,result); + + result=gen.prepareContent(null,buffer,content); + assertEquals(HttpGenerator.Result.OK,result); + assertEquals("Hello World",BufferUtil.toString(buffer)); + assertTrue(BufferUtil.isEmpty(content)); + result=gen.complete(null,buffer); + assertEquals(HttpGenerator.Result.NEED_COMMIT,result); + result=gen.commit(fields,header,buffer,content,true); + assertEquals(HttpGenerator.Result.FLUSH,result); String out = BufferUtil.toString(header); + BufferUtil.clear(header); + BufferUtil.clear(buffer); + + result=gen.complete(null,buffer); + assertEquals(HttpGenerator.Result.OK,result); + + + result=gen.commit(fields,header,null,null,true); + assertEquals(HttpGenerator.Result.NEED_COMPLETE,result); + result=gen.complete(null,null); + assertEquals(HttpGenerator.Result.OK,result); + + assertTrue(out.indexOf("GET /index.html HTTP/1.1")==0); assertTrue(out.indexOf("Content-Length")==-1); + + assertEquals(HttpGenerator.State.END,gen.getState()); + assertEquals(0,gen.getContentWritten()); } + + + @Test public void testHTTP() throws Exception { diff --git a/jetty-io/src/main/java/org/eclipse/jetty/io/nio/SslConnection.java b/jetty-io/src/main/java/org/eclipse/jetty/io/nio/SslConnection.java index 42c253bf2ce..54bd5b42237 100644 --- a/jetty-io/src/main/java/org/eclipse/jetty/io/nio/SslConnection.java +++ b/jetty-io/src/main/java/org/eclipse/jetty/io/nio/SslConnection.java @@ -291,7 +291,7 @@ public class SslConnection extends AbstractConnection implements AsyncConnection boolean progress=process(null,toFlush); // if we received any data, - if (!BufferUtil.isEmpty(_unwrapBuf)) + if (BufferUtil.hasContent(_unwrapBuf)) { // transfer from temp buffer to fill buffer BufferUtil.put(_unwrapBuf,toFill); @@ -302,7 +302,7 @@ public class SslConnection extends AbstractConnection implements AsyncConnection return progress; } // Else if there is some temporary data - else if (!BufferUtil.isEmpty(_unwrapBuf)) + else if (BufferUtil.hasContent(_unwrapBuf)) { // transfer from temp buffer to fill buffer BufferUtil.put(_unwrapBuf,toFill); @@ -332,7 +332,7 @@ public class SslConnection extends AbstractConnection implements AsyncConnection _inbound.compact().flip(); // flush any output data - if (!BufferUtil.isEmpty(_outbound) && (flushed=_endp.flush(_outbound))>0) + if (BufferUtil.hasContent(_outbound) && (flushed=_endp.flush(_outbound))>0) { progress = true; _outbound.compact().flip(); @@ -358,11 +358,11 @@ public class SslConnection extends AbstractConnection implements AsyncConnection case NOT_HANDSHAKING: { // Try unwrapping some application data - if (!BufferUtil.isAtCapacity(toFill) && !BufferUtil.isEmpty(_inbound) && unwrap(toFill)) + if (!BufferUtil.isAtCapacity(toFill) && BufferUtil.hasContent(_inbound) && unwrap(toFill)) progress=true; // Try wrapping some application data - if (!BufferUtil.isEmpty(toFlush) && !BufferUtil.isAtCapacity(_outbound) && wrap(toFlush)) + if (BufferUtil.hasContent(toFlush) && !BufferUtil.isAtCapacity(_outbound) && wrap(toFlush)) progress=true; } break; @@ -418,7 +418,7 @@ public class SslConnection extends AbstractConnection implements AsyncConnection } // If we are reading into the temp buffer and it has some content, then we should be dispatched. - if (toFill==_unwrapBuf && !BufferUtil.isEmpty(_unwrapBuf)) + if (toFill==_unwrapBuf && BufferUtil.hasContent(_unwrapBuf)) _aEndp.asyncDispatch(); } finally @@ -592,8 +592,8 @@ public class SslConnection extends AbstractConnection implements AsyncConnection synchronized (SslConnection.this) { return _endp.isInputShutdown() && - !(_unwrapBuf!=null&&!BufferUtil.isEmpty(_unwrapBuf)) && - !(_inbound!=null&&!BufferUtil.isEmpty(_inbound)); + !(_unwrapBuf!=null&&BufferUtil.hasContent(_unwrapBuf)) && + !(_inbound!=null&&BufferUtil.hasContent(_inbound)); } } @@ -624,9 +624,9 @@ public class SslConnection extends AbstractConnection implements AsyncConnection public int flush(ByteBuffer header, ByteBuffer buffer) throws IOException { - if (!BufferUtil.isEmpty(header)) + if (BufferUtil.hasContent(header)) return flush(header); - if (!BufferUtil.isEmpty(buffer)) + if (BufferUtil.hasContent(buffer)) return flush(buffer); return 0; } diff --git a/jetty-io/src/test/java/org/eclipse/jetty/io/nio/BufferUtilTest.java b/jetty-io/src/test/java/org/eclipse/jetty/io/nio/BufferUtilTest.java new file mode 100644 index 00000000000..c3f5115a760 --- /dev/null +++ b/jetty-io/src/test/java/org/eclipse/jetty/io/nio/BufferUtilTest.java @@ -0,0 +1,169 @@ +package org.eclipse.jetty.io.nio; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.nio.ByteBuffer; + +import org.eclipse.jetty.util.BufferUtil; +import org.junit.Test; + +public class BufferUtilTest +{ + + @Test + public void testPut() throws Exception + { + ByteBuffer to = BufferUtil.allocate(10); + ByteBuffer from=BufferUtil.toBuffer("12345"); + + BufferUtil.clear(to); + assertEquals(5,BufferUtil.put(from,to)); + assertTrue(BufferUtil.isEmpty(from)); + assertEquals("12345",BufferUtil.toString(to)); + + from=BufferUtil.toBuffer("XX67890ZZ"); + from.position(2); + + assertEquals(5,BufferUtil.put(from,to)); + assertEquals(2,from.remaining()); + assertEquals("1234567890",BufferUtil.toString(to)); + } + + @Test + public void testPutUnderMax() throws Exception + { + ByteBuffer to = BufferUtil.allocate(10); + ByteBuffer from=BufferUtil.toBuffer("12345"); + + BufferUtil.clear(to); + assertEquals(5,BufferUtil.put(from,to,10)); + assertTrue(BufferUtil.isEmpty(from)); + assertEquals("12345",BufferUtil.toString(to)); + + from=BufferUtil.toBuffer("XX67890ZZ"); + from.position(2); + + assertEquals(5,BufferUtil.put(from,to,10)); + assertEquals(2,from.remaining()); + assertEquals("1234567890",BufferUtil.toString(to)); + } + + @Test + public void testPutAtMax() throws Exception + { + ByteBuffer to = BufferUtil.allocate(10); + ByteBuffer from=BufferUtil.toBuffer("12345"); + + BufferUtil.clear(to); + assertEquals(5,BufferUtil.put(from,to,5)); + assertTrue(BufferUtil.isEmpty(from)); + assertEquals("12345",BufferUtil.toString(to)); + + from=BufferUtil.toBuffer("XX67890ZZ"); + from.position(2); + + assertEquals(5,BufferUtil.put(from,to,5)); + assertEquals(2,from.remaining()); + assertEquals("1234567890",BufferUtil.toString(to)); + } + + + @Test + public void testPutOverMax() throws Exception + { + ByteBuffer to = BufferUtil.allocate(10); + ByteBuffer from=BufferUtil.toBuffer("12345"); + + BufferUtil.clear(to); + assertEquals(4,BufferUtil.put(from,to,4)); + assertEquals(1,from.remaining()); + assertEquals("1234",BufferUtil.toString(to)); + + from=BufferUtil.toBuffer("XX567890ZZ"); + from.position(2); + + assertEquals(4,BufferUtil.put(from,to,4)); + assertEquals(4,from.remaining()); + assertEquals("12345678",BufferUtil.toString(to)); + } + + + @Test + public void testPutDirect() throws Exception + { + ByteBuffer to = BufferUtil.allocateDirect(10); + ByteBuffer from=BufferUtil.toBuffer("12345"); + + BufferUtil.clear(to); + assertEquals(5,BufferUtil.put(from,to)); + assertTrue(BufferUtil.isEmpty(from)); + assertEquals("12345",BufferUtil.toString(to)); + + from=BufferUtil.toBuffer("XX67890ZZ"); + from.position(2); + + assertEquals(5,BufferUtil.put(from,to)); + assertEquals(2,from.remaining()); + assertEquals("1234567890",BufferUtil.toString(to)); + } + + @Test + public void testPutUnderMaxDirect() throws Exception + { + ByteBuffer to = BufferUtil.allocateDirect(10); + ByteBuffer from=BufferUtil.toBuffer("12345"); + + BufferUtil.clear(to); + assertEquals(5,BufferUtil.put(from,to,10)); + assertTrue(BufferUtil.isEmpty(from)); + assertEquals("12345",BufferUtil.toString(to)); + + from=BufferUtil.toBuffer("XX67890ZZ"); + from.position(2); + + assertEquals(5,BufferUtil.put(from,to,10)); + assertEquals(2,from.remaining()); + assertEquals("1234567890",BufferUtil.toString(to)); + } + + @Test + public void testPutAtMaxDirect() throws Exception + { + ByteBuffer to = BufferUtil.allocateDirect(10); + ByteBuffer from=BufferUtil.toBuffer("12345"); + + BufferUtil.clear(to); + assertEquals(5,BufferUtil.put(from,to,5)); + assertTrue(BufferUtil.isEmpty(from)); + assertEquals("12345",BufferUtil.toString(to)); + + from=BufferUtil.toBuffer("XX67890ZZ"); + from.position(2); + + assertEquals(5,BufferUtil.put(from,to,5)); + assertEquals(2,from.remaining()); + assertEquals("1234567890",BufferUtil.toString(to)); + } + + + @Test + public void testPutOverMaxDirect() throws Exception + { + ByteBuffer to = BufferUtil.allocateDirect(10); + ByteBuffer from=BufferUtil.toBuffer("12345"); + + BufferUtil.clear(to); + assertEquals(4,BufferUtil.put(from,to,4)); + assertEquals(1,from.remaining()); + assertEquals("1234",BufferUtil.toString(to)); + + from=BufferUtil.toBuffer("XX567890ZZ"); + from.position(2); + + assertEquals(4,BufferUtil.put(from,to,4)); + assertEquals(4,from.remaining()); + assertEquals("12345678",BufferUtil.toString(to)); + } +} diff --git a/jetty-io/src/test/java/org/eclipse/jetty/io/nio/SelectChannelEndPointTest.java b/jetty-io/src/test/java/org/eclipse/jetty/io/nio/SelectChannelEndPointTest.java index aafd6d24665..d957e4f88db 100644 --- a/jetty-io/src/test/java/org/eclipse/jetty/io/nio/SelectChannelEndPointTest.java +++ b/jetty-io/src/test/java/org/eclipse/jetty/io/nio/SelectChannelEndPointTest.java @@ -138,15 +138,15 @@ public class SelectChannelEndPointTest progress=true; } - if (!BufferUtil.isEmpty(_in) && BufferUtil.put(_in,_out)>0) + if (BufferUtil.hasContent(_in) && BufferUtil.put(_in,_out)>0) progress=true; - if (!BufferUtil.isEmpty(_out) && _endp.flush(_out)>0) + if (BufferUtil.hasContent(_out) && _endp.flush(_out)>0) progress=true; _out.compact().flip(); - if (!!BufferUtil.isEmpty(_out) && _endp.isInputShutdown()) + if (BufferUtil.isEmpty(_out) && _endp.isInputShutdown()) _endp.shutdownOutput(); } return this; diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/BufferUtil.java b/jetty-util/src/main/java/org/eclipse/jetty/util/BufferUtil.java index b95d03440a4..52a1941aeb7 100644 --- a/jetty-util/src/main/java/org/eclipse/jetty/util/BufferUtil.java +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/BufferUtil.java @@ -68,7 +68,24 @@ public class BufferUtil buf.limit(0); return buf; } + + /* ------------------------------------------------------------ */ + public static void flipToFill(ByteBuffer buffer) + { + buffer.position(buffer.limit()); + buffer.limit(buffer.capacity()); + } + + + /* ------------------------------------------------------------ */ + public static void flipToFlush(ByteBuffer buffer,int position) + { + buffer.limit(buffer.position()); + buffer.position(position); + } + + /* ------------------------------------------------------------ */ public static byte[] toArray(ByteBuffer buffer) { @@ -89,17 +106,93 @@ public class BufferUtil return buf==null || buf.remaining()==0; } + /* ------------------------------------------------------------ */ + public static boolean hasContent(ByteBuffer buf) + { + return buf!=null && buf.remaining()>0; + } + /* ------------------------------------------------------------ */ public static boolean isAtCapacity(ByteBuffer buf) { return buf!=null && buf.limit()==buf.capacity(); } + + /* ------------------------------------------------------------ */ + public static long remaining(ByteBuffer buffer) + { + return buffer==null?0:buffer.remaining(); + } /* ------------------------------------------------------------ */ /** * Put data from one buffer into another, avoiding over/under flows - * @param from - * @param to + * @param from Buffer to take bytes from + * @param to Buffer to put bytes to + * @return number of bytes moved + */ + public static int put(ByteBuffer from, ByteBuffer to, long maxBytes) + { + return put(from,to,maxBytes>=Integer.MAX_VALUE?Integer.MAX_VALUE:(int)maxBytes); + } + + + /* ------------------------------------------------------------ */ + /** + * Put data from one buffer into another, avoiding over/under flows + * @param from Buffer to take bytes from + * @param to Buffer to put bytes to + * @return number of bytes moved + */ + public static int put(ByteBuffer from, ByteBuffer to, int maxBytes) + { + int put; + int pos=to.position(); + try + { + flipToFill(to); + + maxBytes=Math.min(maxBytes,to.remaining()); + int remaining=from.remaining(); + if (remaining>0) + { + if (remaining<=maxBytes) + { + to.put(from); + put=remaining; + } + else if (from.hasArray()) + { + put=maxBytes; + to.put(from.array(),from.arrayOffset()+from.position(),put); + from.position(from.position()+put); + } + else + { + put=maxBytes; + ByteBuffer slice=from.slice(); + slice.limit(put); + to.put(slice); + from.position(from.position()+put); + } + } + else + put=0; + + } + finally + { + flipToFlush(to,pos); + } + return put; + + } + /* ------------------------------------------------------------ */ + /** + * Put data from one buffer into another, avoiding over/under flows + * @param from Buffer to take bytes from + * @param to Buffer to put bytes to + * @return number of bytes moved */ public static int put(ByteBuffer from, ByteBuffer to) { @@ -107,8 +200,7 @@ public class BufferUtil int pos=to.position(); try { - to.position(to.limit()); - to.limit(to.capacity()); + flipToFill(to); int remaining=from.remaining(); if (remaining>0) @@ -139,8 +231,7 @@ public class BufferUtil } finally { - to.limit(to.position()); - to.position(pos); + flipToFlush(to,pos); } return put; @@ -558,4 +649,6 @@ public class BufferUtil } + + }