Skip to content

Commit

Permalink
feat: Options to control failing on malformed, unknown directives.
Browse files Browse the repository at this point in the history
  • Loading branch information
nstdio committed Sep 25, 2021
1 parent de61dd5 commit ba7604a
Show file tree
Hide file tree
Showing 3 changed files with 232 additions and 105 deletions.
70 changes: 62 additions & 8 deletions src/main/java/io/github/nstdio/http/ext/BodyHandlers.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,21 +22,75 @@
*/
package io.github.nstdio.http.ext;

import io.github.nstdio.http.ext.DecompressingBodyHandler.Options;

import java.io.InputStream;
import java.net.http.HttpResponse.BodyHandler;

/**
* 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
* <a href="https://datatracker.ietf.org/doc/html/rfc2616#section-14.11">Content-Encoding</a> header semantics.
*
* @return The decompressing body handler.
*/
public static BodyHandler<InputStream> 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<InputStream> ofDecompressing() {
return new DecompressingBodyHandler();
}
/**
* Creates the new decompressing body handler.
*
* @return The builder for decompressing body handler.
*/
public BodyHandler<InputStream> build() {
var config = new Options(failOnUnsupportedDirectives, failOnUnknownDirectives);

return new DecompressingBodyHandler(config);
}
}
}
114 changes: 68 additions & 46 deletions src/main/java/io/github/nstdio/http/ext/DecompressingBodyHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<InputStream> {

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<InputStream, InputStream> 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<InputStream> apply(ResponseInfo responseInfo) {
var encodingOpt = responseInfo
.headers()
.firstValue(HEADER_CONTENT_ENCODING);
// Visible for testing
Function<InputStream, InputStream> 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<InputStream> 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)
.<BodySubscriber<InputStream>>map(DecompressingBodySubscriber::new)
.orElseGet(BodySubscribers::ofInputStream);
}
return encodings
.stream()
.map(this::decompressionFn)
.reduce(Function::andThen)
.<BodySubscriber<InputStream>>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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
}

0 comments on commit ba7604a

Please sign in to comment.