diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/DecompressorHttp2FrameReader.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/DecompressorHttp2FrameReader.java index bd711d00a8..5de289320f 100644 --- a/codec-http2/src/main/java/io/netty/handler/codec/http2/DecompressorHttp2FrameReader.java +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/DecompressorHttp2FrameReader.java @@ -14,6 +14,13 @@ */ package io.netty.handler.codec.http2; +import static io.netty.handler.codec.http.HttpHeaders.Names.CONTENT_ENCODING; +import static io.netty.handler.codec.http.HttpHeaders.Names.CONTENT_LENGTH; +import static io.netty.handler.codec.http.HttpHeaders.Values.DEFLATE; +import static io.netty.handler.codec.http.HttpHeaders.Values.GZIP; +import static io.netty.handler.codec.http.HttpHeaders.Values.IDENTITY; +import static io.netty.handler.codec.http.HttpHeaders.Values.XDEFLATE; +import static io.netty.handler.codec.http.HttpHeaders.Values.XGZIP; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.ChannelHandlerContext; @@ -29,8 +36,8 @@ import io.netty.handler.codec.http.HttpHeaders; * to the {@code content-encoding} header for each stream. */ public class DecompressorHttp2FrameReader extends DefaultHttp2FrameReader { - private static final AsciiString CONTENT_ENCODING_LOWER_CASE = HttpHeaders.Names.CONTENT_ENCODING.toLowerCase(); - private static final AsciiString CONTENT_LENGTH_LOWER_CASE = HttpHeaders.Names.CONTENT_LENGTH.toLowerCase(); + private static final AsciiString CONTENT_ENCODING_LOWER_CASE = CONTENT_ENCODING.toLowerCase(); + private static final AsciiString CONTENT_LENGTH_LOWER_CASE = CONTENT_LENGTH.toLowerCase(); private static final Http2ConnectionAdapter CLEAN_UP_LISTENER = new Http2ConnectionAdapter() { @Override public void streamRemoved(Http2Stream stream) { @@ -78,12 +85,12 @@ public class DecompressorHttp2FrameReader extends DefaultHttp2FrameReader { * @throws Http2Exception If the specified encoding is not not supported and warrants an exception */ protected EmbeddedChannel newContentDecoder(CharSequence contentEncoding) throws Http2Exception { - if (HttpHeaders.Values.GZIP.equalsIgnoreCase(contentEncoding) || - HttpHeaders.Values.XGZIP.equalsIgnoreCase(contentEncoding)) { + if (GZIP.equalsIgnoreCase(contentEncoding) || + XGZIP.equalsIgnoreCase(contentEncoding)) { return new EmbeddedChannel(ZlibCodecFactory.newZlibDecoder(ZlibWrapper.GZIP)); } - if (HttpHeaders.Values.DEFLATE.equalsIgnoreCase(contentEncoding) || - HttpHeaders.Values.XDEFLATE.equalsIgnoreCase(contentEncoding)) { + if (DEFLATE.equalsIgnoreCase(contentEncoding) || + XDEFLATE.equalsIgnoreCase(contentEncoding)) { final ZlibWrapper wrapper = strict ? ZlibWrapper.ZLIB : ZlibWrapper.ZLIB_OR_NONE; // To be strict, 'deflate' means ZLIB, but some servers were not implemented correctly. return new EmbeddedChannel(ZlibCodecFactory.newZlibDecoder(wrapper)); @@ -101,7 +108,7 @@ public class DecompressorHttp2FrameReader extends DefaultHttp2FrameReader { * @return the expected content encoding of the new content. * @throws Http2Exception if the {@code contentEncoding} is not supported and warrants an exception */ - protected CharSequence getTargetContentEncoding( + protected AsciiString getTargetContentEncoding( @SuppressWarnings("UnusedParameters") CharSequence contentEncoding) throws Http2Exception { return HttpHeaders.Values.IDENTITY; } @@ -114,28 +121,29 @@ public class DecompressorHttp2FrameReader extends DefaultHttp2FrameReader { * @param endOfStream Indicates if the stream has ended * @throws Http2Exception If the {@code content-encoding} is not supported */ - private void initDecoder(int streamId, Http2Headers.Builder builder, boolean endOfStream) + private void initDecoder(int streamId, Http2Headers headers, boolean endOfStream) throws Http2Exception { + // Convert the names into a case-insensitive map. final Http2Stream stream = connection.stream(streamId); if (stream != null) { EmbeddedChannel decoder = stream.decompressor(); if (decoder == null) { if (!endOfStream) { // Determine the content encoding. - CharSequence contentEncoding = builder.get(CONTENT_ENCODING_LOWER_CASE); + AsciiString contentEncoding = headers.get(CONTENT_ENCODING_LOWER_CASE); if (contentEncoding == null) { - contentEncoding = HttpHeaders.Values.IDENTITY; + contentEncoding = IDENTITY; } decoder = newContentDecoder(contentEncoding); if (decoder != null) { stream.decompressor(decoder); // Decode the content and remove or replace the existing headers // so that the message looks like a decoded message. - CharSequence targetContentEncoding = getTargetContentEncoding(contentEncoding); - if (HttpHeaders.Values.IDENTITY.equalsIgnoreCase(targetContentEncoding)) { - builder.remove(CONTENT_ENCODING_LOWER_CASE); + AsciiString targetContentEncoding = getTargetContentEncoding(contentEncoding); + if (IDENTITY.equalsIgnoreCase(targetContentEncoding)) { + headers.remove(CONTENT_ENCODING_LOWER_CASE); } else { - builder.set(CONTENT_ENCODING_LOWER_CASE, targetContentEncoding); + headers.set(CONTENT_ENCODING_LOWER_CASE, targetContentEncoding); } } } @@ -146,7 +154,7 @@ public class DecompressorHttp2FrameReader extends DefaultHttp2FrameReader { // The content length will be for the compressed data. Since we will decompress the data // this content-length will not be correct. Instead of queuing messages or delaying sending // header frames...just remove the content-length header - builder.remove(CONTENT_LENGTH_LOWER_CASE); + headers.remove(CONTENT_LENGTH_LOWER_CASE); } } } @@ -226,18 +234,18 @@ public class DecompressorHttp2FrameReader extends DefaultHttp2FrameReader { } @Override - protected void notifyListenerOnHeadersRead(ChannelHandlerContext ctx, int streamId, Http2Headers.Builder builder, + protected void notifyListenerOnHeadersRead(ChannelHandlerContext ctx, int streamId, Http2Headers headers, int streamDependency, short weight, boolean exclusive, int padding, boolean endOfStream, Http2FrameListener listener) throws Http2Exception { - initDecoder(streamId, builder, endOfStream); - super.notifyListenerOnHeadersRead(ctx, streamId, builder, streamDependency, weight, + initDecoder(streamId, headers, endOfStream); + super.notifyListenerOnHeadersRead(ctx, streamId, headers, streamDependency, weight, exclusive, padding, endOfStream, listener); } @Override - protected void notifyListenerOnHeadersRead(ChannelHandlerContext ctx, int streamId, Http2Headers.Builder builder, + protected void notifyListenerOnHeadersRead(ChannelHandlerContext ctx, int streamId, Http2Headers headers, int padding, boolean endOfStream, Http2FrameListener listener) throws Http2Exception { - initDecoder(streamId, builder, endOfStream); - super.notifyListenerOnHeadersRead(ctx, streamId, builder, padding, endOfStream, listener); + initDecoder(streamId, headers, endOfStream); + super.notifyListenerOnHeadersRead(ctx, streamId, headers, padding, endOfStream, listener); } } diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2FrameReader.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2FrameReader.java index c3881ea62b..259cfa5fdc 100644 --- a/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2FrameReader.java +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2FrameReader.java @@ -372,16 +372,16 @@ public class DefaultHttp2FrameReader implements Http2FrameReader { listener.onDataRead(ctx, streamId, data, padding, endOfStream); } - protected void notifyListenerOnHeadersRead(ChannelHandlerContext ctx, int streamId, Http2Headers.Builder builder, + protected void notifyListenerOnHeadersRead(ChannelHandlerContext ctx, int streamId, Http2Headers headers, int streamDependency, short weight, boolean exclusive, int padding, boolean endOfStream, Http2FrameListener listener) throws Http2Exception { - listener.onHeadersRead(ctx, streamId, builder.build(), streamDependency, + listener.onHeadersRead(ctx, streamId, headers, streamDependency, weight, exclusive, padding, endOfStream); } - protected void notifyListenerOnHeadersRead(ChannelHandlerContext ctx, int streamId, Http2Headers.Builder builder, + protected void notifyListenerOnHeadersRead(ChannelHandlerContext ctx, int streamId, Http2Headers headers, int padding, boolean endOfStream, Http2FrameListener listener) throws Http2Exception { - listener.onHeadersRead(ctx, streamId, builder.build(), padding, endOfStream); + listener.onHeadersRead(ctx, streamId, headers, padding, endOfStream); } private void readDataFrame(ChannelHandlerContext ctx, ByteBuf payload, @@ -428,7 +428,7 @@ public class DefaultHttp2FrameReader implements Http2FrameReader { final HeadersBlockBuilder hdrBlockBuilder = headersBlockBuilder(); hdrBlockBuilder.addFragment(fragment, ctx.alloc(), endOfHeaders); if (endOfHeaders) { - notifyListenerOnHeadersRead(ctx, headersStreamId, hdrBlockBuilder.builder(), + notifyListenerOnHeadersRead(ctx, headersStreamId, hdrBlockBuilder.headers(), streamDependency, weight, exclusive, padding, headersFlags.endOfStream(), listener); close(); } @@ -454,7 +454,7 @@ public class DefaultHttp2FrameReader implements Http2FrameReader { final HeadersBlockBuilder hdrBlockBuilder = headersBlockBuilder(); hdrBlockBuilder.addFragment(fragment, ctx.alloc(), endOfHeaders); if (endOfHeaders) { - notifyListenerOnHeadersRead(ctx, headersStreamId, hdrBlockBuilder.builder(), padding, + notifyListenerOnHeadersRead(ctx, headersStreamId, hdrBlockBuilder.headers(), padding, headersFlags.endOfStream(), listener); close(); } @@ -525,7 +525,7 @@ public class DefaultHttp2FrameReader implements Http2FrameReader { Http2FrameListener listener) throws Http2Exception { headersBlockBuilder().addFragment(fragment, ctx.alloc(), endOfHeaders); if (endOfHeaders) { - Http2Headers headers = headersBlockBuilder().builder().build(); + Http2Headers headers = headersBlockBuilder().headers(); listener.onPushPromiseRead(ctx, pushPromiseStreamId, promisedStreamId, headers, padding); close(); @@ -676,7 +676,7 @@ public class DefaultHttp2FrameReader implements Http2FrameReader { * Builds the headers from the completed headers block. After this is called, this builder * should not be called again. */ - Http2Headers.Builder builder() throws Http2Exception { + Http2Headers headers() throws Http2Exception { try { return headersDecoder.decodeHeaders(headerBlock); } finally { diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2Headers.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2Headers.java index 33618fffdd..6c38739f67 100644 --- a/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2Headers.java +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2Headers.java @@ -12,601 +12,149 @@ * or implied. See the License for the specific language governing permissions and limitations under * the License. */ - package io.netty.handler.codec.http2; -import static io.netty.handler.codec.http2.Http2Headers.PseudoHeaderName.AUTHORITY; -import static io.netty.handler.codec.http2.Http2Headers.PseudoHeaderName.METHOD; -import static io.netty.handler.codec.http2.Http2Headers.PseudoHeaderName.SCHEME; +import io.netty.handler.codec.AsciiString; +import io.netty.handler.codec.BinaryHeaders; +import io.netty.handler.codec.DefaultBinaryHeaders; -import java.util.Iterator; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.NoSuchElementException; -import java.util.Set; -import java.util.TreeSet; +public class DefaultHttp2Headers extends DefaultBinaryHeaders implements Http2Headers { -/** - * An immutable collection of headers sent or received via HTTP/2. - */ -public final class DefaultHttp2Headers extends Http2Headers { - private static final int MAX_VALUE_LENGTH = 0xFFFF; // Length is a 16-bit field - private static final int BUCKET_SIZE = 17; - - private final HeaderEntry[] entries; - private final HeaderEntry head; - private final int size; - - private DefaultHttp2Headers(Builder builder) { - entries = builder.entries; - head = builder.head; - size = builder.size; + public DefaultHttp2Headers() { } @Override - public String get(CharSequence name) { - if (name == null) { - throw new NullPointerException("name"); - } - - int h = hash(name); - int i = index(h); - HeaderEntry e = entries[i]; - while (e != null) { - if (e.hash == h && eq(name, e.key)) { - return e.value; - } - - e = e.next; - } - return null; + public Http2Headers add(AsciiString name, AsciiString value) { + super.add(name, value); + return this; } @Override - public List getAll(CharSequence name) { - if (name == null) { - throw new NullPointerException("name"); - } - - LinkedList values = new LinkedList(); - - int h = hash(name); - int i = index(h); - HeaderEntry e = entries[i]; - while (e != null) { - if (e.hash == h && eq(name, e.key)) { - values.addFirst(e.value); - } - e = e.next; - } - return values; + public Http2Headers add(AsciiString name, Iterable values) { + super.add(name, values); + return this; } @Override - public List> entries() { - List> all = new LinkedList>(); - - HeaderEntry e = head.after; - while (e != head) { - all.add(e); - e = e.after; - } - return all; + public Http2Headers add(AsciiString name, AsciiString... values) { + super.add(name, values); + return this; } @Override - public boolean contains(CharSequence name) { - return get(name) != null; + public Http2Headers add(BinaryHeaders headers) { + super.add(headers); + return this; } @Override - public boolean isEmpty() { - return size == 0; + public Http2Headers set(AsciiString name, AsciiString value) { + super.set(name, value); + return this; } @Override - public int size() { - return size; + public Http2Headers set(AsciiString name, Iterable values) { + super.set(name, values); + return this; } @Override - public Set names() { - Set names = new TreeSet(); - - HeaderEntry e = head.after; - while (e != head) { - names.add(e.key); - e = e.after; - } - return names; + public Http2Headers set(AsciiString name, AsciiString... values) { + super.set(name, values); + return this; } @Override - public Iterator> iterator() { - return new HeaderIterator(); + public Http2Headers set(BinaryHeaders headers) { + super.set(headers); + return this; } @Override - public String forEach(HeaderVisitor visitor) { - if (isEmpty()) { - return null; - } - - HeaderEntry e = head.after; - do { - if (visitor.visit(e)) { - e = e.after; - } else { - return e.getKey(); - } - } while (e != head); - - return null; + public Http2Headers setAll(BinaryHeaders headers) { + super.setAll(headers); + return this; } - /** - * Short cut for {@code new DefaultHttp2Headers.Builder()}. - */ - public static Builder newBuilder() { - return new Builder(); + @Override + public Http2Headers clear() { + super.clear(); + return this; } - /** - * Builds instances of {@link DefaultHttp2Headers}. - */ - public static class Builder implements Http2Headers.Builder { - private HeaderEntry[] entries; - private HeaderEntry head; - private Http2Headers buildResults; - private int size; - - public Builder() { - clear(); - } - - @Override - public String get(CharSequence name) { - if (name == null) { - throw new NullPointerException("name"); - } - - int h = hash(name); - int i = index(h); - HeaderEntry e = entries[i]; - while (e != null) { - if (e.hash == h && eq(name, e.key)) { - return e.value; - } - e = e.next; - } - return null; - } - - @Override - public List getAll(CharSequence name) { - if (name == null) { - throw new NullPointerException("name"); - } - - LinkedList values = new LinkedList(); - - int h = hash(name); - int i = index(h); - HeaderEntry e = entries[i]; - while (e != null) { - if (e.hash == h && eq(name, e.key)) { - values.addFirst(e.value); - } - e = e.next; - } - return values; - } - - @Override - public void set(Http2Headers headers) { - // No need to lazy copy the previous results, since we're starting from scratch. - clear(); - for (Map.Entry entry : headers) { - add(entry.getKey(), entry.getValue()); - } - } - - @Override - public Builder add(CharSequence name, Object value) { - return add(name.toString(), value); - } - - @Override - public Builder add(String name, Object value) { - // If this is the first call on the builder since the last build, copy the previous - // results. - lazyCopy(); - - String lowerCaseName = name.toLowerCase(); - validateHeaderName(lowerCaseName); - String strVal = toString(value); - validateHeaderValue(strVal); - int nameHash = hash(lowerCaseName); - int hashTableIndex = index(nameHash); - add0(nameHash, hashTableIndex, lowerCaseName, strVal); - return this; - } - - @Override - public Builder remove(CharSequence name) { - if (name == null) { - throw new NullPointerException("name"); - } - - // If this is the first call on the builder since the last build, copy the previous - // results. - lazyCopy(); - - remove0(name); - return this; - } - - @Override - public Builder remove(String name) { - if (name == null) { - throw new NullPointerException("name"); - } - - // If this is the first call on the builder since the last build, copy the previous - // results. - lazyCopy(); - - remove0(name.toLowerCase()); - return this; - } - - @Override - public Builder set(CharSequence name, Object value) { - return set(name.toString(), value); - } - - @Override - public Builder set(String name, Object value) { - // If this is the first call on the builder since the last build, copy the previous - // results. - lazyCopy(); - - String lowerCaseName = name.toLowerCase(); - validateHeaderName(lowerCaseName); - String strVal = toString(value); - validateHeaderValue(strVal); - int nameHash = hash(lowerCaseName); - int hashTableIndex = index(nameHash); - remove0(nameHash, hashTableIndex, lowerCaseName); - add0(nameHash, hashTableIndex, lowerCaseName, strVal); - return this; - } - - @Override - public Builder set(String name, Iterable values) { - if (values == null) { - throw new NullPointerException("values"); - } - - // If this is the first call on the builder since the last build, copy the previous - // results. - lazyCopy(); - - String lowerCaseName = name.toLowerCase(); - validateHeaderName(lowerCaseName); - - int nameHash = hash(lowerCaseName); - int hashTableIndex = index(nameHash); - - remove0(nameHash, hashTableIndex, lowerCaseName); - for (Object v : values) { - if (v == null) { - break; - } - String strVal = toString(v); - validateHeaderValue(strVal); - add0(nameHash, hashTableIndex, lowerCaseName, strVal); - } - return this; - } - - @Override - public int size() { - return size; - } - - @Override - public Builder clear() { - // No lazy copy required, since we're just creating a new array. - entries = new HeaderEntry[BUCKET_SIZE]; - head = new HeaderEntry(-1, null, null); - head.before = head.after = head; - buildResults = null; - size = 0; - return this; - } - - @Override - public Builder method(String method) { - return set(METHOD.value(), method); - } - - @Override - public Builder scheme(String scheme) { - return set(SCHEME.value(), scheme); - } - - @Override - public Builder authority(String authority) { - return set(AUTHORITY.value(), authority); - } - - @Override - public Builder path(String path) { - return set(PseudoHeaderName.PATH.value(), path); - } - - @Override - public Builder status(String status) { - return set(PseudoHeaderName.STATUS.value(), status); - } - - @Override - public DefaultHttp2Headers build() { - // If this is the first call on the builder since the last build, copy the previous - // results. - lazyCopy(); - - // Give the multimap over to the headers instance and save the build results for - // future lazy copies if this builder is used again later. - DefaultHttp2Headers headers = new DefaultHttp2Headers(this); - buildResults = headers; - return headers; - } - - /** - * Performs a lazy copy of the last build results, if there are any. For the typical use - * case, headers will only be built once so no copy will be required. If the any method is - * called on the builder after that, it will force a copy of the most recently created - * headers object. - */ - private void lazyCopy() { - if (buildResults != null) { - set(buildResults); - buildResults = null; - } - } - - private void add0(int hash, int hashTableIndex, final String name, final String value) { - // Update the hash table. - HeaderEntry e = entries[hashTableIndex]; - HeaderEntry newEntry; - entries[hashTableIndex] = newEntry = new HeaderEntry(hash, name, value); - newEntry.next = e; - - // Update the linked list. - newEntry.addBefore(head); - size++; - } - - private void remove0(final CharSequence name) { - final int nameHash = hash(name); - final int hashTableIndex = index(nameHash); - remove0(nameHash, hashTableIndex, name); - } - - private void remove0(int hash, int hashTableIndex, CharSequence name) { - HeaderEntry e = entries[hashTableIndex]; - if (e == null) { - return; - } - - for (;;) { - if (e.hash == hash && eq(name, e.key)) { - e.remove(); - size--; - HeaderEntry next = e.next; - if (next != null) { - entries[hashTableIndex] = next; - e = next; - } else { - entries[hashTableIndex] = null; - return; - } - } else { - break; - } - } - - for (;;) { - HeaderEntry next = e.next; - if (next == null) { - break; - } - if (next.hash == hash && eq(name, next.key)) { - e.next = next.next; - next.remove(); - size--; - } else { - e = next; - } - } - } - - private static String toString(Object value) { - if (value == null) { - return null; - } - return value.toString(); - } - - /** - * Validate a HTTP2 header value. Does not validate max length. - */ - private static void validateHeaderValue(String value) { - if (value == null) { - throw new NullPointerException("value"); - } - for (int i = 0; i < value.length(); i++) { - char c = value.charAt(i); - if (c == 0) { - throw new IllegalArgumentException("value contains null character: " + value); - } - } - } - - /** - * Validate a HTTP/2 header name. - */ - private static void validateHeaderName(String name) { - if (name == null) { - throw new NullPointerException("name"); - } - if (name.isEmpty()) { - throw new IllegalArgumentException("name cannot be length zero"); - } - // Since name may only contain ascii characters, for valid names - // name.length() returns the number of bytes when UTF-8 encoded. - if (name.length() > MAX_VALUE_LENGTH) { - throw new IllegalArgumentException("name exceeds allowable length: " + name); - } - for (int i = 0; i < name.length(); i++) { - char c = name.charAt(i); - if (c == 0) { - throw new IllegalArgumentException("name contains null character: " + name); - } - if (c > 127) { - throw new IllegalArgumentException("name contains non-ascii character: " + name); - } - } - // If the name looks like an HTTP/2 pseudo-header, validate it against the list of - // valid pseudo-headers. - if (name.startsWith(PSEUDO_HEADER_PREFIX)) { - if (!Http2Headers.PseudoHeaderName.isPseudoHeader(name)) { - throw new IllegalArgumentException("Invalid HTTP/2 Pseudo-header: " + name); - } - } - } + @Override + public Http2Headers forEachEntry(final BinaryHeaders.BinaryHeaderVisitor visitor) { + super.forEachEntry(visitor); + return this; } - private static int hash(CharSequence name) { - int h = 0; - for (int i = name.length() - 1; i >= 0; i--) { - char c = name.charAt(i); - if (c >= 'A' && c <= 'Z') { - c += 32; - } - h = 31 * h + c; - } - - if (h > 0) { - return h; - } else if (h == Integer.MIN_VALUE) { - return Integer.MAX_VALUE; - } else { - return -h; - } + @Override + public int hashCode() { + return super.hashCode(); } - private static boolean eq(CharSequence name1, CharSequence name2) { - int nameLen = name1.length(); - if (nameLen != name2.length()) { + @Override + public boolean equals(Object o) { + if (!(o instanceof Http2Headers)) { return false; } - for (int i = nameLen - 1; i >= 0; i--) { - char c1 = name1.charAt(i); - char c2 = name2.charAt(i); - if (c1 != c2) { - if (c1 >= 'A' && c1 <= 'Z') { - c1 += 32; - } - if (c2 >= 'A' && c2 <= 'Z') { - c2 += 32; - } - if (c1 != c2) { - return false; - } - } - } - return true; + return super.equals((BinaryHeaders) o); } - private static int index(int hash) { - return hash % BUCKET_SIZE; + @Override + public Http2Headers method(AsciiString value) { + set(PseudoHeaderName.METHOD.value(), value); + return this; } - private final class HeaderIterator implements Iterator> { - - private HeaderEntry current = head; - - @Override - public boolean hasNext() { - return current.after != head; - } - - @Override - public Entry next() { - current = current.after; - - if (current == head) { - throw new NoSuchElementException(); - } - - return current; - } - - @Override - public void remove() { - throw new UnsupportedOperationException(); - } + @Override + public Http2Headers scheme(AsciiString value) { + set(PseudoHeaderName.SCHEME.value(), value); + return this; } - private static final class HeaderEntry implements Map.Entry { - final int hash; - final String key; - final String value; - HeaderEntry next; - HeaderEntry before, after; + @Override + public Http2Headers authority(AsciiString value) { + set(PseudoHeaderName.AUTHORITY.value(), value); + return this; + } - HeaderEntry(int hash, String key, String value) { - this.hash = hash; - this.key = key; - this.value = value; - } + @Override + public Http2Headers path(AsciiString value) { + set(PseudoHeaderName.PATH.value(), value); + return this; + } - void remove() { - before.after = after; - after.before = before; - } + @Override + public Http2Headers status(AsciiString value) { + set(PseudoHeaderName.STATUS.value(), value); + return this; + } - void addBefore(HeaderEntry e) { - after = e; - before = e.before; - before.after = this; - after.before = this; - } + @Override + public AsciiString method() { + return get(PseudoHeaderName.METHOD.value()); + } - @Override - public String getKey() { - return key; - } + @Override + public AsciiString scheme() { + return get(PseudoHeaderName.SCHEME.value()); + } - @Override - public String getValue() { - return value; - } + @Override + public AsciiString authority() { + return get(PseudoHeaderName.AUTHORITY.value()); + } - @Override - public String setValue(String value) { - throw new UnsupportedOperationException(); - } + @Override + public AsciiString path() { + return get(PseudoHeaderName.PATH.value()); + } - @Override - public String toString() { - return key + '=' + value; - } + @Override + public AsciiString status() { + return get(PseudoHeaderName.STATUS.value()); } } diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2HeadersDecoder.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2HeadersDecoder.java index bb2b32a43e..18c1c0a2ff 100644 --- a/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2HeadersDecoder.java +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2HeadersDecoder.java @@ -19,11 +19,12 @@ import static io.netty.handler.codec.http2.Http2CodecUtil.DEFAULT_HEADER_TABLE_S import static io.netty.handler.codec.http2.Http2CodecUtil.DEFAULT_MAX_HEADER_SIZE; import static io.netty.handler.codec.http2.Http2Error.COMPRESSION_ERROR; import static io.netty.handler.codec.http2.Http2Exception.protocolError; -import static io.netty.util.CharsetUtil.UTF_8; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufInputStream; +import io.netty.handler.codec.AsciiString; import java.io.IOException; +import java.io.InputStream; import com.twitter.hpack.Decoder; import com.twitter.hpack.HeaderListener; @@ -65,37 +66,43 @@ public class DefaultHttp2HeadersDecoder implements Http2HeadersDecoder { } @Override - public Http2Headers.Builder decodeHeaders(ByteBuf headerBlock) throws Http2Exception { + public Http2Headers decodeHeaders(ByteBuf headerBlock) throws Http2Exception { + InputStream in = new ByteBufInputStream(headerBlock); try { - final DefaultHttp2Headers.Builder headersBuilder = new DefaultHttp2Headers.Builder(); + final Http2Headers headers = new DefaultHttp2Headers(); HeaderListener listener = new HeaderListener() { @Override public void addHeader(byte[] key, byte[] value, boolean sensitive) { - String keyString = new String(key, UTF_8); - String valueString = new String(value, UTF_8); - headersBuilder.add(keyString, valueString); + headers.add(new AsciiString(key, false), new AsciiString(value, false)); } }; - decoder.decode(new ByteBufInputStream(headerBlock), listener); + decoder.decode(in, listener); boolean truncated = decoder.endHeaderBlock(); if (truncated) { // TODO: what's the right thing to do here? } - if (headersBuilder.size() > maxHeaderListSize) { + if (headers.size() > maxHeaderListSize) { throw protocolError("Number of headers (%d) exceeds maxHeaderListSize (%d)", - headersBuilder.size(), maxHeaderListSize); + headers.size(), maxHeaderListSize); } - return headersBuilder; + return headers; } catch (IOException e) { throw new Http2Exception(COMPRESSION_ERROR, e.getMessage()); } catch (Throwable e) { - // Default handler for any other types of errors that may have occurred. For example, - // the the Header builder throws IllegalArgumentException if the key or value was invalid + // Default handler for any other types of errors that may have occurred. For example, + // the the Header builder throws IllegalArgumentException if the key or value was + // invalid // for any reason (e.g. the key was an invalid pseudo-header). throw new Http2Exception(Http2Error.PROTOCOL_ERROR, e.getMessage(), e); + } finally { + try { + in.close(); + } catch (IOException e) { + throw new Http2Exception(Http2Error.INTERNAL_ERROR, e.getMessage(), e); + } } } } diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2HeadersEncoder.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2HeadersEncoder.java index 83ca377c83..66c33388c8 100644 --- a/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2HeadersEncoder.java +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2HeadersEncoder.java @@ -17,16 +17,15 @@ package io.netty.handler.codec.http2; import static io.netty.handler.codec.http2.Http2CodecUtil.DEFAULT_HEADER_TABLE_SIZE; import static io.netty.handler.codec.http2.Http2Exception.protocolError; -import static io.netty.handler.codec.http2.Http2Headers.PSEUDO_HEADER_PREFIX; -import static io.netty.util.CharsetUtil.UTF_8; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufOutputStream; -import io.netty.buffer.Unpooled; +import io.netty.handler.codec.AsciiString; +import io.netty.handler.codec.BinaryHeaders; +import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; import java.util.Collections; -import java.util.Map.Entry; import java.util.Set; import java.util.TreeSet; @@ -34,7 +33,7 @@ import com.twitter.hpack.Encoder; public class DefaultHttp2HeadersEncoder implements Http2HeadersEncoder { private final Encoder encoder; - private final ByteBuf tableSizeChangeOutput = Unpooled.buffer(); + private final ByteArrayOutputStream tableSizeChangeOutput = new ByteArrayOutputStream(); private final Set sensitiveHeaders = new TreeSet(String.CASE_INSENSITIVE_ORDER); private int maxHeaderListSize = Integer.MAX_VALUE; @@ -49,6 +48,7 @@ public class DefaultHttp2HeadersEncoder implements Http2HeadersEncoder { @Override public void encodeHeaders(Http2Headers headers, ByteBuf buffer) throws Http2Exception { + final OutputStream stream = new ByteBufOutputStream(buffer); try { if (headers.size() > maxHeaderListSize) { throw protocolError("Number of headers (%d) exceeds maxHeaderListSize (%d)", @@ -57,28 +57,37 @@ public class DefaultHttp2HeadersEncoder implements Http2HeadersEncoder { // If there was a change in the table size, serialize the output from the encoder // resulting from that change. - if (tableSizeChangeOutput.isReadable()) { - buffer.writeBytes(tableSizeChangeOutput); - tableSizeChangeOutput.clear(); + if (tableSizeChangeOutput.size() > 0) { + buffer.writeBytes(tableSizeChangeOutput.toByteArray()); + tableSizeChangeOutput.reset(); } - OutputStream stream = new ByteBufOutputStream(buffer); // Write pseudo headers first as required by the HTTP/2 spec. for (Http2Headers.PseudoHeaderName pseudoHeader : Http2Headers.PseudoHeaderName.values()) { - String name = pseudoHeader.value(); - String value = headers.get(name); + AsciiString name = pseudoHeader.value(); + AsciiString value = headers.get(name); if (value != null) { encodeHeader(name, value, stream); } } - for (Entry header : headers) { - if (!header.getKey().startsWith(PSEUDO_HEADER_PREFIX)) { - encodeHeader(header.getKey(), header.getValue(), stream); + headers.forEachEntry(new BinaryHeaders.BinaryHeaderVisitor() { + @Override + public boolean visit(AsciiString name, AsciiString value) throws Exception { + if (!Http2Headers.PseudoHeaderName.isPseudoHeader(name)) { + encodeHeader(name, value, stream); + } + return true; } - } + }); } catch (IOException e) { throw Http2Exception.format(Http2Error.COMPRESSION_ERROR, "Failed encoding headers block: %s", e.getMessage()); + } finally { + try { + stream.close(); + } catch (IOException e) { + throw new Http2Exception(Http2Error.INTERNAL_ERROR, e.getMessage(), e); + } } } @@ -86,7 +95,7 @@ public class DefaultHttp2HeadersEncoder implements Http2HeadersEncoder { public void maxHeaderTableSize(int size) throws Http2Exception { try { // No headers should be emitted. If they are, we throw. - encoder.setMaxHeaderTableSize(new ByteBufOutputStream(tableSizeChangeOutput), size); + encoder.setMaxHeaderTableSize(tableSizeChangeOutput, size); } catch (IOException e) { throw new Http2Exception(Http2Error.COMPRESSION_ERROR, e.getMessage(), e); } @@ -110,8 +119,8 @@ public class DefaultHttp2HeadersEncoder implements Http2HeadersEncoder { return maxHeaderListSize; } - private void encodeHeader(String key, String value, OutputStream stream) throws IOException { + private void encodeHeader(AsciiString key, AsciiString value, OutputStream stream) throws IOException { boolean sensitive = sensitiveHeaders.contains(key); - encoder.encodeHeader(stream, key.getBytes(UTF_8), value.getBytes(UTF_8), sensitive); + encoder.encodeHeader(stream, key.array(), value.array(), sensitive); } } diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/DelegatingHttp2HttpConnectionHandler.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/DelegatingHttp2HttpConnectionHandler.java index d1778eed3a..968acd0488 100644 --- a/codec-http2/src/main/java/io/netty/handler/codec/http2/DelegatingHttp2HttpConnectionHandler.java +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/DelegatingHttp2HttpConnectionHandler.java @@ -20,11 +20,6 @@ import io.netty.channel.ChannelPromise; import io.netty.channel.ChannelPromiseAggregator; import io.netty.handler.codec.http.FullHttpMessage; import io.netty.handler.codec.http.HttpHeaders; -import io.netty.handler.codec.http.HttpRequest; -import io.netty.handler.codec.http.HttpResponse; - -import java.net.URI; -import java.util.Map; /** * Light weight wrapper around {@link DelegatingHttp2ConnectionHandler} to provide HTTP/1.x object to HTTP/2 encoding @@ -45,66 +40,6 @@ public class DelegatingHttp2HttpConnectionHandler extends DelegatingHttp2Connect super(connection, listener); } - /** - * Add HTTP/2 headers based upon HTTP/1.x headers from a {@link HttpHeaders} - * - * @param httpHeaders The HTTP/1.x request object to pull headers from - * @param http2Headers The HTTP/2 headers object to add headers to - */ - private void addHeaders(HttpHeaders httpHeaders, DefaultHttp2Headers.Builder http2Headers) { - String value = httpHeaders.get(HttpHeaders.Names.HOST); - if (value != null) { - URI hostUri = URI.create(value); - // The authority MUST NOT include the deprecated "userinfo" subcomponent - value = hostUri.getAuthority(); - if (value != null) { - http2Headers.authority(value.replaceFirst("^.*@", "")); - } - value = hostUri.getScheme(); - if (value != null) { - http2Headers.scheme(value); - } - httpHeaders.remove(HttpHeaders.Names.HOST); - } - - // Consume the Authority extension header if present - value = httpHeaders.get(HttpUtil.ExtensionHeaders.Names.AUTHORITY); - if (value != null) { - http2Headers.authority(value); - httpHeaders.remove(HttpUtil.ExtensionHeaders.Names.AUTHORITY); - } - - // Consume the Scheme extension header if present - value = httpHeaders.get(HttpUtil.ExtensionHeaders.Names.SCHEME); - if (value != null) { - http2Headers.scheme(value); - httpHeaders.remove(HttpUtil.ExtensionHeaders.Names.SCHEME); - } - } - - /** - * Add HTTP/2 headers based upon HTTP/1.x headers from a {@link HttpRequest} - * - * @param httpRequest The HTTP/1.x request object to pull headers from - * @param http2Headers The HTTP/2 headers object to add headers to - */ - private void addRequestHeaders(HttpRequest httpRequest, DefaultHttp2Headers.Builder http2Headers) { - http2Headers.path(httpRequest.uri()); - http2Headers.method(httpRequest.method().toString()); - addHeaders(httpRequest.headers(), http2Headers); - } - - /** - * Add HTTP/2 headers based upon HTTP/1.x headers from a {@link HttpRequest} - * - * @param httpResponse The HTTP/1.x response object to pull headers from - * @param http2Headers The HTTP/2 headers object to add headers to - */ - private void addResponseHeaders(HttpResponse httpResponse, DefaultHttp2Headers.Builder http2Headers) { - http2Headers.status(Integer.toString(httpResponse.status().code())); - addHeaders(httpResponse.headers(), http2Headers); - } - /** * Get the next stream id either from the {@link HttpHeaders} object or HTTP/2 codec * @@ -114,7 +49,7 @@ public class DelegatingHttp2HttpConnectionHandler extends DelegatingHttp2Connect */ private int getStreamId(HttpHeaders httpHeaders) throws Http2Exception { int streamId = 0; - String value = httpHeaders.get(HttpUtil.ExtensionHeaders.Names.STREAM_ID); + String value = httpHeaders.get(HttpUtil.ExtensionHeaderNames.STREAM_ID.text()); if (value == null) { streamId = nextStreamId(); } else { @@ -124,7 +59,6 @@ public class DelegatingHttp2HttpConnectionHandler extends DelegatingHttp2Connect throw Http2Exception.format(Http2Error.INTERNAL_ERROR, "Invalid user-specified stream id value '%s'", value); } - httpHeaders.remove(HttpUtil.ExtensionHeaders.Names.STREAM_ID); } return streamId; @@ -134,52 +68,33 @@ public class DelegatingHttp2HttpConnectionHandler extends DelegatingHttp2Connect * Handles conversion of a {@link FullHttpMessage} to HTTP/2 frames. */ @Override - @SuppressWarnings("deprecation") public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) { if (msg instanceof FullHttpMessage) { FullHttpMessage httpMsg = (FullHttpMessage) msg; boolean hasData = httpMsg.content().isReadable(); - // Convert and write the headers. - HttpHeaders httpHeaders = httpMsg.headers(); - DefaultHttp2Headers.Builder http2Headers = DefaultHttp2Headers.newBuilder(); - if (msg instanceof HttpRequest) { - addRequestHeaders((HttpRequest) msg, http2Headers); - } else if (msg instanceof HttpResponse) { - addResponseHeaders((HttpResponse) msg, http2Headers); - } - // Provide the user the opportunity to specify the streamId int streamId = 0; try { - streamId = getStreamId(httpHeaders); + streamId = getStreamId(httpMsg.headers()); } catch (Http2Exception e) { httpMsg.release(); promise.setFailure(e); return; } - // The Connection, Keep-Alive, Proxy-Connection, Transfer-Encoding, - // and Upgrade headers are not valid and MUST not be sent. - httpHeaders.remove(HttpHeaders.Names.CONNECTION); - httpHeaders.remove(HttpHeaders.Names.KEEP_ALIVE); - httpHeaders.remove(HttpHeaders.Names.PROXY_CONNECTION); - httpHeaders.remove(HttpHeaders.Names.TRANSFER_ENCODING); - - // Add the HTTP headers which have not been consumed above - for (Map.Entry entry : httpHeaders.entries()) { - http2Headers.add(entry.getKey(), entry.getValue()); - } + // Convert and write the headers. + Http2Headers http2Headers = HttpUtil.toHttp2Headers(httpMsg); if (hasData) { ChannelPromiseAggregator promiseAggregator = new ChannelPromiseAggregator(promise); ChannelPromise headerPromise = ctx.newPromise(); ChannelPromise dataPromise = ctx.newPromise(); promiseAggregator.add(headerPromise, dataPromise); - writeHeaders(ctx, streamId, http2Headers.build(), 0, false, headerPromise); + writeHeaders(ctx, streamId, http2Headers, 0, false, headerPromise); writeData(ctx, streamId, httpMsg.content(), 0, true, dataPromise); } else { - writeHeaders(ctx, streamId, http2Headers.build(), 0, true, promise); + writeHeaders(ctx, streamId, http2Headers, 0, true, promise); } } else { ctx.write(msg, promise); diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/EmptyHttp2Headers.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/EmptyHttp2Headers.java new file mode 100644 index 0000000000..c60aff603f --- /dev/null +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/EmptyHttp2Headers.java @@ -0,0 +1,142 @@ +/* + * 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: + * + * 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.http2; + +import io.netty.handler.codec.AsciiString; +import io.netty.handler.codec.BinaryHeaders; +import io.netty.handler.codec.EmptyBinaryHeaders; + +public final class EmptyHttp2Headers extends EmptyBinaryHeaders implements Http2Headers { + public static final EmptyHttp2Headers INSTANCE = new EmptyHttp2Headers(); + + private EmptyHttp2Headers() { + } + + @Override + public EmptyHttp2Headers add(AsciiString name, AsciiString value) { + super.add(name, value); + return this; + } + + @Override + public EmptyHttp2Headers add(AsciiString name, Iterable values) { + super.add(name, values); + return this; + } + + @Override + public EmptyHttp2Headers add(AsciiString name, AsciiString... values) { + super.add(name, values); + return this; + } + + @Override + public EmptyHttp2Headers add(BinaryHeaders headers) { + super.add(headers); + return this; + } + + @Override + public EmptyHttp2Headers set(AsciiString name, AsciiString value) { + super.set(name, value); + return this; + } + + @Override + public EmptyHttp2Headers set(AsciiString name, Iterable values) { + super.set(name, values); + return this; + } + + @Override + public EmptyHttp2Headers set(AsciiString name, AsciiString... values) { + super.set(name, values); + return this; + } + + @Override + public EmptyHttp2Headers set(BinaryHeaders headers) { + super.set(headers); + return this; + } + + @Override + public EmptyHttp2Headers setAll(BinaryHeaders headers) { + super.setAll(headers); + return this; + } + + @Override + public EmptyHttp2Headers clear() { + return this; + } + + @Override + public EmptyHttp2Headers forEachEntry(BinaryHeaderVisitor visitor) { + super.forEachEntry(visitor); + return this; + } + + @Override + public EmptyHttp2Headers method(AsciiString method) { + throw new UnsupportedOperationException(); + } + + @Override + public EmptyHttp2Headers scheme(AsciiString status) { + throw new UnsupportedOperationException(); + } + + @Override + public EmptyHttp2Headers authority(AsciiString authority) { + throw new UnsupportedOperationException(); + } + + @Override + public EmptyHttp2Headers path(AsciiString path) { + throw new UnsupportedOperationException(); + } + + @Override + public EmptyHttp2Headers status(AsciiString status) { + throw new UnsupportedOperationException(); + } + + @Override + public AsciiString method() { + return get(PseudoHeaderName.METHOD.value()); + } + + @Override + public AsciiString scheme() { + return get(PseudoHeaderName.SCHEME.value()); + } + + @Override + public AsciiString authority() { + return get(PseudoHeaderName.AUTHORITY.value()); + } + + @Override + public AsciiString path() { + return get(PseudoHeaderName.PATH.value()); + } + + @Override + public AsciiString status() { + return get(PseudoHeaderName.STATUS.value()); + } +} diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2Headers.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2Headers.java index 4331fed357..a82e00669d 100644 --- a/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2Headers.java +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2Headers.java @@ -15,72 +15,16 @@ package io.netty.handler.codec.http2; -import java.util.Collections; +import io.netty.handler.codec.AsciiString; +import io.netty.handler.codec.BinaryHeaders; + import java.util.HashSet; -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; /** - * An immutable collection of headers sent or received via HTTP/2. + * A collection of headers sent or received via HTTP/2. */ -public abstract class Http2Headers implements Iterable> { - - public static final Http2Headers EMPTY_HEADERS = new Http2Headers() { - - @Override - public String get(CharSequence name) { - return null; - } - - @Override - public List getAll(CharSequence name) { - return Collections.emptyList(); - } - - @Override - public List> entries() { - return Collections.emptyList(); - } - - @Override - public boolean contains(CharSequence name) { - return false; - } - - @Override - public boolean isEmpty() { - return true; - } - - @Override - public int size() { - return 0; - } - - @Override - public Set names() { - return Collections.emptySet(); - } - - @Override - public Iterator> iterator() { - return entries().iterator(); - } - - @Override - public String forEach(HeaderVisitor visitor) { - return null; - } - }; - - /** - * The prefix used to denote an HTTP/2 psuedo-header. - */ - public static String PSEUDO_HEADER_PREFIX = ":"; +public interface Http2Headers extends BinaryHeaders { /** * HTTP/2 pseudo-headers names. @@ -89,326 +33,133 @@ public abstract class Http2Headers implements Iterable> { /** * {@code :method}. */ - METHOD(PSEUDO_HEADER_PREFIX + "method"), + METHOD(":method"), /** * {@code :scheme}. */ - SCHEME(PSEUDO_HEADER_PREFIX + "scheme"), + SCHEME(":scheme"), /** * {@code :authority}. */ - AUTHORITY(PSEUDO_HEADER_PREFIX + "authority"), + AUTHORITY(":authority"), /** * {@code :path}. */ - PATH(PSEUDO_HEADER_PREFIX + "path"), + PATH(":path"), /** * {@code :status}. */ - STATUS(PSEUDO_HEADER_PREFIX + "status"); + STATUS(":status"); - private final String value; - - PseudoHeaderName(String value) { - this.value = value; + private final AsciiString value; + private static final Set PSEUDO_HEADERS = new HashSet(); + static { + for (PseudoHeaderName pseudoHeader : PseudoHeaderName.values()) { + PSEUDO_HEADERS.add(pseudoHeader.value()); + } } - public String value() { + PseudoHeaderName(String value) { + this.value = new AsciiString(value); + } + + public AsciiString value() { + // Return a slice so that the buffer gets its own reader index. return value; } /** * Indicates whether the given header name is a valid HTTP/2 pseudo header. */ - public static boolean isPseudoHeader(String header) { - if (header == null || !header.startsWith(Http2Headers.PSEUDO_HEADER_PREFIX)) { - // Not a pseudo-header. - return false; - } - - // Check the header name against the set of valid pseudo-headers. - for (PseudoHeaderName pseudoHeader : PseudoHeaderName.values()) { - String pseudoHeaderName = pseudoHeader.value(); - if (pseudoHeaderName.equals(header)) { - // It's a valid pseudo-header. - return true; - } - } - return false; + public static boolean isPseudoHeader(AsciiString header) { + return PSEUDO_HEADERS.contains(header); } } - /** - * Returns the {@link Set} of all header names. - */ - public abstract Set names(); + @Override + Http2Headers add(AsciiString name, AsciiString value); + + @Override + Http2Headers add(AsciiString name, Iterable values); + + @Override + Http2Headers add(AsciiString name, AsciiString... values); + + @Override + Http2Headers add(BinaryHeaders headers); + + @Override + Http2Headers set(AsciiString name, AsciiString value); + + @Override + Http2Headers set(AsciiString name, Iterable values); + + @Override + Http2Headers set(AsciiString name, AsciiString... values); + + @Override + Http2Headers set(BinaryHeaders headers); + + @Override + Http2Headers setAll(BinaryHeaders headers); + + @Override + Http2Headers clear(); + + @Override + Http2Headers forEachEntry(BinaryHeaderVisitor visitor); /** - * Returns the header value with the specified header name. If there is more than one header - * value for the specified header name, the first value is returned. - * - * @return the header value or {@code null} if there is no such header + * Sets the {@link PseudoHeaderName#METHOD} header or {@code null} if there is no such header */ - public abstract String get(CharSequence name); + Http2Headers method(AsciiString value); /** - * Returns the header values with the specified header name. - * - * @return the {@link List} of header values. An empty list if there is no such header. + * Sets the {@link PseudoHeaderName#SCHEME} header if there is no such header */ - public abstract List getAll(CharSequence name); + Http2Headers scheme(AsciiString value); /** - * Returns all header names and values that this frame contains. - * - * @return the {@link List} of the header name-value pairs. An empty list if there is no header - * in this message. + * Sets the {@link PseudoHeaderName#AUTHORITY} header or {@code null} if there is no such header */ - public abstract List> entries(); + Http2Headers authority(AsciiString value); /** - * Returns {@code true} if and only if there is a header with the specified header name. + * Sets the {@link PseudoHeaderName#PATH} header or {@code null} if there is no such header */ - public abstract boolean contains(CharSequence name); + Http2Headers path(AsciiString value); /** - * Checks if no header exists. + * Sets the {@link PseudoHeaderName#STATUS} header or {@code null} if there is no such header */ - public abstract boolean isEmpty(); - - /** - * Gets the number of headers contained in this object. - */ - public abstract int size(); - - /** - * Allows a means to reduce GC pressure while iterating over a collection - */ - public interface HeaderVisitor { - /** - * @return - *
    - *
  • {@code true} if the processor wants to continue the loop and handle the entry.
  • - *
  • {@code false} if the processor wants to stop handling headers and abort the loop.
  • - *
- */ - boolean visit(Map.Entry entry); - } - - /** - * Iterates over the entries contained within this header object in no guaranteed order - * @return {@code null} if the visitor iterated to or beyond the end of the headers. - * The last-visited header name If the {@link HeaderVisitor#visit(Entry)} returned {@code false}. - */ - public abstract String forEach(HeaderVisitor visitor); - - /** - * Interface for the Builder pattern for {@link Http2Headers}. - */ - public interface Builder { - /** - * Build all the collected headers into a {@link Http2Headers}. - * @return The {@link Http2Headers} object which this builder has been used for - */ - Http2Headers build(); - - /** - * Gets the number of headers contained in this object. - */ - int size(); - - /** - * Clears all values from this collection. - */ - Builder clear(); - - /** - * Returns the header value with the specified header name. If there is more than one header - * value for the specified header name, the first value is returned. - *

- * Note that all HTTP2 headers names are lower case and this method will not force {@code name} to lower case. - * @return the header value or {@code null} if there is no such header - */ - String get(CharSequence name); - - /** - * Returns the header values with the specified header name. - *

- * Note that all HTTP2 headers names are lower case and this method will not force {@code name} to lower case. - * @return the {@link List} of header values. An empty list if there is no such header. - */ - List getAll(CharSequence name); - - /** - * Clears all existing headers from this collection and replaces them with the given header - * set. - */ - void set(Http2Headers headers); - - /** - * Adds the given header to the collection. - * @throws IllegalArgumentException if the name or value of this header is invalid for any reason. - */ - Builder add(CharSequence name, Object value); - - /** - * Adds the given header to the collection. - * @throws IllegalArgumentException if the name or value of this header is invalid for any reason. - */ - Builder add(String name, Object value); - - /** - * Removes the header with the given name from this collection. - * This method will not force the {@code name} to lower case before looking for a match. - */ - Builder remove(CharSequence name); - - /** - * Removes the header with the given name from this collection. - * This method will force the {@code name} to lower case before looking for a match. - */ - Builder remove(String name); - - /** - * Sets the given header in the collection, replacing any previous values. - * @throws IllegalArgumentException if the name or value of this header is invalid for any reason. - */ - Builder set(CharSequence name, Object value); - - /** - * Sets the given header in the collection, replacing any previous values. - * @throws IllegalArgumentException if the name or value of this header is invalid for any reason. - */ - Builder set(String name, Object value); - - /** - * Sets the given header in the collection, replacing any previous values. - * @throws IllegalArgumentException if the name or value of this header is invalid for any reason. - */ - Builder set(String name, Iterable values); - - /** - * Sets the {@link PseudoHeaderName#METHOD} header. - */ - Builder method(String method); - - /** - * Sets the {@link PseudoHeaderName#SCHEME} header. - */ - Builder scheme(String scheme); - - /** - * Sets the {@link PseudoHeaderName#AUTHORITY} header. - */ - Builder authority(String authority); - - /** - * Sets the {@link PseudoHeaderName#PATH} header. - */ - Builder path(String path); - - /** - * Sets the {@link PseudoHeaderName#STATUS} header. - */ - Builder status(String status); - } + Http2Headers status(AsciiString value); /** * Gets the {@link PseudoHeaderName#METHOD} header or {@code null} if there is no such header */ - public final String method() { - return get(PseudoHeaderName.METHOD.value()); - } + AsciiString method(); /** * Gets the {@link PseudoHeaderName#SCHEME} header or {@code null} if there is no such header */ - public final String scheme() { - return get(PseudoHeaderName.SCHEME.value()); - } + AsciiString scheme(); /** * Gets the {@link PseudoHeaderName#AUTHORITY} header or {@code null} if there is no such header */ - public final String authority() { - return get(PseudoHeaderName.AUTHORITY.value()); - } + AsciiString authority(); /** * Gets the {@link PseudoHeaderName#PATH} header or {@code null} if there is no such header */ - public final String path() { - return get(PseudoHeaderName.PATH.value()); - } + AsciiString path(); /** * Gets the {@link PseudoHeaderName#STATUS} header or {@code null} if there is no such header */ - public final String status() { - return get(PseudoHeaderName.STATUS.value()); - } - - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - for (String name : names()) { - result = prime * result + name.hashCode(); - Set values = new TreeSet(getAll(name)); - for (String value : values) { - result = prime * result + value.hashCode(); - } - } - return result; - } - - @Override - public boolean equals(Object o) { - if (!(o instanceof Http2Headers)) { - return false; - } - Http2Headers other = (Http2Headers) o; - - // First, check that the set of names match. - Set names = names(); - if (!names.equals(other.names())) { - return false; - } - - // Compare the values for each name. - for (String name : names) { - List values = getAll(name); - List otherValues = other.getAll(name); - if (values.size() != otherValues.size()) { - return false; - } - // Convert the values to a set and remove values from the other object to see if - // they match. - Set valueSet = new HashSet(values); - valueSet.removeAll(otherValues); - if (!valueSet.isEmpty()) { - return false; - } - } - - // They match. - return true; - } - - @Override - public String toString() { - StringBuilder builder = new StringBuilder("Http2Headers["); - for (Map.Entry header : this) { - builder.append(header.getKey()); - builder.append(':'); - builder.append(header.getValue()); - builder.append(','); - } - builder.append(']'); - return builder.toString(); - } + AsciiString status(); } diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2HeadersDecoder.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2HeadersDecoder.java index 2c1207bfda..3ad4b1f687 100644 --- a/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2HeadersDecoder.java +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2HeadersDecoder.java @@ -25,7 +25,7 @@ public interface Http2HeadersDecoder { /** * Decodes the given headers block and returns the headers. */ - Http2Headers.Builder decodeHeaders(ByteBuf headerBlock) throws Http2Exception; + Http2Headers decodeHeaders(ByteBuf headerBlock) throws Http2Exception; /** * Sets the new max header table size for this decoder. diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/HttpUtil.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/HttpUtil.java index 12fb3c1b23..b319b73aaf 100644 --- a/codec-http2/src/main/java/io/netty/handler/codec/http2/HttpUtil.java +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/HttpUtil.java @@ -15,13 +15,49 @@ package io.netty.handler.codec.http2; import io.netty.handler.codec.AsciiString; +import io.netty.handler.codec.BinaryHeaders; +import io.netty.handler.codec.TextHeaderProcessor; +import io.netty.handler.codec.http.DefaultFullHttpRequest; +import io.netty.handler.codec.http.DefaultFullHttpResponse; +import io.netty.handler.codec.http.FullHttpMessage; +import io.netty.handler.codec.http.FullHttpRequest; +import io.netty.handler.codec.http.FullHttpResponse; +import io.netty.handler.codec.http.HttpHeaderUtil; +import io.netty.handler.codec.http.HttpHeaders; import io.netty.handler.codec.http.HttpMethod; +import io.netty.handler.codec.http.HttpRequest; +import io.netty.handler.codec.http.HttpResponse; import io.netty.handler.codec.http.HttpResponseStatus; +import io.netty.handler.codec.http.HttpVersion; + +import java.net.URI; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; /** * Provides utility methods and constants for the HTTP/2 to HTTP conversion */ +@SuppressWarnings("deprecation") public final class HttpUtil { + /** + * The set of headers that should not be directly copied when converting headers from HTTP to HTTP/2. + */ + private static final Set HTTP_TO_HTTP2_HEADER_BLACKLIST = new HashSet(); + static { + HTTP_TO_HTTP2_HEADER_BLACKLIST.add(HttpHeaders.Names.CONNECTION.toLowerCase()); + HTTP_TO_HTTP2_HEADER_BLACKLIST.add(HttpHeaders.Names.KEEP_ALIVE.toLowerCase()); + HTTP_TO_HTTP2_HEADER_BLACKLIST.add(HttpHeaders.Names.PROXY_CONNECTION.toLowerCase()); + HTTP_TO_HTTP2_HEADER_BLACKLIST.add(HttpHeaders.Names.TRANSFER_ENCODING.toLowerCase()); + HTTP_TO_HTTP2_HEADER_BLACKLIST.add(HttpHeaders.Names.HOST.toLowerCase()); + // These are already defined as lower-case. + HTTP_TO_HTTP2_HEADER_BLACKLIST.add(ExtensionHeaderNames.STREAM_ID.text()); + HTTP_TO_HTTP2_HEADER_BLACKLIST.add(ExtensionHeaderNames.AUTHORITY.text()); + HTTP_TO_HTTP2_HEADER_BLACKLIST.add(ExtensionHeaderNames.SCHEME.text()); + HTTP_TO_HTTP2_HEADER_BLACKLIST.add(ExtensionHeaderNames.PATH.text()); + } + /** * This will be the method used for {@link HttpRequest} objects generated * out of the HTTP message flow defined in @@ -48,60 +84,67 @@ public final class HttpUtil { /** * Provides the HTTP header extensions used to carry HTTP/2 information in HTTP objects */ - public static final class ExtensionHeaders { - public static final class Names { - private Names() { } + public enum ExtensionHeaderNames { + /** + * HTTP extension header which will identify the stream id from the HTTP/2 event(s) + * responsible for generating a {@code HttpObject} + *

+ * {@code "x-http2-stream-id"} + */ + STREAM_ID("x-http2-stream-id"), - /** - * HTTP extension header which will identify the stream id - * from the HTTP/2 event(s) responsible for generating a {@code HttpObject} - *

- * {@code "x-http2-stream-id"} - */ - public static final AsciiString STREAM_ID = new AsciiString("x-http2-stream-id"); - /** - * HTTP extension header which will identify the authority pseudo header - * from the HTTP/2 event(s) responsible for generating a {@code HttpObject} - *

- * {@code "x-http2-authority"} - */ - public static final AsciiString AUTHORITY = new AsciiString("x-http2-authority"); - /** - * HTTP extension header which will identify the scheme pseudo header - * from the HTTP/2 event(s) responsible for generating a {@code HttpObject} - *

- * {@code "x-http2-scheme"} - */ - public static final AsciiString SCHEME = new AsciiString("x-http2-scheme"); - /** - * HTTP extension header which will identify the path pseudo header - * from the HTTP/2 event(s) responsible for generating a {@code HttpObject} - *

- * {@code "x-http2-path"} - */ - public static final AsciiString PATH = new AsciiString("x-http2-path"); - /** - * HTTP extension header which will identify the stream id used to create this stream - * in a HTTP/2 push promise frame - *

- * {@code "x-http2-stream-promise-id"} - */ - public static final AsciiString STREAM_PROMISE_ID = new AsciiString("x-http2-stream-promise-id"); - /** - * HTTP extension header which will identify the stream id which this stream is dependent on. - * This stream will be a child node of the stream id associated with this header value. - *

- * {@code "x-http2-stream-dependency-id"} - */ - public static final AsciiString STREAM_DEPENDENCY_ID = new AsciiString("x-http2-stream-dependency-id"); - /** - * HTTP extension header which will identify the weight - * (if non-default and the priority is not on the default stream) of the associated HTTP/2 stream - * responsible responsible for generating a {@code HttpObject} - *

- * {@code "x-http2-stream-weight"} - */ - public static final AsciiString STREAM_WEIGHT = new AsciiString("x-http2-stream-weight"); + /** + * HTTP extension header which will identify the authority pseudo header from the HTTP/2 + * event(s) responsible for generating a {@code HttpObject} + *

+ * {@code "x-http2-authority"} + */ + AUTHORITY("x-http2-authority"), + /** + * HTTP extension header which will identify the scheme pseudo header from the HTTP/2 + * event(s) responsible for generating a {@code HttpObject} + *

+ * {@code "x-http2-scheme"} + */ + SCHEME("x-http2-scheme"), + /** + * HTTP extension header which will identify the path pseudo header from the HTTP/2 event(s) + * responsible for generating a {@code HttpObject} + *

+ * {@code "x-http2-path"} + */ + PATH("x-http2-path"), + /** + * HTTP extension header which will identify the stream id used to create this stream in a + * HTTP/2 push promise frame + *

+ * {@code "x-http2-stream-promise-id"} + */ + STREAM_PROMISE_ID("x-http2-stream-promise-id"), + /** + * HTTP extension header which will identify the stream id which this stream is dependent + * on. This stream will be a child node of the stream id associated with this header value. + *

+ * {@code "x-http2-stream-dependency-id"} + */ + STREAM_DEPENDENCY_ID("x-http2-stream-dependency-id"), + /** + * HTTP extension header which will identify the weight (if non-default and the priority is + * not on the default stream) of the associated HTTP/2 stream responsible responsible for + * generating a {@code HttpObject} + *

+ * {@code "x-http2-stream-weight"} + */ + STREAM_WEIGHT("x-http2-stream-weight"); + + private final AsciiString text; + + private ExtensionHeaderNames(String text) { + this.text = new AsciiString(text); + } + + public AsciiString text() { + return text; } } @@ -112,7 +155,7 @@ public final class HttpUtil { * @return The HTTP/1.x status * @throws Http2Exception If there is a problem translating from HTTP/2 to HTTP/1.x */ - public static HttpResponseStatus parseStatus(String status) throws Http2Exception { + public static HttpResponseStatus parseStatus(AsciiString status) throws Http2Exception { HttpResponseStatus result = null; try { result = HttpResponseStatus.parseLine(status); @@ -127,4 +170,210 @@ public final class HttpUtil { } return result; } + + /** + * Create a new object to contain the response data + * + * @param streamId The stream associated with the response + * @param http2Headers The initial set of HTTP/2 headers to create the response with + * @param validateHttpHeaders + *

    + *
  • {@code true} to validate HTTP headers in the http-codec
  • + *
  • {@code false} not to validate HTTP headers in the http-codec
  • + *
+ * @return A new response object which represents headers/data + * @throws Http2Exception see {@link #addHttp2ToHttpHeaders(int, Http2Headers, FullHttpMessage, Map)} + */ + public static FullHttpResponse toHttpResponse(int streamId, Http2Headers http2Headers, + boolean validateHttpHeaders) throws Http2Exception { + HttpResponseStatus status = parseStatus(http2Headers.status()); + // HTTP/2 does not define a way to carry the version or reason phrase that is included in an + // HTTP/1.1 status line. + FullHttpResponse msg = + new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, status, validateHttpHeaders); + addHttp2ToHttpHeaders(streamId, http2Headers, msg, false); + return msg; + } + + /** + * Create a new object to contain the request data + * + * @param streamId The stream associated with the request + * @param http2Headers The initial set of HTTP/2 headers to create the request with + * @param validateHttpHeaders + *
    + *
  • {@code true} to validate HTTP headers in the http-codec
  • + *
  • {@code false} not to validate HTTP headers in the http-codec
  • + *
+ * @return A new request object which represents headers/data + * @throws Http2Exception see {@link #addHttp2ToHttpHeaders(int, Http2Headers, FullHttpMessage, Map)} + */ + public static FullHttpRequest toHttpRequest(int streamId, Http2Headers http2Headers, boolean validateHttpHeaders) + throws Http2Exception { + // HTTP/2 does not define a way to carry the version identifier that is + // included in the HTTP/1.1 request line. + FullHttpRequest msg = + new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.valueOf(http2Headers + .method().toString()), http2Headers.path().toString(), validateHttpHeaders); + addHttp2ToHttpHeaders(streamId, http2Headers, msg, false); + return msg; + } + + /** + * Translate and add HTTP/2 headers to HTTP/1.x headers + * + * @param streamId The stream associated with {@code sourceHeaders} + * @param sourceHeaders The HTTP/2 headers to convert + * @param destinationMessage The object which will contain the resulting HTTP/1.x headers + * @param addToTrailer {@code true} to add to trailing headers. {@code false} to add to initial + * headers. + * @throws Http2Exception If not all HTTP/2 headers can be translated to HTTP/1.x + */ + public static void addHttp2ToHttpHeaders(int streamId, Http2Headers sourceHeaders, + FullHttpMessage destinationMessage, boolean addToTrailer) + throws Http2Exception { + HttpHeaders headers = addToTrailer ? destinationMessage.trailingHeaders() : destinationMessage.headers(); + boolean request = destinationMessage instanceof HttpRequest; + Http2ToHttpHeaderTranslator visitor = new Http2ToHttpHeaderTranslator(headers, request); + sourceHeaders.forEachEntry(visitor); + if (visitor.cause() != null) { + throw visitor.cause(); + } + + headers.remove(HttpHeaders.Names.TRANSFER_ENCODING); + headers.remove(HttpHeaders.Names.TRAILER); + if (!addToTrailer) { + headers.set(ExtensionHeaderNames.STREAM_ID.text(), streamId); + HttpHeaderUtil.setKeepAlive(destinationMessage, true); + } + } + + /** + * Converts the given HTTP/1.x headers into HTTP/2 headers. + */ + public static Http2Headers toHttp2Headers(FullHttpMessage in) { + final Http2Headers out = new DefaultHttp2Headers(); + HttpHeaders inHeaders = in.headers(); + if (in instanceof HttpRequest) { + HttpRequest request = (HttpRequest) in; + out.path(new AsciiString(request.uri())); + out.method(new AsciiString(request.method().toString())); + + String value = inHeaders.get(HttpHeaders.Names.HOST); + if (value != null) { + URI hostUri = URI.create(value); + // The authority MUST NOT include the deprecated "userinfo" subcomponent + value = hostUri.getAuthority(); + if (value != null) { + out.authority(new AsciiString(value.replaceFirst("^.*@", ""))); + } + value = hostUri.getScheme(); + if (value != null) { + out.scheme(new AsciiString(value)); + } + } + + // Consume the Authority extension header if present + value = inHeaders.get(ExtensionHeaderNames.AUTHORITY.text()); + if (value != null) { + out.authority(new AsciiString(value)); + } + + // Consume the Scheme extension header if present + value = inHeaders.get(ExtensionHeaderNames.SCHEME.text()); + if (value != null) { + out.scheme(new AsciiString(value)); + } + } else if (in instanceof HttpResponse) { + HttpResponse response = (HttpResponse) in; + out.status(new AsciiString(Integer.toString(response.status().code()))); + } + + // Add the HTTP headers which have not been consumed above + inHeaders.forEachEntry(new TextHeaderProcessor() { + @Override + public boolean process(CharSequence name, CharSequence value) throws Exception { + AsciiString aName = AsciiString.of(name); + if (!HTTP_TO_HTTP2_HEADER_BLACKLIST.contains(aName.toLowerCase())) { + AsciiString aValue = AsciiString.of(value); + 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.BinaryHeaderVisitor { + /** + * Translations from HTTP/2 header name to the HTTP/1.x equivalent. + */ + private static final Map REQUEST_HEADER_TRANSLATIONS = + new HashMap(); + private static final Map RESPONSE_HEADER_TRANSLATIONS = + new HashMap(); + static { + RESPONSE_HEADER_TRANSLATIONS.put(Http2Headers.PseudoHeaderName.AUTHORITY.value(), + ExtensionHeaderNames.AUTHORITY.text().toString()); + RESPONSE_HEADER_TRANSLATIONS.put(Http2Headers.PseudoHeaderName.SCHEME.value(), + ExtensionHeaderNames.SCHEME.text().toString()); + REQUEST_HEADER_TRANSLATIONS.putAll(RESPONSE_HEADER_TRANSLATIONS); + RESPONSE_HEADER_TRANSLATIONS.put(Http2Headers.PseudoHeaderName.PATH.value(), + ExtensionHeaderNames.PATH.text().toString()); + } + + private final HttpHeaders output; + private final Map translations; + private Http2Exception e; + + /** + * Create a new instance + * + * @param output The HTTP/1.x headers object to store the results of the translation + * @param request if {@code true}, translates headers using the request translation map. + * Otherwise uses the response translation map. + */ + public Http2ToHttpHeaderTranslator(HttpHeaders output, boolean request) { + this.output = output; + translations = request? REQUEST_HEADER_TRANSLATIONS : RESPONSE_HEADER_TRANSLATIONS; + } + + @Override + public boolean visit(AsciiString name, AsciiString value) { + String translatedName = translations.get(name); + if (translatedName != null || !Http2Headers.PseudoHeaderName.isPseudoHeader(name)) { + if (translatedName == null) { + translatedName = name.toString(); + } + + // http://tools.ietf.org/html/draft-ietf-httpbis-http2-14#section-8.1.2.3 + // All headers that start with ':' are only valid in HTTP/2 context + if (translatedName.isEmpty() || translatedName.charAt(0) == ':') { + e = Http2Exception + .protocolError("Unknown HTTP/2 header '%s' encountered in translation to HTTP/1.x", + translatedName); + return false; + } else { + output.add(translatedName, value.toString()); + } + } + return true; + } + + /** + * Get any exceptions encountered while translating HTTP/2 headers to HTTP/1.x headers + * + * @return + *
    + *
  • {@code null} if no exceptions where encountered
  • + *
  • Otherwise an exception describing what went wrong
  • + *
+ */ + public Http2Exception cause() { + return e; + } + } } diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/InboundHttp2ToHttpAdapter.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/InboundHttp2ToHttpAdapter.java index 264e18b3ad..947f18cf52 100644 --- a/codec-http2/src/main/java/io/netty/handler/codec/http2/InboundHttp2ToHttpAdapter.java +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/InboundHttp2ToHttpAdapter.java @@ -17,26 +17,14 @@ package io.netty.handler.codec.http2; import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.TooLongFrameException; -import io.netty.handler.codec.http.DefaultFullHttpRequest; -import io.netty.handler.codec.http.DefaultFullHttpResponse; import io.netty.handler.codec.http.FullHttpMessage; import io.netty.handler.codec.http.FullHttpRequest; import io.netty.handler.codec.http.FullHttpResponse; import io.netty.handler.codec.http.HttpHeaderUtil; import io.netty.handler.codec.http.HttpHeaders; -import io.netty.handler.codec.http.HttpMethod; -import io.netty.handler.codec.http.HttpResponseStatus; -import io.netty.handler.codec.http.HttpVersion; -import io.netty.handler.codec.http2.Http2Headers.HeaderVisitor; import io.netty.util.collection.IntObjectHashMap; import io.netty.util.collection.IntObjectMap; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Map.Entry; -import java.util.Set; - /** * This adapter provides just header/data events from the HTTP message flow defined * here HTTP/2 Spec Message Flow @@ -48,27 +36,6 @@ public class InboundHttp2ToHttpAdapter extends Http2EventAdapter { private final ImmediateSendDetector sendDetector; protected final IntObjectMap messageMap; - private static final Set HEADERS_TO_EXCLUDE; - private static final Map HEADER_NAME_TRANSLATIONS_REQUEST; - private static final Map HEADER_NAME_TRANSLATIONS_RESPONSE; - - static { - HEADERS_TO_EXCLUDE = new HashSet(); - HEADER_NAME_TRANSLATIONS_REQUEST = new HashMap(); - HEADER_NAME_TRANSLATIONS_RESPONSE = new HashMap(); - for (Http2Headers.PseudoHeaderName http2HeaderName : Http2Headers.PseudoHeaderName.values()) { - HEADERS_TO_EXCLUDE.add(http2HeaderName.value()); - } - - HEADER_NAME_TRANSLATIONS_RESPONSE.put(Http2Headers.PseudoHeaderName.AUTHORITY.value(), - HttpUtil.ExtensionHeaders.Names.AUTHORITY.toString()); - HEADER_NAME_TRANSLATIONS_RESPONSE.put(Http2Headers.PseudoHeaderName.SCHEME.value(), - HttpUtil.ExtensionHeaders.Names.SCHEME.toString()); - HEADER_NAME_TRANSLATIONS_REQUEST.putAll(HEADER_NAME_TRANSLATIONS_RESPONSE); - HEADER_NAME_TRANSLATIONS_RESPONSE.put(Http2Headers.PseudoHeaderName.PATH.value(), - HttpUtil.ExtensionHeaders.Names.PATH.toString()); - } - /** * Creates a new instance * @@ -189,8 +156,8 @@ public class InboundHttp2ToHttpAdapter extends Http2EventAdapter { */ protected FullHttpMessage newMessage(int streamId, Http2Headers headers, boolean validateHttpHeaders) throws Http2Exception { - return connection.isServer() ? newHttpRequest(streamId, headers, validateHttpHeaders) : - newHttpResponse(streamId, headers, validateHttpHeaders); + return connection.isServer() ? HttpUtil.toHttpRequest(streamId, headers, + validateHttpHeaders) : HttpUtil.toHttpResponse(streamId, headers, validateHttpHeaders); } /** @@ -224,7 +191,7 @@ public class InboundHttp2ToHttpAdapter extends Http2EventAdapter { msg = newMessage(streamId, headers, validateHttpHeaders); } else if (allowAppend) { try { - addHttp2ToHttpHeaders(streamId, headers, msg, appendToTrailer); + HttpUtil.addHttp2ToHttpHeaders(streamId, headers, msg, appendToTrailer); } catch (Http2Exception e) { removeMessage(streamId); throw e; @@ -316,7 +283,7 @@ public class InboundHttp2ToHttpAdapter extends Http2EventAdapter { promisedStreamId); } - msg.headers().set(HttpUtil.ExtensionHeaders.Names.STREAM_PROMISE_ID, streamId); + msg.headers().set(HttpUtil.ExtensionHeaderNames.STREAM_PROMISE_ID.text(), streamId); processHeadersEnd(ctx, promisedStreamId, msg, false); } @@ -384,150 +351,4 @@ public class InboundHttp2ToHttpAdapter extends Http2EventAdapter { return null; } } - - /** - * Create a new object to contain the response data - * - * @param streamId The stream associated with the response - * @param http2Headers The initial set of HTTP/2 headers to create the response with - * @param validateHttpHeaders - *
    - *
  • {@code true} to validate HTTP headers in the http-codec
  • - *
  • {@code false} not to validate HTTP headers in the http-codec
  • - *
- * @return A new response object which represents headers/data - * @throws Http2Exception see {@link #addHttp2ToHttpHeaders(int, Http2Headers, FullHttpMessage, Map)} - */ - private static FullHttpMessage newHttpResponse(int streamId, Http2Headers http2Headers, boolean validateHttpHeaders) - throws Http2Exception { - HttpResponseStatus status = HttpUtil.parseStatus(http2Headers.status()); - // HTTP/2 does not define a way to carry the version or reason phrase that is included in an HTTP/1.1 - // status line. - FullHttpMessage msg = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, status, validateHttpHeaders); - addHttp2ToHttpHeaders(streamId, http2Headers, msg, false, HEADER_NAME_TRANSLATIONS_RESPONSE); - return msg; - } - - /** - * Create a new object to contain the request data - * - * @param streamId The stream associated with the request - * @param http2Headers The initial set of HTTP/2 headers to create the request with - * @param validateHttpHeaders - *
    - *
  • {@code true} to validate HTTP headers in the http-codec
  • - *
  • {@code false} not to validate HTTP headers in the http-codec
  • - *
- * @return A new request object which represents headers/data - * @throws Http2Exception see {@link #addHttp2ToHttpHeaders(int, Http2Headers, FullHttpMessage, Map)} - */ - private static FullHttpMessage newHttpRequest(int streamId, Http2Headers http2Headers, boolean validateHttpHeaders) - throws Http2Exception { - // HTTP/2 does not define a way to carry the version identifier that is - // included in the HTTP/1.1 request line. - FullHttpMessage msg = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, - HttpMethod.valueOf(http2Headers.method()), http2Headers.path(), validateHttpHeaders); - addHttp2ToHttpHeaders(streamId, http2Headers, msg, false, HEADER_NAME_TRANSLATIONS_REQUEST); - return msg; - } - - /** - * Translate and add HTTP/2 headers to HTTP/1.x headers - * - * @param streamId The stream associated with {@code sourceHeaders} - * @param sourceHeaders The HTTP/2 headers to convert - * @param destinationMessage The object which will contain the resulting HTTP/1.x headers - * @param addToTrailer {@code true} to add to trailing headers. {@code false} to add to initial headers. - * @throws Http2Exception see {@link #addHttp2ToHttpHeaders(int, Http2Headers, FullHttpMessage, Map)} - */ - private static void addHttp2ToHttpHeaders(int streamId, Http2Headers sourceHeaders, - FullHttpMessage destinationMessage, boolean addToTrailer) throws Http2Exception { - addHttp2ToHttpHeaders(streamId, sourceHeaders, destinationMessage, addToTrailer, - (destinationMessage instanceof FullHttpRequest) ? HEADER_NAME_TRANSLATIONS_REQUEST - : HEADER_NAME_TRANSLATIONS_RESPONSE); - } - - /** - * Translate and add HTTP/2 headers to HTTP/1.x headers - * - * @param streamId The stream associated with {@code sourceHeaders} - * @param sourceHeaders The HTTP/2 headers to convert - * @param destinationMessage The object which will contain the resulting HTTP/1.x headers - * @param addToTrailer {@code true} to add to trailing headers. {@code false} to add to initial headers. - * @param translations A map used to help translate HTTP/2 headers to HTTP/1.x headers - * @throws Http2Exception If not all HTTP/2 headers can be translated to HTTP/1.x - */ - private static void addHttp2ToHttpHeaders(int streamId, Http2Headers sourceHeaders, - FullHttpMessage destinationMessage, boolean addToTrailer, Map translations) - throws Http2Exception { - HttpHeaders headers = addToTrailer ? destinationMessage.trailingHeaders() : destinationMessage.headers(); - HttpAdapterVisitor visitor = new HttpAdapterVisitor(headers, translations); - sourceHeaders.forEach(visitor); - if (visitor.exception() != null) { - throw visitor.exception(); - } - - headers.remove(HttpHeaders.Names.TRANSFER_ENCODING); - headers.remove(HttpHeaders.Names.TRAILER); - if (!addToTrailer) { - headers.set(HttpUtil.ExtensionHeaders.Names.STREAM_ID, streamId); - HttpHeaderUtil.setKeepAlive(destinationMessage, true); - } - } - - /** - * A visitor which translates HTTP/2 headers to HTTP/1 headers - */ - private static final class HttpAdapterVisitor implements HeaderVisitor { - private Map translations; - private HttpHeaders headers; - private Http2Exception e; - - /** - * Create a new instance - * - * @param headers The HTTP/1.x headers object to store the results of the translation - * @param translations A map used to help translate HTTP/2 headers to HTTP/1.x headers - */ - public HttpAdapterVisitor(HttpHeaders headers, Map translations) { - this.translations = translations; - this.headers = headers; - this.e = null; - } - - @Override - public boolean visit(Entry entry) { - String translatedName = translations.get(entry.getKey()); - if (translatedName != null || !HEADERS_TO_EXCLUDE.contains(entry.getKey())) { - if (translatedName == null) { - translatedName = entry.getKey(); - } - - // http://tools.ietf.org/html/draft-ietf-httpbis-http2-14#section-8.1.2.3 - // All headers that start with ':' are only valid in HTTP/2 context - if (translatedName.isEmpty() || translatedName.charAt(0) == ':') { - e = Http2Exception - .protocolError("Unknown HTTP/2 header '%s' encountered in translation to HTTP/1.x", - translatedName); - return false; - } else { - headers.add(translatedName, entry.getValue()); - } - } - return true; - } - - /** - * Get any exceptions encountered while translating HTTP/2 headers to HTTP/1.x headers - * - * @return - *
    - *
  • {@code null} if no exceptions where encountered
  • - *
  • Otherwise an exception describing what went wrong
  • - *
- */ - public Http2Exception exception() { - return e; - } - } } diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/InboundHttp2ToHttpPriorityAdapter.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/InboundHttp2ToHttpPriorityAdapter.java index 369331c7be..1b1bc3e651 100644 --- a/codec-http2/src/main/java/io/netty/handler/codec/http2/InboundHttp2ToHttpPriorityAdapter.java +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/InboundHttp2ToHttpPriorityAdapter.java @@ -15,6 +15,7 @@ package io.netty.handler.codec.http2; import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.AsciiString; import io.netty.handler.codec.TextHeaderProcessor; import io.netty.handler.codec.TooLongFrameException; import io.netty.handler.codec.http.DefaultHttpHeaders; @@ -30,6 +31,12 @@ import io.netty.util.collection.IntObjectMap; * the header/data message flow is more likely. */ public final class InboundHttp2ToHttpPriorityAdapter extends InboundHttp2ToHttpAdapter { + private static final AsciiString OUT_OF_MESSAGE_SEQUENCE_METHOD = new AsciiString( + HttpUtil.OUT_OF_MESSAGE_SEQUENCE_METHOD.toString()); + private static final AsciiString OUT_OF_MESSAGE_SEQUENCE_PATH = new AsciiString( + HttpUtil.OUT_OF_MESSAGE_SEQUENCE_PATH); + private static final AsciiString OUT_OF_MESSAGE_SEQUENCE_RETURN_CODE = new AsciiString( + HttpUtil.OUT_OF_MESSAGE_SEQUENCE_RETURN_CODE.toString()); private final IntObjectMap outOfMessageFlowHeaders; /** @@ -150,33 +157,32 @@ public final class InboundHttp2ToHttpPriorityAdapter extends InboundHttp2ToHttpA * @param headers The headers to remove the priority tree elements from */ private void removePriorityRelatedHeaders(HttpHeaders headers) { - headers.remove(HttpUtil.ExtensionHeaders.Names.STREAM_DEPENDENCY_ID); - headers.remove(HttpUtil.ExtensionHeaders.Names.STREAM_WEIGHT); + headers.remove(HttpUtil.ExtensionHeaderNames.STREAM_DEPENDENCY_ID.text()); + headers.remove(HttpUtil.ExtensionHeaderNames.STREAM_WEIGHT.text()); } /** * Initializes the pseudo header fields for out of message flow HTTP/2 headers - * @param builder The builder to set the pseudo header values + * @param headers The headers to be initialized with pseudo header values */ - private void initializePseudoHeaders(DefaultHttp2Headers.Builder builder) { + private void initializePseudoHeaders(Http2Headers headers) { if (connection.isServer()) { - builder.method(HttpUtil.OUT_OF_MESSAGE_SEQUENCE_METHOD.toString()) - .path(HttpUtil.OUT_OF_MESSAGE_SEQUENCE_PATH); + headers.method(OUT_OF_MESSAGE_SEQUENCE_METHOD).path(OUT_OF_MESSAGE_SEQUENCE_PATH); } else { - builder.status(HttpUtil.OUT_OF_MESSAGE_SEQUENCE_RETURN_CODE.toString()); + headers.status(OUT_OF_MESSAGE_SEQUENCE_RETURN_CODE); } } /** - * Add all the HTTP headers into the HTTP/2 headers {@code builder} object - * @param headers The HTTP headers to translate to HTTP/2 - * @param builder The container for the HTTP/2 headers + * Add all the HTTP headers into the HTTP/2 headers object + * @param httpHeaders The HTTP headers to translate to HTTP/2 + * @param http2Headers The target HTTP/2 headers */ - private void addHttpHeadersToHttp2Headers(HttpHeaders headers, final DefaultHttp2Headers.Builder builder) { - headers.forEachEntry(new TextHeaderProcessor() { + private void addHttpHeadersToHttp2Headers(HttpHeaders httpHeaders, final Http2Headers http2Headers) { + httpHeaders.forEachEntry(new TextHeaderProcessor() { @Override public boolean process(CharSequence name, CharSequence value) throws Exception { - builder.add(name, value); + http2Headers.add(new AsciiString(name), new AsciiString(value)); return true; } }); @@ -209,7 +215,7 @@ public final class InboundHttp2ToHttpPriorityAdapter extends InboundHttp2ToHttpA // and the HTTP message flow exists in OPEN. if (parent != null && !parent.equals(connection.connectionStream())) { HttpHeaders headers = new DefaultHttpHeaders(); - headers.set(HttpUtil.ExtensionHeaders.Names.STREAM_DEPENDENCY_ID, parent.id()); + headers.set(HttpUtil.ExtensionHeaderNames.STREAM_DEPENDENCY_ID.text(), parent.id()); importOutOfMessageFlowHeaders(stream.id(), headers); } } else { @@ -218,7 +224,7 @@ public final class InboundHttp2ToHttpPriorityAdapter extends InboundHttp2ToHttpA removePriorityRelatedHeaders(msg.trailingHeaders()); } else if (!parent.equals(connection.connectionStream())) { HttpHeaders headers = getActiveHeaders(msg); - headers.set(HttpUtil.ExtensionHeaders.Names.STREAM_DEPENDENCY_ID, parent.id()); + headers.set(HttpUtil.ExtensionHeaderNames.STREAM_DEPENDENCY_ID.text(), parent.id()); } } } @@ -236,7 +242,7 @@ public final class InboundHttp2ToHttpPriorityAdapter extends InboundHttp2ToHttpA } else { headers = getActiveHeaders(msg); } - headers.set(HttpUtil.ExtensionHeaders.Names.STREAM_WEIGHT, stream.weight()); + headers.set(HttpUtil.ExtensionHeaderNames.STREAM_WEIGHT.text(), stream.weight()); } @Override @@ -244,15 +250,15 @@ public final class InboundHttp2ToHttpPriorityAdapter extends InboundHttp2ToHttpA boolean exclusive) throws Http2Exception { FullHttpMessage msg = messageMap.get(streamId); if (msg == null) { - HttpHeaders headers = outOfMessageFlowHeaders.remove(streamId); - if (headers == null) { + HttpHeaders httpHeaders = outOfMessageFlowHeaders.remove(streamId); + if (httpHeaders == null) { throw Http2Exception.protocolError("Priority Frame recieved for unknown stream id %d", streamId); } - DefaultHttp2Headers.Builder builder = DefaultHttp2Headers.newBuilder(); - initializePseudoHeaders(builder); - addHttpHeadersToHttp2Headers(headers, builder); - msg = newMessage(streamId, builder.build(), validateHttpHeaders); + Http2Headers http2Headers = new DefaultHttp2Headers(); + initializePseudoHeaders(http2Headers); + addHttpHeadersToHttp2Headers(httpHeaders, http2Headers); + msg = newMessage(streamId, http2Headers, validateHttpHeaders); fireChannelRead(ctx, msg, streamId); } } diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/DataCompressionHttp2Test.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/DataCompressionHttp2Test.java index 533ff7e7e7..e07baabee0 100644 --- a/codec-http2/src/test/java/io/netty/handler/codec/http2/DataCompressionHttp2Test.java +++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/DataCompressionHttp2Test.java @@ -14,6 +14,10 @@ */ package io.netty.handler.codec.http2; +import static io.netty.handler.codec.http.HttpHeaders.Names.CONTENT_ENCODING; +import static io.netty.handler.codec.http.HttpHeaders.Values.DEFLATE; +import static io.netty.handler.codec.http.HttpHeaders.Values.GZIP; +import static io.netty.handler.codec.http2.Http2TestUtil.as; import static io.netty.handler.codec.http2.Http2TestUtil.runInChannel; import static java.util.concurrent.TimeUnit.SECONDS; import static org.junit.Assert.assertEquals; @@ -38,9 +42,9 @@ import io.netty.channel.embedded.EmbeddedChannel; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.nio.NioServerSocketChannel; import io.netty.channel.socket.nio.NioSocketChannel; +import io.netty.handler.codec.AsciiString; import io.netty.handler.codec.compression.ZlibCodecFactory; import io.netty.handler.codec.compression.ZlibWrapper; -import io.netty.handler.codec.http.HttpHeaders; import io.netty.handler.codec.http2.Http2TestUtil.Http2Runnable; import io.netty.util.NetUtil; import io.netty.util.concurrent.Future; @@ -61,6 +65,9 @@ import org.mockito.MockitoAnnotations; * Test for data decompression in the HTTP/2 codec. */ public class DataCompressionHttp2Test { + private static final AsciiString GET = as("GET"); + private static final AsciiString POST = as("POST"); + private static final AsciiString PATH = as("/some/path"); private List dataCapture; @Mock @@ -73,7 +80,6 @@ public class DataCompressionHttp2Test { private ServerBootstrap sb; private Bootstrap cb; private Channel serverChannel; - private Channel serverConnectedChannel; private Channel clientChannel; private CountDownLatch serverLatch; private CountDownLatch clientLatch; @@ -103,7 +109,6 @@ public class DataCompressionHttp2Test { serverConnection), serverListener, serverLatch, false); p.addLast("reader", serverAdapter); p.addLast(Http2CodecUtil.ignoreSettingsHandler()); - serverConnectedChannel = ch; } }); @@ -149,8 +154,8 @@ public class DataCompressionHttp2Test { @Test public void justHeadersNoData() throws Exception { - final Http2Headers headers = new DefaultHttp2Headers.Builder().method("GET").path("/some/path") - .set(HttpHeaders.Names.CONTENT_ENCODING, HttpHeaders.Values.GZIP).build(); + final Http2Headers headers = + new DefaultHttp2Headers().method(GET).path(PATH).set(CONTENT_ENCODING, GZIP); // Required because the decompressor intercepts the onXXXRead events before // our {@link Http2TestUtil$FrameAdapter} does. Http2TestUtil.FrameAdapter.getOrCreateStream(serverConnection, 3, false); @@ -173,8 +178,8 @@ public class DataCompressionHttp2Test { final EmbeddedChannel encoder = new EmbeddedChannel(ZlibCodecFactory.newZlibEncoder(ZlibWrapper.GZIP)); try { final ByteBuf encodedData = encodeData(data, encoder); - final Http2Headers headers = new DefaultHttp2Headers.Builder().method("POST").path("/some/path") - .set(HttpHeaders.Names.CONTENT_ENCODING, HttpHeaders.Values.GZIP).build(); + final Http2Headers headers = + new DefaultHttp2Headers().method(POST).path(PATH).set(CONTENT_ENCODING.toLowerCase(), GZIP); // Required because the decompressor intercepts the onXXXRead events before // our {@link Http2TestUtil$FrameAdapter} does. Http2TestUtil.FrameAdapter.getOrCreateStream(serverConnection, 3, false); @@ -207,8 +212,8 @@ public class DataCompressionHttp2Test { final EmbeddedChannel encoder = new EmbeddedChannel(ZlibCodecFactory.newZlibEncoder(ZlibWrapper.GZIP)); try { final ByteBuf encodedData = encodeData(data, encoder); - final Http2Headers headers = new DefaultHttp2Headers.Builder().method("POST").path("/some/path") - .set(HttpHeaders.Names.CONTENT_ENCODING, HttpHeaders.Values.GZIP).build(); + final Http2Headers headers = + new DefaultHttp2Headers().method(POST).path(PATH).set(CONTENT_ENCODING.toLowerCase(), GZIP); // Required because the decompressor intercepts the onXXXRead events before // our {@link Http2TestUtil$FrameAdapter} does. Http2TestUtil.FrameAdapter.getOrCreateStream(serverConnection, 3, false); @@ -244,8 +249,8 @@ public class DataCompressionHttp2Test { try { final ByteBuf encodedData1 = encodeData(data1, encoder); final ByteBuf encodedData2 = encodeData(data2, encoder); - final Http2Headers headers = new DefaultHttp2Headers.Builder().method("POST").path("/some/path") - .set(HttpHeaders.Names.CONTENT_ENCODING, HttpHeaders.Values.GZIP).build(); + final Http2Headers headers = + new DefaultHttp2Headers().method(POST).path(PATH).set(CONTENT_ENCODING.toLowerCase(), GZIP); // Required because the decompressor intercepts the onXXXRead events before // our {@link Http2TestUtil$FrameAdapter} does. Http2TestUtil.FrameAdapter.getOrCreateStream(serverConnection, 3, false); @@ -288,8 +293,9 @@ public class DataCompressionHttp2Test { data.writeByte((byte) 'a'); } final ByteBuf encodedData = encodeData(data, encoder); - final Http2Headers headers = new DefaultHttp2Headers.Builder().method("POST").path("/some/path") - .set(HttpHeaders.Names.CONTENT_ENCODING, HttpHeaders.Values.DEFLATE).build(); + final Http2Headers headers = + new DefaultHttp2Headers().method(POST).path(PATH) + .set(CONTENT_ENCODING.toLowerCase(), DEFLATE); // Required because the decompressor intercepts the onXXXRead events before // our {@link Http2TestUtil$FrameAdapter} does. Http2TestUtil.FrameAdapter.getOrCreateStream(serverConnection, 3, false); @@ -362,10 +368,6 @@ public class DataCompressionHttp2Test { serverLatch.await(5, SECONDS); } - private void awaitClient() throws Exception { - clientLatch.await(5, SECONDS); - } - private ChannelHandlerContext ctxClient() { return clientChannel.pipeline().firstContext(); } @@ -373,12 +375,4 @@ public class DataCompressionHttp2Test { private ChannelPromise newPromiseClient() { return ctxClient().newPromise(); } - - private ChannelHandlerContext ctxServer() { - return serverConnectedChannel.pipeline().firstContext(); - } - - private ChannelPromise newPromiseServer() { - return ctxServer().newPromise(); - } } diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/DefaultHttp2FrameIOTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/DefaultHttp2FrameIOTest.java index 0d63e671fb..7fafd46a4b 100644 --- a/codec-http2/src/test/java/io/netty/handler/codec/http2/DefaultHttp2FrameIOTest.java +++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/DefaultHttp2FrameIOTest.java @@ -16,6 +16,8 @@ package io.netty.handler.codec.http2; import static io.netty.handler.codec.http2.Http2CodecUtil.MAX_UNSIGNED_INT; +import static io.netty.handler.codec.http2.Http2TestUtil.as; +import static io.netty.handler.codec.http2.Http2TestUtil.randomString; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -257,7 +259,7 @@ public class DefaultHttp2FrameIOTest { @Test public void emptyHeadersShouldRoundtrip() throws Exception { - Http2Headers headers = Http2Headers.EMPTY_HEADERS; + Http2Headers headers = EmptyHttp2Headers.INSTANCE; writer.writeHeaders(ctx, 1, headers, 0, true, promise); ByteBuf frame = captureWrite(); @@ -271,7 +273,7 @@ public class DefaultHttp2FrameIOTest { @Test public void emptyHeadersWithPaddingShouldRoundtrip() throws Exception { - Http2Headers headers = Http2Headers.EMPTY_HEADERS; + Http2Headers headers = EmptyHttp2Headers.INSTANCE; writer.writeHeaders(ctx, 1, headers, 0xFF, true, promise); ByteBuf frame = captureWrite(); @@ -283,6 +285,19 @@ public class DefaultHttp2FrameIOTest { } } + @Test + public void binaryHeadersWithoutPriorityShouldRoundtrip() throws Exception { + Http2Headers headers = dummyBinaryHeaders(); + writer.writeHeaders(ctx, 1, headers, 0, true, promise); + ByteBuf frame = captureWrite(); + try { + reader.readFrame(ctx, frame, listener); + verify(listener).onHeadersRead(eq(ctx), eq(1), eq(headers), eq(0), eq(true)); + } finally { + frame.release(); + } + } + @Test public void headersWithoutPriorityShouldRoundtrip() throws Exception { Http2Headers headers = dummyHeaders(); @@ -373,7 +388,7 @@ public class DefaultHttp2FrameIOTest { @Test public void emptypushPromiseShouldRoundtrip() throws Exception { - Http2Headers headers = Http2Headers.EMPTY_HEADERS; + Http2Headers headers = EmptyHttp2Headers.INSTANCE; writer.writePushPromise(ctx, 1, 2, headers, 0, promise); ByteBuf frame = captureWrite(); @@ -447,18 +462,27 @@ public class DefaultHttp2FrameIOTest { return alloc.buffer().writeBytes("abcdefgh".getBytes(CharsetUtil.UTF_8)); } + private static Http2Headers dummyBinaryHeaders() { + DefaultHttp2Headers headers = new DefaultHttp2Headers(); + for (int ix = 0; ix < 10; ++ix) { + headers.add(randomString(), randomString()); + } + return headers; + } + private static Http2Headers dummyHeaders() { - return DefaultHttp2Headers.newBuilder().method("GET").scheme("https").authority("example.org") - .path("/some/path").add("accept", "*/*").build(); + return new DefaultHttp2Headers().method(as("GET")).scheme(as("https")) + .authority(as("example.org")).path(as("/some/path")) + .add(as("accept"), as("*/*")); } private static Http2Headers largeHeaders() { - DefaultHttp2Headers.Builder builder = DefaultHttp2Headers.newBuilder(); + DefaultHttp2Headers headers = new DefaultHttp2Headers(); for (int i = 0; i < 100; ++i) { String key = "this-is-a-test-header-key-" + i; String value = "this-is-a-test-header-value-" + i; - builder.add(key, value); + headers.add(as(key), as(value)); } - return builder.build(); + return headers; } } diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/DefaultHttp2HeadersDecoderTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/DefaultHttp2HeadersDecoderTest.java index 7d9ba83070..423082fb15 100644 --- a/codec-http2/src/test/java/io/netty/handler/codec/http2/DefaultHttp2HeadersDecoderTest.java +++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/DefaultHttp2HeadersDecoderTest.java @@ -15,6 +15,8 @@ package io.netty.handler.codec.http2; +import static io.netty.handler.codec.http2.Http2TestUtil.as; +import static io.netty.handler.codec.http2.Http2TestUtil.randomBytes; import static io.netty.util.CharsetUtil.UTF_8; import static org.junit.Assert.assertEquals; import io.netty.buffer.ByteBuf; @@ -41,34 +43,28 @@ public class DefaultHttp2HeadersDecoderTest { @Test public void decodeShouldSucceed() throws Exception { - final ByteBuf buf = encode(":method", "GET", "akey", "avalue"); + ByteBuf buf = encode(b(":method"), b("GET"), b("akey"), b("avalue"), randomBytes(), randomBytes()); try { - Http2Headers headers = decoder.decodeHeaders(buf).build(); - assertEquals(2, headers.size()); - assertEquals("GET", headers.method()); - assertEquals("avalue", headers.get("akey")); + Http2Headers headers = decoder.decodeHeaders(buf); + assertEquals(3, headers.size()); + assertEquals("GET", headers.method().toString()); + assertEquals("avalue", headers.get(as("akey")).toString()); } finally { buf.release(); } } - @Test(expected = Http2Exception.class) - public void decodeWithInvalidPseudoHeaderShouldFail() throws Exception { - final ByteBuf buf = encode(":invalid", "GET", "akey", "avalue"); - try { - decoder.decodeHeaders(buf); - } finally { - buf.release(); - } + private byte[] b(String string) { + return string.getBytes(UTF_8); } - private ByteBuf encode(String... entries) throws Exception { - final Encoder encoder = new Encoder(); - final ByteArrayOutputStream stream = new ByteArrayOutputStream(); + private ByteBuf encode(byte[]... entries) throws Exception { + Encoder encoder = new Encoder(); + ByteArrayOutputStream stream = new ByteArrayOutputStream(); for (int ix = 0; ix < entries.length;) { - String key = entries[ix++]; - String value = entries[ix++]; - encoder.encodeHeader(stream, key.getBytes(UTF_8), value.getBytes(UTF_8), false); + byte[] key = entries[ix++]; + byte[] value = entries[ix++]; + encoder.encodeHeader(stream, key, value, false); } return Unpooled.wrappedBuffer(stream.toByteArray()); } diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/DefaultHttp2HeadersEncoderTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/DefaultHttp2HeadersEncoderTest.java index f257913dad..ea3c92deac 100644 --- a/codec-http2/src/test/java/io/netty/handler/codec/http2/DefaultHttp2HeadersEncoderTest.java +++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/DefaultHttp2HeadersEncoderTest.java @@ -15,6 +15,7 @@ package io.netty.handler.codec.http2; +import static io.netty.handler.codec.http2.Http2TestUtil.as; import static org.junit.Assert.assertTrue; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; @@ -36,9 +37,8 @@ public class DefaultHttp2HeadersEncoderTest { @Test public void encodeShouldSucceed() throws Http2Exception { - DefaultHttp2Headers headers = DefaultHttp2Headers.newBuilder().method("GET").add("a", "1").add("a", "2") - .build(); - final ByteBuf buf = Unpooled.buffer(); + Http2Headers headers = headers(); + ByteBuf buf = Unpooled.buffer(); try { encoder.encodeHeaders(headers, buf); assertTrue(buf.writerIndex() > 0); @@ -49,10 +49,13 @@ public class DefaultHttp2HeadersEncoderTest { @Test(expected = Http2Exception.class) public void headersExceedMaxSetSizeShouldFail() throws Http2Exception { - DefaultHttp2Headers headers = DefaultHttp2Headers.newBuilder().method("GET").add("a", "1").add("a", "2") - .build(); - + Http2Headers headers = headers(); encoder.maxHeaderListSize(2); encoder.encodeHeaders(headers, Unpooled.buffer()); } + + private Http2Headers headers() { + return new DefaultHttp2Headers().method(as("GET")).add(as("a"), as("1")) + .add(as("a"), as("2")); + } } diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/DefaultHttp2HeadersTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/DefaultHttp2HeadersTest.java deleted file mode 100644 index f803295d8c..0000000000 --- a/codec-http2/src/test/java/io/netty/handler/codec/http2/DefaultHttp2HeadersTest.java +++ /dev/null @@ -1,113 +0,0 @@ -/* - * 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: - * - * 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.http2; - -import org.junit.Test; - -import java.util.Arrays; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.NoSuchElementException; -import java.util.Set; - -import static org.junit.Assert.*; - - -/** - * Tests for {@link DefaultHttp2Headers}. - */ -public class DefaultHttp2HeadersTest { - - @Test - public void duplicateKeysShouldStoreAllValues() { - DefaultHttp2Headers headers = - DefaultHttp2Headers.newBuilder().add("a", "1").add("a", "2") - .add("a", "3").build(); - List aValues = headers.getAll("a"); - assertEquals(3, aValues.size()); - assertEquals(3, headers.size()); - assertEquals("1", aValues.get(0)); - assertEquals("2", aValues.get(1)); - assertEquals("3", aValues.get(2)); - } - - @Test - public void setHeaderShouldReplacePrevious() { - DefaultHttp2Headers headers = - DefaultHttp2Headers.newBuilder().add("a", "1").add("a", "2") - .add("a", "3").set("a", "4").build(); - assertEquals(1, headers.size()); - assertEquals("4", headers.get("a")); - } - - @Test - public void setHeadersShouldReplacePrevious() { - DefaultHttp2Headers headers = - DefaultHttp2Headers.newBuilder().add("a", "1").add("a", "2") - .add("a", "3").set("a", Arrays.asList("4", "5")).build(); - assertEquals(2, headers.size()); - List list = headers.getAll("a"); - assertEquals(2, list.size()); - assertEquals("4", list.get(0)); - assertEquals("5", list.get(1)); - } - - @Test(expected = NoSuchElementException.class) - public void iterateEmptyHeadersShouldThrow() { - Iterator> iterator = - DefaultHttp2Headers.newBuilder().build().iterator(); - assertFalse(iterator.hasNext()); - iterator.next(); - } - - @Test - public void iterateHeadersShouldReturnAllValues() { - Set headers = new HashSet(); - headers.add("a:1"); - headers.add("a:2"); - headers.add("a:3"); - headers.add("b:1"); - headers.add("b:2"); - headers.add("c:1"); - - // Build the headers from the input set. - DefaultHttp2Headers.Builder builder = DefaultHttp2Headers.newBuilder(); - for (String header : headers) { - String[] parts = header.split(":"); - builder.add(parts[0], parts[1]); - } - - // Now iterate through the headers, removing them from the original set. - for (Map.Entry entry : builder.build()) { - assertTrue(headers.remove(entry.getKey() + ':' + entry.getValue())); - } - - // Make sure we removed them all. - assertTrue(headers.isEmpty()); - } - - @Test(expected = IllegalArgumentException.class) - public void addInvalidPseudoHeaderShouldFail() { - DefaultHttp2Headers.newBuilder().add(":a", "1"); - } - - @Test(expected = IllegalArgumentException.class) - public void setInvalidPseudoHeaderShouldFail() { - DefaultHttp2Headers.newBuilder().set(":a", "1"); - } -} diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/DelegatingHttp2ConnectionHandlerTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/DelegatingHttp2ConnectionHandlerTest.java index 31e3141361..1dd942f77e 100644 --- a/codec-http2/src/test/java/io/netty/handler/codec/http2/DelegatingHttp2ConnectionHandlerTest.java +++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/DelegatingHttp2ConnectionHandlerTest.java @@ -26,7 +26,6 @@ import static io.netty.handler.codec.http2.Http2Error.INTERNAL_ERROR; import static io.netty.handler.codec.http2.Http2Error.NO_ERROR; import static io.netty.handler.codec.http2.Http2Error.PROTOCOL_ERROR; import static io.netty.handler.codec.http2.Http2Exception.protocolError; -import static io.netty.handler.codec.http2.Http2Headers.EMPTY_HEADERS; import static io.netty.handler.codec.http2.Http2Stream.State.HALF_CLOSED_LOCAL; import static io.netty.handler.codec.http2.Http2Stream.State.OPEN; import static io.netty.handler.codec.http2.Http2Stream.State.RESERVED_LOCAL; @@ -277,7 +276,7 @@ public class DelegatingHttp2ConnectionHandlerTest { @Test public void headersReadAfterGoAwayShouldBeIgnored() throws Exception { when(remote.isGoAwayReceived()).thenReturn(true); - decode().onHeadersRead(ctx, STREAM_ID, EMPTY_HEADERS, 0, false); + decode().onHeadersRead(ctx, STREAM_ID, EmptyHttp2Headers.INSTANCE, 0, false); verify(remote, never()).createStream(eq(STREAM_ID), eq(false)); // Verify that the event was absorbed and not propagated to the oberver. @@ -288,53 +287,54 @@ public class DelegatingHttp2ConnectionHandlerTest { @Test public void headersReadForUnknownStreamShouldCreateStream() throws Exception { when(remote.createStream(eq(5), eq(false))).thenReturn(stream); - decode().onHeadersRead(ctx, 5, EMPTY_HEADERS, 0, false); + decode().onHeadersRead(ctx, 5, EmptyHttp2Headers.INSTANCE, 0, false); verify(remote).createStream(eq(5), eq(false)); - verify(listener).onHeadersRead(eq(ctx), eq(5), eq(EMPTY_HEADERS), eq(0), eq(DEFAULT_PRIORITY_WEIGHT), - eq(false), eq(0), eq(false)); + verify(listener).onHeadersRead(eq(ctx), eq(5), eq(EmptyHttp2Headers.INSTANCE), eq(0), + eq(DEFAULT_PRIORITY_WEIGHT), eq(false), eq(0), eq(false)); } @Test public void headersReadForUnknownStreamShouldCreateHalfClosedStream() throws Exception { when(remote.createStream(eq(5), eq(true))).thenReturn(stream); - decode().onHeadersRead(ctx, 5, EMPTY_HEADERS, 0, true); + decode().onHeadersRead(ctx, 5, EmptyHttp2Headers.INSTANCE, 0, true); verify(remote).createStream(eq(5), eq(true)); - verify(listener).onHeadersRead(eq(ctx), eq(5), eq(EMPTY_HEADERS), eq(0), eq(DEFAULT_PRIORITY_WEIGHT), - eq(false), eq(0), eq(true)); + verify(listener).onHeadersRead(eq(ctx), eq(5), eq(EmptyHttp2Headers.INSTANCE), eq(0), + eq(DEFAULT_PRIORITY_WEIGHT), eq(false), eq(0), eq(true)); } @Test public void headersReadForPromisedStreamShouldHalfOpenStream() throws Exception { when(stream.state()).thenReturn(RESERVED_REMOTE); - decode().onHeadersRead(ctx, STREAM_ID, EMPTY_HEADERS, 0, false); + decode().onHeadersRead(ctx, STREAM_ID, EmptyHttp2Headers.INSTANCE, 0, false); verify(stream).openForPush(); - verify(listener).onHeadersRead(eq(ctx), eq(STREAM_ID), eq(EMPTY_HEADERS), eq(0), eq(DEFAULT_PRIORITY_WEIGHT), - eq(false), eq(0), eq(false)); + verify(listener).onHeadersRead(eq(ctx), eq(STREAM_ID), eq(EmptyHttp2Headers.INSTANCE), eq(0), + eq(DEFAULT_PRIORITY_WEIGHT), eq(false), eq(0), eq(false)); } @Test public void headersReadForPromisedStreamShouldCloseStream() throws Exception { when(stream.state()).thenReturn(RESERVED_REMOTE); - decode().onHeadersRead(ctx, STREAM_ID, EMPTY_HEADERS, 0, true); + decode().onHeadersRead(ctx, STREAM_ID, EmptyHttp2Headers.INSTANCE, 0, true); verify(stream).openForPush(); verify(stream).close(); - verify(listener).onHeadersRead(eq(ctx), eq(STREAM_ID), eq(EMPTY_HEADERS), eq(0), eq(DEFAULT_PRIORITY_WEIGHT), - eq(false), eq(0), eq(true)); + verify(listener).onHeadersRead(eq(ctx), eq(STREAM_ID), eq(EmptyHttp2Headers.INSTANCE), eq(0), + eq(DEFAULT_PRIORITY_WEIGHT), eq(false), eq(0), eq(true)); } @Test public void pushPromiseReadAfterGoAwayShouldBeIgnored() throws Exception { when(remote.isGoAwayReceived()).thenReturn(true); - decode().onPushPromiseRead(ctx, STREAM_ID, PUSH_STREAM_ID, EMPTY_HEADERS, 0); + decode().onPushPromiseRead(ctx, STREAM_ID, PUSH_STREAM_ID, EmptyHttp2Headers.INSTANCE, 0); verify(remote, never()).reservePushStream(anyInt(), any(Http2Stream.class)); verify(listener, never()).onPushPromiseRead(eq(ctx), anyInt(), anyInt(), any(Http2Headers.class), anyInt()); } @Test public void pushPromiseReadShouldSucceed() throws Exception { - decode().onPushPromiseRead(ctx, STREAM_ID, PUSH_STREAM_ID, EMPTY_HEADERS, 0); + decode().onPushPromiseRead(ctx, STREAM_ID, PUSH_STREAM_ID, EmptyHttp2Headers.INSTANCE, 0); verify(remote).reservePushStream(eq(PUSH_STREAM_ID), eq(stream)); - verify(listener).onPushPromiseRead(eq(ctx), eq(STREAM_ID), eq(PUSH_STREAM_ID), eq(EMPTY_HEADERS), eq(0)); + verify(listener).onPushPromiseRead(eq(ctx), eq(STREAM_ID), eq(PUSH_STREAM_ID), + eq(EmptyHttp2Headers.INSTANCE), eq(0)); } @Test @@ -528,7 +528,8 @@ public class DelegatingHttp2ConnectionHandlerTest { @Test public void headersWriteAfterGoAwayShouldFail() throws Exception { when(connection.isGoAway()).thenReturn(true); - ChannelFuture future = handler.writeHeaders(ctx, 5, EMPTY_HEADERS, 0, (short) 255, false, 0, false, promise); + ChannelFuture future = handler.writeHeaders( + ctx, 5, EmptyHttp2Headers.INSTANCE, 0, (short) 255, false, 0, false, promise); verify(local, never()).createStream(anyInt(), anyBoolean()); verify(writer, never()).writeHeaders(eq(ctx), anyInt(), any(Http2Headers.class), anyInt(), anyBoolean(), eq(promise)); @@ -538,54 +539,56 @@ public class DelegatingHttp2ConnectionHandlerTest { @Test public void headersWriteForUnknownStreamShouldCreateStream() throws Exception { when(local.createStream(eq(5), eq(false))).thenReturn(stream); - handler.writeHeaders(ctx, 5, EMPTY_HEADERS, 0, false, promise); + handler.writeHeaders(ctx, 5, EmptyHttp2Headers.INSTANCE, 0, false, promise); verify(local).createStream(eq(5), eq(false)); - verify(writer).writeHeaders(eq(ctx), eq(5), eq(EMPTY_HEADERS), eq(0), eq(DEFAULT_PRIORITY_WEIGHT), eq(false), - eq(0), eq(false), eq(promise)); + verify(writer).writeHeaders(eq(ctx), eq(5), eq(EmptyHttp2Headers.INSTANCE), eq(0), + eq(DEFAULT_PRIORITY_WEIGHT), eq(false), eq(0), eq(false), eq(promise)); } @Test public void headersWriteShouldCreateHalfClosedStream() throws Exception { when(local.createStream(eq(5), eq(true))).thenReturn(stream); - handler.writeHeaders(ctx, 5, EMPTY_HEADERS, 0, true, promise); + handler.writeHeaders(ctx, 5, EmptyHttp2Headers.INSTANCE, 0, true, promise); verify(local).createStream(eq(5), eq(true)); - verify(writer).writeHeaders(eq(ctx), eq(5), eq(EMPTY_HEADERS), eq(0), eq(DEFAULT_PRIORITY_WEIGHT), eq(false), - eq(0), eq(true), eq(promise)); + verify(writer).writeHeaders(eq(ctx), eq(5), eq(EmptyHttp2Headers.INSTANCE), eq(0), + eq(DEFAULT_PRIORITY_WEIGHT), eq(false), eq(0), eq(true), eq(promise)); } @Test public void headersWriteShouldOpenStreamForPush() throws Exception { when(stream.state()).thenReturn(RESERVED_LOCAL); - handler.writeHeaders(ctx, STREAM_ID, EMPTY_HEADERS, 0, false, promise); + handler.writeHeaders(ctx, STREAM_ID, EmptyHttp2Headers.INSTANCE, 0, false, promise); verify(stream).openForPush(); verify(stream, never()).closeLocalSide(); - verify(writer).writeHeaders(eq(ctx), eq(STREAM_ID), eq(EMPTY_HEADERS), eq(0), eq(DEFAULT_PRIORITY_WEIGHT), - eq(false), eq(0), eq(false), eq(promise)); + verify(writer).writeHeaders(eq(ctx), eq(STREAM_ID), eq(EmptyHttp2Headers.INSTANCE), eq(0), + eq(DEFAULT_PRIORITY_WEIGHT), eq(false), eq(0), eq(false), eq(promise)); } @Test public void headersWriteShouldClosePushStream() throws Exception { when(stream.state()).thenReturn(RESERVED_LOCAL).thenReturn(HALF_CLOSED_LOCAL); - handler.writeHeaders(ctx, STREAM_ID, EMPTY_HEADERS, 0, true, promise); + handler.writeHeaders(ctx, STREAM_ID, EmptyHttp2Headers.INSTANCE, 0, true, promise); verify(stream).openForPush(); verify(stream).closeLocalSide(); - verify(writer).writeHeaders(eq(ctx), eq(STREAM_ID), eq(EMPTY_HEADERS), eq(0), eq(DEFAULT_PRIORITY_WEIGHT), - eq(false), eq(0), eq(true), eq(promise)); + verify(writer).writeHeaders(eq(ctx), eq(STREAM_ID), eq(EmptyHttp2Headers.INSTANCE), eq(0), + eq(DEFAULT_PRIORITY_WEIGHT), eq(false), eq(0), eq(true), eq(promise)); } @Test public void pushPromiseWriteAfterGoAwayShouldFail() throws Exception { when(connection.isGoAway()).thenReturn(true); - ChannelFuture future = handler.writePushPromise(ctx, STREAM_ID, PUSH_STREAM_ID, EMPTY_HEADERS, 0, promise); + ChannelFuture future = + handler.writePushPromise(ctx, STREAM_ID, PUSH_STREAM_ID, + EmptyHttp2Headers.INSTANCE, 0, promise); assertTrue(future.awaitUninterruptibly().cause() instanceof Http2Exception); } @Test public void pushPromiseWriteShouldReserveStream() throws Exception { - handler.writePushPromise(ctx, STREAM_ID, PUSH_STREAM_ID, EMPTY_HEADERS, 0, promise); + handler.writePushPromise(ctx, STREAM_ID, PUSH_STREAM_ID, EmptyHttp2Headers.INSTANCE, 0, promise); verify(local).reservePushStream(eq(PUSH_STREAM_ID), eq(stream)); - verify(writer).writePushPromise(eq(ctx), eq(STREAM_ID), eq(PUSH_STREAM_ID), eq(EMPTY_HEADERS), eq(0), - eq(promise)); + verify(writer).writePushPromise(eq(ctx), eq(STREAM_ID), eq(PUSH_STREAM_ID), + eq(EmptyHttp2Headers.INSTANCE), eq(0), eq(promise)); } @Test diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/DelegatingHttp2HttpConnectionHandlerTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/DelegatingHttp2HttpConnectionHandlerTest.java index 4dc25a31e3..4eb92e6cda 100644 --- a/codec-http2/src/test/java/io/netty/handler/codec/http2/DelegatingHttp2HttpConnectionHandlerTest.java +++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/DelegatingHttp2HttpConnectionHandlerTest.java @@ -18,6 +18,7 @@ import static io.netty.handler.codec.http.HttpMethod.GET; import static io.netty.handler.codec.http.HttpMethod.POST; import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1; import static io.netty.handler.codec.http2.Http2CodecUtil.ignoreSettingsHandler; +import static io.netty.handler.codec.http2.Http2TestUtil.as; import static io.netty.util.CharsetUtil.UTF_8; import static java.util.concurrent.TimeUnit.SECONDS; import static org.junit.Assert.assertEquals; @@ -144,16 +145,19 @@ public class DelegatingHttp2HttpConnectionHandlerTest { final FullHttpRequest request = new DefaultFullHttpRequest(HTTP_1_1, GET, "/example"); try { final HttpHeaders httpHeaders = request.headers(); - httpHeaders.set(HttpUtil.ExtensionHeaders.Names.STREAM_ID, 5); - httpHeaders.set(HttpHeaders.Names.HOST, "http://my-user_name@www.example.org:5555/example"); - httpHeaders.set(HttpUtil.ExtensionHeaders.Names.AUTHORITY, "www.example.org:5555"); - httpHeaders.set(HttpUtil.ExtensionHeaders.Names.SCHEME, "http"); + httpHeaders.set(HttpUtil.ExtensionHeaderNames.STREAM_ID.text(), 5); + httpHeaders.set(HttpHeaders.Names.HOST, + "http://my-user_name@www.example.org:5555/example"); + httpHeaders.set(HttpUtil.ExtensionHeaderNames.AUTHORITY.text(), "www.example.org:5555"); + httpHeaders.set(HttpUtil.ExtensionHeaderNames.SCHEME.text(), "http"); httpHeaders.add("foo", "goo"); httpHeaders.add("foo", "goo2"); httpHeaders.add("foo2", "goo2"); - final Http2Headers http2Headers = new DefaultHttp2Headers.Builder().method("GET").path("/example") - .authority("www.example.org:5555").scheme("http").add("foo", "goo").add("foo", "goo2") - .add("foo2", "goo2").build(); + final Http2Headers http2Headers = + new DefaultHttp2Headers().method(as("GET")).path(as("/example")) + .authority(as("www.example.org:5555")).scheme(as("http")) + .add(as("foo"), as("goo")).add(as("foo"), as("goo2")) + .add(as("foo2"), as("goo2")); ChannelPromise writePromise = newPromise(); ChannelFuture writeFuture = clientChannel.writeAndFlush(request, writePromise); @@ -162,11 +166,10 @@ public class DelegatingHttp2HttpConnectionHandlerTest { writeFuture.awaitUninterruptibly(2, SECONDS); assertTrue(writeFuture.isSuccess()); awaitRequests(); - final ArgumentCaptor dataCaptor = ArgumentCaptor.forClass(ByteBuf.class); - verify(serverListener).onHeadersRead(any(ChannelHandlerContext.class), eq(5), eq(http2Headers), eq(0), - anyShort(), anyBoolean(), eq(0), eq(true)); + verify(serverListener).onHeadersRead(any(ChannelHandlerContext.class), eq(5), + eq(http2Headers), eq(0), anyShort(), anyBoolean(), eq(0), eq(true)); verify(serverListener, never()).onDataRead(any(ChannelHandlerContext.class), anyInt(), - dataCaptor.capture(), anyInt(), anyBoolean()); + any(ByteBuf.class), anyInt(), anyBoolean()); } finally { request.release(); } @@ -184,9 +187,11 @@ public class DelegatingHttp2HttpConnectionHandlerTest { httpHeaders.add("foo", "goo"); httpHeaders.add("foo", "goo2"); httpHeaders.add("foo2", "goo2"); - final Http2Headers http2Headers = new DefaultHttp2Headers.Builder().method("POST").path("/example") - .authority("www.example.org:5555").scheme("http").add("foo", "goo").add("foo", "goo2") - .add("foo2", "goo2").build(); + final Http2Headers http2Headers = + new DefaultHttp2Headers().method(as("POST")).path(as("/example")) + .authority(as("www.example.org:5555")).scheme(as("http")) + .add(as("foo"), as("goo")).add(as("foo"), as("goo2")) + .add(as("foo2"), as("goo2")); ChannelPromise writePromise = newPromise(); ChannelFuture writeFuture = clientChannel.writeAndFlush(request, writePromise); diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2ConnectionRoundtripTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2ConnectionRoundtripTest.java index 9ac1f5bd8c..dcdaf5c827 100644 --- a/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2ConnectionRoundtripTest.java +++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2ConnectionRoundtripTest.java @@ -15,6 +15,8 @@ package io.netty.handler.codec.http2; +import static io.netty.handler.codec.http2.Http2TestUtil.as; +import static io.netty.handler.codec.http2.Http2TestUtil.randomString; import static io.netty.handler.codec.http2.Http2TestUtil.runInChannel; import static io.netty.util.CharsetUtil.UTF_8; import static java.util.concurrent.TimeUnit.SECONDS; @@ -133,8 +135,7 @@ public class Http2ConnectionRoundtripTest { @Test public void flowControlProperlyChunksLargeMessage() throws Exception { - final Http2Headers headers = new DefaultHttp2Headers.Builder().method("GET").scheme("https") - .authority("example.org").path("/some/path/resource2").build(); + final Http2Headers headers = dummyHeaders(); // Create a large message to send. final int length = 10485760; // 10MB @@ -179,8 +180,7 @@ public class Http2ConnectionRoundtripTest { @Test public void stressTest() throws Exception { - final Http2Headers headers = new DefaultHttp2Headers.Builder().method("GET").scheme("https") - .authority("example.org").path("/some/path/resource2").build(); + final Http2Headers headers = dummyHeaders(); final String text = "hello world"; final String pingMsg = "12345678"; final ByteBuf data = Unpooled.copiedBuffer(text.getBytes()); @@ -257,4 +257,9 @@ public class Http2ConnectionRoundtripTest { } } } + + private Http2Headers dummyHeaders() { + return new DefaultHttp2Headers().method(as("GET")).scheme(as("https")) + .authority(as("example.org")).path(as("/some/path/resource2")).add(randomString(), randomString()); + } } diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2FrameRoundtripTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2FrameRoundtripTest.java index 7c5c235863..d20c938e9d 100644 --- a/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2FrameRoundtripTest.java +++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2FrameRoundtripTest.java @@ -15,16 +15,18 @@ package io.netty.handler.codec.http2; +import static io.netty.handler.codec.http2.Http2TestUtil.as; +import static io.netty.handler.codec.http2.Http2TestUtil.randomString; import static io.netty.handler.codec.http2.Http2TestUtil.runInChannel; import static io.netty.util.CharsetUtil.UTF_8; import static java.util.concurrent.TimeUnit.SECONDS; -import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyInt; import static org.mockito.Matchers.eq; -import static org.mockito.Mockito.verify; import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; import io.netty.bootstrap.Bootstrap; import io.netty.bootstrap.ServerBootstrap; import io.netty.buffer.ByteBuf; @@ -153,9 +155,7 @@ public class Http2FrameRoundtripTest { @Test public void headersFrameWithoutPriorityShouldMatch() throws Exception { - final Http2Headers headers = - new DefaultHttp2Headers.Builder().method("GET").scheme("https") - .authority("example.org").path("/some/path/resource2").build(); + final Http2Headers headers = headers(); runInChannel(clientChannel, new Http2Runnable() { @Override public void run() { @@ -170,9 +170,7 @@ public class Http2FrameRoundtripTest { @Test public void headersFrameWithPriorityShouldMatch() throws Exception { - final Http2Headers headers = - new DefaultHttp2Headers.Builder().method("GET").scheme("https") - .authority("example.org").path("/some/path/resource2").build(); + final Http2Headers headers = headers(); runInChannel(clientChannel, new Http2Runnable() { @Override public void run() { @@ -244,9 +242,7 @@ public class Http2FrameRoundtripTest { @Test public void pushPromiseFrameShouldMatch() throws Exception { - final Http2Headers headers = - new DefaultHttp2Headers.Builder().method("GET").scheme("https") - .authority("example.org").path("/some/path/resource2").build(); + final Http2Headers headers = headers(); runInChannel(clientChannel, new Http2Runnable() { @Override public void run() { @@ -306,9 +302,7 @@ public class Http2FrameRoundtripTest { @Test public void stressTest() throws Exception { - final Http2Headers headers = - new DefaultHttp2Headers.Builder().method("GET").scheme("https") - .authority("example.org").path("/some/path/resource2").build(); + final Http2Headers headers = headers(); final String text = "hello world"; final ByteBuf data = Unpooled.copiedBuffer(text.getBytes()); try { @@ -359,4 +353,9 @@ public class Http2FrameRoundtripTest { private ChannelPromise newPromise() { return ctx().newPromise(); } + + private Http2Headers headers() { + return new DefaultHttp2Headers().method(as("GET")).scheme(as("https")) + .authority(as("example.org")).path(as("/some/path/resource2")).add(randomString(), randomString()); + } } diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2HeaderBlockIOTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2HeaderBlockIOTest.java index ab80fc641b..be5d786cfb 100644 --- a/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2HeaderBlockIOTest.java +++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2HeaderBlockIOTest.java @@ -15,6 +15,8 @@ package io.netty.handler.codec.http2; +import static io.netty.handler.codec.http2.Http2TestUtil.as; +import static io.netty.handler.codec.http2.Http2TestUtil.randomString; import static org.junit.Assert.assertEquals; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; @@ -46,47 +48,37 @@ public class Http2HeaderBlockIOTest { @Test public void roundtripShouldBeSuccessful() throws Http2Exception { - Http2Headers in = - new DefaultHttp2Headers.Builder().method("GET").scheme("https") - .authority("example.org").path("/some/path/resource2") - .add("accept", "image/png").add("cache-control", "no-cache") - .add("custom", "value1").add("custom", "value2") - .add("custom", "value3").add("custom", "custom4").build(); + Http2Headers in = headers(); assertRoundtripSuccessful(in); } @Test public void successiveCallsShouldSucceed() throws Http2Exception { Http2Headers in = - new DefaultHttp2Headers.Builder().method("GET").scheme("https") - .authority("example.org").path("/some/path") - .add("accept", "*/*").build(); + new DefaultHttp2Headers().method(as("GET")).scheme(as("https")) + .authority(as("example.org")).path(as("/some/path")) + .add(as("accept"), as("*/*")); assertRoundtripSuccessful(in); in = - new DefaultHttp2Headers.Builder().method("GET").scheme("https") - .authority("example.org").path("/some/path/resource1") - .add("accept", "image/jpeg").add("cache-control", "no-cache") - .build(); + new DefaultHttp2Headers().method(as("GET")).scheme(as("https")) + .authority(as("example.org")).path(as("/some/path/resource1")) + .add(as("accept"), as("image/jpeg")) + .add(as("cache-control"), as("no-cache")); assertRoundtripSuccessful(in); in = - new DefaultHttp2Headers.Builder().method("GET").scheme("https") - .authority("example.org").path("/some/path/resource2") - .add("accept", "image/png").add("cache-control", "no-cache") - .build(); + new DefaultHttp2Headers().method(as("GET")).scheme(as("https")) + .authority(as("example.org")).path(as("/some/path/resource2")) + .add(as("accept"), as("image/png")) + .add(as("cache-control"), as("no-cache")); assertRoundtripSuccessful(in); } @Test public void setMaxHeaderSizeShouldBeSuccessful() throws Http2Exception { encoder.maxHeaderTableSize(10); - Http2Headers in = - new DefaultHttp2Headers.Builder().method("GET").scheme("https") - .authority("example.org").path("/some/path/resource2") - .add("accept", "image/png").add("cache-control", "no-cache") - .add("custom", "value1").add("custom", "value2") - .add("custom", "value3").add("custom", "custom4").build(); + Http2Headers in = headers(); assertRoundtripSuccessful(in); assertEquals(10, decoder.maxHeaderTableSize()); } @@ -94,7 +86,16 @@ public class Http2HeaderBlockIOTest { private void assertRoundtripSuccessful(Http2Headers in) throws Http2Exception { encoder.encodeHeaders(in, buffer); - Http2Headers out = decoder.decodeHeaders(buffer).build(); + Http2Headers out = decoder.decodeHeaders(buffer); assertEquals(in, out); } + + private Http2Headers headers() { + return new DefaultHttp2Headers().method(as("GET")).scheme(as("https")) + .authority(as("example.org")).path(as("/some/path/resource2")) + .add(as("accept"), as("image/png")).add(as("cache-control"), as("no-cache")) + .add(as("custom"), as("value1")).add(as("custom"), as("value2")) + .add(as("custom"), as("value3")).add(as("custom"), as("custom4")) + .add(randomString(), randomString()); + } } diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2TestUtil.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2TestUtil.java index 1d2f530fff..18c4258647 100644 --- a/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2TestUtil.java +++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/Http2TestUtil.java @@ -17,9 +17,11 @@ package io.netty.handler.codec.http2; import io.netty.buffer.ByteBuf; import io.netty.channel.Channel; import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.AsciiString; import io.netty.handler.codec.ByteToMessageDecoder; import java.util.List; +import java.util.Random; import java.util.concurrent.CountDownLatch; /** @@ -49,7 +51,38 @@ final class Http2TestUtil { }); } - private Http2TestUtil() { } + /** + * Converts a {@link String} into an {@link AsciiString}. + */ + public static AsciiString as(String value) { + return new AsciiString(value); + } + + /** + * Converts a byte array into an {@link AsciiString}. + */ + public static AsciiString as(byte[] value) { + return new AsciiString(value); + } + + /** + * Returns a byte array filled with random data. + */ + public static byte[] randomBytes() { + byte[] data = new byte[100]; + new Random().nextBytes(data); + return data; + } + + /** + * Returns an {@link AsciiString} that wraps a randomly-filled byte array. + */ + public static AsciiString randomString() { + return as(randomBytes()); + } + + private Http2TestUtil() { + } static class FrameAdapter extends ByteToMessageDecoder { private final boolean copyBufs; diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/InboundHttp2ToHttpAdapterTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/InboundHttp2ToHttpAdapterTest.java index f58d3ab348..1d7e271731 100644 --- a/codec-http2/src/test/java/io/netty/handler/codec/http2/InboundHttp2ToHttpAdapterTest.java +++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/InboundHttp2ToHttpAdapterTest.java @@ -14,6 +14,7 @@ */ package io.netty.handler.codec.http2; +import static io.netty.handler.codec.http2.Http2TestUtil.as; import static io.netty.handler.codec.http2.Http2TestUtil.runInChannel; import static java.util.concurrent.TimeUnit.SECONDS; import static org.junit.Assert.assertEquals; @@ -166,12 +167,14 @@ public class InboundHttp2ToHttpAdapterTest { "/some/path/resource2", true); try { HttpHeaders httpHeaders = request.headers(); - httpHeaders.set(HttpUtil.ExtensionHeaders.Names.SCHEME, "https"); - httpHeaders.set(HttpUtil.ExtensionHeaders.Names.AUTHORITY, "example.org"); - httpHeaders.set(HttpUtil.ExtensionHeaders.Names.STREAM_ID, 3); + httpHeaders.set(HttpUtil.ExtensionHeaderNames.SCHEME.text(), "https"); + httpHeaders.set(HttpUtil.ExtensionHeaderNames.AUTHORITY.text(), "example.org"); + httpHeaders.set(HttpUtil.ExtensionHeaderNames.STREAM_ID.text(), 3); httpHeaders.set(HttpHeaders.Names.CONTENT_LENGTH, 0); - final Http2Headers http2Headers = new DefaultHttp2Headers.Builder().method("GET").scheme("https") - .authority("example.org").path("/some/path/resource2").build(); + final Http2Headers http2Headers = + new DefaultHttp2Headers().method(as("GET")).scheme(as("https")) + .authority(as("example.org")) + .path(as("/some/path/resource2")); runInChannel(clientChannel, new Http2Runnable() { @Override public void run() { @@ -197,10 +200,10 @@ public class InboundHttp2ToHttpAdapterTest { "/some/path/resource2", content, true); try { HttpHeaders httpHeaders = request.headers(); - httpHeaders.set(HttpUtil.ExtensionHeaders.Names.STREAM_ID, 3); + httpHeaders.set(HttpUtil.ExtensionHeaderNames.STREAM_ID.text(), 3); httpHeaders.set(HttpHeaders.Names.CONTENT_LENGTH, text.length()); - final Http2Headers http2Headers = new DefaultHttp2Headers.Builder().method("GET") - .path("/some/path/resource2").build(); + final Http2Headers http2Headers = new DefaultHttp2Headers().method(as("GET")) + .path(as("/some/path/resource2")); runInChannel(clientChannel, new Http2Runnable() { @Override public void run() { @@ -227,10 +230,10 @@ public class InboundHttp2ToHttpAdapterTest { "/some/path/resource2", content, true); try { HttpHeaders httpHeaders = request.headers(); - httpHeaders.set(HttpUtil.ExtensionHeaders.Names.STREAM_ID, 3); + httpHeaders.set(HttpUtil.ExtensionHeaderNames.STREAM_ID.text(), 3); httpHeaders.set(HttpHeaders.Names.CONTENT_LENGTH, text.length()); - final Http2Headers http2Headers = new DefaultHttp2Headers.Builder().method("GET") - .path("/some/path/resource2").build(); + final Http2Headers http2Headers = new DefaultHttp2Headers().method(as("GET")) + .path(as("/some/path/resource2")); final int midPoint = text.length() / 2; runInChannel(clientChannel, new Http2Runnable() { @Override @@ -261,10 +264,10 @@ public class InboundHttp2ToHttpAdapterTest { "/some/path/resource2", content, true); try { HttpHeaders httpHeaders = request.headers(); - httpHeaders.set(HttpUtil.ExtensionHeaders.Names.STREAM_ID, 3); + httpHeaders.set(HttpUtil.ExtensionHeaderNames.STREAM_ID.text(), 3); httpHeaders.set(HttpHeaders.Names.CONTENT_LENGTH, text.length()); - final Http2Headers http2Headers = new DefaultHttp2Headers.Builder().method("GET") - .path("/some/path/resource2").build(); + final Http2Headers http2Headers = new DefaultHttp2Headers().method(as("GET")) + .path(as("/some/path/resource2")); runInChannel(clientChannel, new Http2Runnable() { @Override public void run() { @@ -295,16 +298,19 @@ public class InboundHttp2ToHttpAdapterTest { "/some/path/resource2", content, true); try { HttpHeaders httpHeaders = request.headers(); - httpHeaders.set(HttpUtil.ExtensionHeaders.Names.STREAM_ID, 3); + httpHeaders.set(HttpUtil.ExtensionHeaderNames.STREAM_ID.text(), 3); httpHeaders.set(HttpHeaders.Names.CONTENT_LENGTH, text.length()); HttpHeaders trailingHeaders = request.trailingHeaders(); trailingHeaders.set("FoO", "goo"); trailingHeaders.set("foO2", "goo2"); trailingHeaders.add("fOo2", "goo3"); - final Http2Headers http2Headers = new DefaultHttp2Headers.Builder().method("GET") - .path("/some/path/resource2").build(); - final Http2Headers http2Headers2 = new DefaultHttp2Headers.Builder().set("foo", "goo").set("foo2", "goo2") - .add("foo2", "goo3").build(); + final Http2Headers http2Headers = + new DefaultHttp2Headers().method(as("GET")).path( + as("/some/path/resource2")); + final Http2Headers http2Headers2 = + new DefaultHttp2Headers().set(as("foo"), as("goo")) + .set(as("foo2"), as("goo2")) + .add(as("foo2"), as("goo3")); runInChannel(clientChannel, new Http2Runnable() { @Override public void run() { @@ -332,16 +338,19 @@ public class InboundHttp2ToHttpAdapterTest { "/some/path/resource2", content, true); try { HttpHeaders httpHeaders = request.headers(); - httpHeaders.set(HttpUtil.ExtensionHeaders.Names.STREAM_ID, 3); + httpHeaders.set(HttpUtil.ExtensionHeaderNames.STREAM_ID.text(), 3); httpHeaders.set(HttpHeaders.Names.CONTENT_LENGTH, text.length()); HttpHeaders trailingHeaders = request.trailingHeaders(); trailingHeaders.set("Foo", "goo"); trailingHeaders.set("fOo2", "goo2"); trailingHeaders.add("foO2", "goo3"); - final Http2Headers http2Headers = new DefaultHttp2Headers.Builder().method("GET") - .path("/some/path/resource2").build(); - final Http2Headers http2Headers2 = new DefaultHttp2Headers.Builder().set("foo", "goo").set("foo2", "goo2") - .add("foo2", "goo3").build(); + final Http2Headers http2Headers = + new DefaultHttp2Headers().method(as("GET")).path( + as("/some/path/resource2")); + final Http2Headers http2Headers2 = + new DefaultHttp2Headers().set(as("foo"), as("goo")) + .set(as("foo2"), as("goo2")) + .add(as("foo2"), as("goo3")); runInChannel(clientChannel, new Http2Runnable() { @Override public void run() { @@ -374,17 +383,17 @@ public class InboundHttp2ToHttpAdapterTest { "/some/path/resource2", content2, true); try { HttpHeaders httpHeaders = request.headers(); - httpHeaders.set(HttpUtil.ExtensionHeaders.Names.STREAM_ID, 3); + httpHeaders.set(HttpUtil.ExtensionHeaderNames.STREAM_ID.text(), 3); httpHeaders.set(HttpHeaders.Names.CONTENT_LENGTH, text.length()); HttpHeaders httpHeaders2 = request2.headers(); - httpHeaders2.set(HttpUtil.ExtensionHeaders.Names.STREAM_ID, 5); - httpHeaders2.set(HttpUtil.ExtensionHeaders.Names.STREAM_DEPENDENCY_ID, 3); - httpHeaders2.set(HttpUtil.ExtensionHeaders.Names.STREAM_WEIGHT, 123); + httpHeaders2.set(HttpUtil.ExtensionHeaderNames.STREAM_ID.text(), 5); + httpHeaders2.set(HttpUtil.ExtensionHeaderNames.STREAM_DEPENDENCY_ID.text(), 3); + httpHeaders2.set(HttpUtil.ExtensionHeaderNames.STREAM_WEIGHT.text(), 123); httpHeaders2.set(HttpHeaders.Names.CONTENT_LENGTH, text2.length()); - final Http2Headers http2Headers = new DefaultHttp2Headers.Builder().method("PUT") - .path("/some/path/resource").build(); - final Http2Headers http2Headers2 = new DefaultHttp2Headers.Builder().method("PUT") - .path("/some/path/resource2").build(); + final Http2Headers http2Headers = new DefaultHttp2Headers().method(as("PUT")) + .path(as("/some/path/resource")); + final Http2Headers http2Headers2 = new DefaultHttp2Headers().method(as("PUT")) + .path(as("/some/path/resource2")); runInChannel(clientChannel, new Http2Runnable() { @Override public void run() { @@ -423,19 +432,19 @@ public class InboundHttp2ToHttpAdapterTest { HttpUtil.OUT_OF_MESSAGE_SEQUENCE_METHOD, HttpUtil.OUT_OF_MESSAGE_SEQUENCE_PATH, true); try { HttpHeaders httpHeaders = request.headers(); - httpHeaders.set(HttpUtil.ExtensionHeaders.Names.STREAM_ID, 3); + httpHeaders.set(HttpUtil.ExtensionHeaderNames.STREAM_ID.text(), 3); httpHeaders.set(HttpHeaders.Names.CONTENT_LENGTH, text.length()); HttpHeaders httpHeaders2 = request2.headers(); - httpHeaders2.set(HttpUtil.ExtensionHeaders.Names.STREAM_ID, 5); + httpHeaders2.set(HttpUtil.ExtensionHeaderNames.STREAM_ID.text(), 5); httpHeaders2.set(HttpHeaders.Names.CONTENT_LENGTH, text2.length()); - final Http2Headers http2Headers = new DefaultHttp2Headers.Builder().method("PUT") - .path("/some/path/resource").build(); - final Http2Headers http2Headers2 = new DefaultHttp2Headers.Builder().method("PUT") - .path("/some/path/resource2").build(); + final Http2Headers http2Headers = new DefaultHttp2Headers().method(as("PUT")) + .path(as("/some/path/resource")); + final Http2Headers http2Headers2 = new DefaultHttp2Headers().method(as("PUT")) + .path(as("/some/path/resource2")); HttpHeaders httpHeaders3 = request3.headers(); - httpHeaders3.set(HttpUtil.ExtensionHeaders.Names.STREAM_ID, 5); - httpHeaders3.set(HttpUtil.ExtensionHeaders.Names.STREAM_DEPENDENCY_ID, 3); - httpHeaders3.set(HttpUtil.ExtensionHeaders.Names.STREAM_WEIGHT, 222); + httpHeaders3.set(HttpUtil.ExtensionHeaderNames.STREAM_ID.text(), 5); + httpHeaders3.set(HttpUtil.ExtensionHeaderNames.STREAM_DEPENDENCY_ID.text(), 3); + httpHeaders3.set(HttpUtil.ExtensionHeaderNames.STREAM_WEIGHT.text(), 222); httpHeaders3.set(HttpHeaders.Names.CONTENT_LENGTH, 0); runInChannel(clientChannel, new Http2Runnable() { @Override @@ -477,20 +486,20 @@ public class InboundHttp2ToHttpAdapterTest { HttpMethod.GET, "/push/test", true); try { HttpHeaders httpHeaders = response.headers(); - httpHeaders.set(HttpUtil.ExtensionHeaders.Names.STREAM_ID, 3); + httpHeaders.set(HttpUtil.ExtensionHeaderNames.STREAM_ID.text(), 3); httpHeaders.set(HttpHeaders.Names.CONTENT_LENGTH, text.length()); HttpHeaders httpHeaders2 = response2.headers(); - httpHeaders2.set(HttpUtil.ExtensionHeaders.Names.SCHEME, "https"); - httpHeaders2.set(HttpUtil.ExtensionHeaders.Names.AUTHORITY, "example.org"); - httpHeaders2.set(HttpUtil.ExtensionHeaders.Names.STREAM_ID, 5); - httpHeaders2.set(HttpUtil.ExtensionHeaders.Names.STREAM_PROMISE_ID, 3); + httpHeaders2.set(HttpUtil.ExtensionHeaderNames.SCHEME.text(), "https"); + httpHeaders2.set(HttpUtil.ExtensionHeaderNames.AUTHORITY.text(), "example.org"); + httpHeaders2.set(HttpUtil.ExtensionHeaderNames.STREAM_ID.text(), 5); + httpHeaders2.set(HttpUtil.ExtensionHeaderNames.STREAM_PROMISE_ID.text(), 3); httpHeaders2.set(HttpHeaders.Names.CONTENT_LENGTH, text2.length()); httpHeaders = request.headers(); - httpHeaders.set(HttpUtil.ExtensionHeaders.Names.STREAM_ID, 3); + httpHeaders.set(HttpUtil.ExtensionHeaderNames.STREAM_ID.text(), 3); httpHeaders.set(HttpHeaders.Names.CONTENT_LENGTH, 0); - final Http2Headers http2Headers3 = new DefaultHttp2Headers.Builder().method("GET") - .path("/push/test").build(); + final Http2Headers http2Headers3 = new DefaultHttp2Headers().method(as("GET")) + .path(as("/push/test")); runInChannel(clientChannel, new Http2Runnable() { @Override public void run() { @@ -504,9 +513,10 @@ public class InboundHttp2ToHttpAdapterTest { capturedRequests = requestCaptor.getAllValues(); assertEquals(request, capturedRequests.get(0)); - final Http2Headers http2Headers = new DefaultHttp2Headers.Builder().status("200").build(); - final Http2Headers http2Headers2 = new DefaultHttp2Headers.Builder().status("201").scheme("https") - .authority("example.org").build(); + final Http2Headers http2Headers = new DefaultHttp2Headers().status(as("200")); + final Http2Headers http2Headers2 = + new DefaultHttp2Headers().status(as("201")).scheme(as("https")) + .authority(as("example.org")); runInChannel(serverConnectedChannel, new Http2Runnable() { @Override public void run() { @@ -535,11 +545,15 @@ public class InboundHttp2ToHttpAdapterTest { final FullHttpMessage request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.PUT, "/info/test", true); HttpHeaders httpHeaders = request.headers(); - httpHeaders.set(HttpUtil.ExtensionHeaders.Names.STREAM_ID, 3); + httpHeaders.set(HttpUtil.ExtensionHeaderNames.STREAM_ID.text(), 3); httpHeaders.set(HttpHeaders.Names.EXPECT, HttpHeaders.Values.CONTINUE); httpHeaders.set(HttpHeaders.Names.CONTENT_LENGTH, 0); - final Http2Headers http2Headers = new DefaultHttp2Headers.Builder().method("PUT").path("/info/test") - .set(HttpHeaders.Names.EXPECT.toString(), HttpHeaders.Values.CONTINUE).build(); + final Http2Headers http2Headers = + new DefaultHttp2Headers() + .method(as("PUT")) + .path(as("/info/test")) + .set(as(HttpHeaders.Names.EXPECT.toString()), + as(HttpHeaders.Values.CONTINUE.toString())); final FullHttpMessage response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.CONTINUE); final String text = "a big payload"; final ByteBuf payload = Unpooled.copiedBuffer(text.getBytes()); @@ -563,9 +577,9 @@ public class InboundHttp2ToHttpAdapterTest { reset(serverListener); httpHeaders = response.headers(); - httpHeaders.set(HttpUtil.ExtensionHeaders.Names.STREAM_ID, 3); + httpHeaders.set(HttpUtil.ExtensionHeaderNames.STREAM_ID.text(), 3); httpHeaders.set(HttpHeaders.Names.CONTENT_LENGTH, 0); - final Http2Headers http2HeadersResponse = new DefaultHttp2Headers.Builder().status("100").build(); + final Http2Headers http2HeadersResponse = new DefaultHttp2Headers().status(as("100")); runInChannel(serverConnectedChannel, new Http2Runnable() { @Override public void run() { @@ -600,9 +614,9 @@ public class InboundHttp2ToHttpAdapterTest { setClientLatch(1); httpHeaders = response2.headers(); - httpHeaders.set(HttpUtil.ExtensionHeaders.Names.STREAM_ID, 3); + httpHeaders.set(HttpUtil.ExtensionHeaderNames.STREAM_ID.text(), 3); httpHeaders.set(HttpHeaders.Names.CONTENT_LENGTH, 0); - final Http2Headers http2HeadersResponse2 = new DefaultHttp2Headers.Builder().status("200").build(); + final Http2Headers http2HeadersResponse2 = new DefaultHttp2Headers().status(as("200")); runInChannel(serverConnectedChannel, new Http2Runnable() { @Override public void run() { diff --git a/codec/src/main/java/io/netty/handler/codec/AsciiString.java b/codec/src/main/java/io/netty/handler/codec/AsciiString.java index bc0b42fb8d..1ea7309176 100644 --- a/codec/src/main/java/io/netty/handler/codec/AsciiString.java +++ b/codec/src/main/java/io/netty/handler/codec/AsciiString.java @@ -21,6 +21,7 @@ import io.netty.util.internal.EmptyArrays; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Arrays; +import java.util.Comparator; import java.util.List; import java.util.regex.Pattern; import java.util.regex.PatternSyntaxException; @@ -34,6 +35,78 @@ import java.util.regex.PatternSyntaxException; public final class AsciiString implements CharSequence, Comparable { public static final AsciiString EMPTY_STRING = new AsciiString(""); + public static final Comparator CASE_INSENSITIVE_ORDER = new Comparator() { + @Override + public int compare(AsciiString o1, AsciiString o2) { + return CHARSEQUENCE_CASE_INSENSITIVE_ORDER.compare(o1, o2); + } + }; + + public static final Comparator CHARSEQUENCE_CASE_INSENSITIVE_ORDER = + new Comparator() { + @Override + public int compare(CharSequence o1, CharSequence o2) { + if (o1 == o2) { + return 0; + } + + 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) { + byte[] thisValue = a1.value; + byte[] thatValue = a2.value; + for (int i = 0; i < minLength; i++) { + byte v1 = thisValue[i]; + byte v2 = thatValue[i]; + if (v1 == v2) { + continue; + } + int c1 = toLowerCase(v1) & 0xFF; + int c2 = toLowerCase(v2) & 0xFF; + result = c1 - c2; + if (result != 0) { + return result; + } + } + } else if (a1 != null) { + byte[] thisValue = a1.value; + for (int i = 0; i < minLength; i++) { + int c1 = toLowerCase(thisValue[i]) & 0xFF; + int c2 = toLowerCase(o2.charAt(i)); + result = c1 - c2; + if (result != 0) { + return result; + } + } + } else if (a2 != null) { + byte[] thatValue = a2.value; + for (int i = 0; i < minLength; i++) { + int c1 = toLowerCase(o1.charAt(i)); + int c2 = toLowerCase(thatValue[i]) & 0xFF; + result = c1 - c2; + if (result != 0) { + return result; + } + } + } 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; + } + } + } + + return length1 - length2; + } + }; /** * Returns the case-insensitive hash code of the specified string. Note that this method uses the same hashing @@ -105,6 +178,14 @@ public final class AsciiString implements CharSequence, Comparable return a.equals(b); } + /** + * Returns an {@link AsciiString} containing the given character sequence. If the given string + * is already a {@link AsciiString}, just returns the same instance. + */ + public static AsciiString of(CharSequence string) { + return string instanceof AsciiString ? (AsciiString) string : new AsciiString(string); + } + private final byte[] value; private String string; private int hash; @@ -429,43 +510,7 @@ public final class AsciiString implements CharSequence, Comparable * if {@code string} is {@code null}. */ public int compareToIgnoreCase(CharSequence string) { - if (this == string) { - return 0; - } - - int result; - int length1 = length(); - int length2 = string.length(); - int minLength = Math.min(length1, length2); - byte[] thisValue = value; - if (string instanceof AsciiString) { - AsciiString that = (AsciiString) string; - byte[] thatValue = that.value; - for (int i = 0; i < minLength; i ++) { - byte v1 = thisValue[i]; - byte v2 = thatValue[i]; - if (v1 == v2) { - continue; - } - int c1 = toLowerCase(v1) & 0xFF; - int c2 = toLowerCase(v2) & 0xFF; - result = c1 - c2; - if (result != 0) { - return result; - } - } - } else { - for (int i = 0; i < minLength; i ++) { - int c1 = toLowerCase(thisValue[i]) & 0xFF; - int c2 = toLowerCase(string.charAt(i)); - result = c1 - c2; - if (result != 0) { - return result; - } - } - } - - return length1 - length2; + return CHARSEQUENCE_CASE_INSENSITIVE_ORDER.compare(this, string); } /** diff --git a/codec/src/main/java/io/netty/handler/codec/BinaryHeaders.java b/codec/src/main/java/io/netty/handler/codec/BinaryHeaders.java new file mode 100644 index 0000000000..465ad962ec --- /dev/null +++ b/codec/src/main/java/io/netty/handler/codec/BinaryHeaders.java @@ -0,0 +1,369 @@ +/* + * 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: + * + * 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; + +import java.util.HashSet; +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; + +/** + * A typical {@code AsciiString} multimap used by protocols that use binary headers (such as HTTP/2) + * for the representation of arbitrary key-value data. {@link AsciiString} is just a wrapper around + * a byte array but provides some additional utility when handling text data. + */ +public interface BinaryHeaders extends Iterable> { + + /** + * A visitor that helps reduce GC pressure while iterating over a collection of {@link BinaryHeaders}. + */ + public interface BinaryHeaderVisitor { + /** + * @return + *
    + *
  • {@code true} if the processor wants to continue the loop and handle the entry.
  • + *
  • {@code false} if the processor wants to stop handling headers and abort the loop.
  • + *
+ */ + boolean visit(AsciiString name, AsciiString value) throws Exception; + } + + /** + * Returns the value of a header with the specified name. If there are + * more than one values for the specified name, the first value is returned. + * + * @param name the name of the header to search + * @return the first header value if the header is found. + * {@code null} if there's no such header. + */ + AsciiString get(AsciiString name); + + /** + * Returns the value of a header with the specified name. If there are + * more than one values for the specified name, the first value is returned. + * + * @param name the name of the header to search + * @param defaultValue the default value + * @return the first header value if the header is found. + * {@code defaultValue} if there's no such header. + */ + AsciiString get(AsciiString name, AsciiString defaultValue); + + /** + * Returns and removes the value of a header with the specified name. If there are + * more than one values for the specified name, the first value is returned. + * + * @param name the name of the header to search + * @return the first header value or {@code null} if there is no such header + */ + AsciiString getAndRemove(AsciiString name); + + /** + * Returns and removes the value of a header with the specified name. If there are + * more than one values for the specified name, the first value is returned. + * + * @param name the name of the header to search + * @param defaultValue the default value + * @return the first header value or {@code defaultValue} if there is no such header + */ + AsciiString getAndRemove(AsciiString name, AsciiString defaultValue); + + /** + * Returns the values of headers with the specified name + * + * @param name The name of the headers to search + * @return A {@link List} of header values which will be empty if no values are found + */ + List getAll(AsciiString name); + + /** + * Returns and Removes the values of headers with the specified name + * + * @param name The name of the headers to search + * @return A {@link List} of header values which will be empty if no values are found + */ + List getAllAndRemove(AsciiString name); + + /** + * Returns a new {@link List} that contains all headers in this object. Note that modifying the + * returned {@link List} will not affect the state of this object. If you intend to enumerate over the header + * entries only, use {@link #iterator()} instead, which has much less overhead. + */ + List> entries(); + + /** + * Returns {@code true} if and only if this collection contains the header with the specified name. + * + * @param name The name of the header to search for + * @return {@code true} if at least one header is found + */ + boolean contains(AsciiString name); + + /** + * Returns the number of header entries in this collection. + */ + int size(); + + /** + * Returns {@code true} if and only if this collection contains no header entries. + */ + boolean isEmpty(); + + /** + * Returns a new {@link Set} that contains the names of all headers in this object. Note that modifying the + * returned {@link Set} will not affect the state of this object. If you intend to enumerate over the header + * entries only, use {@link #iterator()} instead, which has much less overhead. + */ + Set names(); + + /** + * Adds a new header with the specified name and value. + * + * If the specified value is not a {@link String}, it is converted + * into a {@link String} by {@link Object#toString()}, except in the cases + * of {@link java.util.Date} and {@link java.util.Calendar}, which are formatted to the date + * format defined in RFC2616. + * + * @param name the name of the header being added + * @param value the value of the header being added + * + * @return {@code this} + */ + BinaryHeaders add(AsciiString name, AsciiString value); + + /** + * Adds a new header with the specified name and values. + * + * This getMethod can be represented approximately as the following code: + *
+     * for (Object v: values) {
+     *     if (v == null) {
+     *         break;
+     *     }
+     *     headers.add(name, v);
+     * }
+     * 
+ * + * @param name the name of the headers being set + * @param values the values of the headers being set + * @return {@code this} + */ + BinaryHeaders add(AsciiString name, Iterable values); + + /** + * Adds a new header with the specified name and values. + * + * This getMethod can be represented approximately as the following code: + *
+     * for (Object v: values) {
+     *     if (v == null) {
+     *         break;
+     *     }
+     *     headers.add(name, v);
+     * }
+     * 
+ * + * @param name the name of the headers being set + * @param values the values of the headers being set + * @return {@code this} + */ + BinaryHeaders add(AsciiString name, AsciiString... values); + + /** + * Adds all header entries of the specified {@code headers}. + * + * @return {@code this} + */ + BinaryHeaders add(BinaryHeaders headers); + + /** + * Sets a header with the specified name and value. + * + * If there is an existing header with the same name, it is removed. + * If the specified value is not a {@link String}, it is converted into a + * {@link String} by {@link Object#toString()}, except for {@link java.util.Date} + * and {@link java.util.Calendar}, which are formatted to the date format defined in + * RFC2616. + * + * @param name The name of the header being set + * @param value The value of the header being set + * @return {@code this} + */ + BinaryHeaders set(AsciiString name, AsciiString value); + + /** + * Sets a header with the specified name and values. + * + * If there is an existing header with the same name, it is removed. + * This getMethod can be represented approximately as the following code: + *
+     * headers.remove(name);
+     * for (Object v: values) {
+     *     if (v == null) {
+     *         break;
+     *     }
+     *     headers.add(name, v);
+     * }
+     * 
+ * + * @param name the name of the headers being set + * @param values the values of the headers being set + * @return {@code this} + */ + BinaryHeaders set(AsciiString name, Iterable values); + + /** + * Sets a header with the specified name and values. + * + * If there is an existing header with the same name, it is removed. + * This getMethod can be represented approximately as the following code: + *
+     * headers.remove(name);
+     * for (Object v: values) {
+     *     if (v == null) {
+     *         break;
+     *     }
+     *     headers.add(name, v);
+     * }
+     * 
+ * + * @param name the name of the headers being set + * @param values the values of the headers being set + * @return {@code this} + */ + BinaryHeaders set(AsciiString name, AsciiString... values); + + /** + * Cleans the current header entries and copies all header entries of the specified {@code headers}. + * + * @return {@code this} + */ + BinaryHeaders set(BinaryHeaders headers); + + /** + * Retains all current headers but calls {@link #set(AsciiString, Object)} for each entry in {@code headers} + * @param headers The headers used to {@link #set(AsciiString, Object)} values in this instance + * @return {@code this} + */ + BinaryHeaders setAll(BinaryHeaders headers); + + /** + * Removes the header with the specified name. + * + * @param name The name of the header to remove + * @return {@code true} if and only if at least one entry has been removed + */ + boolean remove(AsciiString name); + + /** + * Removes all headers. + * + * @return {@code this} + */ + BinaryHeaders clear(); + + /** + * 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 contains(AsciiString name, AsciiString value); + + @Override + Iterator> iterator(); + + BinaryHeaders forEachEntry(BinaryHeaderVisitor visitor); + + /** + * Common utilities for {@link BinaryHeaders}. + */ + public static final class Utils { + private static final int HASH_CODE_PRIME = 31; + + /** + * Generates a hash code for a {@link BinaryHeaders} object. + */ + public static int hashCode(BinaryHeaders headers) { + int result = 1; + for (AsciiString name : headers.names()) { + result = HASH_CODE_PRIME * result + name.hashCode(); + Set values = new TreeSet(headers.getAll(name)); + for (AsciiString value : values) { + result = HASH_CODE_PRIME * result + value.hashCode(); + } + } + return result; + } + + /** + * Compares the contents of two {@link BinaryHeaders} objects. + */ + public static boolean equals(BinaryHeaders h1, BinaryHeaders h2) { + // First, check that the set of names match. + Set names = h1.names(); + if (!names.equals(h2.names())) { + return false; + } + + // Compare the values for each name. + for (AsciiString name : names) { + List values = h1.getAll(name); + List otherValues = h2.getAll(name); + if (values.size() != otherValues.size()) { + return false; + } + + // Convert the values to a set and remove values from the other object to see if + // they match. + Set valueSet = new HashSet(values); + valueSet.removeAll(otherValues); + if (!valueSet.isEmpty()) { + return false; + } + } + + return true; + } + + /** + * Generates a {@link String} representation of the {@link BinaryHeaders}, assuming all of + * the names and values are {@code UTF-8} strings. + */ + public static String toStringUtf8(BinaryHeaders headers) { + StringBuilder builder = + new StringBuilder(headers.getClass().getSimpleName()).append('['); + boolean first = true; + Set names = new TreeSet(headers.names()); + for (AsciiString name : names) { + Set valueSet = new TreeSet(headers.getAll(name)); + for (AsciiString value : valueSet) { + if (!first) { + builder.append(", "); + } + first = false; + builder.append(name).append(": ").append(value); + } + } + return builder.append("]").toString(); + } + } +} diff --git a/codec/src/main/java/io/netty/handler/codec/DefaultBinaryHeaders.java b/codec/src/main/java/io/netty/handler/codec/DefaultBinaryHeaders.java new file mode 100644 index 0000000000..467956b3a8 --- /dev/null +++ b/codec/src/main/java/io/netty/handler/codec/DefaultBinaryHeaders.java @@ -0,0 +1,347 @@ +/* + * 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: + * + * 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; + +import io.netty.util.internal.PlatformDependent; + +import java.util.Arrays; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.TreeSet; + +public class DefaultBinaryHeaders implements BinaryHeaders { + private final HeaderMap.ValueUnmarshaller VALUE_UNMARSHALLER = + new HeaderMap.ValueUnmarshaller() { + @Override + public AsciiString unmarshal(CharSequence value) { + return (AsciiString) value; + } + }; + + private final BinaryHeaderVisitor addAll = new BinaryHeaderVisitor() { + @Override + public boolean visit(AsciiString name, AsciiString value) throws Exception { + add(name, value); + return true; + } + }; + private final BinaryHeaderVisitor setAll = new BinaryHeaderVisitor() { + @Override + public boolean visit(AsciiString name, AsciiString value) throws Exception { + set(name, value); + return true; + } + }; + + private final HeaderMap headers; + + public DefaultBinaryHeaders() { + // Binary headers are case-sensitive. It's up the HTTP/1 translation layer to convert headers to + // lowercase. + headers = new HeaderMap(false); + } + + @Override + public BinaryHeaders add(AsciiString name, AsciiString value) { + headers.add(name, value); + return this; + } + + @Override + public BinaryHeaders add(AsciiString name, Iterable values) { + headers.add(name, values); + return this; + } + + @Override + public BinaryHeaders add(AsciiString name, AsciiString... values) { + headers.add(name, values); + return this; + } + + @Override + public BinaryHeaders add(BinaryHeaders headers) { + checkNotNull(headers, "headers"); + + add0(headers); + return this; + } + + private void add0(BinaryHeaders headers) { + if (headers.isEmpty()) { + return; + } + + if (headers instanceof DefaultBinaryHeaders) { + this.headers.add(((DefaultBinaryHeaders) headers).headers); + } else { + forEachEntry(addAll); + } + } + + @Override + public boolean remove(AsciiString name) { + return headers.remove(name); + } + + @Override + public BinaryHeaders set(AsciiString name, AsciiString value) { + headers.set(name, value); + return this; + } + + @Override + public BinaryHeaders set(AsciiString name, Iterable values) { + headers.set(name, values); + return this; + } + + @Override + public BinaryHeaders set(AsciiString name, AsciiString... values) { + headers.set(name, values); + return this; + } + + @Override + public BinaryHeaders set(BinaryHeaders headers) { + checkNotNull(headers, "headers"); + clear(); + add0(headers); + return this; + } + + @Override + public BinaryHeaders setAll(BinaryHeaders headers) { + checkNotNull(headers, "headers"); + + if (headers instanceof DefaultBinaryHeaders) { + this.headers.setAll(((DefaultBinaryHeaders) headers).headers); + } else { + forEachEntry(setAll); + } + + return this; + } + + @Override + public BinaryHeaders clear() { + headers.clear(); + return this; + } + + @Override + public AsciiString get(AsciiString name) { + return (AsciiString) headers.get(name); + } + + @Override + public AsciiString get(AsciiString name, AsciiString defaultValue) { + AsciiString v = get(name); + if (v == null) { + return defaultValue; + } + return v; + } + + @Override + public AsciiString getAndRemove(AsciiString name) { + return (AsciiString) headers.getAndRemove(name); + } + + @Override + public AsciiString getAndRemove(AsciiString name, AsciiString defaultValue) { + AsciiString v = getAndRemove(name); + if (v == null) { + return defaultValue; + } + return v; + } + + @Override + public List getAll(AsciiString name) { + return headers.getAll(name, VALUE_UNMARSHALLER); + } + + @Override + public List getAllAndRemove(AsciiString name) { + return headers.getAllAndRemove(name, VALUE_UNMARSHALLER); + } + + @Override + public List> entries() { + int size = size(); + @SuppressWarnings("unchecked") + final Map.Entry[] all = new Map.Entry[size]; + + headers.forEachEntry(new HeaderMap.EntryVisitor() { + int cnt; + @Override + public boolean visit(Entry entry) { + all[cnt++] = new AsciiStringHeaderEntry(entry); + return true; + } + }); + + return Arrays.asList(all); + } + + @Override + public Iterator> iterator() { + return new AsciiStringHeaderIterator(); + } + + @Override + public boolean contains(AsciiString name) { + return get(name) != null; + } + + @Override + public int size() { + return headers.size(); + } + + @Override + public boolean isEmpty() { + return headers.isEmpty(); + } + + @Override + public boolean contains(AsciiString name, AsciiString value) { + return contains(name, value, false); + } + + public boolean contains(AsciiString name, AsciiString value, boolean ignoreCase) { + return headers.contains(name, value); + } + + @Override + public Set names() { + return names(headers.isIgnoreCase()); + } + + /** + * Get the set of names for all text headers + * @param caseInsensitive {@code true} if names should be added in a case insensitive + * @return The set of names for all text headers + */ + public Set names(boolean caseInsensitive) { + final Set names = caseInsensitive ? new TreeSet(AsciiString.CASE_INSENSITIVE_ORDER) + : new LinkedHashSet(size()); + headers.forEachName(new HeaderMap.NameVisitor() { + @Override + public boolean visit(CharSequence name) { + names.add((AsciiString) name); + return true; + } + }); + return names; + } + + @Override + public BinaryHeaders forEachEntry(final BinaryHeaders.BinaryHeaderVisitor visitor) { + headers.forEachEntry(new HeaderMap.EntryVisitor() { + @Override + public boolean visit(Entry entry) { + try { + return visitor.visit((AsciiString) entry.getKey(), + (AsciiString) entry.getValue()); + } catch (Exception e) { + PlatformDependent.throwException(e); + return false; + } + } + }); + return this; + } + + @Override + public int hashCode() { + return Utils.hashCode(this); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof BinaryHeaders)) { + return false; + } + + return Utils.equals(this, (BinaryHeaders) o); + } + + @Override + public String toString() { + return Utils.toStringUtf8(this); + } + + static void checkNotNull(T value, String name) { + if (value == null) { + throw new NullPointerException(name); + } + } + + private static final class AsciiStringHeaderEntry implements Map.Entry { + private final Entry entry; + + AsciiStringHeaderEntry(Entry entry) { + this.entry = entry; + } + + @Override + public AsciiString getKey() { + return (AsciiString) entry.getKey(); + } + + @Override + public AsciiString getValue() { + return (AsciiString) entry.getValue(); + } + + @Override + public AsciiString setValue(AsciiString value) { + checkNotNull(value, "value"); + return (AsciiString) entry.setValue(value); + } + + @Override + public String toString() { + return entry.toString(); + } + } + + private final class AsciiStringHeaderIterator implements Iterator> { + + private Iterator> iter = headers.iterator(); + + @Override + public boolean hasNext() { + return iter.hasNext(); + } + + @Override + public Entry next() { + Entry entry = iter.next(); + return new AsciiStringHeaderEntry(entry); + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + } +} diff --git a/codec/src/main/java/io/netty/handler/codec/DefaultTextHeaders.java b/codec/src/main/java/io/netty/handler/codec/DefaultTextHeaders.java index eb222c683d..d82ad42a70 100644 --- a/codec/src/main/java/io/netty/handler/codec/DefaultTextHeaders.java +++ b/codec/src/main/java/io/netty/handler/codec/DefaultTextHeaders.java @@ -23,46 +23,64 @@ import java.text.DateFormat; import java.text.ParseException; import java.text.ParsePosition; import java.text.SimpleDateFormat; -import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import java.util.Date; +import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Map.Entry; -import java.util.NoSuchElementException; import java.util.Set; -import java.util.TreeSet; -import java.util.HashSet; import java.util.TimeZone; +import java.util.TreeSet; public class DefaultTextHeaders implements TextHeaders { - private static final int HASH_CODE_PRIME = 31; - private static final int BUCKET_SIZE = 17; + private final HeaderMap.NameConverter NAME_CONVERTER = new HeaderMap.NameConverter() { + @Override + public CharSequence convertName(CharSequence name) { + return DefaultTextHeaders.this.convertName(name); + } + }; + private final HeaderMap.ValueMarshaller VALUE_MARSHALLER = new HeaderMap.ValueMarshaller() { + @Override + public CharSequence marshal(Object value) { + return convertValue(value); + } + }; + private final HeaderMap.ValueUnmarshaller VALUE_UNMARSHALLER = + new HeaderMap.ValueUnmarshaller() { + @Override + public String unmarshal(CharSequence value) { + return value.toString(); + } + }; - private static int index(int hash) { - return Math.abs(hash % BUCKET_SIZE); - } + private final TextHeaderProcessor addAll = new TextHeaderProcessor() { + @Override + public boolean process(CharSequence name, CharSequence value) throws Exception { + headers.add(name, value); + return true; + } + }; - private final HeaderEntry[] entries = new HeaderEntry[BUCKET_SIZE]; - private final HeaderEntry head = new HeaderEntry(this); - private final boolean ignoreCase; - int size; + private final TextHeaderProcessor setAll = new TextHeaderProcessor() { + @Override + public boolean process(CharSequence name, CharSequence value) throws Exception { + headers.set(name, value); + return true; + } + }; + + private final HeaderMap headers; public DefaultTextHeaders() { this(true); } public DefaultTextHeaders(boolean ignoreCase) { - head.before = head.after = head; - this.ignoreCase = ignoreCase; - } - - protected int hashCode(CharSequence name) { - return AsciiString.caseInsensitiveHashCode(name); + headers = new HeaderMap(ignoreCase, NAME_CONVERTER); } protected CharSequence convertName(CharSequence name) { @@ -82,86 +100,28 @@ public class DefaultTextHeaders implements TextHeaders { return value.toString(); } - protected boolean nameEquals(CharSequence a, CharSequence b) { - return equals(a, b, ignoreCase); - } - - protected boolean valueEquals(CharSequence a, CharSequence b, boolean ignoreCase) { - return equals(a, b, ignoreCase); - } - - private static boolean equals(CharSequence a, CharSequence b, boolean ignoreCase) { - if (ignoreCase) { - return AsciiString.equalsIgnoreCase(a, b); - } else { - return AsciiString.equals(a, b); - } - } - @Override public TextHeaders add(CharSequence name, Object value) { - name = convertName(name); CharSequence convertedVal = convertValue(value); - int h = hashCode(name); - int i = index(h); - add0(h, i, name, convertedVal); + headers.add(name, convertedVal); return this; } @Override public TextHeaders add(CharSequence name, Iterable values) { - name = convertName(name); - if (values == null) { - throw new NullPointerException("values"); - } - - int h = hashCode(name); - int i = index(h); - for (Object v: values) { - if (v == null) { - break; - } - CharSequence convertedVal = convertValue(v); - add0(h, i, name, convertedVal); - } + headers.addConvertedValues(name, VALUE_MARSHALLER, values); return this; } @Override public TextHeaders add(CharSequence name, Object... values) { - name = convertName(name); - if (values == null) { - throw new NullPointerException("values"); - } - - int h = hashCode(name); - int i = index(h); - for (Object v: values) { - if (v == null) { - break; - } - CharSequence convertedVal = convertValue(v); - add0(h, i, name, convertedVal); - } + headers.addConvertedValues(name, VALUE_MARSHALLER, values); return this; } - private void add0(int h, int i, CharSequence name, CharSequence value) { - // Update the hash table. - HeaderEntry e = entries[i]; - HeaderEntry newEntry; - entries[i] = newEntry = new HeaderEntry(this, h, name, value); - newEntry.next = e; - - // Update the linked list. - newEntry.addBefore(head); - } - @Override public TextHeaders add(TextHeaders headers) { - if (headers == null) { - throw new NullPointerException("headers"); - } + checkNotNull(headers, "headers"); add0(headers); return this; @@ -173,124 +133,33 @@ public class DefaultTextHeaders implements TextHeaders { } if (headers instanceof DefaultTextHeaders) { - DefaultTextHeaders m = (DefaultTextHeaders) headers; - HeaderEntry e = m.head.after; - while (e != m.head) { - CharSequence name = e.name; - name = convertName(name); - add(name, convertValue(e.value)); - e = e.after; - } + this.headers.add(((DefaultTextHeaders) headers).headers); } else { - for (Entry e: headers.unconvertedEntries()) { - add(e.getKey(), e.getValue()); - } + headers.forEachEntry(addAll); } } @Override public boolean remove(CharSequence name) { - if (name == null) { - throw new NullPointerException("name"); - } - int h = hashCode(name); - int i = index(h); - return remove0(h, i, name); - } - - private boolean remove0(int h, int i, CharSequence name) { - HeaderEntry e = entries[i]; - if (e == null) { - return false; - } - - boolean removed = false; - for (;;) { - if (e.hash == h && nameEquals(e.name, name)) { - e.remove(); - HeaderEntry next = e.next; - if (next != null) { - entries[i] = next; - e = next; - } else { - entries[i] = null; - return true; - } - removed = true; - } else { - break; - } - } - - for (;;) { - HeaderEntry next = e.next; - if (next == null) { - break; - } - if (next.hash == h && nameEquals(next.name, name)) { - e.next = next.next; - next.remove(); - removed = true; - } else { - e = next; - } - } - - return removed; + return headers.remove(name); } @Override public TextHeaders set(CharSequence name, Object value) { - name = convertName(name); CharSequence convertedVal = convertValue(value); - int h = hashCode(name); - int i = index(h); - remove0(h, i, name); - add0(h, i, name, convertedVal); + headers.set(name, convertedVal); return this; } @Override public TextHeaders set(CharSequence name, Iterable values) { - name = convertName(name); - if (values == null) { - throw new NullPointerException("values"); - } - - int h = hashCode(name); - int i = index(h); - - remove0(h, i, name); - for (Object v: values) { - if (v == null) { - break; - } - CharSequence convertedVal = convertValue(v); - add0(h, i, name, convertedVal); - } - + headers.set(name, VALUE_MARSHALLER, values); return this; } @Override public TextHeaders set(CharSequence name, Object... values) { - name = convertName(name); - if (values == null) { - throw new NullPointerException("values"); - } - - int h = hashCode(name); - int i = index(h); - - remove0(h, i, name); - for (Object v: values) { - if (v == null) { - break; - } - CharSequence convertedVal = convertValue(v); - add0(h, i, name, convertedVal); - } - + headers.set(name, VALUE_MARSHALLER, values); return this; } @@ -312,18 +181,9 @@ public class DefaultTextHeaders implements TextHeaders { } if (headers instanceof DefaultTextHeaders) { - DefaultTextHeaders m = (DefaultTextHeaders) headers; - HeaderEntry e = m.head.after; - while (e != m.head) { - CharSequence name = e.name; - name = convertName(name); - set(name, convertValue(e.value)); - e = e.after; - } + this.headers.setAll(((DefaultTextHeaders) headers).headers); } else { - for (Entry e: headers.unconvertedEntries()) { - set(e.getKey(), e.getValue()); - } + headers.forEachEntry(setAll); } return this; @@ -331,34 +191,13 @@ public class DefaultTextHeaders implements TextHeaders { @Override public TextHeaders clear() { - Arrays.fill(entries, null); - head.before = head.after = head; - size = 0; + headers.clear(); return this; } @Override public CharSequence getUnconverted(CharSequence name) { - if (name == null) { - throw new NullPointerException("name"); - } - - int h = hashCode(name); - int i = index(h); - HeaderEntry e = entries[i]; - CharSequence value = null; - // loop until the first header was found - while (e != null) { - if (e.hash == h && nameEquals(e.name, name)) { - value = e.value; - } - - e = e.next; - } - if (value != null) { - return value; - } - return null; + return headers.get(name); } @Override @@ -477,52 +316,7 @@ public class DefaultTextHeaders implements TextHeaders { @Override public CharSequence getUnconvertedAndRemove(CharSequence name) { - if (name == null) { - throw new NullPointerException("name"); - } - int h = hashCode(name); - int i = index(h); - HeaderEntry e = entries[i]; - if (e == null) { - return null; - } - - CharSequence value = null; - for (;;) { - if (e.hash == h && nameEquals(e.name, name)) { - value = e.value; - e.remove(); - HeaderEntry next = e.next; - if (next != null) { - entries[i] = next; - e = next; - } else { - entries[i] = null; - return value; - } - } else { - break; - } - } - - for (;;) { - HeaderEntry next = e.next; - if (next == null) { - break; - } - if (next.hash == h && nameEquals(next.name, name)) { - value = next.value; - e.next = next.next; - next.remove(); - } else { - e = next; - } - } - - if (value != null) { - return value; - } - return null; + return headers.getAndRemove(name); } @Override @@ -641,176 +435,45 @@ public class DefaultTextHeaders implements TextHeaders { @Override public List getAllUnconverted(CharSequence name) { - if (name == null) { - throw new NullPointerException("name"); - } - - List values = new ArrayList(4); - int h = hashCode(name); - int i = index(h); - HeaderEntry e = entries[i]; - while (e != null) { - if (e.hash == h && nameEquals(e.name, name)) { - values.add(e.getValue()); - } - e = e.next; - } - - Collections.reverse(values); - return values; + return headers.getAll(name); } @Override public List getAll(CharSequence name) { - if (name == null) { - throw new NullPointerException("name"); - } - - List values = new ArrayList(4); - int h = hashCode(name); - int i = index(h); - HeaderEntry e = entries[i]; - while (e != null) { - if (e.hash == h && nameEquals(e.name, name)) { - values.add(e.getValue().toString()); - } - e = e.next; - } - - Collections.reverse(values); - return values; + return headers.getAll(name, VALUE_UNMARSHALLER); } @Override public List getAllAndRemove(CharSequence name) { - if (name == null) { - throw new NullPointerException("name"); - } - int h = hashCode(name); - int i = index(h); - HeaderEntry e = entries[i]; - if (e == null) { - return null; - } - - List values = new ArrayList(4); - for (;;) { - if (e.hash == h && nameEquals(e.name, name)) { - values.add(e.getValue().toString()); - e.remove(); - HeaderEntry next = e.next; - if (next != null) { - entries[i] = next; - e = next; - } else { - entries[i] = null; - Collections.reverse(values); - return values; - } - } else { - break; - } - } - - for (;;) { - HeaderEntry next = e.next; - if (next == null) { - break; - } - if (next.hash == h && nameEquals(next.name, name)) { - values.add(next.getValue().toString()); - e.next = next.next; - next.remove(); - } else { - e = next; - } - } - - Collections.reverse(values); - return values; + return headers.getAll(name, VALUE_UNMARSHALLER); } @Override public List getAllUnconvertedAndRemove(CharSequence name) { - if (name == null) { - throw new NullPointerException("name"); - } - int h = hashCode(name); - int i = index(h); - HeaderEntry e = entries[i]; - if (e == null) { - return null; - } - - List values = new ArrayList(4); - for (;;) { - if (e.hash == h && nameEquals(e.name, name)) { - values.add(e.getValue()); - e.remove(); - HeaderEntry next = e.next; - if (next != null) { - entries[i] = next; - e = next; - } else { - entries[i] = null; - Collections.reverse(values); - return values; - } - } else { - break; - } - } - - for (;;) { - HeaderEntry next = e.next; - if (next == null) { - break; - } - if (next.hash == h && nameEquals(next.name, name)) { - values.add(next.getValue()); - e.next = next.next; - next.remove(); - } else { - e = next; - } - } - - Collections.reverse(values); - return values; + return headers.getAllAndRemove(name); } @Override public List> entries() { - int cnt = 0; int size = size(); @SuppressWarnings("unchecked") - Map.Entry[] all = new Map.Entry[size]; + final Map.Entry[] all = new Map.Entry[size]; - HeaderEntry e = head.after; - while (e != head) { - all[cnt ++] = new StringHeaderEntry(e); - e = e.after; - } + headers.forEachEntry(new HeaderMap.EntryVisitor() { + int cnt; + @Override + public boolean visit(Entry entry) { + all[cnt++] = new StringHeaderEntry(entry); + return true; + } + }); - assert size == cnt; return Arrays.asList(all); } @Override public List> unconvertedEntries() { - int cnt = 0; - int size = size(); - @SuppressWarnings("unchecked") - Map.Entry[] all = new Map.Entry[size]; - - HeaderEntry e = head.after; - while (e != head) { - all[cnt ++] = e; - e = e.after; - } - - assert size == cnt; - return Arrays.asList(all); + return headers.entries(); } @Override @@ -820,7 +483,7 @@ public class DefaultTextHeaders implements TextHeaders { @Override public Iterator> unconvertedIterator() { - return new HeaderIterator(); + return headers.iterator(); } @Override @@ -830,12 +493,12 @@ public class DefaultTextHeaders implements TextHeaders { @Override public int size() { - return size; + return headers.size(); } @Override public boolean isEmpty() { - return head == head.after; + return headers.isEmpty(); } @Override @@ -845,34 +508,13 @@ public class DefaultTextHeaders implements TextHeaders { @Override public boolean contains(CharSequence name, Object value, boolean ignoreCase) { - if (name == null) { - throw new NullPointerException("name"); - } - - int h = hashCode(name); - int i = index(h); CharSequence convertedVal = convertValue(value); - HeaderEntry e = entries[i]; - while (e != null) { - if (e.hash == h && nameEquals(e.name, name)) { - if (valueEquals(e.value, convertedVal, ignoreCase)) { - return true; - } - } - e = e.next; - } - return false; + return headers.contains(name, convertedVal, ignoreCase); } @Override public Set unconvertedNames() { - Set names = new LinkedHashSet(size()); - HeaderEntry e = head.after; - while (e != head) { - names.add(e.getKey()); - e = e.after; - } - return names; + return headers.names(); } @Override @@ -886,43 +528,38 @@ public class DefaultTextHeaders implements TextHeaders { * @return The set of names for all text headers */ public Set names(boolean caseInsensitive) { - Set names = caseInsensitive ? new TreeSet(String.CASE_INSENSITIVE_ORDER) - : new LinkedHashSet(size()); - HeaderEntry e = head.after; - while (e != head) { - names.add(e.getKey().toString()); - e = e.after; - } + final Set names = + caseInsensitive ? new TreeSet(String.CASE_INSENSITIVE_ORDER) + : new LinkedHashSet(size()); + headers.forEachName(new HeaderMap.NameVisitor() { + @Override + public boolean visit(CharSequence name) { + names.add(name.toString()); + return true; + } + }); return names; } @Override - public TextHeaders forEachEntry(TextHeaderProcessor processor) { - HeaderEntry e = head.after; - try { - while (e != head) { - if (!processor.process(e.getKey(), e.getValue())) { - break; + public TextHeaders forEachEntry(final TextHeaderProcessor processor) { + headers.forEachEntry(new HeaderMap.EntryVisitor() { + @Override + public boolean visit(Entry entry) { + try { + return processor.process(entry.getKey(), entry.getValue()); + } catch (Exception ex) { + PlatformDependent.throwException(ex); + return false; } - e = e.after; } - } catch (Exception ex) { - PlatformDependent.throwException(ex); - } + }); return this; } @Override public int hashCode() { - int result = 1; - for (String name : names(true)) { - result = HASH_CODE_PRIME * result + name.hashCode(); - Set values = new TreeSet(getAll(name)); - for (String value : values) { - result = HASH_CODE_PRIME * result + value.hashCode(); - } - } - return result; + return headers.hashCode(); } @Override @@ -959,66 +596,9 @@ public class DefaultTextHeaders implements TextHeaders { return true; } - private static final class HeaderEntry implements Map.Entry { - private final DefaultTextHeaders parent; - final int hash; - final CharSequence name; - CharSequence value; - HeaderEntry next; - HeaderEntry before, after; - - HeaderEntry(DefaultTextHeaders parent, int hash, CharSequence name, CharSequence value) { - this.parent = parent; - this.hash = hash; - this.name = name; - this.value = value; - } - - HeaderEntry(DefaultTextHeaders parent) { - this.parent = parent; - hash = -1; - name = null; - value = null; - } - - void remove() { - before.after = after; - after.before = before; - parent.size --; - } - - void addBefore(HeaderEntry e) { - after = e; - before = e.before; - before.after = this; - after.before = this; - parent.size ++; - } - - @Override - public CharSequence getKey() { - return name; - } - - @Override - public CharSequence getValue() { - return value; - } - - @Override - public CharSequence setValue(CharSequence value) { - if (value == null) { - throw new NullPointerException("value"); - } - value = parent.convertValue(value); - CharSequence oldValue = this.value; - this.value = value; - return oldValue; - } - - @Override - public String toString() { - return name.toString() + '=' + value.toString(); + private static void checkNotNull(T value, String name) { + if (value == null) { + throw new NullPointerException(name); } } @@ -1058,50 +638,20 @@ public class DefaultTextHeaders implements TextHeaders { } } - private final class HeaderIterator implements Iterator> { + private final class StringHeaderIterator implements Iterator> { - private HeaderEntry current = head; + private Iterator> iter = headers.iterator(); @Override public boolean hasNext() { - return current.after != head; - } - - @Override - public Entry next() { - current = current.after; - - if (current == head) { - throw new NoSuchElementException(); - } - - return current; - } - - @Override - public void remove() { - throw new UnsupportedOperationException(); - } - } - - private final class StringHeaderIterator implements Iterator> { - - private HeaderEntry current = head; - - @Override - public boolean hasNext() { - return current.after != head; + return iter.hasNext(); } @Override public Entry next() { - current = current.after; + Entry next = iter.next(); - if (current == head) { - throw new NoSuchElementException(); - } - - return new StringHeaderEntry(current); + return new StringHeaderEntry(next); } @Override diff --git a/codec/src/main/java/io/netty/handler/codec/EmptyBinaryHeaders.java b/codec/src/main/java/io/netty/handler/codec/EmptyBinaryHeaders.java new file mode 100644 index 0000000000..15058a4ded --- /dev/null +++ b/codec/src/main/java/io/netty/handler/codec/EmptyBinaryHeaders.java @@ -0,0 +1,169 @@ +/* + * 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: + * + * 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; + +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Map.Entry; +import java.util.Set; + +public class EmptyBinaryHeaders implements BinaryHeaders { + + @Override + public AsciiString get(AsciiString name) { + return null; + } + + @Override + public AsciiString get(AsciiString name, AsciiString defaultValue) { + return defaultValue; + } + + @Override + public AsciiString getAndRemove(AsciiString name) { + return null; + } + + @Override + public AsciiString getAndRemove(AsciiString name, AsciiString defaultValue) { + return defaultValue; + } + + @Override + public List getAll(AsciiString name) { + return Collections.emptyList(); + } + + @Override + public List getAllAndRemove(AsciiString name) { + return Collections.emptyList(); + } + + @Override + public List> entries() { + return Collections.emptyList(); + } + + @Override + public boolean contains(AsciiString name) { + return false; + } + + @Override + public int size() { + return 0; + } + + @Override + public boolean isEmpty() { + return true; + } + + @Override + public Set names() { + return Collections.emptySet(); + } + + @Override + public BinaryHeaders add(AsciiString name, AsciiString value) { + throw new UnsupportedOperationException("read only"); + } + + @Override + public BinaryHeaders add(AsciiString name, Iterable values) { + throw new UnsupportedOperationException("read only"); + } + + @Override + public BinaryHeaders add(AsciiString name, AsciiString... values) { + throw new UnsupportedOperationException("read only"); + } + + @Override + public BinaryHeaders add(BinaryHeaders headers) { + throw new UnsupportedOperationException("read only"); + } + + @Override + public BinaryHeaders set(AsciiString name, AsciiString value) { + throw new UnsupportedOperationException("read only"); + } + + @Override + public BinaryHeaders set(AsciiString name, Iterable values) { + throw new UnsupportedOperationException("read only"); + } + + @Override + public BinaryHeaders set(AsciiString name, AsciiString... values) { + throw new UnsupportedOperationException("read only"); + } + + @Override + public BinaryHeaders set(BinaryHeaders headers) { + throw new UnsupportedOperationException("read only"); + } + + @Override + public BinaryHeaders setAll(BinaryHeaders headers) { + throw new UnsupportedOperationException("read only"); + } + + @Override + public boolean remove(AsciiString name) { + return false; + } + + @Override + public BinaryHeaders clear() { + return this; + } + + @Override + public boolean contains(AsciiString name, AsciiString value) { + return false; + } + + @Override + public Iterator> iterator() { + return entries().iterator(); + } + + @Override + public BinaryHeaders forEachEntry(BinaryHeaderVisitor processor) { + return this; + } + + @Override + public int hashCode() { + return BinaryHeaders.Utils.hashCode(this); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof BinaryHeaders)) { + return false; + } + return ((BinaryHeaders) obj).isEmpty(); + } + + @Override + public String toString() { + return BinaryHeaders.Utils.toStringUtf8(this); + } +} diff --git a/codec/src/main/java/io/netty/handler/codec/HeaderMap.java b/codec/src/main/java/io/netty/handler/codec/HeaderMap.java new file mode 100644 index 0000000000..066515b7cf --- /dev/null +++ b/codec/src/main/java/io/netty/handler/codec/HeaderMap.java @@ -0,0 +1,844 @@ +/* + * 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: + * + * 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; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.NoSuchElementException; +import java.util.Set; +import java.util.TreeSet; + +/** + * Basic map of header names to values. This is meant to be a central storage mechanism by all + * headers implementations. All keys and values are stored as {@link CharSequence}. + */ +public class HeaderMap implements Iterable> { + private static final int BUCKET_SIZE = 17; + private static final int HASH_CODE_PRIME = 31; + + public static final NameConverter IDENTITY_NAME_CONVERTER = new NameConverter() { + @Override + public CharSequence convertName(CharSequence name) { + return name; + } + }; + + public interface EntryVisitor { + boolean visit(Entry entry); + } + + public interface NameVisitor { + boolean visit(CharSequence name); + } + + public interface NameConverter { + CharSequence convertName(CharSequence name); + } + + public interface ValueMarshaller { + CharSequence marshal(Object value); + } + + public interface ValueUnmarshaller { + T unmarshal(CharSequence value); + } + + private final HeaderEntry[] entries = new HeaderEntry[BUCKET_SIZE]; + private final HeaderEntry head = new HeaderEntry(); + private final NameConverter nameConverter; + private final boolean ignoreCase; + int size; + + public HeaderMap() { + this(true); + } + + public HeaderMap(boolean ignoreCase) { + this(ignoreCase, IDENTITY_NAME_CONVERTER); + } + + public HeaderMap(boolean ignoreCase, NameConverter nameConverter) { + this.nameConverter = checkNotNull(nameConverter, "nameConverter"); + head.before = head.after = head; + this.ignoreCase = ignoreCase; + } + + public boolean isIgnoreCase() { + return ignoreCase; + } + + public HeaderMap add(CharSequence name, CharSequence value) { + name = convertName(name); + checkNotNull(value, "value"); + int h = hashCode(name); + int i = index(h); + add0(h, i, name, value); + return this; + } + + public HeaderMap add(CharSequence name, Iterable values) { + name = convertName(name); + checkNotNull(values, "values"); + + int h = hashCode(name); + int i = index(h); + for (CharSequence v: values) { + if (v == null) { + break; + } + add0(h, i, name, v); + } + return this; + } + + public HeaderMap add(CharSequence name, CharSequence... values) { + name = convertName(name); + checkNotNull(values, "values"); + + int h = hashCode(name); + int i = index(h); + for (CharSequence v: values) { + if (v == null) { + break; + } + add0(h, i, name, v); + } + return this; + } + + public HeaderMap addConvertedValues(CharSequence name, ValueMarshaller converter, Iterable values) { + name = convertName(name); + checkNotNull(values, "values"); + checkNotNull(converter, "converter"); + + int h = hashCode(name); + int i = index(h); + for (Object v : values) { + if (v == null) { + break; + } + CharSequence convertedVal = converter.marshal(v); + add0(h, i, name, convertedVal); + } + return this; + } + + public HeaderMap addConvertedValues(CharSequence name, ValueMarshaller converter, Object... values) { + name = convertName(name); + checkNotNull(values, "values"); + checkNotNull(converter, "converter"); + + int h = hashCode(name); + int i = index(h); + for (Object v : values) { + if (v == null) { + break; + } + CharSequence convertedVal = converter.marshal(v); + add0(h, i, name, convertedVal); + } + return this; + } + + private void add0(int h, int i, CharSequence name, CharSequence value) { + // Update the hash table. + HeaderEntry e = entries[i]; + HeaderEntry newEntry; + entries[i] = newEntry = new HeaderEntry(h, name, value); + newEntry.next = e; + + // Update the linked list. + newEntry.addBefore(head); + } + + public HeaderMap add(HeaderMap headers) { + checkNotNull(headers, "headers"); + + add0(headers); + return this; + } + + private void add0(HeaderMap headers) { + if (headers.isEmpty()) { + return; + } + + HeaderMap m = (HeaderMap) headers; + HeaderEntry e = m.head.after; + while (e != m.head) { + add(e.name, e.value); + e = e.after; + } + } + + public boolean remove(CharSequence name) { + checkNotNull(name, "name"); + int h = hashCode(name); + int i = index(h); + return remove0(h, i, name); + } + + private boolean remove0(int h, int i, CharSequence name) { + HeaderEntry e = entries[i]; + if (e == null) { + return false; + } + + boolean removed = false; + for (;;) { + if (e.hash == h && nameEquals(e.name, name)) { + e.remove(); + HeaderEntry next = e.next; + if (next != null) { + entries[i] = next; + e = next; + } else { + entries[i] = null; + return true; + } + removed = true; + } else { + break; + } + } + + for (;;) { + HeaderEntry next = e.next; + if (next == null) { + break; + } + if (next.hash == h && nameEquals(next.name, name)) { + e.next = next.next; + next.remove(); + removed = true; + } else { + e = next; + } + } + + return removed; + } + + public HeaderMap set(CharSequence name, CharSequence value) { + name = convertName(name); + checkNotNull(value, "value"); + int h = hashCode(name); + int i = index(h); + remove0(h, i, name); + add0(h, i, name, value); + return this; + } + + public HeaderMap set(CharSequence name, Iterable values) { + name = convertName(name); + checkNotNull(values, "values"); + + int h = hashCode(name); + int i = index(h); + + remove0(h, i, name); + for (CharSequence v: values) { + if (v == null) { + break; + } + add0(h, i, name, v); + } + + return this; + } + + public HeaderMap set(CharSequence name, CharSequence... values) { + name = convertName(name); + checkNotNull(values, "values"); + + int h = hashCode(name); + int i = index(h); + + remove0(h, i, name); + for (CharSequence v: values) { + if (v == null) { + break; + } + add0(h, i, name, v); + } + + return this; + } + + public HeaderMap set(CharSequence name, ValueMarshaller converter, Iterable values) { + name = convertName(name); + checkNotNull(converter, "converter"); + checkNotNull(values, "values"); + + int h = hashCode(name); + int i = index(h); + + remove0(h, i, name); + for (Object v: values) { + if (v == null) { + break; + } + CharSequence convertedVal = converter.marshal(v); + add0(h, i, name, convertedVal); + } + + return this; + } + + public HeaderMap set(CharSequence name, ValueMarshaller converter, Object... values) { + name = convertName(name); + checkNotNull(converter, "converter"); + checkNotNull(values, "values"); + + int h = hashCode(name); + int i = index(h); + + remove0(h, i, name); + for (Object v: values) { + if (v == null) { + break; + } + CharSequence convertedVal = converter.marshal(v); + add0(h, i, name, convertedVal); + } + + return this; + } + + public HeaderMap set(HeaderMap headers) { + checkNotNull(headers, "headers"); + + clear(); + add0(headers); + return this; + } + + public HeaderMap setAll(HeaderMap headers) { + checkNotNull(headers, "headers"); + + HeaderEntry e = headers.head.after; + while (e != headers.head) { + set(e.name, e.value); + e = e.after; + } + + return this; + } + + public HeaderMap clear() { + Arrays.fill(entries, null); + head.before = head.after = head; + size = 0; + return this; + } + + public CharSequence get(CharSequence name) { + checkNotNull(name, "name"); + + int h = hashCode(name); + int i = index(h); + HeaderEntry e = entries[i]; + CharSequence value = null; + // loop until the first header was found + while (e != null) { + if (e.hash == h && nameEquals(e.name, name)) { + value = e.value; + } + + e = e.next; + } + return value; + } + + public CharSequence get(CharSequence name, CharSequence defaultValue) { + CharSequence v = get(name); + if (v == null) { + return defaultValue; + } + return v; + } + + public CharSequence getAndRemove(CharSequence name) { + checkNotNull(name, "name"); + int h = hashCode(name); + int i = index(h); + HeaderEntry e = entries[i]; + if (e == null) { + return null; + } + + CharSequence value = null; + for (;;) { + if (e.hash == h && nameEquals(e.name, name)) { + value = e.value; + e.remove(); + HeaderEntry next = e.next; + if (next != null) { + entries[i] = next; + e = next; + } else { + entries[i] = null; + return value; + } + } else { + break; + } + } + + for (;;) { + HeaderEntry next = e.next; + if (next == null) { + break; + } + if (next.hash == h && nameEquals(next.name, name)) { + value = next.value; + e.next = next.next; + next.remove(); + } else { + e = next; + } + } + + return value; + } + + public CharSequence getAndRemove(CharSequence name, CharSequence defaultValue) { + CharSequence v = getAndRemove(name); + if (v == null) { + return defaultValue; + } + return v; + } + + public List getAll(CharSequence name) { + checkNotNull(name, "name"); + + List values = new ArrayList(4); + int h = hashCode(name); + int i = index(h); + HeaderEntry e = entries[i]; + while (e != null) { + if (e.hash == h && nameEquals(e.name, name)) { + values.add(e.getValue()); + } + e = e.next; + } + + Collections.reverse(values); + return values; + } + + public List getAllAndRemove(CharSequence name) { + checkNotNull(name, "name"); + int h = hashCode(name); + int i = index(h); + HeaderEntry e = entries[i]; + if (e == null) { + return null; + } + + List values = new ArrayList(4); + for (;;) { + if (e.hash == h && nameEquals(e.name, name)) { + values.add(e.getValue()); + e.remove(); + HeaderEntry next = e.next; + if (next != null) { + entries[i] = next; + e = next; + } else { + entries[i] = null; + Collections.reverse(values); + return values; + } + } else { + break; + } + } + + for (;;) { + HeaderEntry next = e.next; + if (next == null) { + break; + } + if (next.hash == h && nameEquals(next.name, name)) { + values.add(next.getValue()); + e.next = next.next; + next.remove(); + } else { + e = next; + } + } + + Collections.reverse(values); + return values; + } + + public List getAll(CharSequence name, ValueUnmarshaller unmarshaller) { + checkNotNull(name, "name"); + checkNotNull(unmarshaller, "unmarshaller"); + + List values = new ArrayList(4); + int h = hashCode(name); + int i = index(h); + HeaderEntry e = entries[i]; + while (e != null) { + if (e.hash == h && nameEquals(e.name, name)) { + values.add(unmarshaller.unmarshal(e.value)); + } + e = e.next; + } + + Collections.reverse(values); + return values; + } + + public List getAllAndRemove(CharSequence name, ValueUnmarshaller unmarshaller) { + checkNotNull(name, "name"); + checkNotNull(unmarshaller, "unmarshaller"); + + int h = hashCode(name); + int i = index(h); + HeaderEntry e = entries[i]; + if (e == null) { + return null; + } + + List values = new ArrayList(4); + for (;;) { + if (e.hash == h && nameEquals(e.name, name)) { + values.add(unmarshaller.unmarshal(e.value)); + e.remove(); + HeaderEntry next = e.next; + if (next != null) { + entries[i] = next; + e = next; + } else { + entries[i] = null; + Collections.reverse(values); + return values; + } + } else { + break; + } + } + + for (;;) { + HeaderEntry next = e.next; + if (next == null) { + break; + } + if (next.hash == h && nameEquals(next.name, name)) { + values.add(unmarshaller.unmarshal(next.getValue())); + e.next = next.next; + next.remove(); + } else { + e = next; + } + } + + Collections.reverse(values); + return values; + } + + public List> entries() { + int cnt = 0; + int size = size(); + @SuppressWarnings("unchecked") + Map.Entry[] all = new Map.Entry[size]; + + HeaderEntry e = head.after; + while (e != head) { + all[cnt ++] = e; + e = e.after; + } + + assert size == cnt; + return Arrays.asList(all); + } + + @Override + public Iterator> iterator() { + return new HeaderIterator(); + } + + public boolean contains(CharSequence name) { + return get(name) != null; + } + + public int size() { + return size; + } + + public boolean isEmpty() { + return head == head.after; + } + + public boolean contains(CharSequence name, CharSequence value) { + return contains(name, value, false); + } + + public boolean contains(CharSequence name, CharSequence value, boolean ignoreCase) { + checkNotNull(name, "name"); + checkNotNull(value, "value"); + int h = hashCode(name); + int i = index(h); + HeaderEntry e = entries[i]; + while (e != null) { + if (e.hash == h && nameEquals(e.name, name)) { + if (valueEquals(e.value, value, ignoreCase)) { + return true; + } + } + e = e.next; + } + return false; + } + + public Set names() { + return names(ignoreCase); + } + + /** + * Get the set of names for all text headers + * @param caseInsensitive {@code true} if names should be added in a case insensitive + * @return The set of names for all text headers + */ + public Set names(boolean caseInsensitive) { + final Set names = + caseInsensitive ? new TreeSet( + AsciiString.CHARSEQUENCE_CASE_INSENSITIVE_ORDER) + : new LinkedHashSet(size()); + forEachName(new NameVisitor() { + @Override + public boolean visit(CharSequence name) { + names.add(name); + return true; + } + }); + return names; + } + + public HeaderMap forEachEntry(EntryVisitor visitor) { + HeaderEntry e = head.after; + while (e != head) { + if (!visitor.visit(e)) { + break; + } + e = e.after; + } + return this; + } + + public void forEachName(NameVisitor visitor) { + HeaderEntry e = head.after; + while (e != head) { + if (!visitor.visit(e.getKey())) { + return; + } + e = e.after; + } + } + + @Override + public int hashCode() { + int result = 1; + for (CharSequence name : names()) { + result = HASH_CODE_PRIME * result + name.hashCode(); + Set values = new TreeSet(getAll(name)); + for (CharSequence value : values) { + result = HASH_CODE_PRIME * result + value.hashCode(); + } + } + return result; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof HeaderMap)) { + return false; + } + + // First, check that the set of names match. + HeaderMap h2 = (HeaderMap) o; + Set names = names(); + if (!names.equals(h2.names())) { + return false; + } + + // Compare the values for each name. + for (CharSequence name : names) { + List values = getAll(name); + List otherValues = h2.getAll(name); + if (values.size() != otherValues.size()) { + return false; + } + + // Convert the values to a set and remove values from the other object to see if + // they match. + Set valueSet = new HashSet(values); + valueSet.removeAll(otherValues); + if (!valueSet.isEmpty()) { + return false; + } + } + + return true; + } + + @Override + public String toString() { + StringBuilder builder = + new StringBuilder('['); + Set names = names(true); + for (CharSequence name : names) { + Set valueSet = new TreeSet(getAll(name)); + for (CharSequence value : valueSet) { + builder.append(name).append(": ").append(value).append(", "); + } + } + // Now remove the last ", " if there is one. + if (builder.length() >= 3) { + builder.setLength(builder.length() - 2); + } + return builder.append("]").toString(); + } + + private boolean nameEquals(CharSequence a, CharSequence b) { + return equals(a, b, ignoreCase); + } + + private static boolean valueEquals(CharSequence a, CharSequence b, boolean ignoreCase) { + return equals(a, b, ignoreCase); + } + + private static boolean equals(CharSequence a, CharSequence b, boolean ignoreCase) { + if (ignoreCase) { + return AsciiString.equalsIgnoreCase(a, b); + } else { + return AsciiString.equals(a, b); + } + } + + private static int index(int hash) { + return Math.abs(hash % BUCKET_SIZE); + } + + private CharSequence convertName(CharSequence name) { + return nameConverter.convertName(checkNotNull(name, "name")); + } + + private static T checkNotNull(T value, String name) { + if (value == null) { + throw new NullPointerException(name); + } + return value; + } + + private static int hashCode(CharSequence name) { + return AsciiString.caseInsensitiveHashCode(name); + } + + private final class HeaderEntry implements Map.Entry { + final int hash; + final CharSequence name; + CharSequence value; + HeaderEntry next; + HeaderEntry before, after; + + HeaderEntry(int hash, CharSequence name, CharSequence value) { + this.hash = hash; + this.name = name; + this.value = value; + } + + HeaderEntry() { + hash = -1; + name = null; + value = null; + } + + void remove() { + before.after = after; + after.before = before; + --size; + } + + void addBefore(HeaderEntry e) { + after = e; + before = e.before; + before.after = this; + after.before = this; + ++size; + } + + @Override + public CharSequence getKey() { + return name; + } + + @Override + public CharSequence getValue() { + return value; + } + + @Override + public CharSequence setValue(CharSequence value) { + checkNotNull(value, "value"); + checkNotNull(value, "value"); + CharSequence oldValue = this.value; + this.value = value; + return oldValue; + } + + @Override + public String toString() { + return new StringBuilder(name).append('=').append(value).toString(); + } + } + + protected final class HeaderIterator implements Iterator> { + + private HeaderEntry current = head; + + @Override + public boolean hasNext() { + return current.after != head; + } + + @Override + public Entry next() { + current = current.after; + + if (current == head) { + throw new NoSuchElementException(); + } + + return current; + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + } +} diff --git a/codec/src/test/java/io/netty/handler/codec/HeaderMapTest.java b/codec/src/test/java/io/netty/handler/codec/HeaderMapTest.java new file mode 100644 index 0000000000..c6662ed379 --- /dev/null +++ b/codec/src/test/java/io/netty/handler/codec/HeaderMapTest.java @@ -0,0 +1,314 @@ +/* + * 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: + * + * 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; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Random; +import java.util.Set; + +import org.junit.Test; + +/** + * Tests for {@link HeaderMap}. + */ +public class HeaderMapTest { + + @Test + public void binaryHeadersWithSameValuesShouldBeEquivalent() { + byte[] key1 = randomBytes(); + byte[] value1 = randomBytes(); + byte[] key2 = randomBytes(); + byte[] value2 = randomBytes(); + + HeaderMap h1 = new HeaderMap(false); + h1.set(as(key1), as(value1)); + h1.set(as(key2), as(value2)); + + HeaderMap h2 = new HeaderMap(false); + 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)); + } + + @Test + public void binaryHeadersWithSameDuplicateValuesShouldBeEquivalent() { + byte[] k1 = randomBytes(); + byte[] k2 = randomBytes(); + byte[] v1 = randomBytes(); + byte[] v2 = randomBytes(); + byte[] v3 = randomBytes(); + byte[] v4 = randomBytes(); + + HeaderMap h1 = new HeaderMap(false); + h1.set(as(k1), as(v1)); + h1.set(as(k2), as(v2)); + h1.add(as(k2), as(v3)); + h1.add(as(k1), as(v4)); + + HeaderMap h2 = new HeaderMap(false); + 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)); + } + + @Test + public void binaryHeadersWithDifferentValuesShouldNotBeEquivalent() { + byte[] k1 = randomBytes(); + byte[] k2 = randomBytes(); + byte[] v1 = randomBytes(); + byte[] v2 = randomBytes(); + byte[] v3 = randomBytes(); + byte[] v4 = randomBytes(); + + HeaderMap h1 = new HeaderMap(false); + h1.set(as(k1), as(v1)); + h1.set(as(k2), as(v2)); + h1.add(as(k2), as(v3)); + h1.add(as(k1), as(v4)); + + HeaderMap h2 = new HeaderMap(false); + 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)); + } + + @Test + public void binarySetAllShouldMergeHeaders() { + byte[] k1 = randomBytes(); + byte[] k2 = randomBytes(); + byte[] v1 = randomBytes(); + byte[] v2 = randomBytes(); + byte[] v3 = randomBytes(); + byte[] v4 = randomBytes(); + + HeaderMap h1 = new HeaderMap(false); + h1.set(as(k1), as(v1)); + h1.set(as(k2), as(v2)); + h1.add(as(k2), as(v3)); + h1.add(as(k1), as(v4)); + + HeaderMap h2 = new HeaderMap(false); + h2.set(as(k1), as(v1)); + h2.set(as(k2), as(v2)); + h2.add(as(k1), as(v4)); + + HeaderMap expected = new HeaderMap(false); + 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)); + + h1.setAll(h2); + + assertEquals(expected, h1); + } + + @Test + public void binarySetShouldReplacePreviousValues() { + byte[] k1 = randomBytes(); + byte[] v1 = randomBytes(); + byte[] v2 = randomBytes(); + byte[] v3 = randomBytes(); + + HeaderMap h1 = new HeaderMap(false); + h1.add(as(k1), as(v1)); + h1.add(as(k1), as(v2)); + assertEquals(2, h1.size()); + + h1.set(as(k1), as(v3)); + assertEquals(1, h1.size()); + List list = h1.getAll(as(k1)); + assertEquals(1, list.size()); + assertEquals(as(v3), list.get(0)); + } + + @Test + public void headersWithSameValuesShouldBeEquivalent() { + HeaderMap h1 = new HeaderMap(); + h1.set("foo", "goo"); + h1.set("foo2", "goo2"); + + HeaderMap h2 = new HeaderMap(); + 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 headersWithSameDuplicateValuesShouldBeEquivalent() { + HeaderMap h1 = new HeaderMap(); + h1.set("foo", "goo"); + h1.set("foo2", "goo2"); + h1.add("foo2", "goo3"); + h1.add("foo", "goo4"); + + HeaderMap h2 = new HeaderMap(); + 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 headersWithDifferentValuesShouldNotBeEquivalent() { + HeaderMap h1 = new HeaderMap(); + h1.set("foo", "goo"); + h1.set("foo2", "goo2"); + h1.add("foo2", "goo3"); + h1.add("foo", "goo4"); + + HeaderMap h2 = new HeaderMap(); + 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 setAllShouldMergeHeaders() { + HeaderMap h1 = new HeaderMap(); + h1.set("foo", "goo"); + h1.set("foo2", "goo2"); + h1.add("foo2", "goo3"); + h1.add("foo", "goo4"); + + HeaderMap h2 = new HeaderMap(); + h2.set("foo", "goo"); + h2.set("foo2", "goo2"); + h2.add("foo", "goo4"); + + HeaderMap expected = new HeaderMap(); + 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 setShouldReplacePreviousValues() { + HeaderMap h1 = new HeaderMap(); + h1.add("foo", "goo"); + h1.add("foo", "goo2"); + assertEquals(2, h1.size()); + + h1.set("foo", "goo3"); + assertEquals(1, h1.size()); + List list = h1.getAll("foo"); + assertEquals(1, list.size()); + assertEquals("goo3", list.get(0)); + } + + @Test(expected = NoSuchElementException.class) + public void iterateEmptyHeadersShouldThrow() { + Iterator> iterator = + new HeaderMap().iterator(); + assertFalse(iterator.hasNext()); + iterator.next(); + } + + @Test + public void iterateHeadersShouldReturnAllValues() { + Set headers = new HashSet(); + headers.add("a:1"); + headers.add("a:2"); + headers.add("a:3"); + headers.add("b:1"); + headers.add("b:2"); + headers.add("c:1"); + + // Build the headers from the input set. + HeaderMap h1 = new HeaderMap(); + for (String header : headers) { + String[] parts = header.split(":"); + h1.add(parts[0], parts[1]); + } + + // Now iterate through the headers, removing them from the original set. + for (Map.Entry entry : h1) { + assertTrue(headers + .remove(entry.getKey().toString() + ':' + entry.getValue().toString())); + } + + // Make sure we removed them all. + assertTrue(headers.isEmpty()); + } + + @Test + public void getAndRemoveShouldReturnFirstEntry() { + HeaderMap h1 = new HeaderMap(); + h1.add("foo", "goo"); + h1.add("foo", "goo2"); + assertEquals("goo", h1.getAndRemove("foo")); + assertEquals(0, h1.size()); + List values = h1.getAll("foo"); + assertEquals(0, values.size()); + } + + private static byte[] randomBytes() { + byte[] data = new byte[100]; + new Random().nextBytes(data); + return data; + } + + private String as(byte[] bytes) { + return new String(bytes); + } +} diff --git a/example/src/main/java/io/netty/example/http2/client/HttpResponseHandler.java b/example/src/main/java/io/netty/example/http2/client/HttpResponseHandler.java index eb1e9519d5..93d1902b56 100644 --- a/example/src/main/java/io/netty/example/http2/client/HttpResponseHandler.java +++ b/example/src/main/java/io/netty/example/http2/client/HttpResponseHandler.java @@ -76,7 +76,7 @@ public class HttpResponseHandler extends SimpleChannelInboundHandler