Adding support for HTTP/2 binary headers
Motivation: The HTTP/2 spec does not restrict headers to being String. The current implementation of the HTTP/2 codec uses Strings as header keys and values. We should change this so that header keys and values allow binary values. Modifications: Making Http2Headers based on AsciiString, which is a wrapper around a byte[]. Various changes throughout the HTTP/2 codec to use the new interface. Result: HTTP/2 codec no longer requires string headers.
This commit is contained in:
parent
94deea409e
commit
43d097d25a
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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<String> getAll(CharSequence name) {
|
||||
if (name == null) {
|
||||
throw new NullPointerException("name");
|
||||
}
|
||||
|
||||
LinkedList<String> values = new LinkedList<String>();
|
||||
|
||||
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<AsciiString> values) {
|
||||
super.add(name, values);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Entry<String, String>> entries() {
|
||||
List<Map.Entry<String, String>> all = new LinkedList<Map.Entry<String, String>>();
|
||||
|
||||
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<AsciiString> values) {
|
||||
super.set(name, values);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> names() {
|
||||
Set<String> names = new TreeSet<String>();
|
||||
|
||||
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<Entry<String, String>> 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<String> getAll(CharSequence name) {
|
||||
if (name == null) {
|
||||
throw new NullPointerException("name");
|
||||
}
|
||||
|
||||
LinkedList<String> values = new LinkedList<String>();
|
||||
|
||||
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<String, String> 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<Map.Entry<String, String>> {
|
||||
|
||||
private HeaderEntry current = head;
|
||||
|
||||
@Override
|
||||
public boolean hasNext() {
|
||||
return current.after != head;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Entry<String, String> 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<String, String> {
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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<String> sensitiveHeaders = new TreeSet<String>(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<String, String> 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);
|
||||
}
|
||||
}
|
||||
|
@ -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<String, String> 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);
|
||||
|
@ -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<AsciiString> 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<AsciiString> 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());
|
||||
}
|
||||
}
|
@ -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<Entry<String, String>> {
|
||||
|
||||
public static final Http2Headers EMPTY_HEADERS = new Http2Headers() {
|
||||
|
||||
@Override
|
||||
public String get(CharSequence name) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getAll(CharSequence name) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Entry<String, String>> 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<String> names() {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<Entry<String, String>> 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<Entry<String, String>> {
|
||||
/**
|
||||
* {@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<AsciiString> PSEUDO_HEADERS = new HashSet<AsciiString>();
|
||||
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<String> names();
|
||||
@Override
|
||||
Http2Headers add(AsciiString name, AsciiString value);
|
||||
|
||||
@Override
|
||||
Http2Headers add(AsciiString name, Iterable<AsciiString> 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<AsciiString> 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<String> 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<Map.Entry<String, String>> 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
|
||||
* <ul>
|
||||
* <li>{@code true} if the processor wants to continue the loop and handle the entry.</li>
|
||||
* <li>{@code false} if the processor wants to stop handling headers and abort the loop.</li>
|
||||
* </ul>
|
||||
*/
|
||||
boolean visit(Map.Entry<String, String> 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.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* 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<String> 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 <b>not</b> 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<String> values = new TreeSet<String>(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<String> names = names();
|
||||
if (!names.equals(other.names())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Compare the values for each name.
|
||||
for (String name : names) {
|
||||
List<String> values = getAll(name);
|
||||
List<String> 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<String> valueSet = new HashSet<String>(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<String, String> header : this) {
|
||||
builder.append(header.getKey());
|
||||
builder.append(':');
|
||||
builder.append(header.getValue());
|
||||
builder.append(',');
|
||||
}
|
||||
builder.append(']');
|
||||
return builder.toString();
|
||||
}
|
||||
AsciiString status();
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -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<CharSequence> HTTP_TO_HTTP2_HEADER_BLACKLIST = new HashSet<CharSequence>();
|
||||
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}
|
||||
* <p>
|
||||
* {@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}
|
||||
* <p>
|
||||
* {@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}
|
||||
* <p>
|
||||
* {@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}
|
||||
* <p>
|
||||
* {@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}
|
||||
* <p>
|
||||
* {@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
|
||||
* <p>
|
||||
* {@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.
|
||||
* <p>
|
||||
* {@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}
|
||||
* <p>
|
||||
* {@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}
|
||||
* <p>
|
||||
* {@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}
|
||||
* <p>
|
||||
* {@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}
|
||||
* <p>
|
||||
* {@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
|
||||
* <p>
|
||||
* {@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.
|
||||
* <p>
|
||||
* {@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}
|
||||
* <p>
|
||||
* {@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
|
||||
* <ul>
|
||||
* <li>{@code true} to validate HTTP headers in the http-codec</li>
|
||||
* <li>{@code false} not to validate HTTP headers in the http-codec</li>
|
||||
* </ul>
|
||||
* @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
|
||||
* <ul>
|
||||
* <li>{@code true} to validate HTTP headers in the http-codec</li>
|
||||
* <li>{@code false} not to validate HTTP headers in the http-codec</li>
|
||||
* </ul>
|
||||
* @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<AsciiString, String> REQUEST_HEADER_TRANSLATIONS =
|
||||
new HashMap<AsciiString, String>();
|
||||
private static final Map<AsciiString, String> RESPONSE_HEADER_TRANSLATIONS =
|
||||
new HashMap<AsciiString, String>();
|
||||
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<AsciiString, String> 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
|
||||
* <ul>
|
||||
* <li>{@code null} if no exceptions where encountered</li>
|
||||
* <li>Otherwise an exception describing what went wrong</li>
|
||||
* </ul>
|
||||
*/
|
||||
public Http2Exception cause() {
|
||||
return e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 <a href="http://tools.ietf.org/html/draft-ietf-httpbis-http2-14#section-8.1.">HTTP/2 Spec Message Flow</a>
|
||||
@ -48,27 +36,6 @@ public class InboundHttp2ToHttpAdapter extends Http2EventAdapter {
|
||||
private final ImmediateSendDetector sendDetector;
|
||||
protected final IntObjectMap<FullHttpMessage> messageMap;
|
||||
|
||||
private static final Set<String> HEADERS_TO_EXCLUDE;
|
||||
private static final Map<String, String> HEADER_NAME_TRANSLATIONS_REQUEST;
|
||||
private static final Map<String, String> HEADER_NAME_TRANSLATIONS_RESPONSE;
|
||||
|
||||
static {
|
||||
HEADERS_TO_EXCLUDE = new HashSet<String>();
|
||||
HEADER_NAME_TRANSLATIONS_REQUEST = new HashMap<String, String>();
|
||||
HEADER_NAME_TRANSLATIONS_RESPONSE = new HashMap<String, String>();
|
||||
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
|
||||
* <ul>
|
||||
* <li>{@code true} to validate HTTP headers in the http-codec</li>
|
||||
* <li>{@code false} not to validate HTTP headers in the http-codec</li>
|
||||
* </ul>
|
||||
* @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
|
||||
* <ul>
|
||||
* <li>{@code true} to validate HTTP headers in the http-codec</li>
|
||||
* <li>{@code false} not to validate HTTP headers in the http-codec</li>
|
||||
* </ul>
|
||||
* @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<String, String> 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<String, String> 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<String, String> translations) {
|
||||
this.translations = translations;
|
||||
this.headers = headers;
|
||||
this.e = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean visit(Entry<String, String> 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
|
||||
* <ul>
|
||||
* <li>{@code null} if no exceptions where encountered</li>
|
||||
* <li>Otherwise an exception describing what went wrong</li>
|
||||
* </ul>
|
||||
*/
|
||||
public Http2Exception exception() {
|
||||
return e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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<HttpHeaders> 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);
|
||||
}
|
||||
}
|
||||
|
@ -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<ByteBuf> 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();
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
|
@ -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"));
|
||||
}
|
||||
}
|
||||
|
@ -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<String> 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<String> 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<Map.Entry<String, String>> iterator =
|
||||
DefaultHttp2Headers.newBuilder().build().iterator();
|
||||
assertFalse(iterator.hasNext());
|
||||
iterator.next();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void iterateHeadersShouldReturnAllValues() {
|
||||
Set<String> headers = new HashSet<String>();
|
||||
headers.add("a:1");
|
||||
headers.add("a:2");
|
||||
headers.add("a:3");
|
||||
headers.add("b:1");
|
||||
headers.add("b:2");
|
||||
headers.add("c:1");
|
||||
|
||||
// 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<String, String> 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");
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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<ByteBuf> 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);
|
||||
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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() {
|
||||
|
@ -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<CharSequence> {
|
||||
|
||||
public static final AsciiString EMPTY_STRING = new AsciiString("");
|
||||
public static final Comparator<AsciiString> CASE_INSENSITIVE_ORDER = new Comparator<AsciiString>() {
|
||||
@Override
|
||||
public int compare(AsciiString o1, AsciiString o2) {
|
||||
return CHARSEQUENCE_CASE_INSENSITIVE_ORDER.compare(o1, o2);
|
||||
}
|
||||
};
|
||||
|
||||
public static final Comparator<CharSequence> CHARSEQUENCE_CASE_INSENSITIVE_ORDER =
|
||||
new Comparator<CharSequence>() {
|
||||
@Override
|
||||
public int compare(CharSequence o1, CharSequence o2) {
|
||||
if (o1 == o2) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
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<CharSequence>
|
||||
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<CharSequence>
|
||||
* 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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
369
codec/src/main/java/io/netty/handler/codec/BinaryHeaders.java
Normal file
369
codec/src/main/java/io/netty/handler/codec/BinaryHeaders.java
Normal file
@ -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<Map.Entry<AsciiString, AsciiString>> {
|
||||
|
||||
/**
|
||||
* A visitor that helps reduce GC pressure while iterating over a collection of {@link BinaryHeaders}.
|
||||
*/
|
||||
public interface BinaryHeaderVisitor {
|
||||
/**
|
||||
* @return
|
||||
* <ul>
|
||||
* <li>{@code true} if the processor wants to continue the loop and handle the entry.</li>
|
||||
* <li>{@code false} if the processor wants to stop handling headers and abort the loop.</li>
|
||||
* </ul>
|
||||
*/
|
||||
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<AsciiString> 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<AsciiString> 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<Entry<AsciiString, AsciiString>> 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<AsciiString> 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 <a href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.3.1">RFC2616</a>.
|
||||
*
|
||||
* @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:
|
||||
* <pre>
|
||||
* for (Object v: values) {
|
||||
* if (v == null) {
|
||||
* break;
|
||||
* }
|
||||
* headers.add(name, v);
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* @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<AsciiString> values);
|
||||
|
||||
/**
|
||||
* Adds a new header with the specified name and values.
|
||||
*
|
||||
* This getMethod can be represented approximately as the following code:
|
||||
* <pre>
|
||||
* for (Object v: values) {
|
||||
* if (v == null) {
|
||||
* break;
|
||||
* }
|
||||
* headers.add(name, v);
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* @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
|
||||
* <a href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.3.1">RFC2616</a>.
|
||||
*
|
||||
* @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:
|
||||
* <pre>
|
||||
* headers.remove(name);
|
||||
* for (Object v: values) {
|
||||
* if (v == null) {
|
||||
* break;
|
||||
* }
|
||||
* headers.add(name, v);
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* @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<AsciiString> 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:
|
||||
* <pre>
|
||||
* headers.remove(name);
|
||||
* for (Object v: values) {
|
||||
* if (v == null) {
|
||||
* break;
|
||||
* }
|
||||
* headers.add(name, v);
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* @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<Entry<AsciiString, AsciiString>> 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<AsciiString> values = new TreeSet<AsciiString>(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<AsciiString> names = h1.names();
|
||||
if (!names.equals(h2.names())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Compare the values for each name.
|
||||
for (AsciiString name : names) {
|
||||
List<AsciiString> values = h1.getAll(name);
|
||||
List<AsciiString> 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<AsciiString> valueSet = new HashSet<AsciiString>(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<AsciiString> names = new TreeSet<AsciiString>(headers.names());
|
||||
for (AsciiString name : names) {
|
||||
Set<AsciiString> valueSet = new TreeSet<AsciiString>(headers.getAll(name));
|
||||
for (AsciiString value : valueSet) {
|
||||
if (!first) {
|
||||
builder.append(", ");
|
||||
}
|
||||
first = false;
|
||||
builder.append(name).append(": ").append(value);
|
||||
}
|
||||
}
|
||||
return builder.append("]").toString();
|
||||
}
|
||||
}
|
||||
}
|
@ -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<AsciiString> VALUE_UNMARSHALLER =
|
||||
new HeaderMap.ValueUnmarshaller<AsciiString>() {
|
||||
@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<AsciiString> 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<AsciiString> 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<AsciiString> getAll(AsciiString name) {
|
||||
return headers.getAll(name, VALUE_UNMARSHALLER);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AsciiString> getAllAndRemove(AsciiString name) {
|
||||
return headers.getAllAndRemove(name, VALUE_UNMARSHALLER);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Map.Entry<AsciiString, AsciiString>> entries() {
|
||||
int size = size();
|
||||
@SuppressWarnings("unchecked")
|
||||
final Map.Entry<AsciiString, AsciiString>[] all = new Map.Entry[size];
|
||||
|
||||
headers.forEachEntry(new HeaderMap.EntryVisitor() {
|
||||
int cnt;
|
||||
@Override
|
||||
public boolean visit(Entry<CharSequence, CharSequence> entry) {
|
||||
all[cnt++] = new AsciiStringHeaderEntry(entry);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
return Arrays.asList(all);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<Entry<AsciiString, AsciiString>> 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<AsciiString> 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<AsciiString> names(boolean caseInsensitive) {
|
||||
final Set<AsciiString> names = caseInsensitive ? new TreeSet<AsciiString>(AsciiString.CASE_INSENSITIVE_ORDER)
|
||||
: new LinkedHashSet<AsciiString>(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<CharSequence, CharSequence> 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 <T> void checkNotNull(T value, String name) {
|
||||
if (value == null) {
|
||||
throw new NullPointerException(name);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class AsciiStringHeaderEntry implements Map.Entry<AsciiString, AsciiString> {
|
||||
private final Entry<CharSequence, CharSequence> entry;
|
||||
|
||||
AsciiStringHeaderEntry(Entry<CharSequence, CharSequence> 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<Map.Entry<AsciiString, AsciiString>> {
|
||||
|
||||
private Iterator<Entry<CharSequence, CharSequence>> iter = headers.iterator();
|
||||
|
||||
@Override
|
||||
public boolean hasNext() {
|
||||
return iter.hasNext();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Entry<AsciiString, AsciiString> next() {
|
||||
Entry<CharSequence, CharSequence> entry = iter.next();
|
||||
return new AsciiStringHeaderEntry(entry);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remove() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}
|
||||
}
|
@ -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<String> VALUE_UNMARSHALLER =
|
||||
new HeaderMap.ValueUnmarshaller<String>() {
|
||||
@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<CharSequence, CharSequence> 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<CharSequence, CharSequence> 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<CharSequence> getAllUnconverted(CharSequence name) {
|
||||
if (name == null) {
|
||||
throw new NullPointerException("name");
|
||||
}
|
||||
|
||||
List<CharSequence> values = new ArrayList<CharSequence>(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<String> getAll(CharSequence name) {
|
||||
if (name == null) {
|
||||
throw new NullPointerException("name");
|
||||
}
|
||||
|
||||
List<String> values = new ArrayList<String>(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<String> 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<String> values = new ArrayList<String>(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<CharSequence> 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<CharSequence> values = new ArrayList<CharSequence>(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<Map.Entry<String, String>> entries() {
|
||||
int cnt = 0;
|
||||
int size = size();
|
||||
@SuppressWarnings("unchecked")
|
||||
Map.Entry<String, String>[] all = new Map.Entry[size];
|
||||
final Map.Entry<String, String>[] 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<CharSequence, CharSequence> entry) {
|
||||
all[cnt++] = new StringHeaderEntry(entry);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
assert size == cnt;
|
||||
return Arrays.asList(all);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Map.Entry<CharSequence, CharSequence>> unconvertedEntries() {
|
||||
int cnt = 0;
|
||||
int size = size();
|
||||
@SuppressWarnings("unchecked")
|
||||
Map.Entry<CharSequence, CharSequence>[] 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<Entry<CharSequence, CharSequence>> 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<CharSequence> unconvertedNames() {
|
||||
Set<CharSequence> names = new LinkedHashSet<CharSequence>(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<String> names(boolean caseInsensitive) {
|
||||
Set<String> names = caseInsensitive ? new TreeSet<String>(String.CASE_INSENSITIVE_ORDER)
|
||||
: new LinkedHashSet<String>(size());
|
||||
HeaderEntry e = head.after;
|
||||
while (e != head) {
|
||||
names.add(e.getKey().toString());
|
||||
e = e.after;
|
||||
}
|
||||
final Set<String> names =
|
||||
caseInsensitive ? new TreeSet<String>(String.CASE_INSENSITIVE_ORDER)
|
||||
: new LinkedHashSet<String>(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<CharSequence, CharSequence> 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<String> values = new TreeSet<String>(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<CharSequence, CharSequence> {
|
||||
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 <T> 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<Map.Entry<CharSequence, CharSequence>> {
|
||||
private final class StringHeaderIterator implements Iterator<Entry<String, String>> {
|
||||
|
||||
private HeaderEntry current = head;
|
||||
private Iterator<Entry<CharSequence, CharSequence>> iter = headers.iterator();
|
||||
|
||||
@Override
|
||||
public boolean hasNext() {
|
||||
return current.after != head;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Entry<CharSequence, CharSequence> 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<Map.Entry<String, String>> {
|
||||
|
||||
private HeaderEntry current = head;
|
||||
|
||||
@Override
|
||||
public boolean hasNext() {
|
||||
return current.after != head;
|
||||
return iter.hasNext();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Entry<String, String> next() {
|
||||
current = current.after;
|
||||
Entry<CharSequence, CharSequence> next = iter.next();
|
||||
|
||||
if (current == head) {
|
||||
throw new NoSuchElementException();
|
||||
}
|
||||
|
||||
return new StringHeaderEntry(current);
|
||||
return new StringHeaderEntry(next);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -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<AsciiString> getAll(AsciiString name) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AsciiString> getAllAndRemove(AsciiString name) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Entry<AsciiString, AsciiString>> 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<AsciiString> names() {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
|
||||
@Override
|
||||
public BinaryHeaders add(AsciiString name, AsciiString value) {
|
||||
throw new UnsupportedOperationException("read only");
|
||||
}
|
||||
|
||||
@Override
|
||||
public BinaryHeaders add(AsciiString name, Iterable<AsciiString> 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<AsciiString> 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<Entry<AsciiString, AsciiString>> 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);
|
||||
}
|
||||
}
|
844
codec/src/main/java/io/netty/handler/codec/HeaderMap.java
Normal file
844
codec/src/main/java/io/netty/handler/codec/HeaderMap.java
Normal file
@ -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<Entry<CharSequence, CharSequence>> {
|
||||
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<CharSequence, CharSequence> 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> {
|
||||
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<? extends 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 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<? extends 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, 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<CharSequence> getAll(CharSequence name) {
|
||||
checkNotNull(name, "name");
|
||||
|
||||
List<CharSequence> values = new ArrayList<CharSequence>(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<CharSequence> getAllAndRemove(CharSequence name) {
|
||||
checkNotNull(name, "name");
|
||||
int h = hashCode(name);
|
||||
int i = index(h);
|
||||
HeaderEntry e = entries[i];
|
||||
if (e == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
List<CharSequence> values = new ArrayList<CharSequence>(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 <T> List<T> getAll(CharSequence name, ValueUnmarshaller<T> unmarshaller) {
|
||||
checkNotNull(name, "name");
|
||||
checkNotNull(unmarshaller, "unmarshaller");
|
||||
|
||||
List<T> values = new ArrayList<T>(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 <T> List<T> getAllAndRemove(CharSequence name, ValueUnmarshaller<T> 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<T> values = new ArrayList<T>(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<Map.Entry<CharSequence, CharSequence>> entries() {
|
||||
int cnt = 0;
|
||||
int size = size();
|
||||
@SuppressWarnings("unchecked")
|
||||
Map.Entry<CharSequence, CharSequence>[] 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<Entry<CharSequence, CharSequence>> 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<CharSequence> 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<CharSequence> names(boolean caseInsensitive) {
|
||||
final Set<CharSequence> names =
|
||||
caseInsensitive ? new TreeSet<CharSequence>(
|
||||
AsciiString.CHARSEQUENCE_CASE_INSENSITIVE_ORDER)
|
||||
: new LinkedHashSet<CharSequence>(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<CharSequence> values = new TreeSet<CharSequence>(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<CharSequence> names = names();
|
||||
if (!names.equals(h2.names())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Compare the values for each name.
|
||||
for (CharSequence name : names) {
|
||||
List<CharSequence> values = getAll(name);
|
||||
List<CharSequence> 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<CharSequence> valueSet = new HashSet<CharSequence>(values);
|
||||
valueSet.removeAll(otherValues);
|
||||
if (!valueSet.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder builder =
|
||||
new StringBuilder('[');
|
||||
Set<CharSequence> names = names(true);
|
||||
for (CharSequence name : names) {
|
||||
Set<CharSequence> valueSet = new TreeSet<CharSequence>(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> 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<CharSequence, CharSequence> {
|
||||
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<Entry<CharSequence, CharSequence>> {
|
||||
|
||||
private HeaderEntry current = head;
|
||||
|
||||
@Override
|
||||
public boolean hasNext() {
|
||||
return current.after != head;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Entry<CharSequence, CharSequence> next() {
|
||||
current = current.after;
|
||||
|
||||
if (current == head) {
|
||||
throw new NoSuchElementException();
|
||||
}
|
||||
|
||||
return current;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remove() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}
|
||||
}
|
314
codec/src/test/java/io/netty/handler/codec/HeaderMapTest.java
Normal file
314
codec/src/test/java/io/netty/handler/codec/HeaderMapTest.java
Normal file
@ -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<CharSequence> 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<CharSequence> list = h1.getAll("foo");
|
||||
assertEquals(1, list.size());
|
||||
assertEquals("goo3", list.get(0));
|
||||
}
|
||||
|
||||
@Test(expected = NoSuchElementException.class)
|
||||
public void iterateEmptyHeadersShouldThrow() {
|
||||
Iterator<Map.Entry<CharSequence, CharSequence>> iterator =
|
||||
new HeaderMap().iterator();
|
||||
assertFalse(iterator.hasNext());
|
||||
iterator.next();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void iterateHeadersShouldReturnAllValues() {
|
||||
Set<String> headers = new HashSet<String>();
|
||||
headers.add("a:1");
|
||||
headers.add("a:2");
|
||||
headers.add("a:3");
|
||||
headers.add("b:1");
|
||||
headers.add("b:2");
|
||||
headers.add("c:1");
|
||||
|
||||
// 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<CharSequence, CharSequence> 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<CharSequence> 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);
|
||||
}
|
||||
}
|
@ -76,7 +76,7 @@ public class HttpResponseHandler extends SimpleChannelInboundHandler<FullHttpRes
|
||||
|
||||
@Override
|
||||
protected void messageReceived(ChannelHandlerContext ctx, FullHttpResponse msg) throws Exception {
|
||||
String streamIdText = msg.headers().get(HttpUtil.ExtensionHeaders.Names.STREAM_ID);
|
||||
String streamIdText = msg.headers().get(HttpUtil.ExtensionHeaderNames.STREAM_ID.text());
|
||||
if (streamIdText == null) {
|
||||
System.err.println("HttpResponseHandler unexpected message received: " + msg);
|
||||
return;
|
||||
|
@ -21,6 +21,7 @@ import static io.netty.example.http2.Http2ExampleUtil.UPGRADE_RESPONSE_HEADER;
|
||||
import static io.netty.util.internal.logging.InternalLogLevel.INFO;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.handler.codec.AsciiString;
|
||||
import io.netty.handler.codec.http.HttpServerUpgradeHandler;
|
||||
import io.netty.handler.codec.http2.AbstractHttp2ConnectionHandler;
|
||||
import io.netty.handler.codec.http2.DefaultHttp2Connection;
|
||||
@ -68,8 +69,8 @@ public class HelloWorldHttp2Handler extends AbstractHttp2ConnectionHandler {
|
||||
if (evt instanceof HttpServerUpgradeHandler.UpgradeEvent) {
|
||||
// Write an HTTP/2 response to the upgrade request
|
||||
Http2Headers headers =
|
||||
DefaultHttp2Headers.newBuilder().status("200")
|
||||
.set(UPGRADE_RESPONSE_HEADER, "true").build();
|
||||
new DefaultHttp2Headers().status(new AsciiString("200"))
|
||||
.set(new AsciiString(UPGRADE_RESPONSE_HEADER), new AsciiString("true"));
|
||||
writeHeaders(ctx, 1, headers, 0, true, ctx.newPromise());
|
||||
}
|
||||
super.userEventTriggered(ctx, evt);
|
||||
@ -109,7 +110,7 @@ public class HelloWorldHttp2Handler extends AbstractHttp2ConnectionHandler {
|
||||
*/
|
||||
private void sendResponse(ChannelHandlerContext ctx, int streamId, ByteBuf payload) {
|
||||
// Send a frame for the response status
|
||||
Http2Headers headers = DefaultHttp2Headers.newBuilder().status("200").build();
|
||||
Http2Headers headers = new DefaultHttp2Headers().status(new AsciiString("200"));
|
||||
writeHeaders(ctx(), streamId, headers, 0, false, ctx().newPromise());
|
||||
|
||||
writeData(ctx(), streamId, payload, 0, true, ctx().newPromise());
|
||||
|
Loading…
Reference in New Issue
Block a user