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
This commit is contained in:
Jakob Buchgraber 2015-04-14 21:14:00 -07:00 committed by Scott Mitchell
parent a1f0785605
commit d31fa31cdc
39 changed files with 2040 additions and 2354 deletions

View File

@ -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,

View File

@ -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<CharSequence> {
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<CharSequence> nameConverter,
protected DefaultHttpHeaders(boolean validate,
DefaultHeaders.NameValidator<CharSequence> nameValidator,
boolean singleHeaderFields) {
super(true, validate ? VALIDATE_OBJECT_CONVERTER : NO_VALIDATE_OBJECT_CONVERTER, nameConverter,
singleHeaderFields);
super(new TreeMap<CharSequence, Object>(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<CharSequence> {
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;
}
}
}

View File

@ -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<CharSequence> {
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);
}
}
}

View File

@ -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<CharSequence> values = m.headers().getAll(HttpHeaderNames.TRANSFER_ENCODING);
// Make a copy to be able to modify values while iterating
List<CharSequence> values =
new ArrayList<CharSequence>(m.headers().getAll(HttpHeaderNames.TRANSFER_ENCODING));
if (values.isEmpty()) {
return;
}

View File

@ -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<CharSequence, CharSequence> 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) {

View File

@ -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<H extends HttpMessage> 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<CharSequence, CharSequence> header : headers) {
HttpHeadersEncoder.encoderHeader(header.getKey(), header.getValue(), buf);
}
}
private void encodeChunkedContent(ChannelHandlerContext ctx, Object msg, long contentLength, List<Object> out) {

View File

@ -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<CharSequence> 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<CharSequence> SPDY_NAME_CONVERTER = new NameConverter<CharSequence>() {
@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<CharSequence, Object>(),
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<CharSequence> {
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;
}
}
}

View File

@ -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);

View File

@ -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<String> 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());
}
}

View File

@ -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<CharSequence> expectedValues = headers.getAll(name);
List<CharSequence> receivedValues = spdyHeadersFrame.headers().getAll(name);
List<CharSequence> receivedValues = new ArrayList<CharSequence>(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);

View File

@ -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<ByteString> HTTP2_ASCII_TO_LOWER_CONVERTER = new NameConverter<ByteString>() {
@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.
* <p>
*
* <strong>Note</strong> that setting {@code forceKeyToLower} to {@code false} can violate the
* <a href="https://tools.ietf.org/html/draft-ietf-httpbis-http2-16#section-8.1.2">HTTP/2 specification</a>
* 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<ByteString, Object>(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<ByteString>, 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;
}
}
}

View File

@ -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<ByteString, ByteString> header : headers) {
encodeHeader(header.getKey(), header.getValue(), stream);
}
headers.forEachEntry(new EntryVisitor() {
@Override
public boolean visit(Entry<ByteString, ByteString> 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) {

View File

@ -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<Entry<ByteString, ByteString>> iterator();
/**
* Sets the {@link PseudoHeaderName#METHOD} header or {@code null} if there is no such header
*/

View File

@ -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<ByteString, ByteString> 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<CharSequence, CharSequence> 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<CharSequence, CharSequence> 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<ByteString, ByteString> entry) throws Http2Exception {
public void translate(Entry<ByteString, ByteString> 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;
}
}
}

View File

@ -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<CharSequence, CharSequence> entry) throws Exception {
http2Headers.add(AsciiString.of(entry.getKey()), AsciiString.of(entry.getValue()));
return true;
}
});
for (Entry<CharSequence, CharSequence> entry : httpHeaders) {
http2Headers.add(AsciiString.of(entry.getKey()), AsciiString.of(entry.getValue()));
}
} catch (Exception ex) {
PlatformDependent.throwException(ex);
}

View File

@ -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<Entry<ByteString, ByteString>> iter = headers.iterator();
List<ByteString> names = new ArrayList<ByteString>();
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);
}
}

View File

@ -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() {

View File

@ -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<CharSequence, Object>(), NO_NAME_VALIDATOR, CharSequenceConverter.INSTANCE, false);
}
@Override
public StompHeaders add(CharSequence name, CharSequence value) {
super.add(name, value);

View File

@ -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<StompSubframe>
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<CharSequence, CharSequence> entry : frame.headers()) {
headersEncoder.encode(entry);
}
buf.writeByte(StompConstants.LF);
return buf;

View File

@ -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 =

View File

