From d31fa31cdcc5ea2fa96116e3b1265baa180df58a Mon Sep 17 00:00:00 2001 From: Jakob Buchgraber Date: Tue, 14 Apr 2015 21:14:00 -0700 Subject: [PATCH] Faster and more memory efficient headers for HTTP, HTTP/2, STOMP and SPYD. Fixes #3600 Motivation: We noticed that the headers implementation in Netty for HTTP/2 uses quite a lot of memory and that also at least the performance of randomly accessing a header is quite poor. The main concern however was memory usage, as profiling has shown that a DefaultHttp2Headers not only use a lot of memory it also wastes a lot due to the underlying hashmaps having to be resized potentially several times as new headers are being inserted. This is tracked as issue #3600. Modifications: We redesigned the DefaultHeaders to simply take a Map object in its constructor and reimplemented the class using only the Map primitives. That way the implementation is very concise and hopefully easy to understand and it allows each concrete headers implementation to provide its own map or to even use a different headers implementation for processing requests and writing responses i.e. incoming headers need to provide fast random access while outgoing headers need fast insertion and fast iteration. The new implementation can support this with hardly any code changes. It also comes with the advantage that if the Netty project decides to add a third party collections library as a dependency, one can simply plug in one of those very fast and memory efficient map implementations and get faster and smaller headers for free. For now, we are using the JDK's TreeMap for HTTP and HTTP/2 default headers. Result: - Significantly fewer lines of code in the implementation. While the total commit is still roughly 400 lines less, the actual implementation is a lot less. I just added some more tests and microbenchmarks. - Overall performance is up. The current implementation should be significantly faster for insertion and retrieval. However, it is slower when it comes to iteration. There is simply no way a TreeMap can have the same iteration performance as a linked list (as used in the current headers implementation). That's totally fine though, because when looking at the benchmark results @ejona86 pointed out that the performance of the headers is completely dominated by insertion, that is insertion is so significantly faster in the new implementation that it does make up for several times the iteration speed. You can't iterate what you haven't inserted. I am demonstrating that in this spreadsheet [1]. (Actually, iteration performance is only down for HTTP, it's significantly improved for HTTP/2). - Memory is down. The implementation with TreeMap uses on avg ~30% less memory. It also does not produce any garbage while being resized. In load tests for GRPC we have seen a memory reduction of up to 1.2KB per RPC. I summarized the memory improvements in this spreadsheet [1]. The data was generated by [2] using JOL. - While it was my original intend to only improve the memory usage for HTTP/2, it should be similarly improved for HTTP, SPDY and STOMP as they all share a common implementation. [1] https://docs.google.com/spreadsheets/d/1ck3RQklyzEcCLlyJoqDXPCWRGVUuS-ArZf0etSXLVDQ/edit#gid=0 [2] https://gist.github.com/buchgr/4458a8bdb51dd58c82b4 --- .../codec/http/DefaultFullHttpRequest.java | 2 +- .../codec/http/DefaultHttpHeaders.java | 366 +++--- .../codec/http/DefaultLastHttpContent.java | 31 +- .../handler/codec/http/HttpHeaderUtil.java | 5 +- .../codec/http/HttpHeadersEncoder.java | 17 +- .../handler/codec/http/HttpObjectEncoder.java | 5 +- .../codec/spdy/DefaultSpdyHeaders.java | 65 +- .../handler/codec/spdy/SpdyCodecUtil.java | 3 + .../codec/http/DefaultHttpHeadersTest.java | 59 + .../codec/spdy/SpdySessionHandlerTest.java | 11 +- .../codec/http2/DefaultHttp2Headers.java | 120 +- .../http2/DefaultHttp2HeadersEncoder.java | 23 +- .../handler/codec/http2/Http2Headers.java | 10 + .../netty/handler/codec/http2/HttpUtil.java | 38 +- .../InboundHttp2ToHttpPriorityAdapter.java | 11 +- .../codec/http2/DefaultHttp2HeadersTest.java | 68 +- .../handler/codec/http2/Http2TestUtil.java | 6 +- .../codec/stomp/DefaultStompHeaders.java | 6 + .../codec/stomp/StompSubframeEncoder.java | 9 +- .../codec/stomp/StompTestConstants.java | 2 +- .../handler/codec/AsciiHeadersEncoder.java | 7 +- .../io/netty/handler/codec/BinaryHeaders.java | 11 - .../handler/codec/ConvertibleHeaders.java | 7 - .../handler/codec/DefaultBinaryHeaders.java | 241 ++-- .../codec/DefaultConvertibleHeaders.java | 33 +- .../netty/handler/codec/DefaultHeaders.java | 1008 +++++------------ .../handler/codec/DefaultTextHeaders.java | 320 +++--- .../codec/EmptyConvertibleHeaders.java | 8 +- .../io/netty/handler/codec/EmptyHeaders.java | 50 +- .../netty/handler/codec/EmptyTextHeaders.java | 5 - .../java/io/netty/handler/codec/Headers.java | 747 ++++++------ .../io/netty/handler/codec/TextHeaders.java | 19 - .../codec/DefaultBinaryHeadersTest.java | 575 ++++++---- .../handler/codec/DefaultTextHeadersTest.java | 88 +- .../main/java/io/netty/util/AsciiString.java | 85 +- .../example/http/upload/HttpUploadClient.java | 9 +- .../microbench/headers/ExampleHeaders.java | 148 +++ .../microbench/headers/HeadersBenchmark.java | 157 +++ .../microbench/headers/package-info.java | 19 + 39 files changed, 2040 insertions(+), 2354 deletions(-) create mode 100644 codec-http/src/test/java/io/netty/handler/codec/http/DefaultHttpHeadersTest.java create mode 100644 microbench/src/main/java/io/netty/microbench/headers/ExampleHeaders.java create mode 100644 microbench/src/main/java/io/netty/microbench/headers/HeadersBenchmark.java create mode 100644 microbench/src/main/java/io/netty/microbench/headers/package-info.java diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/DefaultFullHttpRequest.java b/codec-http/src/main/java/io/netty/handler/codec/http/DefaultFullHttpRequest.java index 1fbae3bdb5..5178d339c5 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/DefaultFullHttpRequest.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/DefaultFullHttpRequest.java @@ -36,7 +36,7 @@ public class DefaultFullHttpRequest extends DefaultHttpRequest implements FullHt } public DefaultFullHttpRequest(HttpVersion httpVersion, HttpMethod method, String uri, boolean validateHeaders) { - this(httpVersion, method, uri, Unpooled.buffer(0), true); + this(httpVersion, method, uri, Unpooled.buffer(0), validateHeaders); } public DefaultFullHttpRequest(HttpVersion httpVersion, HttpMethod method, String uri, diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/DefaultHttpHeaders.java b/codec-http/src/main/java/io/netty/handler/codec/http/DefaultHttpHeaders.java index 704c1426c3..ab384e43cc 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/DefaultHttpHeaders.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/DefaultHttpHeaders.java @@ -15,14 +15,18 @@ */ package io.netty.handler.codec.http; +import io.netty.handler.codec.DefaultHeaders; import io.netty.handler.codec.DefaultTextHeaders; import io.netty.handler.codec.TextHeaders; import io.netty.util.AsciiString; -import io.netty.util.ByteProcessor; -import io.netty.util.internal.PlatformDependent; import java.util.Calendar; +import java.util.Comparator; import java.util.Date; +import java.util.List; +import java.util.TreeMap; + +import static io.netty.util.internal.ObjectUtil.checkNotNull; public class DefaultHttpHeaders extends DefaultTextHeaders implements HttpHeaders { @@ -46,234 +50,25 @@ public class DefaultHttpHeaders extends DefaultTextHeaders implements HttpHeader LOOKUP_TABLE['='] = -1; } - private static final class HttpHeadersValidationConverter extends DefaultTextValueTypeConverter { - private final boolean validate; - - HttpHeadersValidationConverter(boolean validate) { - this.validate = validate; - } - - @Override - public CharSequence convertObject(Object value) { - if (value == null) { - throw new NullPointerException("value"); - } - - CharSequence seq; - if (value instanceof CharSequence) { - seq = (CharSequence) value; - } else if (value instanceof Number) { - seq = value.toString(); - } else if (value instanceof Date) { - seq = HttpHeaderDateFormat.get().format((Date) value); - } else if (value instanceof Calendar) { - seq = HttpHeaderDateFormat.get().format(((Calendar) value).getTime()); - } else { - seq = value.toString(); - } - - if (validate) { - if (value instanceof AsciiString) { - validateValue((AsciiString) seq); - } else { - validateValue(seq); - } - } - - return seq; - } - - private static final class ValidateValueProcessor implements ByteProcessor { - private final CharSequence seq; - private int state; - - public ValidateValueProcessor(CharSequence seq) { - this.seq = seq; - } - - @Override - public boolean process(byte value) throws Exception { - state = validateValueChar(state, (char) value, seq); - return true; - } - - public int state() { - return state; - } - } - - private static void validateValue(AsciiString seq) { - ValidateValueProcessor processor = new ValidateValueProcessor(seq); - try { - seq.forEachByte(processor); - } catch (Throwable t) { - PlatformDependent.throwException(t); - } - - if (processor.state() != 0) { - throw new IllegalArgumentException("a header value must not end with '\\r' or '\\n':" + seq); - } - } - - private static void validateValue(CharSequence seq) { - int state = 0; - // Start looping through each of the character - for (int index = 0; index < seq.length(); index++) { - state = validateValueChar(state, seq.charAt(index), seq); - } - - if (state != 0) { - throw new IllegalArgumentException("a header value must not end with '\\r' or '\\n':" + seq); - } - } - - private static int validateValueChar(int state, char c, CharSequence seq) { - /* - * State: - * 0: Previous character was neither CR nor LF - * 1: The previous character was CR - * 2: The previous character was LF - */ - if ((c & HIGHEST_INVALID_VALUE_CHAR_MASK) == 0) { - // Check the absolutely prohibited characters. - switch (c) { - case 0x0: // NULL - throw new IllegalArgumentException("a header value contains a prohibited character '\0': " + seq); - case 0x0b: // Vertical tab - throw new IllegalArgumentException("a header value contains a prohibited character '\\v': " + seq); - case '\f': - throw new IllegalArgumentException("a header value contains a prohibited character '\\f': " + seq); - } - } - - // Check the CRLF (HT | SP) pattern - switch (state) { - case 0: - switch (c) { - case '\r': - state = 1; - break; - case '\n': - state = 2; - break; - } - break; - case 1: - switch (c) { - case '\n': - state = 2; - break; - default: - throw new IllegalArgumentException("only '\\n' is allowed after '\\r': " + seq); - } - break; - case 2: - switch (c) { - case '\t': - case ' ': - state = 0; - break; - default: - throw new IllegalArgumentException("only ' ' and '\\t' are allowed after '\\n': " + seq); - } - } - return state; - } - } - - static class HttpHeadersNameConverter implements NameConverter { - protected final boolean validate; - - private static final class ValidateNameProcessor implements ByteProcessor { - private final CharSequence seq; - - public ValidateNameProcessor(CharSequence seq) { - this.seq = seq; - } - - @Override - public boolean process(byte value) throws Exception { - // Check to see if the character is not an ASCII character. - if (value < 0) { - throw new IllegalArgumentException("a header name cannot contain non-ASCII character: " + seq); - } - validateNameChar(value, seq); - return true; - } - } - - HttpHeadersNameConverter(boolean validate) { - this.validate = validate; - } - - @Override - public CharSequence convertName(CharSequence name) { - if (validate) { - if (name instanceof AsciiString) { - validateName((AsciiString) name); - } else { - validateName(name); - } - } - - return name; - } - - private static void validateName(AsciiString name) { - try { - name.forEachByte(new ValidateNameProcessor(name)); - } catch (Throwable t) { - PlatformDependent.throwException(t); - } - } - - private static void validateName(CharSequence name) { - // Go through each characters in the name. - for (int index = 0; index < name.length(); index++) { - char c = name.charAt(index); - - // Check to see if the character is not an ASCII character. - if (c > 127) { - throw new IllegalArgumentException("a header name cannot contain non-ASCII characters: " + name); - } - - // Check for prohibited characters. - validateNameChar(c, name); - } - } - - private static void validateNameChar(int character, CharSequence seq) { - if ((character & HIGHEST_INVALID_NAME_CHAR_MASK) == 0 && LOOKUP_TABLE[character] != 0) { - throw new IllegalArgumentException( - "a header name cannot contain the following prohibited characters: =,;: \\t\\r\\n\\v\\f: " + - seq); - } - } - } - - private static final HttpHeadersValidationConverter - VALIDATE_OBJECT_CONVERTER = new HttpHeadersValidationConverter(true); - private static final HttpHeadersValidationConverter - NO_VALIDATE_OBJECT_CONVERTER = new HttpHeadersValidationConverter(false); - private static final HttpHeadersNameConverter VALIDATE_NAME_CONVERTER = new HttpHeadersNameConverter(true); - private static final HttpHeadersNameConverter NO_VALIDATE_NAME_CONVERTER = new HttpHeadersNameConverter(false); - public DefaultHttpHeaders() { this(true); } public DefaultHttpHeaders(boolean validate) { - this(true, validate? VALIDATE_NAME_CONVERTER : NO_VALIDATE_NAME_CONVERTER, false); + this(validate, false); } protected DefaultHttpHeaders(boolean validate, boolean singleHeaderFields) { - this(true, validate? VALIDATE_NAME_CONVERTER : NO_VALIDATE_NAME_CONVERTER, singleHeaderFields); + this(true, validate ? HeaderNameValidator.INSTANCE : NO_NAME_VALIDATOR, singleHeaderFields); } - protected DefaultHttpHeaders(boolean validate, NameConverter nameConverter, + protected DefaultHttpHeaders(boolean validate, + DefaultHeaders.NameValidator nameValidator, boolean singleHeaderFields) { - super(true, validate ? VALIDATE_OBJECT_CONVERTER : NO_VALIDATE_OBJECT_CONVERTER, nameConverter, - singleHeaderFields); + super(new TreeMap(AsciiString.CHARSEQUENCE_CASE_INSENSITIVE_ORDER), + nameValidator, + validate ? HeaderValueConverterAndValidator.INSTANCE : HeaderValueConverter.INSTANCE, + singleHeaderFields); } @Override @@ -479,4 +274,137 @@ public class DefaultHttpHeaders extends DefaultTextHeaders implements HttpHeader super.clear(); return this; } + + @Override + public int hashCode() { + return size(); + } + + @Override + public boolean equals(Object other) { + if (!(other instanceof HttpHeaders)) { + return false; + } + HttpHeaders headers = (HttpHeaders) other; + return DefaultHeaders.comparatorEquals(this, headers, AsciiString.CHARSEQUENCE_CASE_SENSITIVE_ORDER); + } + + static final class HeaderNameValidator implements DefaultHeaders.NameValidator { + + public static final HeaderNameValidator INSTANCE = new HeaderNameValidator(); + + private HeaderNameValidator() { + } + + @Override + public void validate(CharSequence name) { + // Go through each character in the name + for (int index = 0; index < name.length(); index++) { + char character = name.charAt(index); + + // Check to see if the character is not an ASCII character + if (character > 127) { + throw new IllegalArgumentException("a header name cannot contain non-ASCII characters: " + name); + } + + // Check for prohibited characters. + if ((character & HIGHEST_INVALID_NAME_CHAR_MASK) == 0 && LOOKUP_TABLE[character] != 0) { + throw new IllegalArgumentException( + "a header name cannot contain the following prohibited characters: =,;: \\t\\r\\n\\v\\f: " + + name); + } + } + } + } + + private static class HeaderValueConverter extends CharSequenceConverter { + + public static final HeaderValueConverter INSTANCE = new HeaderValueConverter(); + + @Override + public CharSequence convertObject(Object value) { + checkNotNull(value, "value"); + CharSequence seq; + if (value instanceof CharSequence) { + seq = (CharSequence) value; + } else if (value instanceof Number) { + seq = value.toString(); + } else if (value instanceof Date) { + seq = HttpHeaderDateFormat.get().format((Date) value); + } else if (value instanceof Calendar) { + seq = HttpHeaderDateFormat.get().format(((Calendar) value).getTime()); + } else { + seq = value.toString(); + } + return seq; + } + } + + private static final class HeaderValueConverterAndValidator extends HeaderValueConverter { + + public static final HeaderValueConverterAndValidator INSTANCE = new HeaderValueConverterAndValidator(); + + @Override + public CharSequence convertObject(Object value) { + CharSequence seq = super.convertObject(value); + int state = 0; + // Start looping through each of the character + for (int index = 0; index < seq.length(); index++) { + state = validateValueChar(seq, state, seq.charAt(index)); + } + + if (state != 0) { + throw new IllegalArgumentException("a header value must not end with '\\r' or '\\n':" + seq); + } + return seq; + } + + private static int validateValueChar(CharSequence seq, int state, char character) { + /* + * State: + * 0: Previous character was neither CR nor LF + * 1: The previous character was CR + * 2: The previous character was LF + */ + if ((character & HIGHEST_INVALID_VALUE_CHAR_MASK) == 0) { + // Check the absolutely prohibited characters. + switch (character) { + case 0x0: // NULL + throw new IllegalArgumentException("a header value contains a prohibited character '\0': " + seq); + case 0x0b: // Vertical tab + throw new IllegalArgumentException("a header value contains a prohibited character '\\v': " + seq); + case '\f': + throw new IllegalArgumentException("a header value contains a prohibited character '\\f': " + seq); + } + } + + // Check the CRLF (HT | SP) pattern + switch (state) { + case 0: + switch (character) { + case '\r': + return 1; + case '\n': + return 2; + } + break; + case 1: + switch (character) { + case '\n': + return 2; + default: + throw new IllegalArgumentException("only '\\n' is allowed after '\\r': " + seq); + } + case 2: + switch (character) { + case '\t': + case ' ': + return 0; + default: + throw new IllegalArgumentException("only ' ' and '\\t' are allowed after '\\n': " + seq); + } + } + return state; + } + } } diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/DefaultLastHttpContent.java b/codec-http/src/main/java/io/netty/handler/codec/http/DefaultLastHttpContent.java index 935ec61a9c..3836e0b059 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/DefaultLastHttpContent.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/DefaultLastHttpContent.java @@ -17,6 +17,7 @@ package io.netty.handler.codec.http; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; +import io.netty.handler.codec.DefaultHeaders; import io.netty.util.internal.StringUtil; import java.util.Map; @@ -107,32 +108,24 @@ public class DefaultLastHttpContent extends DefaultHttpContent implements LastHt } private static final class TrailingHttpHeaders extends DefaultHttpHeaders { - private static final class TrailingHttpHeadersNameConverter extends HttpHeadersNameConverter { - TrailingHttpHeadersNameConverter(boolean validate) { - super(validate); - } + private static final class TrailingHttpHeadersNameValidator implements + DefaultHeaders.NameValidator { + + private static final TrailingHttpHeadersNameValidator INSTANCE = new TrailingHttpHeadersNameValidator(); @Override - public CharSequence convertName(CharSequence name) { - name = super.convertName(name); - if (validate) { - if (HttpHeaderNames.CONTENT_LENGTH.equalsIgnoreCase(name) - || HttpHeaderNames.TRANSFER_ENCODING.equalsIgnoreCase(name) - || HttpHeaderNames.TRAILER.equalsIgnoreCase(name)) { - throw new IllegalArgumentException("prohibited trailing header: " + name); - } + public void validate(CharSequence name) { + HeaderNameValidator.INSTANCE.validate(name); + if (HttpHeaderNames.CONTENT_LENGTH.equalsIgnoreCase(name) + || HttpHeaderNames.TRANSFER_ENCODING.equalsIgnoreCase(name) + || HttpHeaderNames.TRAILER.equalsIgnoreCase(name)) { + throw new IllegalArgumentException("prohibited trailing header: " + name); } - return name; } } - private static final TrailingHttpHeadersNameConverter - VALIDATE_NAME_CONVERTER = new TrailingHttpHeadersNameConverter(true); - private static final TrailingHttpHeadersNameConverter - NO_VALIDATE_NAME_CONVERTER = new TrailingHttpHeadersNameConverter(false); - TrailingHttpHeaders(boolean validate) { - super(validate, validate ? VALIDATE_NAME_CONVERTER : NO_VALIDATE_NAME_CONVERTER, false); + super(validate, validate ? TrailingHttpHeadersNameValidator.INSTANCE : NO_NAME_VALIDATOR, false); } } } diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/HttpHeaderUtil.java b/codec-http/src/main/java/io/netty/handler/codec/http/HttpHeaderUtil.java index 3fd220db50..6aeff9a37d 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/HttpHeaderUtil.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/HttpHeaderUtil.java @@ -18,6 +18,7 @@ package io.netty.handler.codec.http; import io.netty.buffer.ByteBuf; +import java.util.ArrayList; import java.util.Iterator; import java.util.List; @@ -230,7 +231,9 @@ public final class HttpHeaderUtil { m.headers().add(HttpHeaderNames.TRANSFER_ENCODING, HttpHeaderValues.CHUNKED); m.headers().remove(HttpHeaderNames.CONTENT_LENGTH); } else { - List values = m.headers().getAll(HttpHeaderNames.TRANSFER_ENCODING); + // Make a copy to be able to modify values while iterating + List values = + new ArrayList(m.headers().getAll(HttpHeaderNames.TRANSFER_ENCODING)); if (values.isEmpty()) { return; } diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/HttpHeadersEncoder.java b/codec-http/src/main/java/io/netty/handler/codec/http/HttpHeadersEncoder.java index 559c6f3ecb..544a3c5302 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/HttpHeadersEncoder.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/HttpHeadersEncoder.java @@ -18,24 +18,14 @@ package io.netty.handler.codec.http; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufUtil; -import io.netty.handler.codec.TextHeaders.EntryVisitor; import io.netty.util.AsciiString; -import java.util.Map.Entry; +final class HttpHeadersEncoder { -final class HttpHeadersEncoder implements EntryVisitor { - - private final ByteBuf buf; - - HttpHeadersEncoder(ByteBuf buf) { - this.buf = buf; + private HttpHeadersEncoder() { } - @Override - public boolean visit(Entry entry) throws Exception { - final CharSequence name = entry.getKey(); - final CharSequence value = entry.getValue(); - final ByteBuf buf = this.buf; + public static void encoderHeader(CharSequence name, CharSequence value, ByteBuf buf) throws Exception { final int nameLen = name.length(); final int valueLen = value.length(); final int entryLen = nameLen + valueLen + 4; @@ -50,7 +40,6 @@ final class HttpHeadersEncoder implements EntryVisitor { buf.setByte(offset ++, '\r'); buf.setByte(offset ++, '\n'); buf.writerIndex(offset); - return true; } private static void writeAscii(ByteBuf buf, int offset, CharSequence value, int valueLen) { diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/HttpObjectEncoder.java b/codec-http/src/main/java/io/netty/handler/codec/http/HttpObjectEncoder.java index 1affe26155..8eff14fa51 100755 --- a/codec-http/src/main/java/io/netty/handler/codec/http/HttpObjectEncoder.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/HttpObjectEncoder.java @@ -24,6 +24,7 @@ import io.netty.util.internal.PlatformDependent; import io.netty.util.internal.StringUtil; import java.util.List; +import java.util.Map.Entry; import static io.netty.buffer.Unpooled.*; import static io.netty.handler.codec.http.HttpConstants.*; @@ -137,7 +138,9 @@ public abstract class HttpObjectEncoder extends MessageTo * Encode the {@link HttpHeaders} into a {@link ByteBuf}. */ protected void encodeHeaders(HttpHeaders headers, ByteBuf buf) throws Exception { - headers.forEachEntry(new HttpHeadersEncoder(buf)); + for (Entry header : headers) { + HttpHeadersEncoder.encoderHeader(header.getKey(), header.getValue(), buf); + } } private void encodeChunkedContent(ChannelHandlerContext ctx, Object msg, long contentLength, List out) { diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdyHeaders.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdyHeaders.java index 53c00b087a..270812c713 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdyHeaders.java +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/DefaultSpdyHeaders.java @@ -16,44 +16,17 @@ package io.netty.handler.codec.spdy; import io.netty.handler.codec.DefaultTextHeaders; -import io.netty.handler.codec.Headers; import io.netty.handler.codec.TextHeaders; -import io.netty.util.AsciiString; -import java.util.Locale; +import java.util.LinkedHashMap; public class DefaultSpdyHeaders extends DefaultTextHeaders implements SpdyHeaders { - private static final Headers.ValueConverter SPDY_VALUE_CONVERTER = - new DefaultTextValueTypeConverter() { - @Override - public CharSequence convertObject(Object value) { - CharSequence seq; - if (value instanceof CharSequence) { - seq = (CharSequence) value; - } else { - seq = value.toString(); - } - - SpdyCodecUtil.validateHeaderValue(seq); - return seq; - } - }; - - private static final NameConverter SPDY_NAME_CONVERTER = new NameConverter() { - @Override - public CharSequence convertName(CharSequence name) { - if (name instanceof AsciiString) { - name = ((AsciiString) name).toLowerCase(); - } else { - name = name.toString().toLowerCase(Locale.US); - } - SpdyCodecUtil.validateHeaderName(name); - return name; - } - }; public DefaultSpdyHeaders() { - super(true, SPDY_VALUE_CONVERTER, SPDY_NAME_CONVERTER); + super(new LinkedHashMap(), + HeaderNameValidator.INSTANCE, + HeaderValueConverterAndValidator.INSTANCE, + false); } @Override @@ -259,4 +232,32 @@ public class DefaultSpdyHeaders extends DefaultTextHeaders implements SpdyHeader super.clear(); return this; } + + private static class HeaderNameValidator implements NameValidator { + + public static final HeaderNameValidator INSTANCE = new HeaderNameValidator(); + + @Override + public void validate(CharSequence name) { + SpdyCodecUtil.validateHeaderName(name); + } + } + + private static class HeaderValueConverterAndValidator extends CharSequenceConverter { + + public static final HeaderValueConverterAndValidator INSTANCE = new HeaderValueConverterAndValidator(); + + @Override + public CharSequence convertObject(Object value) { + CharSequence seq; + if (value instanceof CharSequence) { + seq = (CharSequence) value; + } else { + seq = value.toString(); + } + + SpdyCodecUtil.validateHeaderValue(seq); + return seq; + } + } } diff --git a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyCodecUtil.java b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyCodecUtil.java index 612ebdbcb2..b25167c3a9 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyCodecUtil.java +++ b/codec-http/src/main/java/io/netty/handler/codec/spdy/SpdyCodecUtil.java @@ -305,6 +305,9 @@ final class SpdyCodecUtil { throw new IllegalArgumentException( "name contains null character: " + name); } + if (c >= 'A' && c <= 'Z') { + throw new IllegalArgumentException("name must be all lower case."); + } if (c > 127) { throw new IllegalArgumentException( "name contains non-ascii character: " + name); diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/DefaultHttpHeadersTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/DefaultHttpHeadersTest.java new file mode 100644 index 0000000000..e19a5b4d48 --- /dev/null +++ b/codec-http/src/test/java/io/netty/handler/codec/http/DefaultHttpHeadersTest.java @@ -0,0 +1,59 @@ +/* + * Copyright 2015 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.http; + +import org.junit.Test; + +import java.util.List; + +import static java.util.Arrays.asList; +import static org.junit.Assert.assertEquals; + +public class DefaultHttpHeadersTest { + + @Test + public void keysShouldBeCaseInsensitive() { + DefaultHttpHeaders headers = new DefaultHttpHeaders(); + headers.add("Name", "value1"); + headers.add("name", "value2"); + headers.add("NAME", "value3"); + assertEquals(3, headers.size()); + + List values = asList("value1", "value2", "value3"); + + assertEquals(values, headers.getAll("NAME")); + assertEquals(values, headers.getAll("name")); + assertEquals(values, headers.getAll("Name")); + assertEquals(values, headers.getAll("nAmE")); + } + + @Test + public void keysShouldBeCaseInsensitiveInHeadersEquals() { + DefaultHttpHeaders headers1 = new DefaultHttpHeaders(); + headers1.add("name1", "value1", "value2", "value3"); + headers1.add("nAmE2", "value4"); + + DefaultHttpHeaders headers2 = new DefaultHttpHeaders(); + headers2.add("naMe1", "value1", "value2", "value3"); + headers2.add("NAME2", "value4"); + + assertEquals(headers1, headers1); + assertEquals(headers2, headers2); + assertEquals(headers1, headers2); + assertEquals(headers2, headers1); + assertEquals(headers1.hashCode(), headers2.hashCode()); + } +} diff --git a/codec-http/src/test/java/io/netty/handler/codec/spdy/SpdySessionHandlerTest.java b/codec-http/src/test/java/io/netty/handler/codec/spdy/SpdySessionHandlerTest.java index 4c8a54bf85..a249b9d80c 100644 --- a/codec-http/src/test/java/io/netty/handler/codec/spdy/SpdySessionHandlerTest.java +++ b/codec-http/src/test/java/io/netty/handler/codec/spdy/SpdySessionHandlerTest.java @@ -22,6 +22,7 @@ import io.netty.util.internal.logging.InternalLogger; import io.netty.util.internal.logging.InternalLoggerFactory; import org.junit.Test; +import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -83,7 +84,7 @@ public class SpdySessionHandlerTest { assertEquals(last, spdyHeadersFrame.isLast()); for (CharSequence name: headers.names()) { List expectedValues = headers.getAll(name); - List receivedValues = spdyHeadersFrame.headers().getAll(name); + List receivedValues = new ArrayList(spdyHeadersFrame.headers().getAll(name)); assertTrue(receivedValues.containsAll(expectedValues)); receivedValues.removeAll(expectedValues); assertTrue(receivedValues.isEmpty()); @@ -105,7 +106,7 @@ public class SpdySessionHandlerTest { SpdySynStreamFrame spdySynStreamFrame = new DefaultSpdySynStreamFrame(localStreamId, 0, (byte) 0); - spdySynStreamFrame.headers().set("Compression", "test"); + spdySynStreamFrame.headers().set("compression", "test"); SpdyDataFrame spdyDataFrame = new DefaultSpdyDataFrame(localStreamId); spdyDataFrame.setLast(true); @@ -138,8 +139,8 @@ public class SpdySessionHandlerTest { assertNull(sessionHandler.readOutbound()); SpdyHeadersFrame spdyHeadersFrame = new DefaultSpdyHeadersFrame(localStreamId); - spdyHeadersFrame.headers().add("HEADER", "test1"); - spdyHeadersFrame.headers().add("HEADER", "test2"); + spdyHeadersFrame.headers().add("header", "test1"); + spdyHeadersFrame.headers().add("header", "test2"); sessionHandler.writeInbound(spdyHeadersFrame); assertHeaders(sessionHandler.readOutbound(), localStreamId, false, spdyHeadersFrame.headers()); @@ -245,7 +246,7 @@ public class SpdySessionHandlerTest { SpdySynStreamFrame spdySynStreamFrame = new DefaultSpdySynStreamFrame(localStreamId, 0, (byte) 0); - spdySynStreamFrame.headers().set("Compression", "test"); + spdySynStreamFrame.headers().set("compression", "test"); SpdyDataFrame spdyDataFrame = new DefaultSpdyDataFrame(localStreamId); spdyDataFrame.setLast(true); diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2Headers.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2Headers.java index 1b515de968..a62d32ad2b 100644 --- a/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2Headers.java +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2Headers.java @@ -14,86 +14,19 @@ */ package io.netty.handler.codec.http2; -import static io.netty.util.internal.StringUtil.UPPER_CASE_TO_LOWER_CASE_ASCII_OFFSET; import io.netty.handler.codec.BinaryHeaders; import io.netty.handler.codec.DefaultBinaryHeaders; -import io.netty.util.AsciiString; -import io.netty.util.ByteProcessor; +import io.netty.handler.codec.DefaultHeaders; import io.netty.util.ByteString; -import io.netty.util.internal.PlatformDependent; +import java.io.Serializable; +import java.util.Comparator; +import java.util.List; +import java.util.TreeMap; public class DefaultHttp2Headers extends DefaultBinaryHeaders implements Http2Headers { - private static final ByteProcessor HTTP2_ASCII_UPPERCASE_PROCESSOR = new ByteProcessor() { - @Override - public boolean process(byte value) throws Exception { - return value < 'A' || value > 'Z'; - } - }; - private static final class Http2AsciiToLowerCaseConverter implements ByteProcessor { - private final byte[] result; - private int i; - - public Http2AsciiToLowerCaseConverter(int length) { - result = new byte[length]; - } - - @Override - public boolean process(byte value) throws Exception { - result[i++] = (value >= 'A' && value <= 'Z') - ? (byte) (value + UPPER_CASE_TO_LOWER_CASE_ASCII_OFFSET) : value; - return true; - } - - public byte[] result() { - return result; - } - }; - - private static final NameConverter HTTP2_ASCII_TO_LOWER_CONVERTER = new NameConverter() { - @Override - public ByteString convertName(ByteString name) { - if (name instanceof AsciiString) { - return ((AsciiString) name).toLowerCase(); - } - - try { - if (name.forEachByte(HTTP2_ASCII_UPPERCASE_PROCESSOR) == -1) { - return name; - } - - Http2AsciiToLowerCaseConverter converter = new Http2AsciiToLowerCaseConverter(name.length()); - name.forEachByte(converter); - return new ByteString(converter.result(), false); - } catch (Exception e) { - PlatformDependent.throwException(e); - return null; - } - } - }; - - /** - * Creates an instance that will convert all header names to lowercase. - */ public DefaultHttp2Headers() { - this(true); - } - - /** - * Creates an instance that can be configured to either do header field name conversion to - * lowercase, or not do any conversion at all. - *

