From fef761d03e4f8515d7b220cdd66fcdedeff5455d Mon Sep 17 00:00:00 2001 From: Aayush Atharva Date: Thu, 8 Jul 2021 15:21:27 +0530 Subject: [PATCH] Introduce BrotliEncoder (#11256) Motivation: Currently, Netty only has BrotliDecoder which can decode Brotli encoded data. However, BrotliEncoder is missing which will encode normal data to Brotli encoded data. Modification: Added BrotliEncoder and CompressionOption Result: Fixes #6899. Co-authored-by: Norman Maurer --- NOTICE.txt | 8 + codec-http/pom.xml | 2 +- .../codec/http/HttpContentCompressor.java | 200 +++++++++++++++-- .../http/BrotliContentCompressorTest.java | 87 ++++++++ .../codec/http/HttpContentCompressorTest.java | 1 + codec-http2/pom.xml | 5 + .../CompressorHttp2ConnectionEncoder.java | 96 ++++++++- .../codec/http2/DataCompressionHttp2Test.java | 48 +++++ .../handler/codec/compression/Brotli.java | 10 +- .../codec/compression/BrotliEncoder.java | 92 ++++++++ .../codec/compression/BrotliOptions.java | 54 +++++ .../codec/compression/CompressionOptions.java | 27 +++ .../codec/compression/DeflateOptions.java | 73 +++++++ .../codec/compression/GzipOptions.java | 54 +++++ .../StandardCompressionOptions.java | 71 +++++++ .../codec/compression/BrotliDecoderTest.java | 4 +- .../codec/compression/BrotliEncoderTest.java | 77 +++++++ .../io/netty/util/internal/ObjectUtil.java | 20 ++ license/LICENSE.brotli4j.txt | 201 ++++++++++++++++++ 19 files changed, 1100 insertions(+), 30 deletions(-) create mode 100644 codec-http/src/test/java/io/netty/handler/codec/http/BrotliContentCompressorTest.java create mode 100644 codec/src/main/java/io/netty/handler/codec/compression/BrotliEncoder.java create mode 100644 codec/src/main/java/io/netty/handler/codec/compression/BrotliOptions.java create mode 100644 codec/src/main/java/io/netty/handler/codec/compression/CompressionOptions.java create mode 100644 codec/src/main/java/io/netty/handler/codec/compression/DeflateOptions.java create mode 100644 codec/src/main/java/io/netty/handler/codec/compression/GzipOptions.java create mode 100644 codec/src/main/java/io/netty/handler/codec/compression/StandardCompressionOptions.java create mode 100644 codec/src/test/java/io/netty/handler/codec/compression/BrotliEncoderTest.java create mode 100644 license/LICENSE.brotli4j.txt 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 a99f295c51..d8b680b24b 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 43184b8b00..1e2e995917 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 @@ -778,6 +778,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 d6e81697b1..4936f880d6 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 d66d49c182..a307164dea 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 @@ -235,6 +235,54 @@ 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, new Http2Runnable() { + @Override + public void run() throws Http2Exception { + 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, new Http2Runnable() { + @Override + public void run() throws Http2Exception { + 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 f7917087ed..e4876b6fe9 100644 --- a/common/src/main/java/io/netty/util/internal/ObjectUtil.java +++ b/common/src/main/java/io/netty/util/internal/ObjectUtil.java @@ -41,6 +41,26 @@ public final class ObjectUtil { return arg; } + /** + * 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.