diff --git a/src/main/java/io/github/nstdio/http/ext/BodyHandlers.java b/src/main/java/io/github/nstdio/http/ext/BodyHandlers.java index 0386829..bc71ea4 100644 --- a/src/main/java/io/github/nstdio/http/ext/BodyHandlers.java +++ b/src/main/java/io/github/nstdio/http/ext/BodyHandlers.java @@ -22,6 +22,8 @@ */ package io.github.nstdio.http.ext; +import io.github.nstdio.http.ext.DecompressingBodyHandler.Options; + import java.io.InputStream; import java.net.http.HttpResponse.BodyHandler; @@ -29,14 +31,66 @@ * Implementations of {@code BodyHandler}'s. */ public final class BodyHandlers { - private BodyHandlers() { - } + private BodyHandlers() { + } + + /** + * Wraps response body {@code InputStream} in on-the-fly decompressing {@code InputStream} in accordance with + * Content-Encoding header semantics. + * + * @return The decompressing body handler. + */ + public static BodyHandler ofDecompressing() { + return new DecompressingBodyHandlerBuilder().build(); + } + + /** + * Creates new {@code DecompressingBodyHandlerBuilder} instance. + * + * @return The builder for decompressing body handler. + */ + public static DecompressingBodyHandlerBuilder decompressingBuilder() { + return new DecompressingBodyHandlerBuilder(); + } + + /** + * The builder for decompressing body handler. + */ + public static final class DecompressingBodyHandlerBuilder { + private boolean failOnUnsupportedDirectives = true; + private boolean failOnUnknownDirectives = true; + + /** + * Sets whether throw exception when compression directive not supported or not. + * + * @param failOnUnsupportedDirectives Whether throw exception when compression directive not supported or not + * @return this for fluent chaining. + */ + public DecompressingBodyHandlerBuilder failOnUnsupportedDirectives(boolean failOnUnsupportedDirectives) { + this.failOnUnsupportedDirectives = failOnUnsupportedDirectives; + return this; + } + + /** + * Sets whether throw exception when unknown compression directive encountered or not. + * + * @param failOnUnknownDirectives Whether throw exception when unknown compression directive encountered or not + * @return this for fluent chaining. + */ + public DecompressingBodyHandlerBuilder failOnUnknownDirectives(boolean failOnUnknownDirectives) { + this.failOnUnknownDirectives = failOnUnknownDirectives; + return this; + } - /** - * @return The decompressing body handler. - */ - public static BodyHandler ofDecompressing() { - return new DecompressingBodyHandler(); - } + /** + * Creates the new decompressing body handler. + * + * @return The builder for decompressing body handler. + */ + public BodyHandler build() { + var config = new Options(failOnUnsupportedDirectives, failOnUnknownDirectives); + return new DecompressingBodyHandler(config); + } + } } diff --git a/src/main/java/io/github/nstdio/http/ext/DecompressingBodyHandler.java b/src/main/java/io/github/nstdio/http/ext/DecompressingBodyHandler.java index 345ff73..c0d117e 100644 --- a/src/main/java/io/github/nstdio/http/ext/DecompressingBodyHandler.java +++ b/src/main/java/io/github/nstdio/http/ext/DecompressingBodyHandler.java @@ -22,9 +22,6 @@ */ package io.github.nstdio.http.ext; -import static java.util.function.Predicate.not; -import static java.util.stream.Collectors.toList; - import java.io.IOException; import java.io.InputStream; import java.io.UncheckedIOException; @@ -37,57 +34,82 @@ import java.util.zip.GZIPInputStream; import java.util.zip.InflaterInputStream; +import static java.util.function.Predicate.not; +import static java.util.stream.Collectors.toList; + final class DecompressingBodyHandler implements BodyHandler { - private static final String HEADER_CONTENT_ENCODING = "Content-Encoding"; - private static final Pattern COMMA_PATTERN = Pattern.compile(",", Pattern.LITERAL); - private static final String UNSUPPORTED_DIRECTIVE = "Compression directive '%s' is not supported"; - private static final String UNKNOWN_DIRECTIVE = "Unknown compression directive '%s'"; + private static final String HEADER_CONTENT_ENCODING = "Content-Encoding"; + private static final Pattern COMMA_PATTERN = Pattern.compile(",", Pattern.LITERAL); + private static final String UNSUPPORTED_DIRECTIVE = "Compression directive '%s' is not supported"; + private static final String UNKNOWN_DIRECTIVE = "Unknown compression directive '%s'"; - // Visible for testing - static Function decompressionFn(String directive) { - switch (directive) { - case "x-gzip": - case "gzip": - return in -> { - try { - return new GZIPInputStream(in); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - }; - case "deflate": - return InflaterInputStream::new; - case "compress": - case "br": - throw new UnsupportedOperationException(String.format(UNSUPPORTED_DIRECTIVE, directive)); - default: - throw new IllegalArgumentException(String.format(UNKNOWN_DIRECTIVE, directive)); + private final Options options; + + DecompressingBodyHandler(Options config) { + this.options = config; } - } - @Override - public BodySubscriber apply(ResponseInfo responseInfo) { - var encodingOpt = responseInfo - .headers() - .firstValue(HEADER_CONTENT_ENCODING); + // Visible for testing + Function decompressionFn(String directive) { + switch (directive) { + case "x-gzip": + case "gzip": + return in -> { + try { + return new GZIPInputStream(in); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }; + case "deflate": + return InflaterInputStream::new; + case "compress": + case "br": + if (options.failOnUnsupportedDirectives) { + throw new UnsupportedOperationException(String.format(UNSUPPORTED_DIRECTIVE, directive)); + } + return Function.identity(); + default: + if (options.failOnUnknownDirectives) { + throw new IllegalArgumentException(String.format(UNKNOWN_DIRECTIVE, directive)); + } - if (encodingOpt.isEmpty()) { - return BodySubscribers.ofInputStream(); + return Function.identity(); + } } - var encodings = COMMA_PATTERN - .splitAsStream(encodingOpt.get()) - .map(String::trim) - .filter(not(String::isEmpty)) - .collect(toList()); + @Override + public BodySubscriber apply(ResponseInfo responseInfo) { + var encodingOpt = responseInfo + .headers() + .firstValue(HEADER_CONTENT_ENCODING); + + if (encodingOpt.isEmpty()) { + return BodySubscribers.ofInputStream(); + } + + var encodings = COMMA_PATTERN + .splitAsStream(encodingOpt.get()) + .map(String::trim) + .filter(not(String::isEmpty)) + .collect(toList()); - return encodings - .stream() - .map(DecompressingBodyHandler::decompressionFn) - .reduce(Function::andThen) - .>map(DecompressingBodySubscriber::new) - .orElseGet(BodySubscribers::ofInputStream); - } + return encodings + .stream() + .map(this::decompressionFn) + .reduce(Function::andThen) + .>map(DecompressingBodySubscriber::new) + .orElseGet(BodySubscribers::ofInputStream); + } + + static class Options { + private final boolean failOnUnsupportedDirectives; + private final boolean failOnUnknownDirectives; + Options(boolean failOnUnsupportedDirectives, boolean failOnUnknownDirectives) { + this.failOnUnsupportedDirectives = failOnUnsupportedDirectives; + this.failOnUnknownDirectives = failOnUnknownDirectives; + } + } } diff --git a/src/test/java/io/github/nstdio/http/ext/DecompressingBodyHandlerTest.java b/src/test/java/io/github/nstdio/http/ext/DecompressingBodyHandlerTest.java index 2645b5d..ed85945 100644 --- a/src/test/java/io/github/nstdio/http/ext/DecompressingBodyHandlerTest.java +++ b/src/test/java/io/github/nstdio/http/ext/DecompressingBodyHandlerTest.java @@ -22,65 +22,116 @@ */ package io.github.nstdio.http.ext; -import static io.github.nstdio.http.ext.Compression.deflate; -import static io.github.nstdio.http.ext.Compression.gzip; -import static io.github.nstdio.http.ext.DecompressingBodyHandler.decompressionFn; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; - +import io.github.nstdio.http.ext.DecompressingBodyHandler.Options; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; import java.io.ByteArrayInputStream; +import java.io.InputStream; import java.util.zip.GZIPInputStream; import java.util.zip.InflaterInputStream; +import static io.github.nstdio.http.ext.Compression.deflate; +import static io.github.nstdio.http.ext.Compression.gzip; +import static org.assertj.core.api.Assertions.*; + class DecompressingBodyHandlerTest { - @ParameterizedTest - @ValueSource(strings = {"gzip", "x-gzip"}) - void shouldReturnGzipInputStream(String directive) { - var gzipContent = new ByteArrayInputStream(gzip("abc")); - - //when - var fn = decompressionFn(directive); - var inputStream = fn.apply(gzipContent); - - //then - assertThat(inputStream) - .isInstanceOf(GZIPInputStream.class); - } - - @Test - void shouldReturnDeflateInputStream() { - var deflateContent = new ByteArrayInputStream(deflate("abc")); - - //when - var fn = decompressionFn("deflate"); - var inputStream = fn.apply(deflateContent); - - //then - assertThat(inputStream) - .isInstanceOf(InflaterInputStream.class); - } - - @ParameterizedTest - @ValueSource(strings = {"compress", "br"}) - void shouldThrowUnsupportedOperationException(String directive) { - //when + then - assertThatExceptionOfType(UnsupportedOperationException.class) - .isThrownBy(() -> decompressionFn(directive)) - .withMessage("Compression directive '%s' is not supported", directive); - } - - @ParameterizedTest - @ValueSource(strings = {"", "abc", "gz", "a"}) - void shouldThrowIllegalArgumentException(String directive) { - //when + then - assertThatIllegalArgumentException() - .isThrownBy(() -> decompressionFn(directive)) - .withMessage("Unknown compression directive '%s'", directive); - } + private DecompressingBodyHandler handler; + + @BeforeEach + void setUp() { + handler = new DecompressingBodyHandler(new Options(true, true)); + } + + @ParameterizedTest + @ValueSource(strings = {"gzip", "x-gzip"}) + void shouldReturnGzipInputStream(String directive) { + var gzipContent = new ByteArrayInputStream(gzip("abc")); + + //when + var fn = handler.decompressionFn(directive); + var inputStream = fn.apply(gzipContent); + + //then + assertThat(inputStream) + .isInstanceOf(GZIPInputStream.class); + } + + @Test + void shouldReturnDeflateInputStream() { + var deflateContent = new ByteArrayInputStream(deflate("abc")); + + //when + var fn = handler.decompressionFn("deflate"); + var inputStream = fn.apply(deflateContent); + + //then + assertThat(inputStream) + .isInstanceOf(InflaterInputStream.class); + } + + + @Nested + class FailureControlOptions { + @ParameterizedTest + @ValueSource(strings = {"compress", "br"}) + void shouldThrowUnsupportedOperationException(String directive) { + //given + var options = new Options(true, true); + var handler = new DecompressingBodyHandler(options); + + //when + then + assertThatExceptionOfType(UnsupportedOperationException.class) + .isThrownBy(() -> handler.decompressionFn(directive)) + .withMessage("Compression directive '%s' is not supported", directive); + } + + @ParameterizedTest + @ValueSource(strings = {"", "abc", "gz", "a"}) + void shouldThrowIllegalArgumentException(String directive) { + //when + then + assertThatIllegalArgumentException() + .isThrownBy(() -> handler.decompressionFn(directive)) + .withMessage("Unknown compression directive '%s'", directive); + } + + @ParameterizedTest + @ValueSource(strings = {"compress", "br"}) + @DisplayName("Should not throw exception when 'failOnUnsupportedDirectives' is 'false'") + void shouldNotThrowUnsupportedOperationException(String directive) { + //given + var options = new Options(false, true); + var handler = new DecompressingBodyHandler(options); + var in = InputStream.nullInputStream(); + + //when + var fn = handler.decompressionFn(directive); + var actual = fn.apply(in); + + //then + assertThat(actual).isSameAs(in); + } + + @ParameterizedTest + @ValueSource(strings = {"", "abc", "gz", "a"}) + @DisplayName("Should not throw exception when 'failOnUnknownDirectives' is 'false'") + void shouldNotIllegalArgumentException(String directive) { + //given + var options = new Options(true, false); + var handler = new DecompressingBodyHandler(options); + var in = InputStream.nullInputStream(); + + //when + var fn = handler.decompressionFn(directive); + var actual = fn.apply(in); + + //then + assertThat(actual).isSameAs(in); + } + } }