- * - * Note that setting {@code forceKeyToLower} to {@code false} can violate the - * HTTP/2 specification - * which specifies that a request or response containing an uppercase header field MUST be treated - * as malformed. Only set {@code forceKeyToLower} to {@code false} if you are explicitly using lowercase - * header field names and want to avoid the conversion to lowercase. - * - * @param forceKeyToLower if @{code false} no header name conversion will be performed - */ - public DefaultHttp2Headers(boolean forceKeyToLower) { - super(forceKeyToLower ? HTTP2_ASCII_TO_LOWER_CONVERTER : IDENTITY_NAME_CONVERTER); + super(new TreeMap(Http2HeaderNameComparator.INSTANCE)); } @Override @@ -354,4 +287,45 @@ public class DefaultHttp2Headers extends DefaultBinaryHeaders implements Http2He public ByteString status() { return get(PseudoHeaderName.STATUS.value()); } + + @Override + public int hashCode() { + return size(); + } + + @Override + public boolean equals(Object other) { + if (!(other instanceof Http2Headers)) { + return false; + } + Http2Headers headers = (Http2Headers) other; + return DefaultHeaders.comparatorEquals(this, headers, ByteString.DEFAULT_COMPARATOR); + } + + private static class Http2HeaderNameComparator implements Comparator, Serializable { + + public static final Http2HeaderNameComparator INSTANCE = new Http2HeaderNameComparator(); + private static final long serialVersionUID = 1109871697664666478L; + + @Override + public int compare(ByteString one, ByteString two) { + // Reserved header names come first. + final boolean isPseudoHeader1 = !one.isEmpty() && one.byteAt(0) == ':'; + final boolean isPseudoHeader2 = !two.isEmpty() && two.byteAt(0) == ':'; + if (isPseudoHeader1 != isPseudoHeader2) { + return isPseudoHeader1 ? -1 : 1; + } + final int delta = one.hashCode() - two.hashCode(); + if (delta == 0) { + // If the hash code matches it's very likely for the two strings to be equal + // and thus we optimistically compare them with the much faster equals method. + if (one.equals(two)) { + return 0; + } else { + return ByteString.DEFAULT_COMPARATOR.compare(one, two); + } + } + return delta; + } + } } diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2HeadersEncoder.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2HeadersEncoder.java index 28be6e898f..f0af6d5244 100644 --- a/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2HeadersEncoder.java +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2HeadersEncoder.java @@ -23,9 +23,9 @@ import static io.netty.handler.codec.http2.Http2Exception.connectionError; import static io.netty.util.internal.ObjectUtil.checkNotNull; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufOutputStream; -import io.netty.handler.codec.BinaryHeaders.EntryVisitor; import io.netty.util.ByteString; + import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; @@ -65,26 +65,9 @@ public class DefaultHttp2HeadersEncoder implements Http2HeadersEncoder, Http2Hea tableSizeChangeOutput.reset(); } - // Write pseudo headers first as required by the HTTP/2 spec. - for (Http2Headers.PseudoHeaderName pseudoHeader : Http2Headers.PseudoHeaderName.values()) { - ByteString name = pseudoHeader.value(); - ByteString value = headers.get(name); - if (value != null) { - encodeHeader(name, value, stream); - } + for (Entry header : headers) { + encodeHeader(header.getKey(), header.getValue(), stream); } - - headers.forEachEntry(new EntryVisitor() { - @Override - public boolean visit(Entry entry) throws Exception { - final ByteString name = entry.getKey(); - final ByteString value = entry.getValue(); - if (!Http2Headers.PseudoHeaderName.isPseudoHeader(name)) { - encodeHeader(name, value, stream); - } - return true; - } - }); } catch (Http2Exception e) { throw e; } catch (Throwable t) { diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2Headers.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2Headers.java index 5b30fc713a..61a8bed800 100644 --- a/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2Headers.java +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2Headers.java @@ -20,6 +20,8 @@ import io.netty.util.ByteString; import io.netty.util.CharsetUtil; import java.util.HashSet; +import java.util.Iterator; +import java.util.Map.Entry; import java.util.Set; /** @@ -183,6 +185,14 @@ public interface Http2Headers extends BinaryHeaders { @Override Http2Headers clear(); + /** + * Returns an iterator over all HTTP/2 headers from this instance. The iteration order is as follows: + * 1. All non-pseudo headers (in no particular order). + * 2. Headers with multiple values will have their values appear in insertion order. + */ + @Override + Iterator> iterator(); + /** * Sets the {@link PseudoHeaderName#METHOD} header or {@code null} if there is no such header */ diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/HttpUtil.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/HttpUtil.java index 61c7bbb9db..86fb67cece 100644 --- a/codec-http2/src/main/java/io/netty/handler/codec/http2/HttpUtil.java +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/HttpUtil.java @@ -18,8 +18,6 @@ import static io.netty.handler.codec.http2.Http2Error.PROTOCOL_ERROR; import static io.netty.handler.codec.http2.Http2Exception.connectionError; import static io.netty.handler.codec.http2.Http2Exception.streamError; import static io.netty.util.internal.ObjectUtil.checkNotNull; -import io.netty.handler.codec.BinaryHeaders; -import io.netty.handler.codec.TextHeaders.EntryVisitor; import io.netty.handler.codec.http.DefaultFullHttpRequest; import io.netty.handler.codec.http.DefaultFullHttpResponse; import io.netty.handler.codec.http.FullHttpMessage; @@ -246,9 +244,11 @@ public final class HttpUtil { FullHttpMessage destinationMessage, boolean addToTrailer) throws Http2Exception { HttpHeaders headers = addToTrailer ? destinationMessage.trailingHeaders() : destinationMessage.headers(); boolean request = destinationMessage instanceof HttpRequest; - Http2ToHttpHeaderTranslator visitor = new Http2ToHttpHeaderTranslator(streamId, headers, request); + Http2ToHttpHeaderTranslator translator = new Http2ToHttpHeaderTranslator(streamId, headers, request); try { - sourceHeaders.forEachEntry(visitor); + for (Entry entry : sourceHeaders) { + translator.translate(entry); + } } catch (Http2Exception ex) { throw ex; } catch (Throwable t) { @@ -315,29 +315,25 @@ public final class HttpUtil { final Http2Headers out = new DefaultHttp2Headers(); - inHeaders.forEachEntry(new EntryVisitor() { - @Override - public boolean visit(Entry entry) throws Exception { - final AsciiString aName = AsciiString.of(entry.getKey()).toLowerCase(); - if (!HTTP_TO_HTTP2_HEADER_BLACKLIST.contains(aName)) { - AsciiString aValue = AsciiString.of(entry.getValue()); - // https://tools.ietf.org/html/draft-ietf-httpbis-http2-16#section-8.1.2.2 - // makes a special exception for TE - if (!aName.equalsIgnoreCase(HttpHeaderNames.TE) || - aValue.equalsIgnoreCase(HttpHeaderValues.TRAILERS)) { - out.add(aName, aValue); - } + for (Entry entry : inHeaders) { + final AsciiString aName = AsciiString.of(entry.getKey()).toLowerCase(); + if (!HTTP_TO_HTTP2_HEADER_BLACKLIST.contains(aName)) { + AsciiString aValue = AsciiString.of(entry.getValue()); + // https://tools.ietf.org/html/draft-ietf-httpbis-http2-16#section-8.1.2.2 + // makes a special exception for TE + if (!aName.equalsIgnoreCase(HttpHeaderNames.TE) || + aValue.equalsIgnoreCase(HttpHeaderValues.TRAILERS)) { + out.add(aName, aValue); } - return true; } - }); + } return out; } /** * A visitor which translates HTTP/2 headers to HTTP/1 headers */ - private static final class Http2ToHttpHeaderTranslator implements BinaryHeaders.EntryVisitor { + private static final class Http2ToHttpHeaderTranslator { /** * Translations from HTTP/2 header name to the HTTP/1.x equivalent. */ @@ -372,8 +368,7 @@ public final class HttpUtil { translations = request ? REQUEST_HEADER_TRANSLATIONS : RESPONSE_HEADER_TRANSLATIONS; } - @Override - public boolean visit(Entry entry) throws Http2Exception { + public void translate(Entry entry) throws Http2Exception { final ByteString name = entry.getKey(); final ByteString value = entry.getValue(); ByteString translatedName = translations.get(name); @@ -391,7 +386,6 @@ public final class HttpUtil { output.add(new AsciiString(translatedName, false), new AsciiString(value, false)); } } - return true; } } } diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/InboundHttp2ToHttpPriorityAdapter.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/InboundHttp2ToHttpPriorityAdapter.java index d4e4a0a1d3..6d047b6af3 100644 --- a/codec-http2/src/main/java/io/netty/handler/codec/http2/InboundHttp2ToHttpPriorityAdapter.java +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/InboundHttp2ToHttpPriorityAdapter.java @@ -20,7 +20,6 @@ import static io.netty.handler.codec.http2.Http2Exception.connectionError; import java.util.Map.Entry; import io.netty.channel.ChannelHandlerContext; -import io.netty.handler.codec.TextHeaders.EntryVisitor; import io.netty.handler.codec.http.DefaultHttpHeaders; import io.netty.handler.codec.http.FullHttpMessage; import io.netty.handler.codec.http.HttpHeaders; @@ -137,13 +136,9 @@ public final class InboundHttp2ToHttpPriorityAdapter extends InboundHttp2ToHttpA */ private static void addHttpHeadersToHttp2Headers(HttpHeaders httpHeaders, final Http2Headers http2Headers) { try { - httpHeaders.forEachEntry(new EntryVisitor() { - @Override - public boolean visit(Entry entry) throws Exception { - http2Headers.add(AsciiString.of(entry.getKey()), AsciiString.of(entry.getValue())); - return true; - } - }); + for (Entry entry : httpHeaders) { + http2Headers.add(AsciiString.of(entry.getKey()), AsciiString.of(entry.getValue())); + } } catch (Exception ex) { PlatformDependent.throwException(ex); } diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/DefaultHttp2HeadersTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/DefaultHttp2HeadersTest.java index 52dd37bb8f..4c4abc0a5b 100644 --- a/codec-http2/src/test/java/io/netty/handler/codec/http2/DefaultHttp2HeadersTest.java +++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/DefaultHttp2HeadersTest.java @@ -1,55 +1,57 @@ /* * Copyright 2014 The Netty Project * - * The Netty Project licenses this file to you under the Apache License, version - * 2.0 (the "License"); you may not use this file except in compliance with the - * License. You may obtain a copy of the License at: + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. + * License for the specific language governing permissions and limitations + * under the License. */ + package io.netty.handler.codec.http2; -import static org.junit.Assert.assertArrayEquals; -import static org.junit.Assert.assertEquals; -import io.netty.util.AsciiString; import io.netty.util.ByteString; import io.netty.util.CharsetUtil; - import org.junit.Test; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Map.Entry; + +import static java.util.Arrays.asList; +import static org.junit.Assert.assertTrue; + public class DefaultHttp2HeadersTest { - private static final byte[] NAME_BYTES = { 'T', 'E', 's', 'T' }; - private static final byte[] NAME_BYTES_LOWERCASE = { 't', 'e', 's', 't' }; - private static final ByteString NAME_BYTESTRING = new ByteString(NAME_BYTES); - private static final AsciiString NAME_ASCIISTRING = new AsciiString("Test"); - private static final ByteString VALUE = new ByteString("some value", CharsetUtil.UTF_8); - @Test - public void defaultLowercase() { - Http2Headers headers = new DefaultHttp2Headers().set(NAME_BYTESTRING, VALUE); - assertArrayEquals(NAME_BYTES_LOWERCASE, first(headers).toByteArray()); + public void pseudoHeadersMustComeFirstWhenIterating() { + DefaultHttp2Headers headers = new DefaultHttp2Headers(); + headers.add(bs("name1"), bs("value1"), bs("value2")); + headers.method(bs("POST")); + headers.add(bs("2name"), bs("value3")); + headers.path(bs("/index.html")); + headers.status(bs("200")); + headers.authority(bs("netty.io")); + headers.add(bs("name3"), bs("value4")); + headers.scheme(bs("https")); + + Iterator> iter = headers.iterator(); + List names = new ArrayList(); + for (int i = 0; i < 5; i++) { + names.add(iter.next().getKey()); + } + assertTrue(names.containsAll(asList(bs(":method"), bs(":status"), bs(":path"), bs(":scheme"), + bs(":authority")))); } - @Test - public void defaultLowercaseAsciiString() { - Http2Headers headers = new DefaultHttp2Headers().set(NAME_ASCIISTRING, VALUE); - assertEquals(NAME_ASCIISTRING.toLowerCase(), first(headers)); - } - - @Test - public void caseInsensitive() { - Http2Headers headers = new DefaultHttp2Headers(false).set(NAME_BYTESTRING, VALUE); - assertArrayEquals(NAME_BYTES, first(headers).toByteArray()); - } - - private static ByteString first(Http2Headers headers) { - return headers.names().iterator().next(); + private static ByteString bs(String str) { + return new ByteString(str, CharsetUtil.US_ASCII); } } diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2TestUtil.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2TestUtil.java index 2547e4cb67..3bfa13432e 100644 --- a/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2TestUtil.java +++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2TestUtil.java @@ -60,9 +60,9 @@ final class Http2TestUtil { } /** - * Converts a byte array into an {@link AsciiString}. + * Converts a byte array into a {@link ByteString}. */ - public static ByteString as(byte[] value) { + public static ByteString bs(byte[] value) { return new ByteString(value); } @@ -86,7 +86,7 @@ final class Http2TestUtil { * Returns an {@link AsciiString} that wraps a randomly-filled byte array. */ public static ByteString randomString() { - return as(randomBytes()); + return bs(randomBytes()); } private Http2TestUtil() { diff --git a/codec-stomp/src/main/java/io/netty/handler/codec/stomp/DefaultStompHeaders.java b/codec-stomp/src/main/java/io/netty/handler/codec/stomp/DefaultStompHeaders.java index eaf4ca60ff..a3bb2312ff 100644 --- a/codec-stomp/src/main/java/io/netty/handler/codec/stomp/DefaultStompHeaders.java +++ b/codec-stomp/src/main/java/io/netty/handler/codec/stomp/DefaultStompHeaders.java @@ -19,8 +19,14 @@ package io.netty.handler.codec.stomp; import io.netty.handler.codec.DefaultTextHeaders; import io.netty.handler.codec.TextHeaders; +import java.util.TreeMap; + public class DefaultStompHeaders extends DefaultTextHeaders implements StompHeaders { + public DefaultStompHeaders() { + super(new TreeMap(), NO_NAME_VALIDATOR, CharSequenceConverter.INSTANCE, false); + } + @Override public StompHeaders add(CharSequence name, CharSequence value) { super.add(name, value); diff --git a/codec-stomp/src/main/java/io/netty/handler/codec/stomp/StompSubframeEncoder.java b/codec-stomp/src/main/java/io/netty/handler/codec/stomp/StompSubframeEncoder.java index 668bd48f37..e57aa5e607 100644 --- a/codec-stomp/src/main/java/io/netty/handler/codec/stomp/StompSubframeEncoder.java +++ b/codec-stomp/src/main/java/io/netty/handler/codec/stomp/StompSubframeEncoder.java @@ -25,6 +25,7 @@ import io.netty.util.CharsetUtil; import io.netty.util.internal.PlatformDependent; import java.util.List; +import java.util.Map.Entry; /** * Encodes a {@link StompFrame} or a {@link StompSubframe} into a {@link ByteBuf}. @@ -66,11 +67,9 @@ public class StompSubframeEncoder extends MessageToMessageEncoder buf.writeBytes(frame.command().toString().getBytes(CharsetUtil.US_ASCII)); buf.writeByte(StompConstants.LF); - try { - frame.headers().forEachEntry(new AsciiHeadersEncoder(buf, SeparatorType.COLON, NewlineType.LF)); - } catch (Exception ex) { - buf.release(); - PlatformDependent.throwException(ex); + AsciiHeadersEncoder headersEncoder = new AsciiHeadersEncoder(buf, SeparatorType.COLON, NewlineType.LF); + for (Entry entry : frame.headers()) { + headersEncoder.encode(entry); } buf.writeByte(StompConstants.LF); return buf; diff --git a/codec-stomp/src/test/java/io/netty/handler/codec/stomp/StompTestConstants.java b/codec-stomp/src/test/java/io/netty/handler/codec/stomp/StompTestConstants.java index a427b944cc..e3e49a7daf 100644 --- a/codec-stomp/src/test/java/io/netty/handler/codec/stomp/StompTestConstants.java +++ b/codec-stomp/src/test/java/io/netty/handler/codec/stomp/StompTestConstants.java @@ -18,8 +18,8 @@ package io.netty.handler.codec.stomp; public final class StompTestConstants { public static final String CONNECT_FRAME = "CONNECT\n" + - "host:stomp.github.org\n" + "accept-version:1.1,1.2\n" + + "host:stomp.github.org\n" + '\n' + '\0'; public static final String CONNECTED_FRAME = diff --git a/codec/src/main/java/io/netty/handler/codec/AsciiHeadersEncoder.java b/codec/src/main/java/io/netty/handler/codec/AsciiHeadersEncoder.java index 0531809e81..e73518234f 100644 --- a/codec/src/main/java/io/netty/handler/codec/AsciiHeadersEncoder.java +++ b/codec/src/main/java/io/netty/handler/codec/AsciiHeadersEncoder.java @@ -21,10 +21,9 @@ import java.util.Map.Entry; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufUtil; -import io.netty.handler.codec.TextHeaders.EntryVisitor; import io.netty.util.AsciiString; -public final class AsciiHeadersEncoder implements EntryVisitor { +public final class AsciiHeadersEncoder { /** * The separator characters to insert between a header name and a header value. @@ -78,8 +77,7 @@ public final class AsciiHeadersEncoder implements EntryVisitor { this.newlineType = newlineType; } - @Override - public boolean visit(Entry entry) throws Exception { + public void encode(Entry entry) { final CharSequence name = entry.getKey(); final CharSequence value = entry.getValue(); final ByteBuf buf = this.buf; @@ -119,7 +117,6 @@ public final class AsciiHeadersEncoder implements EntryVisitor { } buf.writerIndex(offset); - return true; } private static void writeAscii(ByteBuf buf, int offset, CharSequence value, int valueLen) { diff --git a/codec/src/main/java/io/netty/handler/codec/BinaryHeaders.java b/codec/src/main/java/io/netty/handler/codec/BinaryHeaders.java index b220320da3..f2a4000836 100644 --- a/codec/src/main/java/io/netty/handler/codec/BinaryHeaders.java +++ b/codec/src/main/java/io/netty/handler/codec/BinaryHeaders.java @@ -24,17 +24,6 @@ import io.netty.util.ByteString; * some additional utility when handling text data. */ public interface BinaryHeaders extends Headers { - /** - * Provides an abstraction to iterate over elements maintained in the {@link Headers} collection. - */ - interface EntryVisitor extends Headers.EntryVisitor { - } - - /** - * Provides an abstraction to iterate over elements maintained in the {@link Headers} collection. - */ - interface NameVisitor extends Headers.NameVisitor { - } @Override BinaryHeaders add(ByteString name, ByteString value); diff --git a/codec/src/main/java/io/netty/handler/codec/ConvertibleHeaders.java b/codec/src/main/java/io/netty/handler/codec/ConvertibleHeaders.java index 6ef16561b5..ca3eae694a 100644 --- a/codec/src/main/java/io/netty/handler/codec/ConvertibleHeaders.java +++ b/codec/src/main/java/io/netty/handler/codec/ConvertibleHeaders.java @@ -89,13 +89,6 @@ public interface ConvertibleHeaders extends Head */ List getAllAndRemoveAndConvert(UnconvertedType name); - /** - * Invokes {@link Headers#entries()} and lazily does a conversion on the results as they are accessed - * - * @return The values corresponding to {@code name} and then lazily converted - */ - List> entriesConverted(); - /** * Invokes {@link Headers#iterator()} and lazily does a conversion on the results as they are accessed * diff --git a/codec/src/main/java/io/netty/handler/codec/DefaultBinaryHeaders.java b/codec/src/main/java/io/netty/handler/codec/DefaultBinaryHeaders.java index ae1b32b507..2183e5d71d 100644 --- a/codec/src/main/java/io/netty/handler/codec/DefaultBinaryHeaders.java +++ b/codec/src/main/java/io/netty/handler/codec/DefaultBinaryHeaders.java @@ -20,128 +20,24 @@ import io.netty.util.internal.PlatformDependent; import java.nio.charset.Charset; import java.text.ParseException; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.TreeMap; public class DefaultBinaryHeaders extends DefaultHeaders implements BinaryHeaders { - private static final ValueConverter OBJECT_TO_BYTE = new ValueConverter() { - private final Charset DEFAULT_CHARSET = CharsetUtil.UTF_8; - @Override - public ByteString convertObject(Object value) { - if (value instanceof ByteString) { - return (ByteString) value; - } - if (value instanceof CharSequence) { - return new ByteString((CharSequence) value, DEFAULT_CHARSET); - } - return new ByteString(value.toString(), DEFAULT_CHARSET); - } - @Override - public ByteString convertInt(int value) { - return new ByteString(String.valueOf(value), DEFAULT_CHARSET); - } - - @Override - public ByteString convertLong(long value) { - return new ByteString(String.valueOf(value), DEFAULT_CHARSET); - } - - @Override - public ByteString convertDouble(double value) { - return new ByteString(String.valueOf(value), DEFAULT_CHARSET); - } - - @Override - public ByteString convertChar(char value) { - return new ByteString(String.valueOf(value), DEFAULT_CHARSET); - } - - @Override - public ByteString convertBoolean(boolean value) { - return new ByteString(String.valueOf(value), DEFAULT_CHARSET); - } - - @Override - public ByteString convertFloat(float value) { - return new ByteString(String.valueOf(value), DEFAULT_CHARSET); - } - - @Override - public int convertToInt(ByteString value) { - return value.parseAsciiInt(); - } - - @Override - public long convertToLong(ByteString value) { - return value.parseAsciiLong(); - } - - @Override - public ByteString convertTimeMillis(long value) { - return new ByteString(String.valueOf(value), DEFAULT_CHARSET); - } - - @Override - public long convertToTimeMillis(ByteString value) { - try { - return HeaderDateFormat.get().parse(value.toString()); - } catch (ParseException e) { - PlatformDependent.throwException(e); - } - return 0; - } - - @Override - public double convertToDouble(ByteString value) { - return value.parseAsciiDouble(); - } - - @Override - public char convertToChar(ByteString value) { - return value.parseChar(); - } - - @Override - public boolean convertToBoolean(ByteString value) { - return value.byteAt(0) != 0; - } - - @Override - public float convertToFloat(ByteString value) { - return value.parseAsciiFloat(); - } - - @Override - public ByteString convertShort(short value) { - return new ByteString(String.valueOf(value), DEFAULT_CHARSET); - } - - @Override - public short convertToShort(ByteString value) { - return value.parseAsciiShort(); - } - - @Override - public ByteString convertByte(byte value) { - return new ByteString(String.valueOf(value), DEFAULT_CHARSET); - } - - @Override - public byte convertToByte(ByteString value) { - return value.byteAt(0); - } - }; - - private static final HashCodeGenerator JAVA_HASH_CODE_GENERATOR = - new JavaHashCodeGenerator(); - protected static final NameConverter IDENTITY_NAME_CONVERTER = new IdentityNameConverter(); + private static final NameValidator NO_NAME_VALIDATOR = DefaultHeaders.NoNameValidator.instance(); public DefaultBinaryHeaders() { - this(IDENTITY_NAME_CONVERTER); + this(new TreeMap(ByteString.DEFAULT_COMPARATOR)); } - public DefaultBinaryHeaders(NameConverter nameConverter) { - super(ByteString.DEFAULT_COMPARATOR, ByteString.DEFAULT_COMPARATOR, - JAVA_HASH_CODE_GENERATOR, OBJECT_TO_BYTE, nameConverter); + public DefaultBinaryHeaders(Map map) { + this(map, NO_NAME_VALIDATOR); + } + + public DefaultBinaryHeaders(Map map, NameValidator nameValidator) { + super(map, nameValidator, ByteStringConverter.INSTANCE); } @Override @@ -347,4 +243,119 @@ public class DefaultBinaryHeaders extends DefaultHeaders implements super.clear(); return this; } + + public static final class ByteStringConverter implements ValueConverter { + + public static final ByteStringConverter INSTANCE = new ByteStringConverter(); + private static final Charset DEFAULT_CHARSET = CharsetUtil.UTF_8; + + private ByteStringConverter() { + } + + @Override + public ByteString convertObject(Object value) { + if (value instanceof ByteString) { + return (ByteString) value; + } + if (value instanceof CharSequence) { + return new ByteString((CharSequence) value, DEFAULT_CHARSET); + } + return new ByteString(value.toString(), DEFAULT_CHARSET); + } + + @Override + public ByteString convertInt(int value) { + return new ByteString(String.valueOf(value), DEFAULT_CHARSET); + } + + @Override + public ByteString convertLong(long value) { + return new ByteString(String.valueOf(value), DEFAULT_CHARSET); + } + + @Override + public ByteString convertDouble(double value) { + return new ByteString(String.valueOf(value), DEFAULT_CHARSET); + } + + @Override + public ByteString convertChar(char value) { + return new ByteString(String.valueOf(value), DEFAULT_CHARSET); + } + + @Override + public ByteString convertBoolean(boolean value) { + return new ByteString(String.valueOf(value), DEFAULT_CHARSET); + } + + @Override + public ByteString convertFloat(float value) { + return new ByteString(String.valueOf(value), DEFAULT_CHARSET); + } + + @Override + public int convertToInt(ByteString value) { + return value.parseAsciiInt(); + } + + @Override + public long convertToLong(ByteString value) { + return value.parseAsciiLong(); + } + + @Override + public ByteString convertTimeMillis(long value) { + return new ByteString(String.valueOf(value), DEFAULT_CHARSET); + } + + @Override + public long convertToTimeMillis(ByteString value) { + try { + return HeaderDateFormat.get().parse(value.toString()); + } catch (ParseException e) { + PlatformDependent.throwException(e); + } + return 0; + } + + @Override + public double convertToDouble(ByteString value) { + return value.parseAsciiDouble(); + } + + @Override + public char convertToChar(ByteString value) { + return value.parseChar(); + } + + @Override + public boolean convertToBoolean(ByteString value) { + return value.byteAt(0) != 0; + } + + @Override + public float convertToFloat(ByteString value) { + return value.parseAsciiFloat(); + } + + @Override + public ByteString convertShort(short value) { + return new ByteString(String.valueOf(value), DEFAULT_CHARSET); + } + + @Override + public short convertToShort(ByteString value) { + return value.parseAsciiShort(); + } + + @Override + public ByteString convertByte(byte value) { + return new ByteString(String.valueOf(value), DEFAULT_CHARSET); + } + + @Override + public byte convertToByte(ByteString value) { + return value.byteAt(0); + } + } } diff --git a/codec/src/main/java/io/netty/handler/codec/DefaultConvertibleHeaders.java b/codec/src/main/java/io/netty/handler/codec/DefaultConvertibleHeaders.java index 7a18943d93..3c7863799d 100644 --- a/codec/src/main/java/io/netty/handler/codec/DefaultConvertibleHeaders.java +++ b/codec/src/main/java/io/netty/handler/codec/DefaultConvertibleHeaders.java @@ -18,6 +18,7 @@ import java.util.ArrayList; import java.util.Comparator; import java.util.Iterator; import java.util.List; +import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.TreeSet; @@ -27,22 +28,11 @@ public class DefaultConvertibleHeaders extends D private final TypeConverter typeConverter; - public DefaultConvertibleHeaders(Comparator keyComparator, - Comparator valueComparator, - HashCodeGenerator hashCodeGenerator, - ValueConverter valueConverter, - TypeConverter typeConverter) { - super(keyComparator, valueComparator, hashCodeGenerator, valueConverter); - this.typeConverter = typeConverter; - } - - public DefaultConvertibleHeaders(Comparator keyComparator, - Comparator valueComparator, - HashCodeGenerator hashCodeGenerator, - ValueConverter valueConverter, - TypeConverter typeConverter, - NameConverter nameConverter) { - super(keyComparator, valueComparator, hashCodeGenerator, valueConverter, nameConverter); + public DefaultConvertibleHeaders(Map map, + NameValidator nameValidator, + ValueConverter valueConverter, + TypeConverter typeConverter) { + super(map, nameValidator, valueConverter); this.typeConverter = typeConverter; } @@ -94,17 +84,6 @@ public class DefaultConvertibleHeaders extends D return allConverted; } - @Override - public List> entriesConverted() { - List> entries = entries(); - List> entriesConverted = new ArrayList>( - entries.size()); - for (int i = 0; i < entries.size(); ++i) { - entriesConverted.add(new ConvertedEntry(entries.get(i))); - } - return entriesConverted; - } - @Override public Iterator> iteratorConverted() { return new ConvertedIterator(); diff --git a/codec/src/main/java/io/netty/handler/codec/DefaultHeaders.java b/codec/src/main/java/io/netty/handler/codec/DefaultHeaders.java index 37b7ba45a8..6e08fcac90 100644 --- a/codec/src/main/java/io/netty/handler/codec/DefaultHeaders.java +++ b/codec/src/main/java/io/netty/handler/codec/DefaultHeaders.java @@ -14,154 +14,59 @@ */ package io.netty.handler.codec; -import io.netty.util.collection.IntObjectHashMap; -import io.netty.util.collection.IntObjectMap; import io.netty.util.concurrent.FastThreadLocal; -import io.netty.util.internal.PlatformDependent; import java.text.DateFormat; import java.text.ParseException; -import java.text.ParsePosition; import java.text.SimpleDateFormat; +import java.util.AbstractMap.SimpleEntry; import java.util.ArrayList; -import java.util.Collections; import java.util.Comparator; -import java.util.Date; import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Map.Entry; -import java.util.NoSuchElementException; import java.util.Set; import java.util.TimeZone; -import java.util.TreeSet; +import java.util.Date; +import java.util.TreeMap; -import static io.netty.util.internal.ObjectUtil.*; +import static io.netty.util.internal.ObjectUtil.checkNotNull; +import static java.util.Collections.emptyList; +import static java.util.Collections.singletonList; +import static java.util.Collections.unmodifiableList; +import static java.util.Collections.unmodifiableSet; + +/** + * Default implementation of {@link Headers}; + * + * @param the type of the header name and value. + */ public class DefaultHeaders implements Headers { - /** - * Allows users of this interface to specify a hash code other than the default {@link Object#hashCode()} - */ - public interface HashCodeGenerator { - /** - * Obtain the hash code for the given {@code name} - * - * @param name The name to generate the hash code for - * @return The hash code for {@code name} - */ - int generateHashCode(T name); - } - /** - * Allows users to convert the {@code name} elements before being processed by this map - */ - public interface NameConverter { - /** - * Convert the {@code name} to some other form of the same object type - * - * @param name The object to convert - * @return The results of the conversion - */ - T convertName(T name); - } - - /** - * Uses the {@link #hashCode()} method to generate the hash code. - */ - public static final class JavaHashCodeGenerator implements HashCodeGenerator { - @Override - public int generateHashCode(T name) { - return name.hashCode(); - } - } - - /** - * A name converted which does not covert but instead just returns this {@code name} unchanged - */ - public static final class IdentityNameConverter implements NameConverter { - @Override - public T convertName(T name) { - return name; - } - } - - private static final int HASH_CODE_PRIME = 31; - private static final int DEFAULT_BUCKET_SIZE = 17; - private static final int DEFAULT_MAP_SIZE = 4; - private static final NameConverter DEFAULT_NAME_CONVERTER = new IdentityNameConverter(); - - private final IntObjectMap entries; - private final IntObjectMap tailEntries; - private final HeaderEntry head; - private final Comparator keyComparator; - private final Comparator valueComparator; - private final HashCodeGenerator hashCodeGenerator; + private final Map map; + private final NameValidator nameValidator; private final ValueConverter valueConverter; - private final NameConverter nameConverter; - private final int bucketSize; int size; - @SuppressWarnings("unchecked") - public DefaultHeaders(Comparator keyComparator, Comparator valueComparator, - HashCodeGenerator hashCodeGenerator, ValueConverter typeConverter) { - this(keyComparator, valueComparator, hashCodeGenerator, typeConverter, - (NameConverter) DEFAULT_NAME_CONVERTER); - } + boolean hasMultipleValues; - public DefaultHeaders(Comparator keyComparator, Comparator valueComparator, - HashCodeGenerator hashCodeGenerator, ValueConverter typeConverter, NameConverter nameConverter) { - this(keyComparator, valueComparator, hashCodeGenerator, typeConverter, nameConverter, DEFAULT_BUCKET_SIZE, - DEFAULT_MAP_SIZE); - } - - public DefaultHeaders(Comparator keyComparator, Comparator valueComparator, - HashCodeGenerator hashCodeGenerator, ValueConverter valueConverter, NameConverter nameConverter, - int bucketSize, int initialMapSize) { - if (keyComparator == null) { - throw new NullPointerException("keyComparator"); - } - if (valueComparator == null) { - throw new NullPointerException("valueComparator"); - } - if (hashCodeGenerator == null) { - throw new NullPointerException("hashCodeGenerator"); - } - if (valueConverter == null) { - throw new NullPointerException("valueConverter"); - } - if (nameConverter == null) { - throw new NullPointerException("nameConverter"); - } - if (bucketSize < 1) { - throw new IllegalArgumentException("bucketSize must be a positive integer"); - } - head = new HeaderEntry(); - head.before = head.after = head; - this.keyComparator = keyComparator; - this.valueComparator = valueComparator; - this.hashCodeGenerator = hashCodeGenerator; - this.valueConverter = valueConverter; - this.nameConverter = nameConverter; - this.bucketSize = bucketSize; - entries = new IntObjectHashMap(initialMapSize); - tailEntries = new IntObjectHashMap(initialMapSize); + public DefaultHeaders(Map map, NameValidator nameValidator, ValueConverter valueConverter) { + this.map = checkNotNull(map, "map"); + this.nameValidator = checkNotNull(nameValidator, "nameValidator"); + this.valueConverter = checkNotNull(valueConverter, "valueConverter"); } @Override + @SuppressWarnings("unchecked") public T get(T name) { - checkNotNull(name, "name"); - - int h = hashCodeGenerator.generateHashCode(name); - int i = index(h); - HeaderEntry e = entries.get(i); - while (e != null) { - if (e.hash == h && keyComparator.compare(e.name, name) == 0) { - return e.value; - } - e = e.next; + Object value = map.get(name); + if (isList(value)) { + return ((List) value).get(0); } - return null; + return (T) value; } @Override @@ -174,56 +79,19 @@ public class DefaultHeaders implements Headers { } @Override + @SuppressWarnings("unchecked") public T getAndRemove(T name) { - checkNotNull(name, "name"); - int h = hashCodeGenerator.generateHashCode(name); - int i = index(h); - HeaderEntry e = entries.get(i); - if (e == null) { + Object value = map.remove(name); + if (value == null) { return null; } - - T value = null; - for (;;) { - if (e.hash == h && keyComparator.compare(e.name, name) == 0) { - if (value == null) { - value = e.value; - } - e.remove(); - HeaderEntry next = e.next; - if (next != null) { - entries.put(i, next); - e = next; - } else { - entries.remove(i); - tailEntries.remove(i); - return value; - } - } else { - break; - } + if (isList(value)) { + List value0 = (List) value; + size -= value0.size(); + return value0.get(0); } - - for (;;) { - HeaderEntry next = e.next; - if (next == null) { - break; - } - if (next.hash == h && keyComparator.compare(e.name, name) == 0) { - if (value == null) { - value = next.value; - } - e.next = next.next; - if (e.next == null) { - tailEntries.put(i, e); - } - next.remove(); - } else { - e = next; - } - } - - return value; + size--; + return (T) value; } @Override @@ -236,84 +104,23 @@ public class DefaultHeaders implements Headers { } @Override + @SuppressWarnings("unchecked") public List getAll(T name) { - checkNotNull(name, "name"); - List values = new ArrayList(4); - int h = hashCodeGenerator.generateHashCode(name); - int i = index(h); - HeaderEntry e = entries.get(i); - while (e != null) { - if (e.hash == h && keyComparator.compare(e.name, name) == 0) { - values.add(e.value); - } - e = e.next; + Object value = map.get(name); + if (isList(value)) { + return unmodifiableList((List) value); } - - return values; + if (value == null) { + return emptyList(); + } + return singletonList((T) value); } @Override public List getAllAndRemove(T name) { - checkNotNull(name, "name"); - int h = hashCodeGenerator.generateHashCode(name); - int i = index(h); - HeaderEntry e = entries.get(i); - if (e == null) { - return null; - } - - List values = new ArrayList(4); - for (;;) { - if (e.hash == h && keyComparator.compare(e.name, name) == 0) { - values.add(e.value); - e.remove(); - HeaderEntry next = e.next; - if (next != null) { - entries.put(i, next); - e = next; - } else { - entries.remove(i); - tailEntries.remove(i); - return values; - } - } else { - break; - } - } - - for (;;) { - HeaderEntry next = e.next; - if (next == null) { - break; - } - if (next.hash == h && keyComparator.compare(next.name, name) == 0) { - values.add(next.value); - e.next = next.next; - if (e.next == null) { - tailEntries.put(i, e); - } - next.remove(); - } else { - e = next; - } - } - - return values; - } - - @Override - public List> entries() { - final int size = size(); - List> localEntries = new ArrayList>(size); - - HeaderEntry e = head.after; - while (e != head) { - localEntries.add(e); - e = e.after; - } - - assert size == localEntries.size(); - return localEntries; + List all = getAll(name); + remove(name); + return all; } @Override @@ -322,8 +129,13 @@ public class DefaultHeaders implements Headers { } @Override + @SuppressWarnings("unchecked") public boolean contains(T name, T value) { - return contains(name, value, keyComparator, valueComparator); + Object values = map.get(name); + if (isList(values)) { + return ((List) values).contains(value); + } + return values != null && values.equals(value); } @Override @@ -377,41 +189,19 @@ public class DefaultHeaders implements Headers { } @Override - public boolean contains(T name, T value, Comparator comparator) { - return contains(name, value, comparator, comparator); - } - - @Override - public boolean contains(T name, T value, - Comparator keyComparator, Comparator valueComparator) { - checkNotNull(name, "name"); - checkNotNull(value, "value"); - checkNotNull(keyComparator, "keyComparator"); - checkNotNull(valueComparator, "valueComparator"); - int h = hashCodeGenerator.generateHashCode(name); - int i = index(h); - HeaderEntry e = entries.get(i); - while (e != null) { - if (e.hash == h && - keyComparator.compare(e.name, name) == 0 && - valueComparator.compare(e.value, value) == 0) { - return true; + @SuppressWarnings("unchecked") + public boolean contains(T name, T value, Comparator valueComparator) { + Object values = map.get(name); + if (isList(values)) { + List values0 = (List) values; + for (int i = 0; i < values0.size(); i++) { + if (valueComparator.compare(value, values0.get(i)) == 0) { + return true; + } } - e = e.next; + return false; } - return false; - } - - @Override - public boolean containsObject(T name, Object value, Comparator comparator) { - return containsObject(name, value, comparator, comparator); - } - - @Override - public boolean containsObject(T name, Object value, Comparator keyComparator, - Comparator valueComparator) { - return contains( - name, valueConverter.convertObject(checkNotNull(value, "value")), keyComparator, valueComparator); + return values != null && valueComparator.compare((T) values, value) == 0; } @Override @@ -421,73 +211,54 @@ public class DefaultHeaders implements Headers { @Override public boolean isEmpty() { - return head == head.after; + return map.isEmpty(); } @Override public Set names() { - final Set names = new TreeSet(keyComparator); - - HeaderEntry e = head.after; - while (e != head) { - names.add(e.name); - e = e.after; - } - - return names; - } - - @Override - public List namesList() { - final List names = new ArrayList(size()); - - HeaderEntry e = head.after; - while (e != head) { - names.add(e.name); - e = e.after; - } - - return names; + return unmodifiableSet(map.keySet()); } @Override public Headers add(T name, T value) { - name = convertName(name); + validateName(name); checkNotNull(value, "value"); - int h = hashCodeGenerator.generateHashCode(name); - int i = index(h); - add0(h, i, name, value); + Object prevValue = map.put(name, value); + size++; + if (prevValue != null) { + appendValue(name, value, prevValue); + } return this; } + @SuppressWarnings("unchecked") + private void appendValue(T name, T value, Object prevValue) { + hasMultipleValues = true; + if (isList(prevValue)) { + ((List) prevValue).add(value); + map.put(name, prevValue); + } else { + List values = newList(); + values.add((T) prevValue); + values.add(value); + map.put(name, values); + } + } + @Override public Headers add(T name, Iterable values) { - name = convertName(name); checkNotNull(values, "values"); - - int h = hashCodeGenerator.generateHashCode(name); - int i = index(h); - for (T v : values) { - if (v == null) { - break; - } - add0(h, i, name, v); + for (T value : values) { + add(name, value); } return this; } @Override public Headers add(T name, T... values) { - name = convertName(name); checkNotNull(values, "values"); - - int h = hashCodeGenerator.generateHashCode(name); - int i = index(h); - for (T v : values) { - if (v == null) { - break; - } - add0(h, i, name, v); + for (int i = 0; i < values.length; i++) { + add(name, values[i]); } return this; } @@ -499,36 +270,18 @@ public class DefaultHeaders implements Headers { @Override public Headers addObject(T name, Iterable values) { - name = convertName(name); checkNotNull(values, "values"); - - int h = hashCodeGenerator.generateHashCode(name); - int i = index(h); - for (Object o : values) { - if (o == null) { - break; - } - T converted = valueConverter.convertObject(o); - checkNotNull(converted, "converted"); - add0(h, i, name, converted); + for (Object value : values) { + addObject(name, value); } return this; } @Override public Headers addObject(T name, Object... values) { - name = convertName(name); checkNotNull(values, "values"); - - int h = hashCodeGenerator.generateHashCode(name); - int i = index(h); - for (Object o : values) { - if (o == null) { - break; - } - T converted = valueConverter.convertObject(o); - checkNotNull(converted, "converted"); - add0(h, i, name, converted); + for (int i = 0; i < values.length; i++) { + addObject(name, values[i]); } return this; } @@ -579,102 +332,90 @@ public class DefaultHeaders implements Headers { } @Override - public Headers add(Headers headers) { + public Headers add(Headers headers) { checkNotNull(headers, "headers"); - - add0(headers); + if (headers == this) { + throw new IllegalArgumentException("can't add to itself."); + } + for (Entry header : headers) { + add(header.getKey(), header.getValue()); + } return this; } @Override public Headers set(T name, T value) { - name = convertName(name); + validateName(name); checkNotNull(value, "value"); - int h = hashCodeGenerator.generateHashCode(name); - int i = index(h); - remove0(h, i, name); - add0(h, i, name, value); + Object oldValue = map.put(name, value); + updateSizeAfterSet(oldValue, 1); return this; } @Override public Headers set(T name, Iterable values) { - name = convertName(name); + validateName(name); checkNotNull(values, "values"); - - int h = hashCodeGenerator.generateHashCode(name); - int i = index(h); - remove0(h, i, name); - for (T v : values) { - if (v == null) { - break; - } - add0(h, i, name, v); + List list = newList(); + for (T value : values) { + list.add(checkNotNull(value, "value")); } - + Object oldValue = map.put(name, list); + updateSizeAfterSet(oldValue, list.size()); + hasMultipleValues = true; return this; } @Override public Headers set(T name, T... values) { - name = convertName(name); + validateName(name); checkNotNull(values, "values"); - - int h = hashCodeGenerator.generateHashCode(name); - int i = index(h); - remove0(h, i, name); - for (T v : values) { - if (v == null) { - break; - } - add0(h, i, name, v); + List list = newList(values.length); + for (int i = 0; i < values.length; i++) { + list.add(checkNotNull(values[i], "value")); } - + Object oldValue = map.put(name, list); + updateSizeAfterSet(oldValue, values.length); + hasMultipleValues = true; return this; } @Override public Headers setObject(T name, Object value) { - return set(name, valueConverter.convertObject(checkNotNull(value, "value"))); + checkNotNull(value, "value"); + T convertedValue = checkNotNull(valueConverter.convertObject(value), "convertedValue"); + return set(name, convertedValue); } @Override public Headers setObject(T name, Iterable values) { - name = convertName(name); + validateName(name); checkNotNull(values, "values"); - - int h = hashCodeGenerator.generateHashCode(name); - int i = index(h); - remove0(h, i, name); - for (Object o : values) { - if (o == null) { - break; - } - T converted = valueConverter.convertObject(o); - checkNotNull(converted, "converted"); - add0(h, i, name, converted); + List list = newList(); + for (Object value : values) { + value = checkNotNull(value, "value"); + T convertedValue = checkNotNull(valueConverter.convertObject(value), "convertedValue"); + list.add(convertedValue); } - + Object oldValue = map.put(name, list); + updateSizeAfterSet(oldValue, list.size()); + hasMultipleValues = true; return this; } @Override public Headers setObject(T name, Object... values) { - name = convertName(name); + validateName(name); checkNotNull(values, "values"); - - int h = hashCodeGenerator.generateHashCode(name); - int i = index(h); - remove0(h, i, name); - for (Object o : values) { - if (o == null) { - break; - } - T converted = valueConverter.convertObject(o); - checkNotNull(converted, "converted"); - add0(h, i, name, converted); + List list = newList(values.length); + for (int i = 0; i < values.length; i++) { + Object value = checkNotNull(values[i], "value"); + T convertedValue = checkNotNull(valueConverter.convertObject(value), "convertedValue"); + list.add(convertedValue); } - + Object oldValue = map.put(name, list); + updateSizeAfterSet(oldValue, list.size()); + hasMultipleValues = true; return this; } @@ -724,80 +465,50 @@ public class DefaultHeaders implements Headers { } @Override - public Headers set(Headers headers) { + public Headers set(Headers headers) { checkNotNull(headers, "headers"); - + if (headers == this) { + return this; + } clear(); - add0(headers); + add(headers); return this; } @Override - public Headers setAll(Headers headers) { + public Headers setAll(Headers headers) { checkNotNull(headers, "headers"); - - if (headers instanceof DefaultHeaders) { - DefaultHeaders m = (DefaultHeaders) headers; - HeaderEntry e = m.head.after; - while (e != m.head) { - set(e.name, e.value); - e = e.after; - } - } else { - try { - headers.forEachEntry(setAllVisitor()); - } catch (Exception ex) { - PlatformDependent.throwException(ex); - } + if (headers == this) { + return this; + } + for (T key : headers.names()) { + remove(key); + } + for (Entry entry : headers) { + add(entry.getKey(), entry.getValue()); } - return this; } @Override public boolean remove(T name) { - checkNotNull(name, "name"); - int h = hashCodeGenerator.generateHashCode(name); - int i = index(h); - return remove0(h, i, name); + return getAndRemove(name) != null; } @Override public Headers clear() { - entries.clear(); - tailEntries.clear(); - head.before = head.after = head; + map.clear(); + hasMultipleValues = false; size = 0; return this; } @Override + @SuppressWarnings("unchecked") public Iterator> iterator() { - return new KeyValueHeaderIterator(); - } - - @Override - public Map.Entry forEachEntry(EntryVisitor visitor) throws Exception { - HeaderEntry e = head.after; - while (e != head) { - if (!visitor.visit(e)) { - return e; - } - e = e.after; - } - return null; - } - - @Override - public T forEachName(NameVisitor visitor) throws Exception { - HeaderEntry e = head.after; - while (e != head) { - if (!visitor.visit(e.name)) { - return e.name; - } - e = e.after; - } - return null; + Object iter = map.entrySet().iterator(); + return !hasMultipleValues ? (Iterator>) iter + : new NameValueIterator((Iterator>) iter); } @Override @@ -871,7 +582,7 @@ public class DefaultHeaders implements Headers { } @Override - public short getInt(T name, short defaultValue) { + public short getShort(T name, short defaultValue) { Short v = getShort(name); return v == null ? defaultValue : v; } @@ -1143,85 +854,43 @@ public class DefaultHeaders implements Headers { } @Override + @SuppressWarnings("unchecked") public boolean equals(Object o) { if (!(o instanceof DefaultHeaders)) { return false; } - - @SuppressWarnings("unchecked") - DefaultHeaders h2 = (DefaultHeaders) o; - // First, check that the set of names match. Don't use a TreeSet for comparison - // because we want to force the keyComparator to be used for all comparisons - List namesList = namesList(); - List otherNamesList = h2.namesList(); - if (!equals(namesList, otherNamesList, keyComparator)) { - return false; - } - - // Compare the values for each name. Don't use a TreeSet for comparison - // because we want to force the valueComparator to be used for all comparisons - Set names = new TreeSet(keyComparator); - names.addAll(namesList); - for (T name : names) { - if (!equals(getAll(name), h2.getAll(name), valueComparator)) { - return false; - } - } - - return true; + DefaultHeaders other = (DefaultHeaders) o; + return size() == other.size() && map.equals(other.map); } /** - * Compare two lists using the {@code comparator} for all comparisons (not using the equals() operator) - * @param lhs Left hand side - * @param rhs Right hand side - * @param comparator Comparator which will be used for all comparisons (equals() on objects will not be used) - * @return True if {@code lhs} == {@code rhs} according to {@code comparator}. False otherwise. + * This method is purposefully kept simple and returns {@link #size()} as the hash code. + * There are two compelling reasons for keeping {@link #hashCode()} simple: + * 1) It's difficult to get it right as the hash code mostly depends on the {@link Map} + * implementation. Simply using {@link Map#hashCode()} doesn't work as for + * example {@link TreeMap#hashCode()} does not fulfill the contract between {@link Object#hashCode()} and + * {@link Object#equals(Object)} when it's used with a {@link Comparator} that is not consistent with + * {@link Object#equals(Object)}. + * 2) The {@link #hashCode()} function doesn't appear to be important for {@link Headers} implementations. It's + * solely there because we override {@link Object#equals(Object)}, which makes it easier to use {@link Headers} + * in tests, but also has little practical use outside of tests. */ - private static boolean equals(List lhs, List rhs, Comparator comparator) { - final int lhsSize = lhs.size(); - if (lhsSize != rhs.size()) { - return false; - } - - // Don't use a TreeSet to do the comparison. We want to force the comparator - // to be used instead of the object's equals() - Collections.sort(lhs, comparator); - Collections.sort(rhs, comparator); - for (int i = 0; i < lhsSize; ++i) { - if (comparator.compare(lhs.get(i), rhs.get(i)) != 0) { - return false; - } - } - return true; - } - @Override public int hashCode() { - int result = 1; - for (T name : names()) { - result = HASH_CODE_PRIME * result + name.hashCode(); - List values = getAll(name); - Collections.sort(values, valueComparator); - for (int i = 0; i < values.size(); ++i) { - result = HASH_CODE_PRIME * result + hashCodeGenerator.generateHashCode(values.get(i)); - } - } - return result; + return size(); } @Override public String toString() { StringBuilder builder = new StringBuilder(getClass().getSimpleName()).append('['); + String separator = ""; for (T name : names()) { List values = getAll(name); - Collections.sort(values, valueComparator); for (int i = 0; i < values.size(); ++i) { - builder.append(name).append(": ").append(values.get(i)).append(", "); + builder.append(separator); + builder.append(name).append(": ").append(values.get(i)); } - } - if (builder.length() > 2) { // remove the trailing ", " - builder.setLength(builder.length() - 2); + separator = ", "; } return builder.append(']').toString(); } @@ -1230,200 +899,75 @@ public class DefaultHeaders implements Headers { return valueConverter; } - private T convertName(T name) { - return nameConverter.convertName(checkNotNull(name, "name")); + @SuppressWarnings("unchecked") + private void updateSizeAfterSet(Object oldValue, int newSize) { + if (isList(oldValue)) { + size -= ((List) oldValue).size(); + } else if (oldValue != null) { + size--; + } + size += newSize; } - private int index(int hash) { - return Math.abs(hash % bucketSize); + private void validateName(T name) { + checkNotNull(name, "name"); + nameValidator.validate(name); } - private void add0(Headers headers) { - if (headers.isEmpty()) { - return; - } - - if (headers instanceof DefaultHeaders) { - DefaultHeaders m = (DefaultHeaders) headers; - HeaderEntry e = m.head.after; - while (e != m.head) { - add(e.name, e.value); - e = e.after; - } - } else { - try { - headers.forEachEntry(addAllVisitor()); - } catch (Exception ex) { - PlatformDependent.throwException(ex); - } - } + private static boolean isList(Object value) { + return value != null && value.getClass().equals(ValuesList.class); } - private void add0(int h, int i, T name, T value) { - // Update the per-bucket hash table linked list - HeaderEntry newEntry = new HeaderEntry(h, name, value); - HeaderEntry oldTail = tailEntries.get(i); - if (oldTail == null) { - entries.put(i, newEntry); - } else { - oldTail.next = newEntry; - } - tailEntries.put(i, newEntry); - - // Update the overall insertion order linked list - newEntry.addBefore(head); + private List newList() { + return newList(4); } - private boolean remove0(int h, int i, T name) { - HeaderEntry e = entries.get(i); - if (e == null) { - return false; - } - - boolean removed = false; - for (;;) { - if (e.hash == h && keyComparator.compare(e.name, name) == 0) { - e.remove(); - HeaderEntry next = e.next; - if (next != null) { - entries.put(i, next); - e = next; - } else { - entries.remove(i); - tailEntries.remove(i); - return true; - } - removed = true; - } else { - break; - } - } - - for (;;) { - HeaderEntry next = e.next; - if (next == null) { - break; - } - if (next.hash == h && keyComparator.compare(next.name, name) == 0) { - e.next = next.next; - if (e.next == null) { - tailEntries.put(i, e); - } - next.remove(); - removed = true; - } else { - e = next; - } - } - - return removed; + private List newList(int initialSize) { + return new ValuesList(initialSize); } - private EntryVisitor setAllVisitor() { - return new EntryVisitor() { - @Override - public boolean visit(Entry entry) { - set(entry.getKey(), entry.getValue()); - return true; - } - }; - } + private final class NameValueIterator implements Iterator> { + private final Iterator> iter; + private T name; + private List values; + private int valuesIdx; - private EntryVisitor addAllVisitor() { - return new EntryVisitor() { - @Override - public boolean visit(Entry entry) { - add(entry.getKey(), entry.getValue()); - return true; - } - }; - } - - private final class HeaderEntry implements Map.Entry { - final int hash; - final T name; - T value; - /** - * In bucket linked list - */ - HeaderEntry next; - /** - * Overall insertion order linked list - */ - HeaderEntry before, after; - - HeaderEntry(int hash, T name, T value) { - this.hash = hash; - this.name = name; - this.value = value; + NameValueIterator(Iterator> iter) { + this.iter = iter; } - HeaderEntry() { - hash = -1; - name = null; - value = null; - } - - void remove() { - before.after = after; - after.before = before; - --size; - } - - void addBefore(HeaderEntry e) { - after = e; - before = e.before; - before.after = this; - after.before = this; - ++size; - } - - @Override - public T getKey() { - return name; - } - - @Override - public T getValue() { - return value; - } - - @Override - public T setValue(T value) { - checkNotNull(value, "value"); - T oldValue = this.value; - this.value = value; - return oldValue; - } - - @Override - public String toString() { - return new StringBuilder() - .append(name) - .append('=') - .append(value) - .toString(); - } - } - - protected final class KeyValueHeaderIterator implements Iterator> { - - private HeaderEntry current = head; - @Override public boolean hasNext() { - return current.after != head; + return iter.hasNext() || values != null; } @Override + @SuppressWarnings("unchecked") public Entry next() { - current = current.after; - - if (current == head) { - throw new NoSuchElementException(); + if (name == null) { + Entry entry = iter.next(); + if (!isList(entry.getValue())) { + return (Entry) entry; + } + initListIterator(entry); } + return fetchNextEntryFromList(); + } - return current; + @SuppressWarnings("unchecked") + private void initListIterator(Entry entry) { + name = entry.getKey(); + values = (List) entry.getValue(); + valuesIdx = 0; + } + + private Entry fetchNextEntryFromList() { + Entry next = new SimpleListEntry(name, values, valuesIdx++); + if (values.size() == valuesIdx) { + name = null; + values = null; + } + return next; } @Override @@ -1432,8 +976,36 @@ public class DefaultHeaders implements Headers { } } + private static class SimpleListEntry extends SimpleEntry { + private static final long serialVersionUID = 542062057061955051L; + private final List values; + private final int idx; + + SimpleListEntry(T name, List values, int idx) { + super(name, values.get(idx)); + this.values = values; + this.idx = idx; + } + + @Override + public T setValue(T value) { + T oldValue = super.setValue(value); + values.set(idx, value); + return oldValue; + } + } + + private static final class ValuesList extends ArrayList { + private static final long serialVersionUID = -2434101246756843900L; + + ValuesList(int initialSize) { + super(initialSize); + } + } + /** - * This DateFormat decodes 3 formats of {@link java.util.Date}, but only encodes the one, the first: + * This {@link DateFormat} decodes 3 formats of {@link Date}. + * *
    *
  • Sun, 06 Nov 1994 08:49:37 GMT: standard specification, the only one with valid generation
  • *
  • Sun, 06 Nov 1994 08:49:37 GMT: obsolete specification
  • @@ -1441,14 +1013,13 @@ public class DefaultHeaders implements Headers { *
*/ static final class HeaderDateFormat { - private static final ParsePosition parsePos = new ParsePosition(0); private static final FastThreadLocal dateFormatThreadLocal = new FastThreadLocal() { - @Override - protected HeaderDateFormat initialValue() { - return new HeaderDateFormat(); - } - }; + @Override + protected HeaderDateFormat initialValue() { + return new HeaderDateFormat(); + } + }; static HeaderDateFormat get() { return dateFormatThreadLocal.get(); @@ -1462,6 +1033,7 @@ public class DefaultHeaders implements Headers { * */ private final DateFormat dateFormat1 = new SimpleDateFormat("E, dd MMM yyyy HH:mm:ss z", Locale.ENGLISH); + /** * First obsolete format: * @@ -1470,6 +1042,7 @@ public class DefaultHeaders implements Headers { * */ private final DateFormat dateFormat2 = new SimpleDateFormat("E, dd-MMM-yy HH:mm:ss z", Locale.ENGLISH); + /** * Second obsolete format * @@ -1487,31 +1060,58 @@ public class DefaultHeaders implements Headers { } long parse(String text) throws ParseException { - Date date = dateFormat1.parse(text, parsePos); + Date date = dateFormat1.parse(text); if (date == null) { - date = dateFormat2.parse(text, parsePos); + date = dateFormat2.parse(text); } if (date == null) { - date = dateFormat3.parse(text, parsePos); + date = dateFormat3.parse(text); } if (date == null) { throw new ParseException(text, 0); } return date.getTime(); } + } - long parse(String text, long defaultValue) { - Date date = dateFormat1.parse(text, parsePos); - if (date == null) { - date = dateFormat2.parse(text, parsePos); - } - if (date == null) { - date = dateFormat3.parse(text, parsePos); - } - if (date == null) { - return defaultValue; - } - return date.getTime(); + public interface NameValidator { + void validate(T name); + } + + public static final class NoNameValidator implements NameValidator { + + @SuppressWarnings("rawtypes") + private static final NameValidator INSTANCE = new NoNameValidator(); + + private NoNameValidator() { + } + + @SuppressWarnings("unchecked") + public static NameValidator instance() { + return (NameValidator) INSTANCE; + } + + @Override + public void validate(T name) { } } + + public static boolean comparatorEquals(Headers a, Headers b, Comparator valueComparator) { + if (a.size() != b.size()) { + return false; + } + for (V name : a.names()) { + List otherValues = b.getAll(name); + List values = a.getAll(name); + if (otherValues.size() != values.size()) { + return false; + } + for (int i = 0; i < otherValues.size(); i++) { + if (valueComparator.compare(otherValues.get(i), values.get(i)) != 0) { + return false; + } + } + } + return true; + } } diff --git a/codec/src/main/java/io/netty/handler/codec/DefaultTextHeaders.java b/codec/src/main/java/io/netty/handler/codec/DefaultTextHeaders.java index 2f1d75ed39..633488cb90 100644 --- a/codec/src/main/java/io/netty/handler/codec/DefaultTextHeaders.java +++ b/codec/src/main/java/io/netty/handler/codec/DefaultTextHeaders.java @@ -24,184 +24,37 @@ import io.netty.util.internal.PlatformDependent; import io.netty.util.internal.StringUtil; import java.text.ParseException; -import java.util.Comparator; import java.util.Iterator; +import java.util.Map; +import java.util.TreeMap; public class DefaultTextHeaders extends DefaultConvertibleHeaders implements TextHeaders { - private static final HashCodeGenerator CHARSEQUECE_CASE_INSENSITIVE_HASH_CODE_GENERATOR = - new HashCodeGenerator() { - @Override - public int generateHashCode(CharSequence name) { - return AsciiString.caseInsensitiveHashCode(name); - } - }; - private static final HashCodeGenerator CHARSEQUECE_CASE_SENSITIVE_HASH_CODE_GENERATOR = - new JavaHashCodeGenerator(); - - public static class DefaultTextValueTypeConverter implements ValueConverter { - @Override - public CharSequence convertObject(Object value) { - if (value instanceof CharSequence) { - return (CharSequence) value; - } - return value.toString(); - } - - @Override - public CharSequence convertInt(int value) { - return String.valueOf(value); - } - - @Override - public CharSequence convertLong(long value) { - return String.valueOf(value); - } - - @Override - public CharSequence convertDouble(double value) { - return String.valueOf(value); - } - - @Override - public CharSequence convertChar(char value) { - return String.valueOf(value); - } - - @Override - public CharSequence convertBoolean(boolean value) { - return String.valueOf(value); - } - - @Override - public CharSequence convertFloat(float value) { - return String.valueOf(value); - } - - @Override - public boolean convertToBoolean(CharSequence value) { - return Boolean.parseBoolean(value.toString()); - } - - @Override - public CharSequence convertByte(byte value) { - return String.valueOf(value); - } - - @Override - public byte convertToByte(CharSequence value) { - return Byte.valueOf(value.toString()); - } - - @Override - public char convertToChar(CharSequence value) { - return value.charAt(0); - } - - @Override - public CharSequence convertShort(short value) { - return String.valueOf(value); - } - - @Override - public short convertToShort(CharSequence value) { - return Short.valueOf(value.toString()); - } - - @Override - public int convertToInt(CharSequence value) { - return Integer.parseInt(value.toString()); - } - - @Override - public long convertToLong(CharSequence value) { - return Long.parseLong(value.toString()); - } - - @Override - public AsciiString convertTimeMillis(long value) { - return new AsciiString(String.valueOf(value)); - } - - @Override - public long convertToTimeMillis(CharSequence value) { - try { - return HeaderDateFormat.get().parse(value.toString()); - } catch (ParseException e) { - PlatformDependent.throwException(e); - } - return 0; - } - - @Override - public float convertToFloat(CharSequence value) { - return Float.valueOf(value.toString()); - } - - @Override - public double convertToDouble(CharSequence value) { - return Double.valueOf(value.toString()); - } - } - - private static final ValueConverter CHARSEQUENCE_FROM_OBJECT_CONVERTER = - new DefaultTextValueTypeConverter(); - private static final TypeConverter CHARSEQUENCE_TO_STRING_CONVERTER = - new TypeConverter() { - @Override - public String toConvertedType(CharSequence value) { - return value.toString(); - } - - @Override - public CharSequence toUnconvertedType(String value) { - return value; - } - }; - - private static final NameConverter CHARSEQUENCE_IDENTITY_CONVERTER = - new IdentityNameConverter(); - /** - * An estimate of the size of a header value. - */ - private static final int DEFAULT_VALUE_SIZE = 10; + public static final NameValidator NO_NAME_VALIDATOR = NoNameValidator.instance(); private final ValuesComposer valuesComposer; public DefaultTextHeaders() { - this(true); + this(false); } - public DefaultTextHeaders(boolean ignoreCase) { - this(ignoreCase, CHARSEQUENCE_FROM_OBJECT_CONVERTER, CHARSEQUENCE_IDENTITY_CONVERTER, false); + public DefaultTextHeaders(boolean singleHeaderFields) { + this(new TreeMap(CHARSEQUENCE_CASE_INSENSITIVE_ORDER), NO_NAME_VALIDATOR, + CharSequenceConverter.INSTANCE, singleHeaderFields); } - public DefaultTextHeaders(boolean ignoreCase, boolean singleHeaderFields) { - this(ignoreCase, CHARSEQUENCE_FROM_OBJECT_CONVERTER, CHARSEQUENCE_IDENTITY_CONVERTER, singleHeaderFields); - } - - protected DefaultTextHeaders(boolean ignoreCase, Headers.ValueConverter valueConverter, - NameConverter nameConverter) { - this(ignoreCase, valueConverter, nameConverter, false); - } - - public DefaultTextHeaders(boolean ignoreCase, ValueConverter valueConverter, - NameConverter nameConverter, boolean singleHeaderFields) { - super(comparator(ignoreCase), comparator(ignoreCase), - ignoreCase ? CHARSEQUECE_CASE_INSENSITIVE_HASH_CODE_GENERATOR - : CHARSEQUECE_CASE_SENSITIVE_HASH_CODE_GENERATOR, valueConverter, - CHARSEQUENCE_TO_STRING_CONVERTER, nameConverter); + public DefaultTextHeaders(Map map, + NameValidator nameValidator, + ValueConverter valueConverter, + boolean singleHeaderFields) { + super(map, nameValidator, valueConverter, CharSequenceToStringConverter.INSTANCE); valuesComposer = singleHeaderFields ? new SingleHeaderValuesComposer() : new MultipleFieldsValueComposer(); } @Override public boolean contains(CharSequence name, CharSequence value, boolean ignoreCase) { - return contains(name, value, comparator(ignoreCase)); - } - - @Override - public boolean containsObject(CharSequence name, Object value, boolean ignoreCase) { - return containsObject(name, value, comparator(ignoreCase)); + return contains(name, value, + ignoreCase ? CHARSEQUENCE_CASE_INSENSITIVE_ORDER : CHARSEQUENCE_CASE_SENSITIVE_ORDER); } @Override @@ -398,10 +251,6 @@ public class DefaultTextHeaders extends DefaultConvertibleHeaders comparator(boolean ignoreCase) { - return ignoreCase ? CHARSEQUENCE_CASE_INSENSITIVE_ORDER : CHARSEQUENCE_CASE_SENSITIVE_ORDER; - } - /* * This interface enables different implementations for adding/setting header values. * Concrete implementations can control how values are added, for example to add all @@ -413,6 +262,7 @@ public class DefaultTextHeaders extends DefaultConvertibleHeaders values); + TextHeaders addObject(CharSequence name, Object value); TextHeaders addObject(CharSequence name, Iterable values); TextHeaders addObject(CharSequence name, Object... values); @@ -446,6 +296,12 @@ public class DefaultTextHeaders extends DefaultConvertibleHeaders values) { DefaultTextHeaders.super.addObject(name, values); @@ -534,6 +390,11 @@ public class DefaultTextHeaders extends DefaultConvertibleHeaders values) { return addEscapedValue(name, commaSeparate(objectEscaper(), values)); @@ -579,7 +440,8 @@ public class DefaultTextHeaders extends DefaultConvertibleHeaders CharSequence commaSeparate(CsvValueEscaper escaper, T... values) { - StringBuilder sb = new StringBuilder(values.length * DEFAULT_VALUE_SIZE); + final int lengthEstimate = 10; + StringBuilder sb = new StringBuilder(values.length * lengthEstimate); if (values.length > 0) { int end = values.length - 1; for (int i = 0; i < end; i++) { @@ -625,4 +487,130 @@ public class DefaultTextHeaders extends DefaultConvertibleHeaders { + + private static final CharSequenceToStringConverter INSTANCE = new CharSequenceToStringConverter(); + + @Override + public String toConvertedType(CharSequence value) { + return value.toString(); + } + + @Override + public CharSequence toUnconvertedType(String value) { + return value; + } + } + + protected static class CharSequenceConverter implements ValueConverter { + + public static final CharSequenceConverter INSTANCE = new CharSequenceConverter(); + + @Override + public CharSequence convertObject(Object value) { + if (value instanceof CharSequence) { + return (CharSequence) value; + } + return value.toString(); + } + + @Override + public CharSequence convertInt(int value) { + return String.valueOf(value); + } + + @Override + public CharSequence convertLong(long value) { + return String.valueOf(value); + } + + @Override + public CharSequence convertDouble(double value) { + return String.valueOf(value); + } + + @Override + public CharSequence convertChar(char value) { + return String.valueOf(value); + } + + @Override + public CharSequence convertBoolean(boolean value) { + return String.valueOf(value); + } + + @Override + public CharSequence convertFloat(float value) { + return String.valueOf(value); + } + + @Override + public boolean convertToBoolean(CharSequence value) { + return Boolean.parseBoolean(value.toString()); + } + + @Override + public CharSequence convertByte(byte value) { + return String.valueOf(value); + } + + @Override + public byte convertToByte(CharSequence value) { + return Byte.valueOf(value.toString()); + } + + @Override + public char convertToChar(CharSequence value) { + if (value.length() == 0) { + throw new IllegalArgumentException("'value' is empty."); + } + return value.charAt(0); + } + + @Override + public CharSequence convertShort(short value) { + return String.valueOf(value); + } + + @Override + public short convertToShort(CharSequence value) { + return Short.valueOf(value.toString()); + } + + @Override + public int convertToInt(CharSequence value) { + return Integer.parseInt(value.toString()); + } + + @Override + public long convertToLong(CharSequence value) { + return Long.parseLong(value.toString()); + } + + @Override + public CharSequence convertTimeMillis(long value) { + return String.valueOf(value); + } + + @Override + public long convertToTimeMillis(CharSequence value) { + try { + return HeaderDateFormat.get().parse(value.toString()); + } catch (ParseException e) { + PlatformDependent.throwException(e); + } + return 0; + } + + @Override + public float convertToFloat(CharSequence value) { + return Float.valueOf(value.toString()); + } + + @Override + public double convertToDouble(CharSequence value) { + return Double.valueOf(value.toString()); + } + } } diff --git a/codec/src/main/java/io/netty/handler/codec/EmptyConvertibleHeaders.java b/codec/src/main/java/io/netty/handler/codec/EmptyConvertibleHeaders.java index 9a4ce83876..ea3b077bbe 100644 --- a/codec/src/main/java/io/netty/handler/codec/EmptyConvertibleHeaders.java +++ b/codec/src/main/java/io/netty/handler/codec/EmptyConvertibleHeaders.java @@ -54,14 +54,10 @@ public class EmptyConvertibleHeaders extends return Collections.emptyList(); } - @Override - public List> entriesConverted() { - return Collections.emptyList(); - } - @Override public Iterator> iteratorConverted() { - return entriesConverted().iterator(); + List> empty = Collections.emptyList(); + return empty.iterator(); } @Override diff --git a/codec/src/main/java/io/netty/handler/codec/EmptyHeaders.java b/codec/src/main/java/io/netty/handler/codec/EmptyHeaders.java index 48c5e52c3f..47e9c5e266 100644 --- a/codec/src/main/java/io/netty/handler/codec/EmptyHeaders.java +++ b/codec/src/main/java/io/netty/handler/codec/EmptyHeaders.java @@ -88,7 +88,7 @@ public class EmptyHeaders implements Headers { } @Override - public short getInt(T name, short defaultValue) { + public short getShort(T name, short defaultValue) { return defaultValue; } @@ -232,11 +232,6 @@ public class EmptyHeaders implements Headers { return defaultValue; } - @Override - public List> entries() { - return Collections.emptyList(); - } - @Override public boolean contains(T name) { return false; @@ -298,24 +293,7 @@ public class EmptyHeaders implements Headers { } @Override - public boolean contains(T name, T value, Comparator comparator) { - return false; - } - - @Override - public boolean contains(T name, T value, - Comparator keyComparator, Comparator valueComparator) { - return false; - } - - @Override - public boolean containsObject(T name, Object value, Comparator comparator) { - return false; - } - - @Override - public boolean containsObject(T name, Object value, Comparator keyComparator, - Comparator valueComparator) { + public boolean contains(T name, T value, Comparator valueComparator) { return false; } @@ -334,11 +312,6 @@ public class EmptyHeaders implements Headers { return Collections.emptySet(); } - @Override - public List namesList() { - return Collections.emptyList(); - } - @Override public Headers add(T name, T value) { throw new UnsupportedOperationException("read only"); @@ -415,7 +388,7 @@ public class EmptyHeaders implements Headers { } @Override - public Headers add(Headers headers) { + public Headers add(Headers headers) { throw new UnsupportedOperationException("read only"); } @@ -495,12 +468,12 @@ public class EmptyHeaders implements Headers { } @Override - public Headers set(Headers headers) { + public Headers set(Headers headers) { throw new UnsupportedOperationException("read only"); } @Override - public Headers setAll(Headers headers) { + public Headers setAll(Headers headers) { throw new UnsupportedOperationException("read only"); } @@ -516,17 +489,8 @@ public class EmptyHeaders implements Headers { @Override public Iterator> iterator() { - return entries().iterator(); - } - - @Override - public Entry forEachEntry(Headers.EntryVisitor visitor) throws Exception { - return null; - } - - @Override - public T forEachName(Headers.NameVisitor visitor) throws Exception { - return null; + List> empty = Collections.emptyList(); + return empty.iterator(); } @Override diff --git a/codec/src/main/java/io/netty/handler/codec/EmptyTextHeaders.java b/codec/src/main/java/io/netty/handler/codec/EmptyTextHeaders.java index a221086860..d9db4e31cf 100644 --- a/codec/src/main/java/io/netty/handler/codec/EmptyTextHeaders.java +++ b/codec/src/main/java/io/netty/handler/codec/EmptyTextHeaders.java @@ -25,11 +25,6 @@ public class EmptyTextHeaders extends EmptyConvertibleHeaders extends Iterable> { - /** - * Provides an abstraction to iterate over elements maintained in the {@link Headers} collection. - */ - interface EntryVisitor { - /** - * @return
    - *
  • {@code true} if the processor wants to continue the loop and handle the entry.
  • - *
  • {@code false} if the processor wants to stop handling headers and abort the loop.
  • - *
- */ - boolean visit(Map.Entry entry) throws Exception; - } +public interface Headers extends Iterable> { /** - * Provides an abstraction to iterate over elements maintained in the {@link Headers} collection. - */ - interface NameVisitor { - /** - * @return
    - *
  • {@code true} if the processor wants to continue the loop and handle the entry.
  • - *
  • {@code false} if the processor wants to stop handling headers and abort the loop.
  • - *
- */ - boolean visit(T name) throws Exception; - } - - /** - * Converts to/from a generic object to the type of the name for this map + * Converts to/from a generic object to the type of the headers. */ interface ValueConverter { T convertObject(Object value); @@ -92,458 +66,469 @@ public interface Headers extends Iterable> { } /** - * Returns the value of a header with the specified name. If there are more than one values for the specified name, - * the first value is returned. + * Returns the value of a header with the specified name. If there is more than one value for the specified name, + * the first value in insertion order is returned. * - * @param name the name of the header to search - * @return the first header value if the header is found. {@code null} if there's no such header. + * @param name the name of the header to retrieve + * @return the first header value if the header is found. {@code null} if there's no such header */ T get(T name); /** - * Returns the value of a header with the specified name. If there are more than one values for the specified name, - * the first value is returned. + * Returns the value of a header with the specified name. If there is more than one value for the specified name, + * the first value in insertion order is returned. * - * @param name the name of the header to search + * @param name the name of the header to retrieve * @param defaultValue the default value - * @return the first header value if the header is found. {@code defaultValue} if there's no such header. + * @return the first header value or {@code defaultValue} if there is no such header */ T get(T name, T defaultValue); /** - * Returns and removes the value of a header with the specified name. If there are more than one values for the - * specified name, the first value is returned. + * Returns the value of a header with the specified name and removes it from this object. If there is more than + * one value for the specified name, the first value in insertion order is returned. * - * @param name the name of the header to search + * @param name the name of the header to retrieve * @return the first header value or {@code null} if there is no such header */ T getAndRemove(T name); /** - * Returns and removes the value of a header with the specified name. If there are more than one values for the - * specified name, the first value is returned. + * Returns the value of a header with the specified name and removes it from this object. If there is more than + * one value for the specified name, the first value in insertion order is returned. * - * @param name the name of the header to search + * @param name the name of the header to retrieve * @param defaultValue the default value * @return the first header value or {@code defaultValue} if there is no such header */ T getAndRemove(T name, T defaultValue); /** - * Returns the values of headers with the specified name + * Returns all values for the header with the specified name. The returned {@link List} can't be modified. * - * @param name The name of the headers to search - * @return A {@link List} of header values which will be empty if no values are found + * @param name the name of the header to retrieve + * @return a {@link List} of header values or an empty {@link List} if no values are found. */ List getAll(T name); /** - * Returns and Removes the values of headers with the specified name + * Returns all values for the header with the specified name and removes them from this object. + * The returned {@link List} can't be modified. * - * @param name The name of the headers to search - * @return A {@link List} of header values which will be empty if no values are found + * @param name the name of the header to retrieve + * @return a {@link List} of header values or an empty {@link List} if no values are found. */ List getAllAndRemove(T name); /** - * Returns the boolean value of a header with the specified name. If there are more than one values for the - * specified name, the first value is returned. + * Returns the {@code boolean} value of a header with the specified name. If there is more than one value for the + * specified name, the first value in insertion order is returned. * - * @param name the name of the header to search - * @return the first header value if the header is found and its value is a boolean. {@code null} if there's no such - * header or its value is not a boolean. + * @param name the name of the header to retrieve + * @return the {@code boolean} value of the first value in insertion order or {@code null} if there is no such + * value or it can't be converted to {@code boolean}. */ Boolean getBoolean(T name); /** - * Returns the boolean value of a header with the specified name. If there are more than one values for the - * specified name, the first value is returned. + * Returns the {@code boolean} value of a header with the specified name. If there is more than one value for the + * specified name, the first value in insertion order is returned. * - * @param name the name of the header to search + * @param name the name of the header to retrieve * @param defaultValue the default value - * @return the first header value if the header is found and its value is a boolean. {@code defaultValue} if there's - * no such header or its value is not a boolean. + * @return the {@code boolean} value of the first value in insertion order or {@code defaultValue} if there is no + * such value or it can't be converted to {@code boolean}. */ boolean getBoolean(T name, boolean defaultValue); /** - * Returns the byte value of a header with the specified name. If there are more than one values for the specified - * name, the first value is returned. + * Returns the {@code byte} value of a header with the specified name. If there is more than one value for the + * specified name, the first value in insertion order is returned. * - * @param name the name of the header to search - * @return the first header value if the header is found and its value is a byte. {@code null} if there's no such - * header or its value is not a byte. + * @param name the name of the header to retrieve + * @return the {@code byte} value of the first value in insertion order or {@code null} if there is no such + * value or it can't be converted to {@code byte}. */ Byte getByte(T name); /** - * Returns the byte value of a header with the specified name. If there are more than one values for the specified - * name, the first value is returned. + * Returns the {@code byte} value of a header with the specified name. If there is more than one value for the + * specified name, the first value in insertion order is returned. * - * @param name the name of the header to search + * @param name the name of the header to retrieve * @param defaultValue the default value - * @return the first header value if the header is found and its value is a byte. {@code defaultValue} if there's no - * such header or its value is not a byte. + * @return the {@code byte} value of the first value in insertion order or {@code defaultValue} if there is no + * such value or it can't be converted to {@code byte}. */ byte getByte(T name, byte defaultValue); /** - * Returns the char value of a header with the specified name. If there are more than one values for the specified - * name, the first value is returned. + * Returns the {@code char} value of a header with the specified name. If there is more than one value for the + * specified name, the first value in insertion order is returned. * - * @param name the name of the header to search - * @return the first header value if the header is found and its value is a char. {@code null} if there's no such - * header or its value is not a char. + * @param name the name of the header to retrieve + * @return the {@code char} value of the first value in insertion order or {@code null} if there is no such + * value or it can't be converted to {@code char}. */ Character getChar(T name); /** - * Returns the char value of a header with the specified name. If there are more than one values for the specified - * name, the first value is returned. + * Returns the {@code char} value of a header with the specified name. If there is more than one value for the + * specified name, the first value in insertion order is returned. * - * @param name the name of the header to search + * @param name the name of the header to retrieve * @param defaultValue the default value - * @return the first header value if the header is found and its value is a char. {@code defaultValue} if there's no - * such header or its value is not a char. + * @return the {@code char} value of the first value in insertion order or {@code defaultValue} if there is no + * such value or it can't be converted to {@code char}. */ char getChar(T name, char defaultValue); /** - * Returns the short value of a header with the specified name. If there are more than one values for the specified - * name, the first value is returned. + * Returns the {@code short} value of a header with the specified name. If there is more than one value for the + * specified name, the first value in insertion order is returned. * - * @param name the name of the header to search - * @return the first header value if the header is found and its value is a short. {@code null} if there's no such - * header or its value is not a short. + * @param name the name of the header to retrieve + * @return the {@code short} value of the first value in insertion order or {@code null} if there is no such + * value or it can't be converted to {@code short}. */ Short getShort(T name); /** - * Returns the short value of a header with the specified name. If there are more than one values for the specified - * name, the first value is returned. + * Returns the {@code short} value of a header with the specified name. If there is more than one value for the + * specified name, the first value in insertion order is returned. * - * @param name the name of the header to search + * @param name the name of the header to retrieve * @param defaultValue the default value - * @return the first header value if the header is found and its value is a short. {@code defaultValue} if there's - * no such header or its value is not a short. + * @return the {@code short} value of the first value in insertion order or {@code defaultValue} if there is no + * such value or it can't be converted to {@code short}. */ - short getInt(T name, short defaultValue); + short getShort(T name, short defaultValue); /** - * Returns the integer value of a header with the specified name. If there are more than one values for the - * specified name, the first value is returned. + * Returns the {@code int} value of a header with the specified name. If there is more than one value for the + * specified name, the first value in insertion order is returned. * - * @param name the name of the header to search - * @return the first header value if the header is found and its value is an integer. {@code null} if there's no - * such header or its value is not an integer. + * @param name the name of the header to retrieve + * @return the {@code int} value of the first value in insertion order or {@code null} if there is no such + * value or it can't be converted to {@code int}. */ Integer getInt(T name); /** - * Returns the integer value of a header with the specified name. If there are more than one values for the - * specified name, the first value is returned. + * Returns the {@code int} value of a header with the specified name. If there is more than one value for the + * specified name, the first value in insertion order is returned. * - * @param name the name of the header to search + * @param name the name of the header to retrieve * @param defaultValue the default value - * @return the first header value if the header is found and its value is an integer. {@code defaultValue} if - * there's no such header or its value is not an integer. + * @return the {@code int} value of the first value in insertion order or {@code defaultValue} if there is no + * such value or it can't be converted to {@code int}. */ int getInt(T name, int defaultValue); /** - * Returns the long value of a header with the specified name. If there are more than one values for the specified - * name, the first value is returned. + * Returns the {@code long} value of a header with the specified name. If there is more than one value for the + * specified name, the first value in insertion order is returned. * - * @param name the name of the header to search - * @return the first header value if the header is found and its value is a long. {@code null} if there's no such - * header or its value is not a long. + * @param name the name of the header to retrieve + * @return the {@code long} value of the first value in insertion order or {@code null} if there is no such + * value or it can't be converted to {@code long}. */ Long getLong(T name); /** - * Returns the long value of a header with the specified name. If there are more than one values for the specified - * name, the first value is returned. + * Returns the {@code long} value of a header with the specified name. If there is more than one value for the + * specified name, the first value in insertion order is returned. * - * @param name the name of the header to search + * @param name the name of the header to retrieve * @param defaultValue the default value - * @return the first header value if the header is found and its value is a long. {@code defaultValue} if there's no - * such header or its value is not a long. + * @return the {@code long} value of the first value in insertion order or {@code defaultValue} if there is no + * such value or it can't be converted to {@code long}. */ long getLong(T name, long defaultValue); /** - * Returns the float value of a header with the specified name. If there are more than one values for the specified - * name, the first value is returned. + * Returns the {@code float} value of a header with the specified name. If there is more than one value for the + * specified name, the first value in insertion order is returned. * - * @param name the name of the header to search - * @return the first header value if the header is found and its value is a float. {@code null} if there's no such - * header or its value is not a float. + * @param name the name of the header to retrieve + * @return the {@code float} value of the first value in insertion order or {@code null} if there is no such + * value or it can't be converted to {@code float}. */ Float getFloat(T name); /** - * Returns the float value of a header with the specified name. If there are more than one values for the specified - * name, the first value is returned. + * Returns the {@code float} value of a header with the specified name. If there is more than one value for the + * specified name, the first value in insertion order is returned. * - * @param name the name of the header to search + * @param name the name of the header to retrieve * @param defaultValue the default value - * @return the first header value if the header is found and its value is a float. {@code defaultValue} if there's - * no such header or its value is not a float. + * @return the {@code float} value of the first value in insertion order or {@code defaultValue} if there is no + * such value or it can't be converted to {@code float}. */ float getFloat(T name, float defaultValue); /** - * Returns the double value of a header with the specified name. If there are more than one values for the specified - * name, the first value is returned. + * Returns the {@code double} value of a header with the specified name. If there is more than one value for the + * specified name, the first value in insertion order is returned. * - * @param name the name of the header to search - * @return the first header value if the header is found and its value is a double. {@code null} if there's no such - * header or its value is not a double. + * @param name the name of the header to retrieve + * @return the {@code double} value of the first value in insertion order or {@code null} if there is no such + * value or it can't be converted to {@code double}. */ Double getDouble(T name); /** - * Returns the double value of a header with the specified name. If there are more than one values for the specified - * name, the first value is returned. + * Returns the {@code double} value of a header with the specified name. If there is more than one value for the + * specified name, the first value in insertion order is returned. * - * @param name the name of the header to search + * @param name the name of the header to retrieve * @param defaultValue the default value - * @return the first header value if the header is found and its value is a double. {@code defaultValue} if there's - * no such header or its value is not a double. + * @return the {@code double} value of the first value in insertion order or {@code defaultValue} if there is no + * such value or it can't be converted to {@code double}. */ double getDouble(T name, double defaultValue); /** - * Returns the date value of a header with the specified name as milliseconds. If there are more than one values for - * the specified name, the first value is returned. + * Returns the value of a header with the specified name in milliseconds. If there is more than one value for the + * specified name, the first value in insertion order is returned. * - * @param name The name of the header to search - * @return the first header value in milliseconds if the header is found and its value is a date. {@code null} if - * there's no such header or its value is not a date. + * @param name the name of the header to retrieve + * @return the milliseconds value of the first value in insertion order or {@code null} if there is no such + * value or it can't be converted to milliseconds. */ Long getTimeMillis(T name); /** - * Returns the date value of a header with the specified name as milliseconds. If there are more than one values for - * the specified name, the first value is returned. + * Returns the value of a header with the specified name in milliseconds. If there is more than one value for the + * specified name, the first value in insertion order is returned. * - * @param name The name of the header to search - * @param defaultValue default value - * @return the first header value in milliseconds if the header is found and its value is a date. - * {@code defaultValue} if there's no such header or its value is not a date. + * @param name the name of the header to retrieve + * @param defaultValue the default value + * @return the milliseconds value of the first value in insertion order or {@code defaultValue} if there is no such + * value or it can't be converted to milliseconds. */ long getTimeMillis(T name, long defaultValue); /** - * Returns and removes the boolean value of a header with the specified name. If there are more than one values for - * the specified name, the first value is returned. + * Returns the {@code boolean} value of a header with the specified {@code name} and removes the header from this + * object. If there is more than one value for the specified name, the first value in insertion order is returned. + * In any case all values for {@code name} are removed. * - * @param name the name of the header to search - * @return the first header value if the header is found and its value is a boolean. {@code null} if there's no such - * header or its value is not a boolean. + * @param name the name of the header to retrieve + * @return the {@code boolean} value of the first value in insertion order or {@code null} if there is no + * such value or it can't be converted to {@code boolean}. */ Boolean getBooleanAndRemove(T name); /** - * Returns and removes the boolean value of a header with the specified name. If there are more than one values for - * the specified name, the first value is returned. + * Returns the {@code boolean} value of a header with the specified {@code name} and removes the header from this + * object. If there is more than one value for the specified name, the first value in insertion order is returned. + * In any case all values for {@code name} are removed. * * @param name the name of the header to search * @param defaultValue the default value - * @return the first header value if the header is found and its value is a boolean. {@code defaultValue} if there - * is no such header or its value of header is not a boolean. + * @return the {@code boolean} value of the first value in insertion order or {@code defaultValue} if there is no + * such value or it can't be converted to {@code boolean}. */ boolean getBooleanAndRemove(T name, boolean defaultValue); /** - * Returns and removes the byte value of a header with the specified name. If there are more than one values for the - * specified name, the first value is returned. + * Returns the {@code byte} value of a header with the specified {@code name} and removes the header from this + * object. If there is more than one value for the specified name, the first value in insertion order is returned. + * In any case all values for {@code name} are removed. * * @param name the name of the header to search - * @return the first header value if the header is found and its value is a byte. {@code null} if there's no such - * header or its value is not a byte. + * @return the {@code byte} value of the first value in insertion order or {@code null} if there is no + * such value or it can't be converted to {@code byte}. */ Byte getByteAndRemove(T name); /** - * Returns and removes the byte value of a header with the specified name. If there are more than one values for the - * specified name, the first value is returned. + * Returns the {@code byte} value of a header with the specified {@code name} and removes the header from this + * object. If there is more than one value for the specified name, the first value in insertion order is returned. + * In any case all values for {@code name} are removed. * * @param name the name of the header to search * @param defaultValue the default value - * @return the first header value if the header is found and its value is a byte. {@code defaultValue} if there is - * no such header or its value of header is not a byte. + * @return the {@code byte} value of the first value in insertion order or {@code defaultValue} if there is no + * such value or it can't be converted to {@code byte}. */ byte getByteAndRemove(T name, byte defaultValue); /** - * Returns and removes the char value of a header with the specified name. If there are more than one values for the - * specified name, the first value is returned. + * Returns the {@code char} value of a header with the specified {@code name} and removes the header from this + * object. If there is more than one value for the specified name, the first value in insertion order is returned. + * In any case all values for {@code name} are removed. * * @param name the name of the header to search - * @return the first header value if the header is found and its value is a char. {@code null} if there's no such - * header or its value is not a char. + * @return the {@code char} value of the first value in insertion order or {@code null} if there is no + * such value or it can't be converted to {@code char}. */ Character getCharAndRemove(T name); /** - * Returns and removes the char value of a header with the specified name. If there are more than one values for the - * specified name, the first value is returned. + * Returns the {@code char} value of a header with the specified {@code name} and removes the header from this + * object. If there is more than one value for the specified name, the first value in insertion order is returned. + * In any case all values for {@code name} are removed. * * @param name the name of the header to search * @param defaultValue the default value - * @return the first header value if the header is found and its value is a char. {@code defaultValue} if there is - * no such header or its value of header is not a char. + * @return the {@code char} value of the first value in insertion order or {@code defaultValue} if there is no + * such value or it can't be converted to {@code char}. */ char getCharAndRemove(T name, char defaultValue); /** - * Returns and removes the short value of a header with the specified name. If there are more than one values for - * the specified name, the first value is returned. + * Returns the {@code short} value of a header with the specified {@code name} and removes the header from this + * object. If there is more than one value for the specified name, the first value in insertion order is returned. + * In any case all values for {@code name} are removed. * * @param name the name of the header to search - * @return the first header value if the header is found and its value is a short. {@code null} if there's no such - * header or its value is not a short. + * @return the {@code short} value of the first value in insertion order or {@code null} if there is no + * such value or it can't be converted to {@code short}. */ Short getShortAndRemove(T name); /** - * Returns and removes the short value of a header with the specified name. If there are more than one values for - * the specified name, the first value is returned. + * Returns the {@code short} value of a header with the specified {@code name} and removes the header from this + * object. If there is more than one value for the specified name, the first value in insertion order is returned. + * In any case all values for {@code name} are removed. * * @param name the name of the header to search * @param defaultValue the default value - * @return the first header value if the header is found and its value is a short. {@code defaultValue} if there is - * no such header or its value of header is not a short. + * @return the {@code short} value of the first value in insertion order or {@code defaultValue} if there is no + * such value or it can't be converted to {@code short}. */ short getShortAndRemove(T name, short defaultValue); /** - * Returns and removes the integer value of a header with the specified name. If there are more than one values for - * the specified name, the first value is returned. + * Returns the {@code int} value of a header with the specified {@code name} and removes the header from this + * object. If there is more than one value for the specified name, the first value in insertion order is returned. + * In any case all values for {@code name} are removed. * * @param name the name of the header to search - * @return the first header value if the header is found and its value is an integer. {@code null} if there's no - * such header or its value is not an integer. + * @return the {@code int} value of the first value in insertion order or {@code null} if there is no + * such value or it can't be converted to {@code int}. */ Integer getIntAndRemove(T name); /** - * Returns and removes the integer value of a header with the specified name. If there are more than one values for - * the specified name, the first value is returned. + * Returns the {@code int} value of a header with the specified {@code name} and removes the header from this + * object. If there is more than one value for the specified name, the first value in insertion order is returned. + * In any case all values for {@code name} are removed. * * @param name the name of the header to search * @param defaultValue the default value - * @return the first header value if the header is found and its value is an integer. {@code defaultValue} if there - * is no such header or its value of header is not an integer. + * @return the {@code int} value of the first value in insertion order or {@code defaultValue} if there is no + * such value or it can't be converted to {@code int}. */ int getIntAndRemove(T name, int defaultValue); /** - * Returns and removes the long value of a header with the specified name. If there are more than one values for the - * specified name, the first value is returned. + * Returns the {@code long} value of a header with the specified {@code name} and removes the header from this + * object. If there is more than one value for the specified name, the first value in insertion order is returned. + * In any case all values for {@code name} are removed. * * @param name the name of the header to search - * @return the first header value if the header is found and its value is a long. {@code null} if there's no such - * header or its value is not a long. + * @return the {@code long} value of the first value in insertion order or {@code null} if there is no + * such value or it can't be converted to {@code long}. */ Long getLongAndRemove(T name); /** - * Returns and removes the long value of a header with the specified name. If there are more than one values for the - * specified name, the first value is returned. + * Returns the {@code long} value of a header with the specified {@code name} and removes the header from this + * object. If there is more than one value for the specified name, the first value in insertion order is returned. + * In any case all values for {@code name} are removed. * * @param name the name of the header to search * @param defaultValue the default value - * @return the first header value if the header is found and its value is a long. {@code defaultValue} if there's no - * such header or its value is not a long. + * @return the {@code long} value of the first value in insertion order or {@code defaultValue} if there is no + * such value or it can't be converted to {@code long}. */ long getLongAndRemove(T name, long defaultValue); /** - * Returns and removes the float value of a header with the specified name. If there are more than one values for - * the specified name, the first value is returned. + * Returns the {@code float} value of a header with the specified {@code name} and removes the header from this + * object. If there is more than one value for the specified name, the first value in insertion order is returned. + * In any case all values for {@code name} are removed. * * @param name the name of the header to search - * @return the first header value if the header is found and its value is a float. {@code null} if there's no such - * header or its value is not a float. + * @return the {@code float} value of the first value in insertion order or {@code null} if there is no + * such value or it can't be converted to {@code float}. */ Float getFloatAndRemove(T name); /** - * Returns and removes the float value of a header with the specified name. If there are more than one values for - * the specified name, the first value is returned. + * Returns the {@code float} value of a header with the specified {@code name} and removes the header from this + * object. If there is more than one value for the specified name, the first value in insertion order is returned. + * In any case all values for {@code name} are removed. * * @param name the name of the header to search * @param defaultValue the default value - * @return the first header value if the header is found and its value is a float. {@code defaultValue} if there's - * no such header or its value is not a float. + * @return the {@code float} value of the first value in insertion order or {@code defaultValue} if there is no + * such value or it can't be converted to {@code float}. */ float getFloatAndRemove(T name, float defaultValue); /** - * Returns and removes the double value of a header with the specified name. If there are more than one values for - * the specified name, the first value is returned. + * Returns the {@code double} value of a header with the specified {@code name} and removes the header from this + * object. If there is more than one value for the specified name, the first value in insertion order is returned. + * In any case all values for {@code name} are removed. * * @param name the name of the header to search - * @return the first header value if the header is found and its value is a double. {@code null} if there's no such - * header or its value is not a double. + * @return the {@code double} value of the first value in insertion order or {@code null} if there is no + * such value or it can't be converted to {@code double}. */ Double getDoubleAndRemove(T name); /** - * Returns and removes the double value of a header with the specified name. If there are more than one values for - * the specified name, the first value is returned. + * Returns the {@code double} value of a header with the specified {@code name} and removes the header from this + * object. If there is more than one value for the specified name, the first value in insertion order is returned. + * In any case all values for {@code name} are removed. * * @param name the name of the header to search * @param defaultValue the default value - * @return the first header value if the header is found and its value is a double. {@code defaultValue} if there's - * no such header or its value is not a double. + * @return the {@code double} value of the first value in insertion order or {@code defaultValue} if there is no + * such value or it can't be converted to {@code double}. */ double getDoubleAndRemove(T name, double defaultValue); /** - * Returns and removes the date value of a header with the specified name as milliseconds. If there are more than - * one values for the specified name, the first value is returned. + * Returns the value of a header with the specified {@code name} in milliseconds and removes the header from this + * object. If there is more than one value for the specified {@code name}, the first value in insertion order is + * returned. In any case all values for {@code name} are removed. * - * @param name The name of the header to search - * @return the first header value in milliseconds if the header is found and its value is a date. {@code null} if - * there's no such header or its value is not a date. + * @param name the name of the header to retrieve + * @return the milliseconds value of the first value in insertion order or {@code null} if there is no such + * value or it can't be converted to milliseconds. */ Long getTimeMillisAndRemove(T name); /** - * Returns and removes the date value of a header with the specified name as milliseconds. If there are more than - * one values for the specified name, the first value is returned. + * Returns the value of a header with the specified {@code name} in milliseconds and removes the header from this + * object. If there is more than one value for the specified {@code name}, the first value in insertion order is + * returned. In any case all values for {@code name} are removed. * - * @param name The name of the header to search - * @param defaultValue default value - * @return the first header value in milliseconds if the header is found and its value is a date. - * {@code defaultValue} if there's no such header or its value is not a date. + * @param name the name of the header to retrieve + * @param defaultValue the default value + * @return the milliseconds value of the first value in insertion order or {@code defaultValue} if there is no such + * value or it can't be converted to milliseconds. */ long getTimeMillisAndRemove(T name, long defaultValue); /** - * Returns a new {@link List} that contains all headers in this object. Note that modifying the returned - * {@link List} will not affect the state of this object. If you intend to enumerate over the header entries only, - * use {@link #iterator()} instead, which has much less overhead. - */ - List> entries(); - - /** - * Returns {@code true} if and only if this collection contains the header with the specified name. + * Returns {@code true} if a header with the {@code name} exists, {@code false} otherwise. * - * @param name The name of the header to search for - * @return {@code true} if at least one header is found + * @param name the header name */ boolean contains(T name); /** - * Returns {@code true} if a header with the name and value exists. - * + * Returns {@code true} if a header with the {@code name} and {@code value} exists, {@code false} otherwise. + *

+ * The {@link Object#equals(Object)} method is used to test for equality of {@code value}. + *

* @param name the header name - * @param value the header value - * @return {@code true} if it contains it {@code false} otherwise */ boolean contains(T name, T value); @@ -638,353 +623,281 @@ public interface Headers extends Iterable> { boolean containsTimeMillis(T name, long value); /** - * Returns {@code true} if a header with the name and value exists. + * Returns {@code true} if a header with the {@code name} and {@code value} exists. * * @param name the header name * @param value the header value - * @param comparator The comparator to use when comparing {@code name} and {@code value} to entries in this map + * @param valueComparator The comparator to use when comparing {@code value} to entries in this map * @return {@code true} if it contains it {@code false} otherwise */ - boolean contains(T name, T value, Comparator comparator); + boolean contains(T name, T value, Comparator valueComparator); /** - * Returns {@code true} if a header with the name and value exists. - * - * @param name the header name - * @param value the header value - * @param keyComparator The comparator to use when comparing {@code name} to names in this map - * @param valueComparator The comparator to use when comparing {@code value} to values in this map - * @return {@code true} if it contains it {@code false} otherwise - */ - boolean contains(T name, T value, Comparator keyComparator, Comparator valueComparator); - - /** - * Returns {@code true} if a header with the name and value exists. - * - * @param name the header name - * @param value the header value - * @param comparator The comparator to use when comparing {@code name} and {@code value} to entries in this map - * @return {@code true} if it contains it {@code false} otherwise - */ - boolean containsObject(T name, Object value, Comparator comparator); - - /** - * Returns {@code true} if a header with the name and value exists. - * - * @param name the header name - * @param value the header value - * @param keyComparator The comparator to use when comparing {@code name} to names in this map - * @param valueComparator The comparator to use when comparing {@code value} to values in this map - * @return {@code true} if it contains it {@code false} otherwise - */ - boolean containsObject(T name, Object value, Comparator keyComparator, - Comparator valueComparator); - - /** - * Returns the number of header entries in this collection. + * Returns the number of headers in this object. */ int size(); /** - * Returns {@code true} if and only if this collection contains no header entries. + * Returns {@code true} if {@link #size()} equals {@code 0}. */ boolean isEmpty(); /** - * Returns a new {@link Set} that contains the names of all headers in this object. Note that modifying the returned - * {@link Set} will not affect the state of this object. If you intend to enumerate over the header entries only, - * use {@link #iterator()} instead, which has much less overhead. + * Returns a {@link Set} of all header names in this object. The returned {@link Set} cannot be modified. */ Set names(); /** - * Returns a new {@link List} that contains the names of all headers in this object. Note that modifying the - * returned {@link List} will not affect the state of this object. If you intend to enumerate over the header - * entries only, use {@link #iterator()} instead, which has much less overhead. - */ - List namesList(); - - /** - * Adds a new header with the specified name and value. If the specified value is not a {@link String}, it is - * converted into a {@link String} by {@link Object#toString()}, except in the cases of {@link java.util.Date} and - * {@link java.util.Calendar}, which are formatted to the date format defined in RFC2616. + * Adds a new header with the specified {@code name} and {@code value}. * - * @param name the name of the header being added - * @param value the value of the header being added + * @param name the name of the header + * @param value the value of the header * @return {@code this} */ Headers add(T name, T value); /** - * Adds a new header with the specified name and values. This getMethod can be represented approximately as the - * following code: + * Adds new headers with the specified {@code name} and {@code values}. This method is semantically equivalent to * *
-     * for (Object v : values) {
-     *     if (v == null) {
-     *         break;
-     *     }
-     *     headers.add(name, v);
+     * for (T value : values) {
+     *     headers.add(name, value);
      * }
      * 
* - * @param name the name of the headepublic abstract rs being set - * @param values the values of the headers being set + * @param name the header name + * @param values the values of the header * @return {@code this} */ Headers add(T name, Iterable values); /** - * Adds a new header with the specified name and values. This getMethod can be represented approximately as the - * following code: + * Adds new headers with the specified {@code name} and {@code values}. This method is semantically equivalent to * *
-     * for (Object v : values) {
-     *     if (v == null) {
-     *         break;
-     *     }
-     *     headers.add(name, v);
+     * for (T value : values) {
+     *     headers.add(name, value);
      * }
      * 
* - * @param name the name of the headepublic abstract rs being set - * @param values the values of the headers being set + * @param name the header name + * @param values the values of the header * @return {@code this} */ Headers add(T name, T... values); /** - * Adds a new header with the specified name and value. If the specified value is not a {@link String}, it is - * converted into a {@link String} by {@link Object#toString()}, except in the cases of {@link java.util.Date} and - * {@link java.util.Calendar}, which are formatted to the date format defined in RFC2616. + * Adds a new header. Before the {@code value} is add, it's converted to type {@code T} by a call to + * {@link ValueConverter#convertObject(java.lang.Object)}. * - * @param name the name of the header being added - * @param value the value of the header being added + * @param name the header name + * @param value the value of the header * @return {@code this} */ Headers addObject(T name, Object value); /** - * Adds a new header with the specified name and values. This getMethod can be represented approximately as the - * following code: + * Adds a new header with the specified name and values. This method is equivalent to * *
      * for (Object v : values) {
-     *     if (v == null) {
-     *         break;
-     *     }
-     *     headers.add(name, v);
+     *     headers.addObject(name, v);
      * }
      * 
* - * @param name the name of the headepublic abstract rs being set - * @param values the values of the headers being set + * @param name the header name + * @param values the value of the header * @return {@code this} */ Headers addObject(T name, Iterable values); /** - * Adds a new header with the specified name and values. This getMethod can be represented approximately as the - * following code: + * Adds a new header with the specified name and values. This method is equivalent to * *
      * for (Object v : values) {
-     *     if (v == null) {
-     *         break;
-     *     }
-     *     headers.add(name, v);
+     *     headers.addObject(name, v);
      * }
      * 
* - * @param name the name of the headepublic abstract rs being set - * @param values the values of the headers being set + * @param name the header name + * @param values the value of the header * @return {@code this} */ Headers addObject(T name, Object... values); /** - * Add the {@code name} to {@code value}. - * @param name The name to modify - * @param value The value + * Adds a new header. + * + * @param name the header name + * @param value the value of the header * @return {@code this} */ Headers addBoolean(T name, boolean value); /** - * Add the {@code name} to {@code value}. - * @param name The name to modify - * @param value The value + * Adds a new header. + * + * @param name the header name + * @param value the value of the header * @return {@code this} */ Headers addByte(T name, byte value); /** - * Add the {@code name} to {@code value}. - * @param name The name to modify - * @param value The value + * Adds a new header. + * + * @param name the header name + * @param value the value of the header * @return {@code this} */ Headers addChar(T name, char value); /** - * Add the {@code name} to {@code value}. - * @param name The name to modify - * @param value The value + * Adds a new header. + * + * @param name the header name + * @param value the value of the header * @return {@code this} */ Headers addShort(T name, short value); /** - * Add the {@code name} to {@code value}. - * @param name The name to modify - * @param value The value + * Adds a new header. + * + * @param name the header name + * @param value the value of the header * @return {@code this} */ Headers addInt(T name, int value); /** - * Add the {@code name} to {@code value}. - * @param name The name to modify - * @param value The value + * Adds a new header. + * + * @param name the header name + * @param value the value of the header * @return {@code this} */ Headers addLong(T name, long value); /** - * Add the {@code name} to {@code value}. - * @param name The name to modify - * @param value The value + * Adds a new header. + * + * @param name the header name + * @param value the value of the header * @return {@code this} */ Headers addFloat(T name, float value); /** - * Add the {@code name} to {@code value}. - * @param name The name to modify - * @param value The value + * Adds a new header. + * + * @param name the header name + * @param value the value of the header * @return {@code this} */ Headers addDouble(T name, double value); /** - * Add the {@code name} to {@code value}. - * @param name The name to modify - * @param value The value + * Adds a new header. + * + * @param name the header name + * @param value the value of the header * @return {@code this} */ Headers addTimeMillis(T name, long value); /** - * Adds all header entries of the specified {@code headers}. + * Adds all header names and values of {@code headers} to this object. * + * @throws IllegalArgumentException if {@code headers == this}. * @return {@code this} */ - Headers add(Headers headers); + Headers add(Headers headers); /** - * Sets a header with the specified name and value. If there is an existing header with the same name, it is - * removed. If the specified value is not a {@link String}, it is converted into a {@link String} by - * {@link Object#toString()}, except for {@link java.util.Date} and {@link java.util.Calendar}, which are formatted - * to the date format defined in RFC2616. + * Sets a header with the specified name and value. Any existing headers with the same name are overwritten. * - * @param name The name of the header being set - * @param value The value of the header being set + * @param name the header name + * @param value the value of the header * @return {@code this} */ Headers set(T name, T value); /** - * Sets a header with the specified name and values. If there is an existing header with the same name, it is - * removed. This getMethod can be represented approximately as the following code: + * Sets a new header with the specified name and values. This method is equivalent to * *
-     * headers.remove(name);
-     * for (Object v : values) {
-     *     if (v == null) {
-     *         break;
-     *     }
-     *     headers.add(name, v);
+     * for (T v : values) {
+     *     headers.addObject(name, v);
      * }
      * 
* - * @param name the name of the headers being set - * @param values the values of the headers being set + * @param name the header name + * @param values the value of the header * @return {@code this} */ Headers set(T name, Iterable values); /** - * Sets a header with the specified name and values. If there is an existing header with the same name, it is - * removed. This getMethod can be represented approximately as the following code: + * Sets a header with the specified name and values. Any existing headers with this name are removed. This method + * is equivalent to: * *
      * headers.remove(name);
-     * for (Object v : values) {
-     *     if (v == null) {
-     *         break;
-     *     }
+     * for (T v : values) {
      *     headers.add(name, v);
      * }
      * 
* - * @param name the name of the headers being set - * @param values the values of the headers being set + * @param name the header name + * @param values the value of the header * @return {@code this} */ Headers set(T name, T... values); /** - * Sets a header with the specified name and value. If there is an existing header with the same name, it is - * removed. If the specified value is not a {@link String}, it is converted into a {@link String} by - * {@link Object#toString()}, except for {@link java.util.Date} and {@link java.util.Calendar}, which are formatted - * to the date format defined in RFC2616. + * Sets a new header. Any existing headers with this name are removed. Before the {@code value} is add, it's + * converted to type {@code T} by a call to {@link ValueConverter#convertObject(java.lang.Object)}. * - * @param name The name of the header being set - * @param value The value of the header being set + * @param name the header name + * @param value the value of the header + * @throws NullPointerException if either {@code name} or {@code value} before or after its conversion is + * {@code null}. * @return {@code this} */ Headers setObject(T name, Object value); /** - * Sets a header with the specified name and values. If there is an existing header with the same name, it is - * removed. This getMethod can be represented approximately as the following code: + * Sets a header with the specified name and values. Any existing headers with this name are removed. This method + * is equivalent to: * *
      * headers.remove(name);
      * for (Object v : values) {
-     *     if (v == null) {
-     *         break;
-     *     }
-     *     headers.add(name, v);
+     *     headers.addObject(name, v);
      * }
      * 
* - * @param name the name of the headers being set - * @param values the values of the headers being set + * @param name the header name + * @param values the values of the header * @return {@code this} */ Headers setObject(T name, Iterable values); /** - * Sets a header with the specified name and values. If there is an existing header with the same name, it is - * removed. This getMethod can be represented approximately as the following code: + * Sets a header with the specified name and values. Any existing headers with this name are removed. This method + * is equivalent to: * *
      * headers.remove(name);
      * for (Object v : values) {
-     *     if (v == null) {
-     *         break;
-     *     }
-     *     headers.add(name, v);
+     *     headers.addObject(name, v);
      * }
      * 
* - * @param name the name of the headers being set - * @param values the values of the headers being set + * @param name the header name + * @param values the values of the header * @return {@code this} */ Headers setObject(T name, Object... values); @@ -1062,30 +975,30 @@ public interface Headers extends Iterable> { Headers setTimeMillis(T name, long value); /** - * Cleans the current header entries and copies all header entries of the specified {@code headers}. + * Clears the current header entries and copies all header entries of the specified {@code headers}. * * @return {@code this} */ - Headers set(Headers headers); + Headers set(Headers headers); /** - * Retains all current headers but calls {@link #set(Object, Object)} for each entry in {@code headers} + * Retains all current headers but calls {@link #set(T, T)} for each entry in {@code headers}. * - * @param headers The headers used to {@link #set(Object, Object)} values in this instance + * @param headers The headers used to {@link #set(T, T)} values in this instance * @return {@code this} */ - Headers setAll(Headers headers); + Headers setAll(Headers headers); /** - * Removes the header with the specified name. + * Removes all headers with the specified {@code name}. * - * @param name The name of the header to remove - * @return {@code true} if and only if at least one entry has been removed + * @param name the header name + * @return {@code true} if at least one entry has been removed. */ boolean remove(T name); /** - * Removes all headers. + * Removes all headers. After a call to this method {@link #size()} equals {@code 0}. * * @return {@code this} */ @@ -1093,18 +1006,4 @@ public interface Headers extends Iterable> { @Override Iterator> iterator(); - - /** - * Provides an abstraction to iterate over elements maintained in the {@link Headers} collection. - * @param visitor The visitor which will visit each element in this map - * @return The last entry before iteration stopped or {@code null} if iteration went past the end - */ - Map.Entry forEachEntry(EntryVisitor visitor) throws Exception; - - /** - * Provides an abstraction to iterate over elements maintained in the {@link Headers} collection. - * @param visitor The visitor which will visit each element in this map - * @return The last key before iteration stopped or {@code null} if iteration went past the end - */ - T forEachName(NameVisitor visitor) throws Exception; } diff --git a/codec/src/main/java/io/netty/handler/codec/TextHeaders.java b/codec/src/main/java/io/netty/handler/codec/TextHeaders.java index 0eb14ff071..394ed9d403 100644 --- a/codec/src/main/java/io/netty/handler/codec/TextHeaders.java +++ b/codec/src/main/java/io/netty/handler/codec/TextHeaders.java @@ -25,17 +25,6 @@ package io.netty.handler.codec; * . */ public interface TextHeaders extends ConvertibleHeaders { - /** - * A visitor that helps reduce GC pressure while iterating over a collection of {@link Headers}. - */ - interface EntryVisitor extends Headers.EntryVisitor { - } - - /** - * A visitor that helps reduce GC pressure while iterating over a collection of {@link Headers}. - */ - interface NameVisitor extends Headers.NameVisitor { - } /** * Returns {@code true} if a header with the name and value exists. @@ -45,14 +34,6 @@ public interface TextHeaders extends ConvertibleHeaders { */ boolean contains(CharSequence name, CharSequence value, boolean ignoreCase); - /** - * Returns {@code true} if a header with the name and value exists. - * @param name the header name - * @param value the header value - * @return {@code true} if it contains it {@code false} otherwise - */ - boolean containsObject(CharSequence name, Object value, boolean ignoreCase); - @Override TextHeaders add(CharSequence name, CharSequence value); diff --git a/codec/src/test/java/io/netty/handler/codec/DefaultBinaryHeadersTest.java b/codec/src/test/java/io/netty/handler/codec/DefaultBinaryHeadersTest.java index c4bfb43b95..3da629c4ef 100644 --- a/codec/src/test/java/io/netty/handler/codec/DefaultBinaryHeadersTest.java +++ b/codec/src/test/java/io/netty/handler/codec/DefaultBinaryHeadersTest.java @@ -14,20 +14,25 @@ */ package io.netty.handler.codec; +import static java.util.Arrays.asList; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; -import io.netty.util.AsciiString; +import static org.junit.Assert.fail; + import io.netty.util.ByteString; -import java.util.HashSet; +import java.text.ParsePosition; import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Map.Entry; import java.util.NoSuchElementException; -import java.util.Random; -import java.util.Set; +import io.netty.util.CharsetUtil; import org.junit.Test; /** @@ -36,107 +41,186 @@ import org.junit.Test; public class DefaultBinaryHeadersTest { @Test - public void binaryHeadersWithSameValuesShouldBeEquivalent() { - byte[] key1 = randomBytes(); - byte[] value1 = randomBytes(); - byte[] key2 = randomBytes(); - byte[] value2 = randomBytes(); + public void addShouldIncreaseAndRemoveShouldDecreaseTheSize() { + DefaultBinaryHeaders headers = new DefaultBinaryHeaders(); + assertEquals(0, headers.size()); + headers.add(bs("name1"), bs("value1"), bs("value2")); + assertEquals(2, headers.size()); + headers.add(bs("name2"), bs("value3"), bs("value4")); + assertEquals(4, headers.size()); + headers.add(bs("name3"), bs("value5")); + assertEquals(5, headers.size()); - DefaultBinaryHeaders h1 = new DefaultBinaryHeaders(); - h1.set(as(key1), as(value1)); - h1.set(as(key2), as(value2)); - - DefaultBinaryHeaders h2 = new DefaultBinaryHeaders(); - h2.set(as(key1), as(value1)); - h2.set(as(key2), as(value2)); - - assertTrue(h1.equals(h2)); - assertTrue(h2.equals(h1)); - assertTrue(h2.equals(h2)); - assertTrue(h1.equals(h1)); + headers.remove(bs("name3")); + assertEquals(4, headers.size()); + headers.remove(bs("name1")); + assertEquals(2, headers.size()); + headers.remove(bs("name2")); + assertEquals(0, headers.size()); + assertTrue(headers.isEmpty()); } @Test - public void binaryHeadersWithSameDuplicateValuesShouldBeEquivalent() { - byte[] k1 = randomBytes(); - byte[] k2 = randomBytes(); - byte[] v1 = randomBytes(); - byte[] v2 = randomBytes(); - byte[] v3 = randomBytes(); - byte[] v4 = randomBytes(); - - DefaultBinaryHeaders h1 = new DefaultBinaryHeaders(); - h1.set(as(k1), as(v1)); - h1.set(as(k2), as(v2)); - h1.add(as(k2), as(v3)); - h1.add(as(k1), as(v4)); - - DefaultBinaryHeaders h2 = new DefaultBinaryHeaders(); - h2.set(as(k1), as(v1)); - h2.set(as(k2), as(v2)); - h2.add(as(k1), as(v4)); - h2.add(as(k2), as(v3)); - - assertTrue(h1.equals(h2)); - assertTrue(h2.equals(h1)); - assertTrue(h2.equals(h2)); - assertTrue(h1.equals(h1)); + public void afterClearHeadersShouldBeEmpty() { + DefaultBinaryHeaders headers = new DefaultBinaryHeaders(); + headers.add(bs("name1"), bs("value1")); + headers.add(bs("name2"), bs("value2")); + assertEquals(2, headers.size()); + headers.clear(); + assertEquals(0, headers.size()); + assertTrue(headers.isEmpty()); + assertFalse(headers.contains(bs("name1"))); + assertFalse(headers.contains(bs("name2"))); } @Test - public void binaryHeadersWithDifferentValuesShouldNotBeEquivalent() { - byte[] k1 = randomBytes(); - byte[] k2 = randomBytes(); - byte[] v1 = randomBytes(); - byte[] v2 = randomBytes(); - byte[] v3 = randomBytes(); - byte[] v4 = randomBytes(); - - DefaultBinaryHeaders h1 = new DefaultBinaryHeaders(); - h1.set(as(k1), as(v1)); - h1.set(as(k2), as(v2)); - h1.add(as(k2), as(v3)); - h1.add(as(k1), as(v4)); - - DefaultBinaryHeaders h2 = new DefaultBinaryHeaders(); - h2.set(as(k1), as(v1)); - h2.set(as(k2), as(v2)); - h2.add(as(k1), as(v4)); - - assertFalse(h1.equals(h2)); - assertFalse(h2.equals(h1)); - assertTrue(h2.equals(h2)); - assertTrue(h1.equals(h1)); + public void removingANameForASecondTimeShouldReturnFalse() { + DefaultBinaryHeaders headers = new DefaultBinaryHeaders(); + headers.add(bs("name1"), bs("value1")); + headers.add(bs("name2"), bs("value2")); + assertTrue(headers.remove(bs("name2"))); + assertFalse(headers.remove(bs("name2"))); } @Test - public void binarySetAllShouldMergeHeaders() { - byte[] k1 = randomBytes(); - byte[] k2 = randomBytes(); - byte[] v1 = randomBytes(); - byte[] v2 = randomBytes(); - byte[] v3 = randomBytes(); - byte[] v4 = randomBytes(); + public void multipleValuesPerNameShouldBeAllowed() { + DefaultBinaryHeaders headers = new DefaultBinaryHeaders(); + headers.add(bs("name"), bs("value1")); + headers.add(bs("name"), bs("value2")); + headers.add(bs("name"), bs("value3")); + assertEquals(3, headers.size()); + List values = headers.getAll(bs("name")); + assertEquals(3, values.size()); + assertTrue(values.containsAll(asList(bs("value1"), bs("value2"), bs("value3")))); + } + + @Test + public void testContains() { + DefaultBinaryHeaders headers = new DefaultBinaryHeaders(); + headers.addBoolean(bs("boolean"), true); + assertTrue(headers.containsBoolean(bs("boolean"), true)); + assertFalse(headers.containsBoolean(bs("boolean"), false)); + + headers.addLong(bs("long"), Long.MAX_VALUE); + assertTrue(headers.containsLong(bs("long"), Long.MAX_VALUE)); + assertFalse(headers.containsLong(bs("long"), Long.MIN_VALUE)); + + headers.addInt(bs("int"), Integer.MIN_VALUE); + assertTrue(headers.containsInt(bs("int"), Integer.MIN_VALUE)); + assertFalse(headers.containsInt(bs("int"), Integer.MAX_VALUE)); + + headers.addShort(bs("short"), Short.MAX_VALUE); + assertTrue(headers.containsShort(bs("short"), Short.MAX_VALUE)); + assertFalse(headers.containsShort(bs("short"), Short.MIN_VALUE)); + + headers.addChar(bs("char"), Character.MAX_VALUE); + assertTrue(headers.containsChar(bs("char"), Character.MAX_VALUE)); + assertFalse(headers.containsChar(bs("char"), Character.MIN_VALUE)); + + headers.addByte(bs("byte"), Byte.MAX_VALUE); + assertTrue(headers.containsByte(bs("byte"), Byte.MAX_VALUE)); + assertFalse(headers.containsLong(bs("byte"), Byte.MIN_VALUE)); + + headers.addDouble(bs("double"), Double.MAX_VALUE); + assertTrue(headers.containsDouble(bs("double"), Double.MAX_VALUE)); + assertFalse(headers.containsDouble(bs("double"), Double.MIN_VALUE)); + + headers.addFloat(bs("float"), Float.MAX_VALUE); + assertTrue(headers.containsFloat(bs("float"), Float.MAX_VALUE)); + assertFalse(headers.containsFloat(bs("float"), Float.MIN_VALUE)); + + long millis = System.currentTimeMillis(); + headers.addTimeMillis(bs("millis"), millis); + assertTrue(headers.containsTimeMillis(bs("millis"), millis)); + // This test doesn't work on midnight, January 1, 1970 UTC + assertFalse(headers.containsTimeMillis(bs("millis"), 0)); + + headers.addObject(bs("object"), "Hello World"); + assertTrue(headers.containsObject(bs("object"), "Hello World")); + assertFalse(headers.containsObject(bs("object"), "")); + + headers.add(bs("name"), bs("value")); + assertTrue(headers.contains(bs("name"), bs("value"))); + assertFalse(headers.contains(bs("name"), bs("value1"))); + } + + @Test + public void canMixConvertedAndNormalValues() { + DefaultBinaryHeaders headers = new DefaultBinaryHeaders(); + headers.add(bs("name"), bs("value")); + headers.addInt(bs("name"), 100); + headers.addBoolean(bs("name"), false); + + assertEquals(3, headers.size()); + assertTrue(headers.contains(bs("name"))); + assertTrue(headers.contains(bs("name"), bs("value"))); + assertTrue(headers.containsInt(bs("name"), 100)); + assertTrue(headers.containsBoolean(bs("name"), false)); + } + + @Test + public void testGetAndRemove() { + DefaultBinaryHeaders headers = new DefaultBinaryHeaders(); + headers.add(bs("name1"), bs("value1")); + headers.add(bs("name2"), bs("value2"), bs("value3")); + headers.add(bs("name3"), bs("value4"), bs("value5"), bs("value6")); + + assertEquals(bs("value1"), headers.getAndRemove(bs("name1"), bs("defaultvalue"))); + assertEquals(bs("value2"), headers.getAndRemove(bs("name2"))); + assertNull(headers.getAndRemove(bs("name2"))); + assertEquals(asList(bs("value4"), bs("value5"), bs("value6")), headers.getAllAndRemove(bs("name3"))); + assertEquals(0, headers.size()); + assertNull(headers.getAndRemove(bs("noname"))); + assertEquals(bs("defaultvalue"), headers.getAndRemove(bs("noname"), bs("defaultvalue"))); + } + + @Test + public void whenNameContainsMultipleValuesGetShouldReturnTheFirst() { + DefaultBinaryHeaders headers = new DefaultBinaryHeaders(); + headers.add(bs("name1"), bs("value1"), bs("value2")); + assertEquals(bs("value1"), headers.get(bs("name1"))); + } + + @Test + public void getWithDefaultValueWorks() { + DefaultBinaryHeaders headers = new DefaultBinaryHeaders(); + headers.add(bs("name1"), bs("value1")); + + assertEquals(bs("value1"), headers.get(bs("name1"), bs("defaultvalue"))); + assertEquals(bs("defaultvalue"), headers.get(bs("noname"), bs("defaultvalue"))); + } + + @Test + public void setShouldOverWritePreviousValue() { + DefaultBinaryHeaders headers = new DefaultBinaryHeaders(); + headers.set(bs("name"), bs("value1")); + headers.set(bs("name"), bs("value2")); + assertEquals(1, headers.size()); + assertEquals(1, headers.getAll(bs("name")).size()); + assertEquals(bs("value2"), headers.getAll(bs("name")).get(0)); + assertEquals(bs("value2"), headers.get(bs("name"))); + } + + @Test + public void setAllShouldOverwriteSomeAndLeaveOthersUntouched() { DefaultBinaryHeaders h1 = new DefaultBinaryHeaders(); - h1.set(as(k1), as(v1)); - h1.set(as(k2), as(v2)); - h1.add(as(k2), as(v3)); - h1.add(as(k1), as(v4)); + + h1.add(bs("name1"), bs("value1")); + h1.add(bs("name2"), bs("value2")); + h1.add(bs("name2"), bs("value3")); + h1.add(bs("name3"), bs("value4")); DefaultBinaryHeaders h2 = new DefaultBinaryHeaders(); - h2.set(as(k1), as(v1)); - h2.set(as(k2), as(v2)); - h2.add(as(k1), as(v4)); + h2.add(bs("name1"), bs("value5")); + h2.add(bs("name2"), bs("value6")); + h2.add(bs("name1"), bs("value7")); DefaultBinaryHeaders expected = new DefaultBinaryHeaders(); - expected.set(as(k1), as(v1)); - expected.set(as(k2), as(v2)); - expected.add(as(k2), as(v3)); - expected.add(as(k1), as(v4)); - expected.set(as(k1), as(v1)); - expected.set(as(k2), as(v2)); - expected.set(as(k1), as(v4)); + expected.add(bs("name1"), bs("value5")); + expected.add(bs("name2"), bs("value6")); + expected.add(bs("name1"), bs("value7")); + expected.add(bs("name3"), bs("value4")); h1.setAll(h2); @@ -144,118 +228,64 @@ public class DefaultBinaryHeadersTest { } @Test - public void binarySetShouldReplacePreviousValues() { - byte[] k1 = randomBytes(); - byte[] v1 = randomBytes(); - byte[] v2 = randomBytes(); - byte[] v3 = randomBytes(); + public void headersWithSameNamesAndValuesShouldBeEquivalent() { + DefaultBinaryHeaders headers1 = new DefaultBinaryHeaders(); + headers1.add(bs("name1"), bs("value1")); + headers1.add(bs("name2"), bs("value2")); + headers1.add(bs("name2"), bs("value3")); - DefaultBinaryHeaders h1 = new DefaultBinaryHeaders(); - h1.add(as(k1), as(v1)); - h1.add(as(k1), as(v2)); - assertEquals(2, h1.size()); + DefaultBinaryHeaders headers2 = new DefaultBinaryHeaders(); + headers2.add(bs("name1"), bs("value1")); + headers2.add(bs("name2"), bs("value2")); + headers2.add(bs("name2"), bs("value3")); - h1.set(as(k1), as(v3)); - assertEquals(1, h1.size()); - List list = h1.getAll(as(k1)); - assertEquals(1, list.size()); - assertEquals(as(v3), list.get(0)); + assertEquals(headers1, headers2); + assertEquals(headers2, headers1); + assertEquals(headers1, headers1); + assertEquals(headers2, headers2); + assertEquals(headers1.hashCode(), headers2.hashCode()); + assertEquals(headers1.hashCode(), headers1.hashCode()); + assertEquals(headers2.hashCode(), headers2.hashCode()); } @Test - public void headersWithSameValuesShouldBeEquivalent() { - DefaultBinaryHeaders h1 = new DefaultBinaryHeaders(); - h1.set(as("foo"), as("goo")); - h1.set(as("foo2"), as("goo2")); + public void emptyHeadersShouldBeEqual() { + DefaultBinaryHeaders headers1 = new DefaultBinaryHeaders(); + DefaultBinaryHeaders headers2 = new DefaultBinaryHeaders(); + assertNotSame(headers1, headers2); + assertEquals(headers1, headers2); + assertEquals(headers1.hashCode(), headers2.hashCode()); + } + @Test + public void headersWithSameNamesButDifferentValuesShouldNotBeEquivalent() { + DefaultBinaryHeaders headers1 = new DefaultBinaryHeaders(); + headers1.add(bs("name1"), bs("value1")); + DefaultBinaryHeaders headers2 = new DefaultBinaryHeaders(); + headers1.add(bs("name1"), bs("value2")); + assertNotEquals(headers1, headers2); + } + + @Test + public void subsetOfHeadersShouldNotBeEquivalent() { + DefaultBinaryHeaders headers1 = new DefaultBinaryHeaders(); + headers1.add(bs("name1"), bs("value1")); + headers1.add(bs("name2"), bs("value2")); + DefaultBinaryHeaders headers2 = new DefaultBinaryHeaders(); + headers1.add(bs("name1"), bs("value1")); + assertNotEquals(headers1, headers2); + } + + @Test + public void headersWithDifferentNamesAndValuesShouldNotBeEquivalent() { + DefaultBinaryHeaders h1 = new DefaultBinaryHeaders(); + h1.set(bs("name1"), bs("value1")); DefaultBinaryHeaders h2 = new DefaultBinaryHeaders(); - h2.set(as("foo"), as("goo")); - h2.set(as("foo2"), as("goo2")); - - assertTrue(h1.equals(h2)); - assertTrue(h2.equals(h1)); - assertTrue(h2.equals(h2)); - assertTrue(h1.equals(h1)); - } - - @Test - public void headersWithSameDuplicateValuesShouldBeEquivalent() { - DefaultBinaryHeaders h1 = new DefaultBinaryHeaders(); - h1.set(as("foo"), as("goo")); - h1.set(as("foo2"), as("goo2")); - h1.add(as("foo2"), as("goo3")); - h1.add(as("foo"), as("goo4")); - - DefaultBinaryHeaders h2 = new DefaultBinaryHeaders(); - h2.set(as("foo"), as("goo")); - h2.set(as("foo2"), as("goo2")); - h2.add(as("foo"), as("goo4")); - h2.add(as("foo2"), as("goo3")); - - assertTrue(h1.equals(h2)); - assertTrue(h2.equals(h1)); - assertTrue(h2.equals(h2)); - assertTrue(h1.equals(h1)); - } - - @Test - public void headersWithDifferentValuesShouldNotBeEquivalent() { - DefaultBinaryHeaders h1 = new DefaultBinaryHeaders(); - h1.set(as("foo"), as("goo")); - h1.set(as("foo2"), as("goo2")); - h1.add(as("foo2"), as("goo3")); - h1.add(as("foo"), as("goo4")); - - DefaultBinaryHeaders h2 = new DefaultBinaryHeaders(); - h2.set(as("foo"), as("goo")); - h2.set(as("foo2"), as("goo2")); - h2.add(as("foo"), as("goo4")); - - assertFalse(h1.equals(h2)); - assertFalse(h2.equals(h1)); - assertTrue(h2.equals(h2)); - assertTrue(h1.equals(h1)); - } - - @Test - public void setAllShouldMergeHeaders() { - DefaultBinaryHeaders h1 = new DefaultBinaryHeaders(); - h1.set(as("foo"), as("goo")); - h1.set(as("foo2"), as("goo2")); - h1.add(as("foo2"), as("goo3")); - h1.add(as("foo"), as("goo4")); - - DefaultBinaryHeaders h2 = new DefaultBinaryHeaders(); - h2.set(as("foo"), as("goo")); - h2.set(as("foo2"), as("goo2")); - h2.add(as("foo"), as("goo4")); - - DefaultBinaryHeaders expected = new DefaultBinaryHeaders(); - expected.set(as("foo"), as("goo")); - expected.set(as("foo2"), as("goo2")); - expected.add(as("foo2"), as("goo3")); - expected.add(as("foo"), as("goo4")); - expected.set(as("foo"), as("goo")); - expected.set(as("foo2"), as("goo2")); - expected.set(as("foo"), as("goo4")); - - h1.setAll(h2); - - assertEquals(expected, h1); - } - - @Test - public void setShouldReplacePreviousValues() { - DefaultBinaryHeaders h1 = new DefaultBinaryHeaders(); - h1.add(as("foo"), as("goo")); - h1.add(as("foo"), as("goo2")); - assertEquals(2, h1.size()); - - h1.set(as("foo"), as("goo3")); - assertEquals(1, h1.size()); - List list = h1.getAll(as("foo")); - assertEquals(1, list.size()); - assertEquals(as("goo3"), list.get(0)); + h2.set(bs("name2"), bs("value2")); + assertNotEquals(h1, h2); + assertNotEquals(h2, h1); + assertEquals(h1, h1); + assertEquals(h2, h2); } @Test(expected = NoSuchElementException.class) @@ -266,53 +296,138 @@ public class DefaultBinaryHeadersTest { } @Test - public void iterateHeadersShouldReturnAllValues() { - Set headers = new HashSet(); - headers.add("a:1"); - headers.add("a:2"); - headers.add("a:3"); - headers.add("b:1"); - headers.add("b:2"); - headers.add("c:1"); + public void iteratorShouldReturnAllNameValuePairs() { + DefaultBinaryHeaders headers1 = new DefaultBinaryHeaders(); + headers1.add(bs("name1"), bs("value1"), bs("value2")); + headers1.add(bs("name2"), bs("value3")); + headers1.add(bs("name3"), bs("value4"), bs("value5"), bs("value6")); + headers1.add(bs("name1"), bs("value7"), bs("value8")); + assertEquals(8, headers1.size()); - // Build the headers from the input set. - DefaultBinaryHeaders h1 = new DefaultBinaryHeaders(); - for (String header : headers) { - String[] parts = header.split(":"); - h1.add(as(parts[0]), as(parts[1])); + DefaultBinaryHeaders headers2 = new DefaultBinaryHeaders(); + for (Entry entry : headers1) { + Object v = entry.getValue(); + headers2.add(entry.getKey(), entry.getValue()); } - // Now iterate through the headers, removing them from the original set. - for (Map.Entry entry : h1) { - assertTrue(headers.remove(entry.getKey().toString() + ':' + entry.getValue().toString())); - } - - // Make sure we removed them all. - assertTrue(headers.isEmpty()); + assertEquals(headers1, headers2); } @Test - public void getAndRemoveShouldReturnFirstEntry() { - DefaultBinaryHeaders h1 = new DefaultBinaryHeaders(); - h1.add(as("foo"), as("goo")); - h1.add(as("foo"), as("goo2")); - assertEquals(as("goo"), h1.getAndRemove(as("foo"))); - assertEquals(0, h1.size()); - List values = h1.getAll(as("foo")); - assertEquals(0, values.size()); + public void iteratorSetValueShouldChangeHeaderValue() { + DefaultBinaryHeaders headers = new DefaultBinaryHeaders(); + headers.add(bs("name1"), bs("value1"), bs("value2"), bs("value3")); + headers.add(bs("name2"), bs("value4")); + assertEquals(4, headers.size()); + + Iterator> iter = headers.iterator(); + while (iter.hasNext()) { + Entry header = iter.next(); + if (bs("name1").equals(header.getKey()) && bs("value2").equals(header.getValue())) { + header.setValue(bs("updatedvalue2")); + assertEquals(bs("updatedvalue2"), header.getValue()); + } + if (bs("name1").equals(header.getKey()) && bs("value3").equals(header.getValue())) { + header.setValue(bs("updatedvalue3")); + assertEquals(bs("updatedvalue3"), header.getValue()); + } + } + + assertEquals(4, headers.size()); + assertTrue(headers.contains(bs("name1"), bs("updatedvalue2"))); + assertFalse(headers.contains(bs("name1"), bs("value2"))); + assertTrue(headers.contains(bs("name1"), bs("updatedvalue3"))); + assertFalse(headers.contains(bs("name1"), bs("value3"))); } - private static byte[] randomBytes() { - byte[] data = new byte[100]; - new Random().nextBytes(data); - return data; + @Test + public void getAllReturnsEmptyListForUnknownName() { + DefaultBinaryHeaders headers = new DefaultBinaryHeaders(); + assertEquals(0, headers.getAll(bs("noname")).size()); } - private AsciiString as(byte[] bytes) { - return new AsciiString(bytes); + @Test + public void canNotModifyTheListReturnedByGetAll() { + DefaultBinaryHeaders headers = new DefaultBinaryHeaders(); + headers.add(bs("name1"), bs("value1")); + headers.add(bs("name2"), bs("value2"), bs("value3")); + + // Test for single value names. + try { + headers.getAll(bs("name1")).add(bs("value")); + fail(); + } catch (UnsupportedOperationException e) { + // for checkstyle + } + try { + headers.getAll(bs("name1")).remove(0); + fail(); + } catch (UnsupportedOperationException e) { + // for checkstyle + } + + // Test for multi value names. + try { + headers.getAll(bs("name2")).add(bs("value")); + fail(); + } catch (UnsupportedOperationException e) { + // for checkstyle + } + try { + headers.getAll(bs("name2")).remove(0); + fail(); + } catch (UnsupportedOperationException e) { + // for checkstyle + } + + // Test for names that don't exist. + try { + headers.getAll(bs("name3")).add(bs("value")); + fail(); + } catch (UnsupportedOperationException e) { + // for checkstyle + } + try { + headers.getAll(bs("name3")).remove(0); + fail(); + } catch (UnsupportedOperationException e) { + // for checkstyle + } } - private AsciiString as(String value) { - return new AsciiString(value); + @Test + public void setHeadersShouldClearAndOverwrite() { + DefaultBinaryHeaders headers1 = new DefaultBinaryHeaders(); + headers1.add(bs("name"), bs("value")); + + DefaultBinaryHeaders headers2 = new DefaultBinaryHeaders(); + headers2.add(bs("name"), bs("newvalue")); + headers2.add(bs("name1"), bs("value1")); + + headers1.set(headers2); + assertEquals(headers1, headers2); + } + + @Test + public void setAllHeadersShouldOnlyOverwriteHeaders() { + DefaultBinaryHeaders headers1 = new DefaultBinaryHeaders(); + headers1.add(bs("name"), bs("value")); + headers1.add(bs("name1"), bs("value1")); + + DefaultBinaryHeaders headers2 = new DefaultBinaryHeaders(); + headers2.add(bs("name"), bs("newvalue")); + headers2.add(bs("name2"), bs("value2")); + + DefaultBinaryHeaders expected = new DefaultBinaryHeaders(); + expected.add(bs("name"), bs("newvalue")); + expected.add(bs("name1"), bs("value1")); + expected.add(bs("name2"), bs("value2")); + + headers1.setAll(headers2); + assertEquals(headers1, expected); + } + + private ByteString bs(String value) { + return new ByteString(value, CharsetUtil.US_ASCII); } } diff --git a/codec/src/test/java/io/netty/handler/codec/DefaultTextHeadersTest.java b/codec/src/test/java/io/netty/handler/codec/DefaultTextHeadersTest.java index 8be40ab036..142f0aacc7 100644 --- a/codec/src/test/java/io/netty/handler/codec/DefaultTextHeadersTest.java +++ b/codec/src/test/java/io/netty/handler/codec/DefaultTextHeadersTest.java @@ -16,8 +16,6 @@ package io.netty.handler.codec; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; import static io.netty.util.internal.StringUtil.COMMA; import static io.netty.util.internal.StringUtil.DOUBLE_QUOTE; import java.util.Arrays; @@ -31,88 +29,6 @@ public class DefaultTextHeadersTest { private static final String HEADER_NAME = "testHeader"; - @Test - public void testEqualsMultipleHeaders() { - DefaultTextHeaders h1 = new DefaultTextHeaders(); - h1.set("Foo", "goo"); - h1.set("foo2", "goo2"); - - DefaultTextHeaders h2 = new DefaultTextHeaders(); - h2.set("FoO", "goo"); - h2.set("fOO2", "goo2"); - - assertTrue(h1.equals(h2)); - assertTrue(h2.equals(h1)); - assertTrue(h2.equals(h2)); - assertTrue(h1.equals(h1)); - } - - @Test - public void testEqualsDuplicateMultipleHeaders() { - DefaultTextHeaders h1 = new DefaultTextHeaders(); - h1.set("FOO", "goo"); - h1.set("Foo2", "goo2"); - h1.add("fOo2", "goo3"); - h1.add("foo", "goo4"); - - DefaultTextHeaders h2 = new DefaultTextHeaders(); - h2.set("foo", "goo"); - h2.set("foo2", "goo2"); - h2.add("foo", "goo4"); - h2.add("foO2", "goo3"); - - assertTrue(h1.equals(h2)); - assertTrue(h2.equals(h1)); - assertTrue(h2.equals(h2)); - assertTrue(h1.equals(h1)); - } - - @Test - public void testNotEqualsDuplicateMultipleHeaders() { - DefaultTextHeaders h1 = new DefaultTextHeaders(); - h1.set("FOO", "goo"); - h1.set("foo2", "goo2"); - h1.add("foo2", "goo3"); - h1.add("foo", "goo4"); - - DefaultTextHeaders h2 = new DefaultTextHeaders(); - h2.set("foo", "goo"); - h2.set("foo2", "goo2"); - h2.add("foo", "goo4"); - - assertFalse(h1.equals(h2)); - assertFalse(h2.equals(h1)); - assertTrue(h2.equals(h2)); - assertTrue(h1.equals(h1)); - } - - @Test - public void testSetAll() { - DefaultTextHeaders h1 = new DefaultTextHeaders(); - h1.set("FOO", "goo"); - h1.set("foo2", "goo2"); - h1.add("foo2", "goo3"); - h1.add("foo", "goo4"); - - DefaultTextHeaders h2 = new DefaultTextHeaders(); - h2.set("foo", "goo"); - h2.set("foo2", "goo2"); - h2.add("foo", "goo4"); - - DefaultTextHeaders expected = new DefaultTextHeaders(); - expected.set("FOO", "goo"); - expected.set("foo2", "goo2"); - expected.add("foo2", "goo3"); - expected.add("foo", "goo4"); - expected.set("foo", "goo"); - expected.set("foo2", "goo2"); - expected.set("foo", "goo4"); - - h1.setAll(h2); - - assertEquals(expected, h1); - } - @Test public void addCharSequences() { final TextHeaders headers = newDefaultTextHeaders(); @@ -325,11 +241,11 @@ public class DefaultTextHeadersTest { } private static TextHeaders newDefaultTextHeaders() { - return new DefaultTextHeaders(); + return new DefaultTextHeaders(false); } private static TextHeaders newCsvTextHeaders() { - return new DefaultTextHeaders(true, true); + return new DefaultTextHeaders(true); } private static void addValues(final TextHeaders headers, HeaderValue... headerValues) { diff --git a/common/src/main/java/io/netty/util/AsciiString.java b/common/src/main/java/io/netty/util/AsciiString.java index 92b1db52ac..097fe33d6c 100644 --- a/common/src/main/java/io/netty/util/AsciiString.java +++ b/common/src/main/java/io/netty/util/AsciiString.java @@ -54,64 +54,53 @@ public final class AsciiString extends ByteString implements CharSequence, Compa }; public static final Comparator CHARSEQUENCE_CASE_INSENSITIVE_ORDER = new Comparator() { + @Override public int compare(CharSequence o1, CharSequence o2) { - if (o1 == o2) { - return 0; + int len1 = o1.length(); + int delta = len1 - o2.length(); + if (delta != 0) { + return delta; } - - AsciiString a1 = o1 instanceof AsciiString ? (AsciiString) o1 : null; - AsciiString a2 = o2 instanceof AsciiString ? (AsciiString) o2 : null; - - int result; - int length1 = o1.length(); - int length2 = o2.length(); - int minLength = Math.min(length1, length2); - if (a1 != null && a2 != null) { - final int a1Len = minLength + a1.arrayOffset(); + if (o1.getClass().equals(AsciiString.class) && o2.getClass().equals(AsciiString.class)) { + AsciiString a1 = (AsciiString) o1; + AsciiString a2 = (AsciiString) o2; + final int a1Len = a1.length() + a1.arrayOffset(); for (int i = a1.arrayOffset(), j = a2.arrayOffset(); i < a1Len; i++, j++) { - byte v1 = a1.value[i]; - byte v2 = a2.value[j]; - if (v1 == v2) { - continue; - } - int c1 = toLowerCase(v1); - int c2 = toLowerCase(v2); - result = c1 - c2; - if (result != 0) { - return result; - } - } - } else if (a1 != null) { - for (int i = a1.arrayOffset(), j = 0; j < minLength; i++, j++) { - int c1 = toLowerCase(a1.value[i]); - int c2 = toLowerCase(o2.charAt(j)); - result = c1 - c2; - if (result != 0) { - return result; - } - } - } else if (a2 != null) { - for (int i = 0, j = a2.arrayOffset(); i < minLength; i++, j++) { - int c1 = toLowerCase(o1.charAt(i)); - int c2 = toLowerCase(a2.value[j]); - result = c1 - c2; - if (result != 0) { - return result; + byte c1 = a1.value[i]; + byte c2 = a2.value[j]; + if (c1 != c2) { + if (c1 >= 'A' && c1 <= 'Z') { + c1 += 32; + } + if (c2 >= 'A' && c2 <= 'Z') { + c2 += 32; + } + delta = c1 - c2; + if (delta != 0) { + return delta; + } } } } else { - for (int i = 0; i < minLength; i++) { - int c1 = toLowerCase(o1.charAt(i)); - int c2 = toLowerCase(o2.charAt(i)); - result = c1 - c2; - if (result != 0) { - return result; + for (int i = len1 - 1; i >= 0; i --) { + char c1 = o1.charAt(i); + char c2 = o2.charAt(i); + if (c1 != c2) { + if (c1 >= 'A' && c1 <= 'Z') { + c1 += 32; + } + if (c2 >= 'A' && c2 <= 'Z') { + c2 += 32; + } + delta = c1 - c2; + if (delta != 0) { + return delta; + } } } } - - return length1 - length2; + return 0; } }; diff --git a/example/src/main/java/io/netty/example/http/upload/HttpUploadClient.java b/example/src/main/java/io/netty/example/http/upload/HttpUploadClient.java index ebada20a63..41fc23b7c9 100644 --- a/example/src/main/java/io/netty/example/http/upload/HttpUploadClient.java +++ b/example/src/main/java/io/netty/example/http/upload/HttpUploadClient.java @@ -45,6 +45,8 @@ import java.io.File; import java.io.FileNotFoundException; import java.net.InetSocketAddress; import java.net.URI; +import java.util.ArrayList; +import java.util.Iterator; import java.util.List; import java.util.Map.Entry; @@ -186,12 +188,17 @@ public final class HttpUploadClient { ); // send request - List> entries = headers.entriesConverted(); channel.writeAndFlush(request); // Wait for the server to close the connection. channel.closeFuture().sync(); + // convert headers to list + List> entries = new ArrayList>(headers.size()); + Iterator> iterConverted = headers.iteratorConverted(); + while (iterConverted.hasNext()) { + entries.add(iterConverted.next()); + } return entries; } diff --git a/microbench/src/main/java/io/netty/microbench/headers/ExampleHeaders.java b/microbench/src/main/java/io/netty/microbench/headers/ExampleHeaders.java new file mode 100644 index 0000000000..606ead4d70 --- /dev/null +++ b/microbench/src/main/java/io/netty/microbench/headers/ExampleHeaders.java @@ -0,0 +1,148 @@ +/* + * Copyright 2015 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.microbench.headers; + +import java.util.EnumMap; +import java.util.HashMap; +import java.util.Map; + +public final class ExampleHeaders { + + public enum HeaderExample { + THREE, + FIVE, + SIX, + EIGHT, + ELEVEN, + TWENTYTWO, + THIRTY + } + + public static final Map> EXAMPLES = + new EnumMap>(HeaderExample.class); + + static { + Map header = new HashMap(); + header.put(":method", "GET"); + header.put(":scheme", "https"); + header.put(":path", "/index.html"); + EXAMPLES.put(HeaderExample.THREE, header); + + // Headers used by Norman's HTTP benchmarks with wrk + header = new HashMap(); + header.put("Method", "GET"); + header.put("Path", "/plaintext"); + header.put("Host", "localhost"); + header.put("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"); + header.put("Connection", "keep-alive"); + EXAMPLES.put(HeaderExample.FIVE, header); + + header = new HashMap(); + header.put(":authority", "127.0.0.1:33333"); + header.put(":method", "POST"); + header.put(":path", "/grpc.testing.TestService/UnaryCall"); + header.put(":scheme", "http"); + header.put("content-type", "application/grpc"); + header.put("te", "trailers"); + EXAMPLES.put(HeaderExample.SIX, header); + + header = new HashMap(); + header.put(":method", "POST"); + header.put(":scheme", "http"); + header.put(":path", "/google.pubsub.v2.PublisherService/CreateTopic"); + header.put(":authority", "pubsub.googleapis.com"); + header.put("grpc-timeout", "1S"); + header.put("content-type", "application/grpc+proto"); + header.put("grpc-encoding", "gzip"); + header.put("authorization", "Bearer y235.wef315yfh138vh31hv93hv8h3v"); + EXAMPLES.put(HeaderExample.EIGHT, header); + + header = new HashMap(); + header.put(":host", "twitter.com"); + header.put(":method", "GET"); + header.put(":path", "/"); + header.put(":scheme", "https"); + header.put(":version", "HTTP/1.1"); + header.put("accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8"); + header.put("accept-encoding", "gzip, deflate, sdch"); + header.put("accept-language", "en-US,en;q=0.8"); + header.put("cache-control", "max-age=0"); + header.put("cookie:", "noneofyourbusiness"); + header.put("user-agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko)"); + EXAMPLES.put(HeaderExample.ELEVEN, header); + + header = new HashMap(); + header.put("cache-control", "no-cache, no-store, must-revalidate, pre-check=0, post-check=0"); + header.put("content-encoding", "gzip"); + header.put("content-security-policy", "default-src https:; connect-src https:;"); + header.put("content-type", "text/html;charset=utf-8"); + header.put("date", "Wed, 22 Apr 2015 00:40:28 GMT"); + header.put("expires", "Tue, 31 Mar 1981 05:00:00 GMT"); + header.put("last-modified", "Wed, 22 Apr 2015 00:40:28 GMT"); + header.put("ms", "ms"); + header.put("pragma", "no-cache"); + header.put("server", "tsa_b"); + header.put("set-cookie", "noneofyourbusiness"); + header.put("status", "200 OK"); + header.put("strict-transport-security", "max-age=631138519"); + header.put("version", "HTTP/1.1"); + header.put("x-connection-hash", "e176fe40accc1e2c613a34bc1941aa98"); + header.put("x-content-type-options", "nosniff"); + header.put("x-frame-options", "SAMEORIGIN"); + header.put("x-response-time", "22"); + header.put("x-transaction", "a54142ede693444d9"); + header.put("x-twitter-response-tags", "BouncerCompliant"); + header.put("x-ua-compatible", "IE=edge,chrome=1"); + header.put("x-xss-protection", "1; mode=block"); + EXAMPLES.put(HeaderExample.TWENTYTWO, header); + + header = new HashMap(); + header.put("Cache-Control", "no-cache"); + header.put("Content-Encoding", "gzip"); + header.put("Content-Security-Policy", "default-src *; script-src assets-cdn.github.com ..."); + header.put("Content-Type", "text/html; charset=utf-8"); + header.put("Date", "Fri, 10 Apr 2015 02:15:38 GMT"); + header.put("Server", "GitHub.com"); + header.put("Set-Cookie", "_gh_sess=eyJzZXNza...; path=/; secure; HttpOnly"); + header.put("Status", "200 OK"); + header.put("Strict-Transport-Security", "max-age=31536000; includeSubdomains; preload"); + header.put("Transfer-Encoding", "chunked"); + header.put("Vary", "X-PJAX"); + header.put("X-Content-Type-Options", "nosniff"); + header.put("X-Frame-Options", "deny"); + header.put("X-GitHub-Request-Id", "1"); + header.put("X-GitHub-Session-Id", "1"); + header.put("X-GitHub-User", "buchgr"); + header.put("X-Request-Id", "28f245e02fc872dcf7f31149e52931dd"); + header.put("X-Runtime", "0.082529"); + header.put("X-Served-By", "b9c2a233f7f3119b174dbd8be2"); + header.put("X-UA-Compatible", "IE=Edge,chrome=1"); + header.put("X-XSS-Protection", "1; mode=block"); + header.put("Via", "http/1.1 ir50.fp.bf1.yahoo.com (ApacheTrafficServer)"); + header.put("Content-Language", "en"); + header.put("Connection", "keep-alive"); + header.put("Pragma", "no-cache"); + header.put("Expires", "Sat, 01 Jan 2000 00:00:00 GMT"); + header.put("X-Moose", "majestic"); + header.put("x-ua-compatible", "IE=edge"); + header.put("CF-Cache-Status", "HIT"); + header.put("CF-RAY", "6a47f4f911e3-"); + EXAMPLES.put(HeaderExample.THIRTY, header); + } + + private ExampleHeaders() { + } +} diff --git a/microbench/src/main/java/io/netty/microbench/headers/HeadersBenchmark.java b/microbench/src/main/java/io/netty/microbench/headers/HeadersBenchmark.java new file mode 100644 index 0000000000..fc57991f9d --- /dev/null +++ b/microbench/src/main/java/io/netty/microbench/headers/HeadersBenchmark.java @@ -0,0 +1,157 @@ +/* + * Copyright 2015 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.microbench.headers; + +import io.netty.handler.codec.http.DefaultHttpHeaders; +import io.netty.handler.codec.http2.DefaultHttp2Headers; +import io.netty.handler.codec.http2.Http2Headers; +import io.netty.microbench.util.AbstractMicrobenchmark; +import io.netty.util.AsciiString; +import io.netty.util.ByteString; +import io.netty.util.CharsetUtil; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Threads; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.infra.Blackhole; + +import java.util.Map; +import java.util.Map.Entry; +import java.util.concurrent.TimeUnit; + +@Threads(1) +@State(Scope.Benchmark) +@Warmup(iterations = 5) +@Measurement(iterations = 10) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +public class HeadersBenchmark extends AbstractMicrobenchmark { + + @Param + ExampleHeaders.HeaderExample exampleHeader; + + AsciiString[] httpNames; + AsciiString[] httpValues; + + ByteString[] http2Names; + ByteString[] http2Values; + + DefaultHttpHeaders httpHeaders; + DefaultHttp2Headers http2Headers; + + @Setup(Level.Trial) + public void setup() { + Map headers = ExampleHeaders.EXAMPLES.get(exampleHeader); + httpNames = new AsciiString[headers.size()]; + httpValues = new AsciiString[headers.size()]; + http2Names = new ByteString[headers.size()]; + http2Values = new ByteString[headers.size()]; + httpHeaders = new DefaultHttpHeaders(false); + http2Headers = new DefaultHttp2Headers(); + int idx = 0; + for (Map.Entry header : headers.entrySet()) { + String name = header.getKey(); + String value = header.getValue(); + httpNames[idx] = new AsciiString(name); + httpValues[idx] = new AsciiString(value); + http2Names[idx] = new ByteString(name, CharsetUtil.US_ASCII); + http2Values[idx] = new ByteString(value, CharsetUtil.US_ASCII); + idx++; + httpHeaders.add(new AsciiString(name), new AsciiString(value)); + http2Headers.add(new ByteString(name, CharsetUtil.US_ASCII), new ByteString(value, CharsetUtil.US_ASCII)); + } + } + + @Benchmark + @BenchmarkMode(Mode.AverageTime) + public void httpGet(Blackhole bh) { + for (AsciiString name : httpNames) { + bh.consume(httpHeaders.get(name)); + } + } + + @Benchmark + @BenchmarkMode(Mode.AverageTime) + public DefaultHttpHeaders httpPut() { + DefaultHttpHeaders headers = new DefaultHttpHeaders(false); + for (int i = 0; i < httpNames.length; i++) { + headers.add(httpNames[i], httpValues[i]); + } + return headers; + } + + @Benchmark + @BenchmarkMode(Mode.AverageTime) + public void httpIterate(Blackhole bh) { + for (Entry entry : httpHeaders) { + bh.consume(entry); + } + } + + @Benchmark + @BenchmarkMode(Mode.AverageTime) + public void http2Get(Blackhole bh) { + for (ByteString name : http2Names) { + bh.consume(http2Headers.get(name)); + } + } + + @Benchmark + @BenchmarkMode(Mode.AverageTime) + public DefaultHttp2Headers http2Put() { + DefaultHttp2Headers headers = new DefaultHttp2Headers(); + for (int i = 0; i < httpNames.length; i++) { + headers.add(httpNames[i], httpValues[i]); + } + return headers; + } + + @Benchmark + @BenchmarkMode(Mode.AverageTime) + public void http2IterateNew(Blackhole bh) { + for (Entry entry : http2Headers) { + bh.consume(entry); + } + } + + @Benchmark + @BenchmarkMode(Mode.AverageTime) + public void http2IterateOld(Blackhole bh) { + // This is how we had to iterate in the Http2HeadersEncoder when writing the frames on the wire + // in order to ensure that reserved headers come first. + for (Http2Headers.PseudoHeaderName pseudoHeader : Http2Headers.PseudoHeaderName.values()) { + ByteString name = pseudoHeader.value(); + ByteString value = http2Headers.get(name); + if (value != null) { + bh.consume(value); + } + } + for (Entry entry : http2Headers) { + final ByteString name = entry.getKey(); + final ByteString value = entry.getValue(); + if (!Http2Headers.PseudoHeaderName.isPseudoHeader(name)) { + bh.consume(value); + } + } + } +} diff --git a/microbench/src/main/java/io/netty/microbench/headers/package-info.java b/microbench/src/main/java/io/netty/microbench/headers/package-info.java new file mode 100644 index 0000000000..c7ef25659c --- /dev/null +++ b/microbench/src/main/java/io/netty/microbench/headers/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2015 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +/** + * Benchmarks for HTTP and HTTP/2 Headers. + */ +package io.netty.microbench.headers;