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 <norman_maurer@apple.com>
This commit is contained in:
parent
7825fa8d7a
commit
fef761d03e
@ -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)
|
* license/LICENSE.dnsinfo.txt (Apple Public Source License 2.0)
|
||||||
* HOMEPAGE:
|
* HOMEPAGE:
|
||||||
* https://www.opensource.apple.com/source/configd/configd-453.19/dnsinfo/dnsinfo.h
|
* 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
|
||||||
|
@ -74,7 +74,7 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.aayushatharva.brotli4j</groupId>
|
<groupId>com.aayushatharva.brotli4j</groupId>
|
||||||
<artifactId>brotli4j</artifactId>
|
<artifactId>brotli4j</artifactId>
|
||||||
<scope>test</scope>
|
<optional>true</optional>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.aayushatharva.brotli4j</groupId>
|
<groupId>com.aayushatharva.brotli4j</groupId>
|
||||||
|
@ -17,8 +17,15 @@ package io.netty.handler.codec.http;
|
|||||||
|
|
||||||
import io.netty.channel.ChannelHandlerContext;
|
import io.netty.channel.ChannelHandlerContext;
|
||||||
import io.netty.channel.embedded.EmbeddedChannel;
|
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.ZlibCodecFactory;
|
||||||
import io.netty.handler.codec.compression.ZlibWrapper;
|
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;
|
import io.netty.util.internal.ObjectUtil;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -30,6 +37,11 @@ import io.netty.util.internal.ObjectUtil;
|
|||||||
*/
|
*/
|
||||||
public class HttpContentCompressor extends HttpContentEncoder {
|
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 compressionLevel;
|
||||||
private final int windowBits;
|
private final int windowBits;
|
||||||
private final int memLevel;
|
private final int memLevel;
|
||||||
@ -53,6 +65,7 @@ public class HttpContentCompressor extends HttpContentEncoder {
|
|||||||
* best compression. {@code 0} means no compression. The default
|
* best compression. {@code 0} means no compression. The default
|
||||||
* compression level is {@code 6}.
|
* compression level is {@code 6}.
|
||||||
*/
|
*/
|
||||||
|
@Deprecated
|
||||||
public HttpContentCompressor(int compressionLevel) {
|
public HttpContentCompressor(int compressionLevel) {
|
||||||
this(compressionLevel, 15, 8, 0);
|
this(compressionLevel, 15, 8, 0);
|
||||||
}
|
}
|
||||||
@ -76,6 +89,7 @@ public class HttpContentCompressor extends HttpContentEncoder {
|
|||||||
* memory. Larger values result in better and faster compression
|
* memory. Larger values result in better and faster compression
|
||||||
* at the expense of memory usage. The default value is {@code 8}
|
* at the expense of memory usage. The default value is {@code 8}
|
||||||
*/
|
*/
|
||||||
|
@Deprecated
|
||||||
public HttpContentCompressor(int compressionLevel, int windowBits, int memLevel) {
|
public HttpContentCompressor(int compressionLevel, int windowBits, int memLevel) {
|
||||||
this(compressionLevel, windowBits, memLevel, 0);
|
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
|
* body exceeds the threshold. The value should be a non negative
|
||||||
* number. {@code 0} will enable compression for all responses.
|
* number. {@code 0} will enable compression for all responses.
|
||||||
*/
|
*/
|
||||||
|
@Deprecated
|
||||||
public HttpContentCompressor(int compressionLevel, int windowBits, int memLevel, int contentSizeThreshold) {
|
public HttpContentCompressor(int compressionLevel, int windowBits, int memLevel, int contentSizeThreshold) {
|
||||||
this.compressionLevel = ObjectUtil.checkInRange(compressionLevel, 0, 9, "compressionLevel");
|
this.compressionLevel = ObjectUtil.checkInRange(compressionLevel, 0, 9, "compressionLevel");
|
||||||
this.windowBits = ObjectUtil.checkInRange(windowBits, 9, 15, "windowBits");
|
this.windowBits = ObjectUtil.checkInRange(windowBits, 9, 15, "windowBits");
|
||||||
this.memLevel = ObjectUtil.checkInRange(memLevel, 1, 9, "memLevel");
|
this.memLevel = ObjectUtil.checkInRange(memLevel, 1, 9, "memLevel");
|
||||||
this.contentSizeThreshold = ObjectUtil.checkPositiveOrZero(contentSizeThreshold, "contentSizeThreshold");
|
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
|
@Override
|
||||||
@ -131,30 +214,109 @@ public class HttpContentCompressor extends HttpContentEncoder {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
ZlibWrapper wrapper = determineWrapper(acceptEncoding);
|
if (supportsCompressionOptions) {
|
||||||
if (wrapper == null) {
|
String targetContentEncoding = determineEncoding(acceptEncoding);
|
||||||
return null;
|
if (targetContentEncoding == null) {
|
||||||
}
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
String targetContentEncoding;
|
if (targetContentEncoding.equals("gzip")) {
|
||||||
switch (wrapper) {
|
return new Result(targetContentEncoding,
|
||||||
case GZIP:
|
new EmbeddedChannel(ctx.channel().id(), ctx.channel().metadata().hasDisconnect(),
|
||||||
targetContentEncoding = "gzip";
|
ctx.channel().config(), ZlibCodecFactory.newZlibEncoder(
|
||||||
break;
|
ZlibWrapper.GZIP, gzipOptions.compressionLevel()
|
||||||
case ZLIB:
|
, gzipOptions.windowBits(), gzipOptions.memLevel())));
|
||||||
targetContentEncoding = "deflate";
|
}
|
||||||
break;
|
if (targetContentEncoding.equals("deflate")) {
|
||||||
default:
|
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();
|
throw new Error();
|
||||||
}
|
} else {
|
||||||
|
ZlibWrapper wrapper = determineWrapper(acceptEncoding);
|
||||||
|
if (wrapper == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return new Result(
|
String targetContentEncoding;
|
||||||
targetContentEncoding,
|
switch (wrapper) {
|
||||||
new EmbeddedChannel(ctx.channel().id(), ctx.channel().metadata().hasDisconnect(),
|
case GZIP:
|
||||||
ctx.channel().config(), ZlibCodecFactory.newZlibEncoder(
|
targetContentEncoding = "gzip";
|
||||||
wrapper, compressionLevel, windowBits, memLevel)));
|
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")
|
@SuppressWarnings("FloatingPointEquality")
|
||||||
protected ZlibWrapper determineWrapper(String acceptEncoding) {
|
protected ZlibWrapper determineWrapper(String acceptEncoding) {
|
||||||
float starQ = -1.0f;
|
float starQ = -1.0f;
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -778,6 +778,7 @@ public class HttpContentCompressorTest {
|
|||||||
assertThat(res.headers().get(HttpHeaderNames.CONTENT_LENGTH), is(nullValue()));
|
assertThat(res.headers().get(HttpHeaderNames.CONTENT_LENGTH), is(nullValue()));
|
||||||
assertThat(res.headers().get(HttpHeaderNames.CONTENT_ENCODING), is("gzip"));
|
assertThat(res.headers().get(HttpHeaderNames.CONTENT_ENCODING), is("gzip"));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void assertAssembledEncodedResponse(EmbeddedChannel ch) {
|
private static void assertAssembledEncodedResponse(EmbeddedChannel ch) {
|
||||||
Object o = ch.readOutbound();
|
Object o = ch.readOutbound();
|
||||||
assertThat(o, is(instanceOf(AssembledHttpResponse.class)));
|
assertThat(o, is(instanceOf(AssembledHttpResponse.class)));
|
||||||
|
@ -88,6 +88,11 @@
|
|||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
<optional>true</optional>
|
<optional>true</optional>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.aayushatharva.brotli4j</groupId>
|
||||||
|
<artifactId>brotli4j</artifactId>
|
||||||
|
<optional>true</optional>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</project>
|
</project>
|
||||||
|
|
||||||
|
@ -21,14 +21,21 @@ import io.netty.channel.ChannelHandlerContext;
|
|||||||
import io.netty.channel.ChannelPromise;
|
import io.netty.channel.ChannelPromise;
|
||||||
import io.netty.channel.embedded.EmbeddedChannel;
|
import io.netty.channel.embedded.EmbeddedChannel;
|
||||||
import io.netty.handler.codec.ByteToMessageDecoder;
|
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.ZlibCodecFactory;
|
||||||
import io.netty.handler.codec.compression.ZlibWrapper;
|
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.concurrent.PromiseCombiner;
|
||||||
import io.netty.util.internal.ObjectUtil;
|
import io.netty.util.internal.ObjectUtil;
|
||||||
import io.netty.util.internal.UnstableApi;
|
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_ENCODING;
|
||||||
import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_LENGTH;
|
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.DEFLATE;
|
||||||
import static io.netty.handler.codec.http.HttpHeaderValues.GZIP;
|
import static io.netty.handler.codec.http.HttpHeaderValues.GZIP;
|
||||||
import static io.netty.handler.codec.http.HttpHeaderValues.IDENTITY;
|
import static io.netty.handler.codec.http.HttpHeaderValues.IDENTITY;
|
||||||
@ -41,19 +48,35 @@ import static io.netty.handler.codec.http.HttpHeaderValues.X_GZIP;
|
|||||||
*/
|
*/
|
||||||
@UnstableApi
|
@UnstableApi
|
||||||
public class CompressorHttp2ConnectionEncoder extends DecoratingHttp2ConnectionEncoder {
|
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_COMPRESSION_LEVEL = 6;
|
||||||
public static final int DEFAULT_WINDOW_BITS = 15;
|
public static final int DEFAULT_WINDOW_BITS = 15;
|
||||||
public static final int DEFAULT_MEM_LEVEL = 8;
|
public static final int DEFAULT_MEM_LEVEL = 8;
|
||||||
|
|
||||||
private final int compressionLevel;
|
private int compressionLevel;
|
||||||
private final int windowBits;
|
private int windowBits;
|
||||||
private final int memLevel;
|
private int memLevel;
|
||||||
private final Http2Connection.PropertyKey propertyKey;
|
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) {
|
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,
|
public CompressorHttp2ConnectionEncoder(Http2ConnectionEncoder delegate, int compressionLevel, int windowBits,
|
||||||
int memLevel) {
|
int memLevel) {
|
||||||
super(delegate);
|
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
|
@Override
|
||||||
@ -190,6 +252,10 @@ public class CompressorHttp2ConnectionEncoder extends DecoratingHttp2ConnectionE
|
|||||||
if (DEFLATE.contentEqualsIgnoreCase(contentEncoding) || X_DEFLATE.contentEqualsIgnoreCase(contentEncoding)) {
|
if (DEFLATE.contentEqualsIgnoreCase(contentEncoding) || X_DEFLATE.contentEqualsIgnoreCase(contentEncoding)) {
|
||||||
return newCompressionChannel(ctx, ZlibWrapper.ZLIB);
|
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
|
// 'identity' or unsupported
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -212,9 +278,25 @@ public class CompressorHttp2ConnectionEncoder extends DecoratingHttp2ConnectionE
|
|||||||
* @param wrapper Defines what type of encoder should be used
|
* @param wrapper Defines what type of encoder should be used
|
||||||
*/
|
*/
|
||||||
private EmbeddedChannel newCompressionChannel(final ChannelHandlerContext ctx, ZlibWrapper wrapper) {
|
private EmbeddedChannel newCompressionChannel(final ChannelHandlerContext ctx, ZlibWrapper wrapper) {
|
||||||
return new EmbeddedChannel(ctx.channel().id(), ctx.channel().metadata().hasDisconnect(),
|
if (supportsCompressionOptions) {
|
||||||
ctx.channel().config(), ZlibCodecFactory.newZlibEncoder(wrapper, compressionLevel, windowBits,
|
if (wrapper == ZlibWrapper.GZIP && gzipCompressionOptions != null) {
|
||||||
memLevel));
|
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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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
|
@Test
|
||||||
public void deflateEncodingWriteLargeMessage() throws Exception {
|
public void deflateEncodingWriteLargeMessage() throws Exception {
|
||||||
final int BUFFER_SIZE = 1 << 12;
|
final int BUFFER_SIZE = 1 << 12;
|
||||||
|
@ -25,6 +25,7 @@ public final class Brotli {
|
|||||||
|
|
||||||
private static final InternalLogger logger = InternalLoggerFactory.getInstance(Brotli.class);
|
private static final InternalLogger logger = InternalLoggerFactory.getInstance(Brotli.class);
|
||||||
private static final ClassNotFoundException CNFE;
|
private static final ClassNotFoundException CNFE;
|
||||||
|
private static Throwable cause;
|
||||||
|
|
||||||
static {
|
static {
|
||||||
ClassNotFoundException cnfe = null;
|
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 in the classpath, try to load the native library and initialize brotli4j.
|
||||||
if (cnfe == null) {
|
if (cnfe == null) {
|
||||||
Throwable cause = Brotli4jLoader.getUnavailabilityCause();
|
cause = Brotli4jLoader.getUnavailabilityCause();
|
||||||
if (cause != null) {
|
if (cause != null) {
|
||||||
logger.debug("Failed to load brotli4j; Brotli support will be unavailable.", cause);
|
logger.debug("Failed to load brotli4j; Brotli support will be unavailable.", cause);
|
||||||
}
|
}
|
||||||
@ -70,6 +71,13 @@ public final class Brotli {
|
|||||||
Brotli4jLoader.ensureAvailability();
|
Brotli4jLoader.ensureAvailability();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns {@link Throwable} of unavailability cause
|
||||||
|
*/
|
||||||
|
public static Throwable cause() {
|
||||||
|
return cause;
|
||||||
|
}
|
||||||
|
|
||||||
private Brotli() {
|
private Brotli() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 <a href="https://github.com/google/brotli">brotli</a>.
|
||||||
|
*/
|
||||||
|
@ChannelHandler.Sharable
|
||||||
|
public final class BrotliEncoder extends MessageToByteEncoder<ByteBuf> {
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -136,7 +136,7 @@ public class BrotliDecoderTest {
|
|||||||
decompressed.release();
|
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();
|
final int compressedLength = data.readableBytes();
|
||||||
int written = 0, length = RANDOM.nextInt(100);
|
int written = 0, length = RANDOM.nextInt(100);
|
||||||
while (written + length < compressedLength) {
|
while (written + length < compressedLength) {
|
||||||
@ -155,7 +155,7 @@ public class BrotliDecoderTest {
|
|||||||
data.release();
|
data.release();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ByteBuf readDecompressed(final EmbeddedChannel channel) {
|
private static ByteBuf readDecompressed(final EmbeddedChannel channel) {
|
||||||
CompositeByteBuf decompressed = Unpooled.compositeBuffer();
|
CompositeByteBuf decompressed = Unpooled.compositeBuffer();
|
||||||
ByteBuf msg;
|
ByteBuf msg;
|
||||||
while ((msg = channel.readInbound()) != null) {
|
while ((msg = channel.readInbound()) != null) {
|
||||||
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
@ -41,6 +41,26 @@ public final class ObjectUtil {
|
|||||||
return arg;
|
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> 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}.
|
* Checks that the given argument is not null. If it is, throws {@link IllegalArgumentException}.
|
||||||
* Otherwise, returns the argument.
|
* Otherwise, returns the argument.
|
||||||
|
201
license/LICENSE.brotli4j.txt
Normal file
201
license/LICENSE.brotli4j.txt
Normal file
@ -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.
|
Loading…
Reference in New Issue
Block a user