diff --git a/NOTICE.txt b/NOTICE.txt
index 0015bfcc95..5ce1082385 100644
--- a/NOTICE.txt
+++ b/NOTICE.txt
@@ -254,3 +254,11 @@ This private header is also used by Apple's open source
* license/LICENSE.dnsinfo.txt (Apple Public Source License 2.0)
* HOMEPAGE:
* https://www.opensource.apple.com/source/configd/configd-453.19/dnsinfo/dnsinfo.h
+
+This product optionally depends on 'Brotli4j', Brotli compression and
+decompression for Java., which can be obtained at:
+
+ * LICENSE:
+ * license/LICENSE.brotli4j.txt (Apache License 2.0)
+ * HOMEPAGE:
+ * https://github.com/hyperxpro/Brotli4j
diff --git a/codec-http/pom.xml b/codec-http/pom.xml
index 84b33e7d5c..3a77e6d3d4 100644
--- a/codec-http/pom.xml
+++ b/codec-http/pom.xml
@@ -74,7 +74,7 @@
com.aayushatharva.brotli4j
brotli4j
- test
+ true
com.aayushatharva.brotli4j
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/HttpContentCompressor.java b/codec-http/src/main/java/io/netty/handler/codec/http/HttpContentCompressor.java
index bd233be1f1..1fe3af5e22 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/http/HttpContentCompressor.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/HttpContentCompressor.java
@@ -17,8 +17,15 @@ package io.netty.handler.codec.http;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.embedded.EmbeddedChannel;
+import io.netty.handler.codec.compression.Brotli;
+import io.netty.handler.codec.compression.BrotliEncoder;
import io.netty.handler.codec.compression.ZlibCodecFactory;
import io.netty.handler.codec.compression.ZlibWrapper;
+import io.netty.handler.codec.compression.BrotliOptions;
+import io.netty.handler.codec.compression.CompressionOptions;
+import io.netty.handler.codec.compression.DeflateOptions;
+import io.netty.handler.codec.compression.GzipOptions;
+import io.netty.handler.codec.compression.StandardCompressionOptions;
import io.netty.util.internal.ObjectUtil;
/**
@@ -30,6 +37,11 @@ import io.netty.util.internal.ObjectUtil;
*/
public class HttpContentCompressor extends HttpContentEncoder {
+ private final boolean supportsCompressionOptions;
+ private final BrotliOptions brotliOptions;
+ private final GzipOptions gzipOptions;
+ private final DeflateOptions deflateOptions;
+
private final int compressionLevel;
private final int windowBits;
private final int memLevel;
@@ -53,6 +65,7 @@ public class HttpContentCompressor extends HttpContentEncoder {
* best compression. {@code 0} means no compression. The default
* compression level is {@code 6}.
*/
+ @Deprecated
public HttpContentCompressor(int compressionLevel) {
this(compressionLevel, 15, 8, 0);
}
@@ -76,6 +89,7 @@ public class HttpContentCompressor extends HttpContentEncoder {
* memory. Larger values result in better and faster compression
* at the expense of memory usage. The default value is {@code 8}
*/
+ @Deprecated
public HttpContentCompressor(int compressionLevel, int windowBits, int memLevel) {
this(compressionLevel, windowBits, memLevel, 0);
}
@@ -103,11 +117,80 @@ public class HttpContentCompressor extends HttpContentEncoder {
* body exceeds the threshold. The value should be a non negative
* number. {@code 0} will enable compression for all responses.
*/
+ @Deprecated
public HttpContentCompressor(int compressionLevel, int windowBits, int memLevel, int contentSizeThreshold) {
this.compressionLevel = ObjectUtil.checkInRange(compressionLevel, 0, 9, "compressionLevel");
this.windowBits = ObjectUtil.checkInRange(windowBits, 9, 15, "windowBits");
this.memLevel = ObjectUtil.checkInRange(memLevel, 1, 9, "memLevel");
this.contentSizeThreshold = ObjectUtil.checkPositiveOrZero(contentSizeThreshold, "contentSizeThreshold");
+ this.brotliOptions = null;
+ this.gzipOptions = null;
+ this.deflateOptions = null;
+ supportsCompressionOptions = false;
+ }
+
+ /**
+ * Create a new {@link HttpContentCompressor} Instance with specified
+ * {@link CompressionOptions}s and contentSizeThreshold set to {@code 0}
+ *
+ * @param compressionOptions {@link CompressionOptions} or {@code null} if the default
+ * should be used.
+ */
+ public HttpContentCompressor(CompressionOptions... compressionOptions) {
+ this(0, compressionOptions);
+ }
+
+ /**
+ * Create a new {@link HttpContentCompressor} instance with specified
+ * {@link CompressionOptions}s
+ *
+ * @param contentSizeThreshold
+ * The response body is compressed when the size of the response
+ * body exceeds the threshold. The value should be a non negative
+ * number. {@code 0} will enable compression for all responses.
+ * @param compressionOptions {@link CompressionOptions} or {@code null}
+ * if the default should be used.
+ */
+ public HttpContentCompressor(int contentSizeThreshold, CompressionOptions... compressionOptions) {
+ this.contentSizeThreshold = ObjectUtil.checkPositiveOrZero(contentSizeThreshold, "contentSizeThreshold");
+ BrotliOptions brotliOptions = null;
+ GzipOptions gzipOptions = null;
+ DeflateOptions deflateOptions = null;
+ if (compressionOptions == null || compressionOptions.length == 0) {
+ brotliOptions = StandardCompressionOptions.brotli();
+ gzipOptions = StandardCompressionOptions.gzip();
+ deflateOptions = StandardCompressionOptions.deflate();
+ } else {
+ ObjectUtil.deepCheckNotNull("compressionOptionsIterable", compressionOptions);
+ for (CompressionOptions compressionOption : compressionOptions) {
+ if (compressionOption instanceof BrotliOptions) {
+ brotliOptions = (BrotliOptions) compressionOption;
+ } else if (compressionOption instanceof GzipOptions) {
+ gzipOptions = (GzipOptions) compressionOption;
+ } else if (compressionOption instanceof DeflateOptions) {
+ deflateOptions = (DeflateOptions) compressionOption;
+ } else {
+ throw new IllegalArgumentException("Unsupported " + CompressionOptions.class.getSimpleName() +
+ ": " + compressionOption);
+ }
+ }
+ if (brotliOptions == null) {
+ brotliOptions = StandardCompressionOptions.brotli();
+ }
+ if (gzipOptions == null) {
+ gzipOptions = StandardCompressionOptions.gzip();
+ }
+ if (deflateOptions == null) {
+ deflateOptions = StandardCompressionOptions.deflate();
+ }
+ }
+ this.brotliOptions = brotliOptions;
+ this.gzipOptions = gzipOptions;
+ this.deflateOptions = deflateOptions;
+ this.compressionLevel = -1;
+ this.windowBits = -1;
+ this.memLevel = -1;
+ supportsCompressionOptions = true;
}
@Override
@@ -131,30 +214,109 @@ public class HttpContentCompressor extends HttpContentEncoder {
return null;
}
- ZlibWrapper wrapper = determineWrapper(acceptEncoding);
- if (wrapper == null) {
- return null;
- }
+ if (supportsCompressionOptions) {
+ String targetContentEncoding = determineEncoding(acceptEncoding);
+ if (targetContentEncoding == null) {
+ return null;
+ }
- String targetContentEncoding;
- switch (wrapper) {
- case GZIP:
- targetContentEncoding = "gzip";
- break;
- case ZLIB:
- targetContentEncoding = "deflate";
- break;
- default:
+ if (targetContentEncoding.equals("gzip")) {
+ return new Result(targetContentEncoding,
+ new EmbeddedChannel(ctx.channel().id(), ctx.channel().metadata().hasDisconnect(),
+ ctx.channel().config(), ZlibCodecFactory.newZlibEncoder(
+ ZlibWrapper.GZIP, gzipOptions.compressionLevel()
+ , gzipOptions.windowBits(), gzipOptions.memLevel())));
+ }
+ if (targetContentEncoding.equals("deflate")) {
+ return new Result(targetContentEncoding,
+ new EmbeddedChannel(ctx.channel().id(), ctx.channel().metadata().hasDisconnect(),
+ ctx.channel().config(), ZlibCodecFactory.newZlibEncoder(
+ ZlibWrapper.ZLIB, deflateOptions.compressionLevel(),
+ deflateOptions.windowBits(), deflateOptions.memLevel())));
+ }
+ if (targetContentEncoding.equals("br")) {
+ return new Result(targetContentEncoding,
+ new EmbeddedChannel(ctx.channel().id(), ctx.channel().metadata().hasDisconnect(),
+ ctx.channel().config(), new BrotliEncoder(brotliOptions.parameters())));
+ }
throw new Error();
- }
+ } else {
+ ZlibWrapper wrapper = determineWrapper(acceptEncoding);
+ if (wrapper == null) {
+ return null;
+ }
- return new Result(
- targetContentEncoding,
- new EmbeddedChannel(ctx.channel().id(), ctx.channel().metadata().hasDisconnect(),
- ctx.channel().config(), ZlibCodecFactory.newZlibEncoder(
- wrapper, compressionLevel, windowBits, memLevel)));
+ String targetContentEncoding;
+ switch (wrapper) {
+ case GZIP:
+ targetContentEncoding = "gzip";
+ break;
+ case ZLIB:
+ targetContentEncoding = "deflate";
+ break;
+ default:
+ throw new Error();
+ }
+
+ return new Result(
+ targetContentEncoding,
+ new EmbeddedChannel(ctx.channel().id(), ctx.channel().metadata().hasDisconnect(),
+ ctx.channel().config(), ZlibCodecFactory.newZlibEncoder(
+ wrapper, compressionLevel, windowBits, memLevel)));
+ }
}
+ @SuppressWarnings("FloatingPointEquality")
+ protected String determineEncoding(String acceptEncoding) {
+ float starQ = -1.0f;
+ float brQ = -1.0f;
+ float gzipQ = -1.0f;
+ float deflateQ = -1.0f;
+ for (String encoding : acceptEncoding.split(",")) {
+ float q = 1.0f;
+ int equalsPos = encoding.indexOf('=');
+ if (equalsPos != -1) {
+ try {
+ q = Float.parseFloat(encoding.substring(equalsPos + 1));
+ } catch (NumberFormatException e) {
+ // Ignore encoding
+ q = 0.0f;
+ }
+ }
+ if (encoding.contains("*")) {
+ starQ = q;
+ } else if (encoding.contains("br") && q > brQ) {
+ brQ = q;
+ } else if (encoding.contains("gzip") && q > gzipQ) {
+ gzipQ = q;
+ } else if (encoding.contains("deflate") && q > deflateQ) {
+ deflateQ = q;
+ }
+ }
+ if (brQ > 0.0f || gzipQ > 0.0f || deflateQ > 0.0f) {
+ if (brQ != -1.0f && brQ >= gzipQ) {
+ return Brotli.isAvailable() ? "br" : null;
+ } else if (gzipQ != -1.0f && gzipQ >= deflateQ) {
+ return "gzip";
+ } else if (deflateQ != -1.0f) {
+ return "deflate";
+ }
+ }
+ if (starQ > 0.0f) {
+ if (brQ == -1.0f) {
+ return Brotli.isAvailable() ? "br" : null;
+ }
+ if (gzipQ == -1.0f) {
+ return "gzip";
+ }
+ if (deflateQ == -1.0f) {
+ return "deflate";
+ }
+ }
+ return null;
+ }
+
+ @Deprecated
@SuppressWarnings("FloatingPointEquality")
protected ZlibWrapper determineWrapper(String acceptEncoding) {
float starQ = -1.0f;
diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/BrotliContentCompressorTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/BrotliContentCompressorTest.java
new file mode 100644
index 0000000000..2e69cbf9f1
--- /dev/null
+++ b/codec-http/src/test/java/io/netty/handler/codec/http/BrotliContentCompressorTest.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2021 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:
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+package io.netty.handler.codec.http;
+
+import io.netty.channel.embedded.EmbeddedChannel;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.CoreMatchers.instanceOf;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.CoreMatchers.nullValue;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.jupiter.api.Assertions.*;
+
+class BrotliContentCompressorTest {
+
+ @Test
+ void testGetTargetContentEncoding() {
+ HttpContentCompressor compressor = new HttpContentCompressor();
+
+ String[] tests = {
+ // Accept-Encoding -> Content-Encoding
+ "", null,
+ "*", "br",
+ "*;q=0.0", null,
+ "br", "br",
+ "compress, br;q=0.5", "br",
+ "br; q=0.5, identity", "br",
+ "br; q=0, deflate", "br",
+ };
+ for (int i = 0; i < tests.length; i += 2) {
+ String acceptEncoding = tests[i];
+ String contentEncoding = tests[i + 1];
+ String targetEncoding = compressor.determineEncoding(acceptEncoding);
+ assertEquals(contentEncoding, targetEncoding);
+ }
+ }
+
+ @Test
+ void testAcceptEncodingHttpRequest() {
+ EmbeddedChannel ch = new EmbeddedChannel(new HttpContentCompressor(null));
+ ch.writeInbound(newRequest());
+ FullHttpRequest fullHttpRequest = ch.readInbound();
+ fullHttpRequest.release();
+
+ HttpResponse res = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
+ res.headers().set(HttpHeaderNames.TRANSFER_ENCODING, HttpHeaderValues.CHUNKED);
+ ch.writeOutbound(res);
+
+ assertEncodedResponse(ch);
+
+ assertTrue(ch.close().isSuccess());
+ }
+
+ private static void assertEncodedResponse(EmbeddedChannel ch) {
+ Object o = ch.readOutbound();
+ assertThat(o, is(instanceOf(HttpResponse.class)));
+
+ assertEncodedResponse((HttpResponse) o);
+ }
+
+ private static void assertEncodedResponse(HttpResponse res) {
+ assertThat(res, is(not(instanceOf(HttpContent.class))));
+ assertThat(res.headers().get(HttpHeaderNames.TRANSFER_ENCODING), is("chunked"));
+ assertThat(res.headers().get(HttpHeaderNames.CONTENT_LENGTH), is(nullValue()));
+ assertThat(res.headers().get(HttpHeaderNames.CONTENT_ENCODING), is("br"));
+ }
+
+ private static FullHttpRequest newRequest() {
+ FullHttpRequest req = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/");
+ req.headers().set(HttpHeaderNames.ACCEPT_ENCODING, "br, gzip, deflate");
+ return req;
+ }
+}
diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/HttpContentCompressorTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/HttpContentCompressorTest.java
index eda3ddd6cc..be1a91ced0 100644
--- a/codec-http/src/test/java/io/netty/handler/codec/http/HttpContentCompressorTest.java
+++ b/codec-http/src/test/java/io/netty/handler/codec/http/HttpContentCompressorTest.java
@@ -657,6 +657,7 @@ public class HttpContentCompressorTest {
assertThat(res.headers().get(HttpHeaderNames.CONTENT_LENGTH), is(nullValue()));
assertThat(res.headers().get(HttpHeaderNames.CONTENT_ENCODING), is("gzip"));
}
+
private static void assertAssembledEncodedResponse(EmbeddedChannel ch) {
Object o = ch.readOutbound();
assertThat(o, is(instanceOf(AssembledHttpResponse.class)));
diff --git a/codec-http2/pom.xml b/codec-http2/pom.xml
index 0e3e13178d..c787a53c62 100644
--- a/codec-http2/pom.xml
+++ b/codec-http2/pom.xml
@@ -88,6 +88,11 @@
test
true
+
+ com.aayushatharva.brotli4j
+ brotli4j
+ true
+
diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/CompressorHttp2ConnectionEncoder.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/CompressorHttp2ConnectionEncoder.java
index 3f6c5a8ae6..499081515d 100644
--- a/codec-http2/src/main/java/io/netty/handler/codec/http2/CompressorHttp2ConnectionEncoder.java
+++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/CompressorHttp2ConnectionEncoder.java
@@ -21,14 +21,21 @@ import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelPromise;
import io.netty.channel.embedded.EmbeddedChannel;
import io.netty.handler.codec.ByteToMessageDecoder;
+import io.netty.handler.codec.compression.BrotliEncoder;
import io.netty.handler.codec.compression.ZlibCodecFactory;
import io.netty.handler.codec.compression.ZlibWrapper;
+import io.netty.handler.codec.compression.BrotliOptions;
+import io.netty.handler.codec.compression.CompressionOptions;
+import io.netty.handler.codec.compression.DeflateOptions;
+import io.netty.handler.codec.compression.GzipOptions;
+import io.netty.handler.codec.compression.StandardCompressionOptions;
import io.netty.util.concurrent.PromiseCombiner;
import io.netty.util.internal.ObjectUtil;
import io.netty.util.internal.UnstableApi;
import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_ENCODING;
import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_LENGTH;
+import static io.netty.handler.codec.http.HttpHeaderValues.BR;
import static io.netty.handler.codec.http.HttpHeaderValues.DEFLATE;
import static io.netty.handler.codec.http.HttpHeaderValues.GZIP;
import static io.netty.handler.codec.http.HttpHeaderValues.IDENTITY;
@@ -41,19 +48,35 @@ import static io.netty.handler.codec.http.HttpHeaderValues.X_GZIP;
*/
@UnstableApi
public class CompressorHttp2ConnectionEncoder extends DecoratingHttp2ConnectionEncoder {
+ // We cannot remove this because it'll be breaking change
public static final int DEFAULT_COMPRESSION_LEVEL = 6;
public static final int DEFAULT_WINDOW_BITS = 15;
public static final int DEFAULT_MEM_LEVEL = 8;
- private final int compressionLevel;
- private final int windowBits;
- private final int memLevel;
+ private int compressionLevel;
+ private int windowBits;
+ private int memLevel;
private final Http2Connection.PropertyKey propertyKey;
+ private final boolean supportsCompressionOptions;
+
+ private BrotliOptions brotliOptions;
+ private GzipOptions gzipCompressionOptions;
+ private DeflateOptions deflateOptions;
+
+ /**
+ * Create a new {@link CompressorHttp2ConnectionEncoder} instance
+ * with default implementation of {@link StandardCompressionOptions}
+ */
public CompressorHttp2ConnectionEncoder(Http2ConnectionEncoder delegate) {
- this(delegate, DEFAULT_COMPRESSION_LEVEL, DEFAULT_WINDOW_BITS, DEFAULT_MEM_LEVEL);
+ this(delegate, StandardCompressionOptions.brotli(), StandardCompressionOptions.gzip(),
+ StandardCompressionOptions.deflate());
}
+ /**
+ * Create a new {@link CompressorHttp2ConnectionEncoder} instance
+ */
+ @Deprecated
public CompressorHttp2ConnectionEncoder(Http2ConnectionEncoder delegate, int compressionLevel, int windowBits,
int memLevel) {
super(delegate);
@@ -71,6 +94,45 @@ public class CompressorHttp2ConnectionEncoder extends DecoratingHttp2ConnectionE
}
}
});
+
+ supportsCompressionOptions = false;
+ }
+
+ /**
+ * Create a new {@link CompressorHttp2ConnectionEncoder} with
+ * specified {@link StandardCompressionOptions}
+ */
+ public CompressorHttp2ConnectionEncoder(Http2ConnectionEncoder delegate,
+ CompressionOptions... compressionOptionsArgs) {
+ super(delegate);
+ ObjectUtil.checkNotNull(compressionOptionsArgs, "CompressionOptions");
+ ObjectUtil.deepCheckNotNull("CompressionOptions", compressionOptionsArgs);
+
+ for (CompressionOptions compressionOptions : compressionOptionsArgs) {
+ if (compressionOptions instanceof BrotliOptions) {
+ brotliOptions = (BrotliOptions) compressionOptions;
+ } else if (compressionOptions instanceof GzipOptions) {
+ gzipCompressionOptions = (GzipOptions) compressionOptions;
+ } else if (compressionOptions instanceof DeflateOptions) {
+ deflateOptions = (DeflateOptions) compressionOptions;
+ } else {
+ throw new IllegalArgumentException("Unsupported " + CompressionOptions.class.getSimpleName() +
+ ": " + compressionOptions);
+ }
+ }
+
+ supportsCompressionOptions = true;
+
+ propertyKey = connection().newKey();
+ connection().addListener(new Http2ConnectionAdapter() {
+ @Override
+ public void onStreamRemoved(Http2Stream stream) {
+ final EmbeddedChannel compressor = stream.getProperty(propertyKey);
+ if (compressor != null) {
+ cleanup(stream, compressor);
+ }
+ }
+ });
}
@Override
@@ -190,6 +252,10 @@ public class CompressorHttp2ConnectionEncoder extends DecoratingHttp2ConnectionE
if (DEFLATE.contentEqualsIgnoreCase(contentEncoding) || X_DEFLATE.contentEqualsIgnoreCase(contentEncoding)) {
return newCompressionChannel(ctx, ZlibWrapper.ZLIB);
}
+ if (brotliOptions != null && BR.contentEqualsIgnoreCase(contentEncoding)) {
+ return new EmbeddedChannel(ctx.channel().id(), ctx.channel().metadata().hasDisconnect(),
+ ctx.channel().config(), new BrotliEncoder(brotliOptions.parameters()));
+ }
// 'identity' or unsupported
return null;
}
@@ -212,9 +278,25 @@ public class CompressorHttp2ConnectionEncoder extends DecoratingHttp2ConnectionE
* @param wrapper Defines what type of encoder should be used
*/
private EmbeddedChannel newCompressionChannel(final ChannelHandlerContext ctx, ZlibWrapper wrapper) {
- return new EmbeddedChannel(ctx.channel().id(), ctx.channel().metadata().hasDisconnect(),
- ctx.channel().config(), ZlibCodecFactory.newZlibEncoder(wrapper, compressionLevel, windowBits,
- memLevel));
+ if (supportsCompressionOptions) {
+ if (wrapper == ZlibWrapper.GZIP && gzipCompressionOptions != null) {
+ return new EmbeddedChannel(ctx.channel().id(), ctx.channel().metadata().hasDisconnect(),
+ ctx.channel().config(), ZlibCodecFactory.newZlibEncoder(wrapper,
+ gzipCompressionOptions.compressionLevel(), gzipCompressionOptions.windowBits(),
+ gzipCompressionOptions.memLevel()));
+ } else if (wrapper == ZlibWrapper.ZLIB && deflateOptions != null) {
+ return new EmbeddedChannel(ctx.channel().id(), ctx.channel().metadata().hasDisconnect(),
+ ctx.channel().config(), ZlibCodecFactory.newZlibEncoder(wrapper,
+ deflateOptions.compressionLevel(), deflateOptions.windowBits(),
+ deflateOptions.memLevel()));
+ } else {
+ throw new IllegalArgumentException("Unsupported ZlibWrapper: " + wrapper);
+ }
+ } else {
+ return new EmbeddedChannel(ctx.channel().id(), ctx.channel().metadata().hasDisconnect(),
+ ctx.channel().config(), ZlibCodecFactory.newZlibEncoder(wrapper, compressionLevel, windowBits,
+ memLevel));
+ }
}
/**
diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/DataCompressionHttp2Test.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/DataCompressionHttp2Test.java
index 00cedb895d..11debc1151 100644
--- a/codec-http2/src/test/java/io/netty/handler/codec/http2/DataCompressionHttp2Test.java
+++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/DataCompressionHttp2Test.java
@@ -215,6 +215,48 @@ public class DataCompressionHttp2Test {
}
}
+ @Test
+ public void brotliEncodingSingleEmptyMessage() throws Exception {
+ final String text = "";
+ final ByteBuf data = Unpooled.copiedBuffer(text.getBytes());
+ bootstrapEnv(data.readableBytes());
+ try {
+ final Http2Headers headers = new DefaultHttp2Headers().method(POST).path(PATH)
+ .set(HttpHeaderNames.CONTENT_ENCODING, HttpHeaderValues.BR);
+
+ runInChannel(clientChannel, () -> {
+ clientEncoder.writeHeaders(ctxClient(), 3, headers, 0, false, newPromiseClient());
+ clientEncoder.writeData(ctxClient(), 3, data.retain(), 0, true, newPromiseClient());
+ clientHandler.flush(ctxClient());
+ });
+ awaitServer();
+ assertEquals(text, serverOut.toString(CharsetUtil.UTF_8.name()));
+ } finally {
+ data.release();
+ }
+ }
+
+ @Test
+ public void brotliEncodingSingleMessage() throws Exception {
+ final String text = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbccccccccccccccccccccccc";
+ final ByteBuf data = Unpooled.copiedBuffer(text.getBytes(CharsetUtil.UTF_8.name()));
+ bootstrapEnv(data.readableBytes());
+ try {
+ final Http2Headers headers = new DefaultHttp2Headers().method(POST).path(PATH)
+ .set(HttpHeaderNames.CONTENT_ENCODING, HttpHeaderValues.BR);
+
+ runInChannel(clientChannel, () -> {
+ clientEncoder.writeHeaders(ctxClient(), 3, headers, 0, false, newPromiseClient());
+ clientEncoder.writeData(ctxClient(), 3, data.retain(), 0, true, newPromiseClient());
+ clientHandler.flush(ctxClient());
+ });
+ awaitServer();
+ assertEquals(text, serverOut.toString(CharsetUtil.UTF_8.name()));
+ } finally {
+ data.release();
+ }
+ }
+
@Test
public void deflateEncodingWriteLargeMessage() throws Exception {
final int BUFFER_SIZE = 1 << 12;
diff --git a/codec/src/main/java/io/netty/handler/codec/compression/Brotli.java b/codec/src/main/java/io/netty/handler/codec/compression/Brotli.java
index fa79d81543..19935ee9a1 100644
--- a/codec/src/main/java/io/netty/handler/codec/compression/Brotli.java
+++ b/codec/src/main/java/io/netty/handler/codec/compression/Brotli.java
@@ -25,6 +25,7 @@ public final class Brotli {
private static final InternalLogger logger = InternalLoggerFactory.getInstance(Brotli.class);
private static final ClassNotFoundException CNFE;
+ private static Throwable cause;
static {
ClassNotFoundException cnfe = null;
@@ -42,7 +43,7 @@ public final class Brotli {
// If in the classpath, try to load the native library and initialize brotli4j.
if (cnfe == null) {
- Throwable cause = Brotli4jLoader.getUnavailabilityCause();
+ cause = Brotli4jLoader.getUnavailabilityCause();
if (cause != null) {
logger.debug("Failed to load brotli4j; Brotli support will be unavailable.", cause);
}
@@ -70,6 +71,13 @@ public final class Brotli {
Brotli4jLoader.ensureAvailability();
}
+ /**
+ * Returns {@link Throwable} of unavailability cause
+ */
+ public static Throwable cause() {
+ return cause;
+ }
+
private Brotli() {
}
}
diff --git a/codec/src/main/java/io/netty/handler/codec/compression/BrotliEncoder.java b/codec/src/main/java/io/netty/handler/codec/compression/BrotliEncoder.java
new file mode 100644
index 0000000000..233f132570
--- /dev/null
+++ b/codec/src/main/java/io/netty/handler/codec/compression/BrotliEncoder.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2021 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:
+ *
+ * https://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.compression;
+
+import com.aayushatharva.brotli4j.encoder.Encoder;
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.ByteBufUtil;
+import io.netty.buffer.Unpooled;
+import io.netty.channel.ChannelHandler;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.handler.codec.MessageToByteEncoder;
+import io.netty.util.ReferenceCountUtil;
+import io.netty.util.internal.ObjectUtil;
+
+/**
+ * Compress a {@link ByteBuf} with the brotli format.
+ *
+ * See brotli.
+ */
+@ChannelHandler.Sharable
+public final class BrotliEncoder extends MessageToByteEncoder {
+
+ private final Encoder.Parameters parameters;
+
+ /**
+ * Create a new {@link BrotliEncoder} Instance
+ * with {@link Encoder.Parameters#setQuality(int)} set to 4
+ * and {@link Encoder.Parameters#setMode(Encoder.Mode)} set to {@link Encoder.Mode#TEXT}
+ */
+ public BrotliEncoder() {
+ this(BrotliOptions.DEFAULT);
+ }
+
+ /**
+ * Create a new {@link BrotliEncoder} Instance
+ *
+ * @param parameters {@link Encoder.Parameters} Instance
+ */
+ public BrotliEncoder(Encoder.Parameters parameters) {
+ this.parameters = ObjectUtil.checkNotNull(parameters, "Parameters");
+ }
+
+ /**
+ * Create a new {@link BrotliEncoder} Instance
+ *
+ * @param brotliOptions {@link BrotliOptions} to use.
+ */
+ public BrotliEncoder(BrotliOptions brotliOptions) {
+ this(brotliOptions.parameters());
+ }
+
+ @Override
+ protected void encode(ChannelHandlerContext ctx, ByteBuf msg, ByteBuf out) throws Exception {
+ // NO-OP
+ }
+
+ @Override
+ protected ByteBuf allocateBuffer(ChannelHandlerContext ctx, ByteBuf msg, boolean preferDirect) throws Exception {
+ // If ByteBuf is unreadable, then return EMPTY_BUFFER.
+ if (!msg.isReadable()) {
+ return Unpooled.EMPTY_BUFFER;
+ }
+
+ try {
+ byte[] uncompressed = ByteBufUtil.getBytes(msg, msg.readerIndex(), msg.readableBytes(), false);
+ byte[] compressed = Encoder.compress(uncompressed, parameters);
+ if (preferDirect) {
+ ByteBuf out = ctx.alloc().ioBuffer(compressed.length);
+ out.writeBytes(compressed);
+ return out;
+ } else {
+ return Unpooled.wrappedBuffer(compressed);
+ }
+ } catch (Exception e) {
+ ReferenceCountUtil.release(msg);
+ throw e;
+ }
+ }
+}
diff --git a/codec/src/main/java/io/netty/handler/codec/compression/BrotliOptions.java b/codec/src/main/java/io/netty/handler/codec/compression/BrotliOptions.java
new file mode 100644
index 0000000000..b4473e04c0
--- /dev/null
+++ b/codec/src/main/java/io/netty/handler/codec/compression/BrotliOptions.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2021 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:
+ *
+ * https://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.compression;
+
+import com.aayushatharva.brotli4j.encoder.Encoder;
+import io.netty.util.internal.ObjectUtil;
+
+/**
+ * {@link BrotliOptions} holds {@link Encoder.Parameters} for
+ * Brotli compression.
+ */
+public final class BrotliOptions implements CompressionOptions {
+
+ private final Encoder.Parameters parameters;
+
+ /**
+ * Default implementation of {@link BrotliOptions} with{@link Encoder.Parameters#setQuality(int)} set to 4
+ * and {@link Encoder.Parameters#setMode(Encoder.Mode)} set to {@link Encoder.Mode#TEXT}
+ */
+ static final BrotliOptions DEFAULT = new BrotliOptions(
+ new Encoder.Parameters().setQuality(4).setMode(Encoder.Mode.TEXT)
+ );
+
+ /**
+ * Create a new {@link BrotliOptions}
+ *
+ * @param parameters {@link Encoder.Parameters} Instance
+ * @throws NullPointerException If {@link Encoder.Parameters} is {@code null}
+ */
+ BrotliOptions(Encoder.Parameters parameters) {
+ this.parameters = ObjectUtil.checkNotNull(parameters, "Parameters");
+
+ if (!Brotli.isAvailable()) {
+ throw new IllegalStateException("Brotli is not available", Brotli.cause());
+ }
+ }
+
+ public Encoder.Parameters parameters() {
+ return parameters;
+ }
+}
diff --git a/codec/src/main/java/io/netty/handler/codec/compression/CompressionOptions.java b/codec/src/main/java/io/netty/handler/codec/compression/CompressionOptions.java
new file mode 100644
index 0000000000..9ee964619c
--- /dev/null
+++ b/codec/src/main/java/io/netty/handler/codec/compression/CompressionOptions.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2021 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:
+ *
+ * https://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.compression;
+
+/**
+ * {@link CompressionOptions} provides compression options for
+ * various types of compressor types, like Brotli.
+ *
+ * A {@link CompressionOptions} instance is thread-safe
+ * and should be shared between multiple instances of Compressor.
+ */
+public interface CompressionOptions {
+ // Empty
+}
diff --git a/codec/src/main/java/io/netty/handler/codec/compression/DeflateOptions.java b/codec/src/main/java/io/netty/handler/codec/compression/DeflateOptions.java
new file mode 100644
index 0000000000..f43125f5f4
--- /dev/null
+++ b/codec/src/main/java/io/netty/handler/codec/compression/DeflateOptions.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2021 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:
+ *
+ * https://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.compression;
+
+import io.netty.util.internal.ObjectUtil;
+
+/**
+ * {@link DeflateOptions} holds {@link #compressionLevel()},
+ * {@link #memLevel()} and {@link #windowBits()} for Deflate compression.
+ */
+public class DeflateOptions implements CompressionOptions {
+
+ private final int compressionLevel;
+ private final int windowBits;
+ private final int memLevel;
+
+ /**
+ * Default implementation of {@link DeflateOptions} with
+ * {@link #compressionLevel} set to 6, {@link #windowBits} set to 15
+ * and {@link #memLevel} set to 8.
+ */
+ static final DeflateOptions DEFAULT = new DeflateOptions(
+ 6, 15, 8
+ );
+
+ /**
+ * Create a new {@link DeflateOptions} Instance
+ *
+ * @param compressionLevel {@code 1} yields the fastest compression and {@code 9} yields the
+ * best compression. {@code 0} means no compression. The default
+ * compression level is {@code 6}.
+ *
+ * @param windowBits The base two logarithm of the size of the history buffer. The
+ * value should be in the range {@code 9} to {@code 15} inclusive.
+ * Larger values result in better compression at the expense of
+ * memory usage. The default value is {@code 15}.
+ *
+ * @param memLevel How much memory should be allocated for the internal compression
+ * state. {@code 1} uses minimum memory and {@code 9} uses maximum
+ * memory. Larger values result in better and faster compression
+ * at the expense of memory usage. The default value is {@code 8}
+ */
+ DeflateOptions(int compressionLevel, int windowBits, int memLevel) {
+ this.compressionLevel = ObjectUtil.checkInRange(compressionLevel, 0, 9, "compressionLevel");
+ this.windowBits = ObjectUtil.checkInRange(windowBits, 9, 15, "windowBits");
+ this.memLevel = ObjectUtil.checkInRange(memLevel, 1, 9, "memLevel");
+ }
+
+ public int compressionLevel() {
+ return compressionLevel;
+ }
+
+ public int windowBits() {
+ return windowBits;
+ }
+
+ public int memLevel() {
+ return memLevel;
+ }
+}
diff --git a/codec/src/main/java/io/netty/handler/codec/compression/GzipOptions.java b/codec/src/main/java/io/netty/handler/codec/compression/GzipOptions.java
new file mode 100644
index 0000000000..b1f0e0e969
--- /dev/null
+++ b/codec/src/main/java/io/netty/handler/codec/compression/GzipOptions.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2021 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:
+ *
+ * https://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.compression;
+
+/**
+ * {@link GzipOptions} holds {@link #compressionLevel()},
+ * {@link #memLevel()} and {@link #windowBits()} for Gzip compression.
+ * This class is an extension of {@link DeflateOptions}
+ */
+public final class GzipOptions extends DeflateOptions {
+
+ /**
+ * Default implementation of {@link GzipOptions} with
+ * {@link #compressionLevel()} set to 6, {@link #windowBits()} set to 15
+ * and {@link #memLevel()} set to 8.
+ */
+ static final GzipOptions DEFAULT = new GzipOptions(
+ 6, 15, 8
+ );
+
+ /**
+ * Create a new {@link GzipOptions} Instance
+ *
+ * @param compressionLevel {@code 1} yields the fastest compression and {@code 9} yields the
+ * best compression. {@code 0} means no compression. The default
+ * compression level is {@code 6}.
+ *
+ * @param windowBits The base two logarithm of the size of the history buffer. The
+ * value should be in the range {@code 9} to {@code 15} inclusive.
+ * Larger values result in better compression at the expense of
+ * memory usage. The default value is {@code 15}.
+ *
+ * @param memLevel How much memory should be allocated for the internal compression
+ * state. {@code 1} uses minimum memory and {@code 9} uses maximum
+ * memory. Larger values result in better and faster compression
+ * at the expense of memory usage. The default value is {@code 8}
+ */
+ GzipOptions(int compressionLevel, int windowBits, int memLevel) {
+ super(compressionLevel, windowBits, memLevel);
+ }
+}
diff --git a/codec/src/main/java/io/netty/handler/codec/compression/StandardCompressionOptions.java b/codec/src/main/java/io/netty/handler/codec/compression/StandardCompressionOptions.java
new file mode 100644
index 0000000000..1332d135cd
--- /dev/null
+++ b/codec/src/main/java/io/netty/handler/codec/compression/StandardCompressionOptions.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2021 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:
+ *
+ * https://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.compression;
+
+import com.aayushatharva.brotli4j.encoder.Encoder;
+
+/**
+ * Standard Compression Options for {@link BrotliOptions},
+ * {@link GzipOptions} and {@link DeflateOptions}
+ */
+public final class StandardCompressionOptions {
+
+ private StandardCompressionOptions() {
+ // Prevent outside initialization
+ }
+
+ /**
+ * @see BrotliOptions#DEFAULT
+ */
+ public static BrotliOptions brotli() {
+ return BrotliOptions.DEFAULT;
+ }
+
+ /**
+ * @see BrotliOptions#BrotliOptions(Encoder.Parameters)
+ */
+ public static BrotliOptions brotli(Encoder.Parameters parameters) {
+ return new BrotliOptions(parameters);
+ }
+
+ /**
+ * @see GzipOptions#DEFAULT
+ */
+ public static GzipOptions gzip() {
+ return GzipOptions.DEFAULT;
+ }
+
+ /**
+ * @see GzipOptions#GzipOptions(int, int, int)
+ */
+ public static GzipOptions gzip(int compressionLevel, int windowBits, int memLevel) {
+ return new GzipOptions(compressionLevel, windowBits, memLevel);
+ }
+
+ /**
+ * @see DeflateOptions#DEFAULT
+ */
+ public static DeflateOptions deflate() {
+ return DeflateOptions.DEFAULT;
+ }
+
+ /**
+ * @see DeflateOptions#DeflateOptions(int, int, int)
+ */
+ public static DeflateOptions deflate(int compressionLevel, int windowBits, int memLevel) {
+ return new DeflateOptions(compressionLevel, windowBits, memLevel);
+ }
+}
diff --git a/codec/src/test/java/io/netty/handler/codec/compression/BrotliDecoderTest.java b/codec/src/test/java/io/netty/handler/codec/compression/BrotliDecoderTest.java
index 7bb50bfa93..87e62a962c 100644
--- a/codec/src/test/java/io/netty/handler/codec/compression/BrotliDecoderTest.java
+++ b/codec/src/test/java/io/netty/handler/codec/compression/BrotliDecoderTest.java
@@ -136,7 +136,7 @@ public class BrotliDecoderTest {
decompressed.release();
}
- private void testDecompressionOfBatchedFlow(final ByteBuf expected, final ByteBuf data) {
+ private void testDecompressionOfBatchedFlow(final ByteBuf expected, final ByteBuf data) {
final int compressedLength = data.readableBytes();
int written = 0, length = RANDOM.nextInt(100);
while (written + length < compressedLength) {
@@ -155,7 +155,7 @@ public class BrotliDecoderTest {
data.release();
}
- private static ByteBuf readDecompressed(final EmbeddedChannel channel) {
+ private static ByteBuf readDecompressed(final EmbeddedChannel channel) {
CompositeByteBuf decompressed = Unpooled.compositeBuffer();
ByteBuf msg;
while ((msg = channel.readInbound()) != null) {
diff --git a/codec/src/test/java/io/netty/handler/codec/compression/BrotliEncoderTest.java b/codec/src/test/java/io/netty/handler/codec/compression/BrotliEncoderTest.java
new file mode 100644
index 0000000000..b240c64764
--- /dev/null
+++ b/codec/src/test/java/io/netty/handler/codec/compression/BrotliEncoderTest.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2021 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:
+ *
+ * https://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.compression;
+
+import com.aayushatharva.brotli4j.decoder.Decoder;
+import com.aayushatharva.brotli4j.decoder.DecoderJNI;
+import com.aayushatharva.brotli4j.decoder.DirectDecompress;
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.CompositeByteBuf;
+import io.netty.buffer.Unpooled;
+import io.netty.channel.embedded.EmbeddedChannel;
+import io.netty.util.internal.PlatformDependent;
+import org.junit.jupiter.api.condition.DisabledIf;
+
+@DisabledIf(value = "isNotSupported", disabledReason = "Brotli is not supported on this platform")
+public class BrotliEncoderTest extends AbstractEncoderTest {
+
+ static {
+ try {
+ Brotli.ensureAvailability();
+ } catch (Throwable throwable) {
+ throw new ExceptionInInitializerError(throwable);
+ }
+ }
+
+ @Override
+ public EmbeddedChannel createChannel() {
+ return new EmbeddedChannel(new BrotliEncoder());
+ }
+
+ @Override
+ protected ByteBuf decompress(ByteBuf compressed, int originalLength) throws Exception {
+ byte[] compressedArray = new byte[compressed.readableBytes()];
+ compressed.readBytes(compressedArray);
+ compressed.release();
+
+ DirectDecompress decompress = Decoder.decompress(compressedArray);
+ if (decompress.getResultStatus() == DecoderJNI.Status.ERROR) {
+ throw new DecompressionException("Brotli stream corrupted");
+ }
+
+ byte[] decompressed = decompress.getDecompressedData();
+ return Unpooled.wrappedBuffer(decompressed);
+ }
+
+ @Override
+ protected ByteBuf readDecompressed(final int dataLength) throws Exception {
+ CompositeByteBuf decompressed = Unpooled.compositeBuffer();
+ ByteBuf msg;
+ while ((msg = channel.readOutbound()) != null) {
+ if (msg.isReadable()) {
+ decompressed.addComponent(true, decompress(msg, -1));
+ } else {
+ msg.release();
+ }
+ }
+ return decompressed;
+ }
+
+ static boolean isNotSupported() {
+ return (PlatformDependent.isOsx() || PlatformDependent.isWindows())
+ && "aarch_64".equals(PlatformDependent.normalizedArch());
+ }
+}
diff --git a/common/src/main/java/io/netty/util/internal/ObjectUtil.java b/common/src/main/java/io/netty/util/internal/ObjectUtil.java
index 8c35573812..674af247de 100644
--- a/common/src/main/java/io/netty/util/internal/ObjectUtil.java
+++ b/common/src/main/java/io/netty/util/internal/ObjectUtil.java
@@ -42,6 +42,26 @@ public final class ObjectUtil {
return Objects.requireNonNull(arg, text);
}
+ /**
+ * Check that the given varargs is not null and does not contain elements
+ * null elements.
+ *
+ * If it is, throws {@link NullPointerException}.
+ * Otherwise, returns the argument.
+ */
+ public static T[] deepCheckNotNull(String text, T... varargs) {
+ if (varargs == null) {
+ throw new NullPointerException(text);
+ }
+
+ for (T element : varargs) {
+ if (element == null) {
+ throw new NullPointerException(text);
+ }
+ }
+ return varargs;
+ }
+
/**
* Checks that the given argument is not null. If it is, throws {@link IllegalArgumentException}.
* Otherwise, returns the argument.
diff --git a/license/LICENSE.brotli4j.txt b/license/LICENSE.brotli4j.txt
new file mode 100644
index 0000000000..20e4bd8566
--- /dev/null
+++ b/license/LICENSE.brotli4j.txt
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ https://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed 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
+
+ https://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.