@ -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<CharSequence, CharSequence> entry) throws Exception {
public void encode(Entry<CharSequence, CharSequence> 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) {

View File

@ -24,17 +24,6 @@ import io.netty.util.ByteString;
* some additional utility when handling text data.
*/
public interface BinaryHeaders extends Headers<ByteString> {
/**
* Provides an abstraction to iterate over elements maintained in the {@link Headers} collection.
*/
interface EntryVisitor extends Headers.EntryVisitor<ByteString> {
}
/**
* Provides an abstraction to iterate over elements maintained in the {@link Headers} collection.
*/
interface NameVisitor extends Headers.NameVisitor<ByteString> {
}
@Override
BinaryHeaders add(ByteString name, ByteString value);

View File

@ -89,13 +89,6 @@ public interface ConvertibleHeaders<UnconvertedType, ConvertedType> extends Head
*/
List<ConvertedType> 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<Map.Entry<ConvertedType, ConvertedType>> entriesConverted();
/**
* Invokes {@link Headers#iterator()} and lazily does a conversion on the results as they are accessed
*

View File

@ -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<ByteString> implements BinaryHeaders {
private static final ValueConverter<ByteString> OBJECT_TO_BYTE = new ValueConverter<ByteString>() {
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<ByteString> JAVA_HASH_CODE_GENERATOR =
new JavaHashCodeGenerator<ByteString>();
protected static final NameConverter<ByteString> IDENTITY_NAME_CONVERTER = new IdentityNameConverter<ByteString>();
private static final NameValidator<ByteString> NO_NAME_VALIDATOR = DefaultHeaders.NoNameValidator.instance();
public DefaultBinaryHeaders() {
this(IDENTITY_NAME_CONVERTER);
this(new TreeMap<ByteString, Object>(ByteString.DEFAULT_COMPARATOR));
}
public DefaultBinaryHeaders(NameConverter<ByteString> nameConverter) {
super(ByteString.DEFAULT_COMPARATOR, ByteString.DEFAULT_COMPARATOR,
JAVA_HASH_CODE_GENERATOR, OBJECT_TO_BYTE, nameConverter);
public DefaultBinaryHeaders(Map<ByteString, Object> map) {
this(map, NO_NAME_VALIDATOR);
}
public DefaultBinaryHeaders(Map<ByteString, Object> map, NameValidator<ByteString> nameValidator) {
super(map, nameValidator, ByteStringConverter.INSTANCE);
}
@Override
@ -347,4 +243,119 @@ public class DefaultBinaryHeaders extends DefaultHeaders<ByteString> implements
super.clear();
return this;
}
public static final class ByteStringConverter implements ValueConverter<ByteString> {
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);
}
}
}

View File

@ -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<UnconvertedType, ConvertedType> extends D
private final TypeConverter<UnconvertedType, ConvertedType> typeConverter;
public DefaultConvertibleHeaders(Comparator<? super UnconvertedType> keyComparator,
Comparator<? super UnconvertedType> valueComparator,
HashCodeGenerator<UnconvertedType> hashCodeGenerator,
ValueConverter<UnconvertedType> valueConverter,
TypeConverter<UnconvertedType, ConvertedType> typeConverter) {
super(keyComparator, valueComparator, hashCodeGenerator, valueConverter);
this.typeConverter = typeConverter;
}
public DefaultConvertibleHeaders(Comparator<? super UnconvertedType> keyComparator,
Comparator<? super UnconvertedType> valueComparator,
HashCodeGenerator<UnconvertedType> hashCodeGenerator,
ValueConverter<UnconvertedType> valueConverter,
TypeConverter<UnconvertedType, ConvertedType> typeConverter,
NameConverter<UnconvertedType> nameConverter) {
super(keyComparator, valueComparator, hashCodeGenerator, valueConverter, nameConverter);
public DefaultConvertibleHeaders(Map<UnconvertedType, Object> map,
NameValidator<UnconvertedType> nameValidator,
ValueConverter<UnconvertedType> valueConverter,
TypeConverter<UnconvertedType, ConvertedType> typeConverter) {
super(map, nameValidator, valueConverter);
this.typeConverter = typeConverter;
}
@ -94,17 +84,6 @@ public class DefaultConvertibleHeaders<UnconvertedType, ConvertedType> extends D
return allConverted;
}
@Override
public List<Entry<ConvertedType, ConvertedType>> entriesConverted() {
List<Entry<UnconvertedType, UnconvertedType>> entries = entries();
List<Entry<ConvertedType, ConvertedType>> entriesConverted = new ArrayList<Entry<ConvertedType, ConvertedType>>(
entries.size());
for (int i = 0; i < entries.size(); ++i) {
entriesConverted.add(new ConvertedEntry(entries.get(i)));
}
return entriesConverted;
}
@Override
public Iterator<Entry<ConvertedType, ConvertedType>> iteratorConverted() {
return new ConvertedIterator();

File diff suppressed because it is too large Load Diff

View File

@ -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<CharSequence, String> implements TextHeaders {
private static final HashCodeGenerator<CharSequence> CHARSEQUECE_CASE_INSENSITIVE_HASH_CODE_GENERATOR =
new HashCodeGenerator<CharSequence>() {
@Override
public int generateHashCode(CharSequence name) {
return AsciiString.caseInsensitiveHashCode(name);
}
};
private static final HashCodeGenerator<CharSequence> CHARSEQUECE_CASE_SENSITIVE_HASH_CODE_GENERATOR =
new JavaHashCodeGenerator<CharSequence>();
public static class DefaultTextValueTypeConverter implements ValueConverter<CharSequence> {
@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> CHARSEQUENCE_FROM_OBJECT_CONVERTER =
new DefaultTextValueTypeConverter();
private static final TypeConverter<CharSequence, String> CHARSEQUENCE_TO_STRING_CONVERTER =
new TypeConverter<CharSequence, String>() {
@Override
public String toConvertedType(CharSequence value) {
return value.toString();
}
@Override
public CharSequence toUnconvertedType(String value) {
return value;
}
};
private static final NameConverter<CharSequence> CHARSEQUENCE_IDENTITY_CONVERTER =
new IdentityNameConverter<CharSequence>();
/**
* An estimate of the size of a header value.
*/
private static final int DEFAULT_VALUE_SIZE = 10;
public static final NameValidator<CharSequence> 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, Object>(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<CharSequence> valueConverter,
NameConverter<CharSequence> nameConverter) {
this(ignoreCase, valueConverter, nameConverter, false);
}
public DefaultTextHeaders(boolean ignoreCase, ValueConverter<CharSequence> valueConverter,
NameConverter<CharSequence> 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<CharSequence, Object> map,
NameValidator<CharSequence> nameValidator,
ValueConverter<CharSequence> 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<CharSequence,
return this;
}
private static Comparator<CharSequence> 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<CharSequence,
TextHeaders add(CharSequence name, CharSequence... values);
TextHeaders add(CharSequence name, Iterable<? extends CharSequence> 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<CharSequence,
return DefaultTextHeaders.this;
}
@Override
public TextHeaders addObject(CharSequence name, Object value) {
DefaultTextHeaders.super.addObject(name, value);
return DefaultTextHeaders.this;
}
@Override
public TextHeaders addObject(CharSequence name, Iterable<?> values) {
DefaultTextHeaders.super.addObject(name, values);
@ -534,6 +390,11 @@ public class DefaultTextHeaders extends DefaultConvertibleHeaders<CharSequence,
return addEscapedValue(name, commaSeparate(charSequenceEscaper(), values));
}
@Override
public TextHeaders addObject(CharSequence name, Object value) {
return addEscapedValue(name, objectEscaper().escape(value));
}
@Override
public TextHeaders addObject(CharSequence name, Iterable<?> values) {
return addEscapedValue(name, commaSeparate(objectEscaper(), values));
@ -579,7 +440,8 @@ public class DefaultTextHeaders extends DefaultConvertibleHeaders<CharSequence,
}
private <T> CharSequence commaSeparate(CsvValueEscaper<T> 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<CharSequence,
*/
CharSequence escape(T value);
}
private static final class CharSequenceToStringConverter implements TypeConverter<CharSequence, String> {
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<CharSequence> {
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());
}
}
}

View File

@ -54,14 +54,10 @@ public class EmptyConvertibleHeaders<UnconvertedType, ConvertedType> extends
return Collections.emptyList();
}
@Override
public List<Entry<ConvertedType, ConvertedType>> entriesConverted() {
return Collections.emptyList();
}
@Override
public Iterator<Entry<ConvertedType, ConvertedType>> iteratorConverted() {
return entriesConverted().iterator();
List<Entry<ConvertedType, ConvertedType>> empty = Collections.emptyList();
return empty.iterator();
}
@Override

View File

@ -88,7 +88,7 @@ public class EmptyHeaders<T> implements Headers<T> {
}
@Override
public short getInt(T name, short defaultValue) {
public short getShort(T name, short defaultValue) {
return defaultValue;
}
@ -232,11 +232,6 @@ public class EmptyHeaders<T> implements Headers<T> {
return defaultValue;
}
@Override
public List<Entry<T, T>> entries() {
return Collections.emptyList();
}
@Override
public boolean contains(T name) {
return false;
@ -298,24 +293,7 @@ public class EmptyHeaders<T> implements Headers<T> {
}
@Override
public boolean contains(T name, T value, Comparator<? super T> comparator) {
return false;
}
@Override
public boolean contains(T name, T value,
Comparator<? super T> keyComparator, Comparator<? super T> valueComparator) {
return false;
}
@Override
public boolean containsObject(T name, Object value, Comparator<? super T> comparator) {
return false;
}
@Override
public boolean containsObject(T name, Object value, Comparator<? super T> keyComparator,
Comparator<? super T> valueComparator) {
public boolean contains(T name, T value, Comparator<? super T> valueComparator) {
return false;
}
@ -334,11 +312,6 @@ public class EmptyHeaders<T> implements Headers<T> {
return Collections.emptySet();
}
@Override
public List<T> namesList() {
return Collections.emptyList();
}
@Override
public Headers<T> add(T name, T value) {
throw new UnsupportedOperationException("read only");
@ -415,7 +388,7 @@ public class EmptyHeaders<T> implements Headers<T> {
}
@Override
public Headers<T> add(Headers<T> headers) {
public Headers<T> add(Headers<? extends T> headers) {
throw new UnsupportedOperationException("read only");
}
@ -495,12 +468,12 @@ public class EmptyHeaders<T> implements Headers<T> {
}
@Override
public Headers<T> set(Headers<T> headers) {
public Headers<T> set(Headers<? extends T> headers) {
throw new UnsupportedOperationException("read only");
}
@Override
public Headers<T> setAll(Headers<T> headers) {
public Headers<T> setAll(Headers<? extends T> headers) {
throw new UnsupportedOperationException("read only");
}
@ -516,17 +489,8 @@ public class EmptyHeaders<T> implements Headers<T> {
@Override
public Iterator<Entry<T, T>> iterator() {
return entries().iterator();
}
@Override
public Entry<T, T> forEachEntry(Headers.EntryVisitor<T> visitor) throws Exception {
return null;
}
@Override
public T forEachName(Headers.NameVisitor<T> visitor) throws Exception {
return null;
List<Entry<T, T>> empty = Collections.emptyList();
return empty.iterator();
}
@Override

View File

@ -25,11 +25,6 @@ public class EmptyTextHeaders extends EmptyConvertibleHeaders<CharSequence, Stri
return false;
}
@Override
public boolean containsObject(CharSequence name, Object value, boolean ignoreCase) {
return false;
}
@Override
public TextHeaders add(CharSequence name, CharSequence value) {
super.add(name, value);

File diff suppressed because it is too large Load Diff

View File

@ -25,17 +25,6 @@ package io.netty.handler.codec;
* .
*/
public interface TextHeaders extends ConvertibleHeaders<CharSequence, String> {
/**
* A visitor that helps reduce GC pressure while iterating over a collection of {@link Headers}.
*/
interface EntryVisitor extends Headers.EntryVisitor<CharSequence> {
}
/**
* A visitor that helps reduce GC pressure while iterating over a collection of {@link Headers}.
*/
interface NameVisitor extends Headers.NameVisitor<CharSequence> {
}
/**
* Returns {@code true} if a header with the name and value exists.
@ -45,14 +34,6 @@ public interface TextHeaders extends ConvertibleHeaders<CharSequence, String> {
*/
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);

View File

@ -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<ByteString> 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<ByteString> 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<ByteString> 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<String> headers = new HashSet<String>();
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<ByteString, ByteString> 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<ByteString, ByteString> 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<ByteString> 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<Entry<ByteString, ByteString>> iter = headers.iterator();
while (iter.hasNext()) {
Entry<ByteString, ByteString> 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);
}
}

View File

@ -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) {

View File

@ -54,64 +54,53 @@ public final class AsciiString extends ByteString implements CharSequence, Compa
};
public static final Comparator<CharSequence> CHARSEQUENCE_CASE_INSENSITIVE_ORDER = new Comparator<CharSequence>() {
@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;
}
};

View File

@ -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<Entry<String, String>> entries = headers.entriesConverted();
channel.writeAndFlush(request);
// Wait for the server to close the connection.
channel.closeFuture().sync();
// convert headers to list
List<Entry<String, String>> entries = new ArrayList<Entry<String, String>>(headers.size());
Iterator<Entry<String, String>> iterConverted = headers.iteratorConverted();
while (iterConverted.hasNext()) {
entries.add(iterConverted.next());
}
return entries;
}

View File

@ -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<HeaderExample, Map<String, String>> EXAMPLES =
new EnumMap<HeaderExample, Map<String, String>>(HeaderExample.class);
static {
Map<String, String> header = new HashMap<String, String>();
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<String, String>();
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<String, String>();
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<String, String>();
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<String, String>();
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<String, String>();
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<String, String>();
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() {
}
}

View File

@ -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<String, String> 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<String, String> 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<CharSequence, CharSequence> 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<ByteString, ByteString> 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<ByteString, ByteString> entry : http2Headers) {
final ByteString name = entry.getKey();
final ByteString value = entry.getValue();
if (!Http2Headers.PseudoHeaderName.isPseudoHeader(name)) {
bh.consume(value);
}
}
}
}

View File

@ -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;