Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,6 @@ public class AwsJson1ProtocolTests {
@HttpClientRequestTests
@ProtocolTestFilter(
skipTests = {
// TODO: implement content-encoding
"SDKAppliedContentEncoding_awsJson1_0",
"SDKAppendsGzipAndIgnoresHttpProvidedEncoding_awsJson1_0",

// Skipping top-level input defaults isn't necessary in Smithy-Java given it uses builders and
// the defaults don't impact nullability. This applies to the following tests.
"AwsJson10ClientSkipsTopLevelDefaultValuesInInput",
Expand All @@ -42,8 +38,9 @@ public void requestTest(DataStream expected, DataStream actual) {
Node.parse(new String(ByteBufferUtils.getBytes(expected.asByteBuffer()),
StandardCharsets.UTF_8)));
}
assertEquals(expectedJson, new StringBuildingSubscriber(actual).getResult());

if (expected.contentType() != null) { // Skip request compression tests since they do not have expected body
assertEquals(expectedJson, new StringBuildingSubscriber(actual).getResult());
}
}

@HttpClientResponseTests
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,6 @@
skipOperations = {
// We dont ignore defaults on input shapes
"aws.protocoltests.restjson#OperationWithDefaults",
// TODO: support content-encoding
"aws.protocoltests.restjson#PutWithContentEncoding"
})
public class RestJson1ProtocolTests {
private static final String EMPTY_BODY = "";
Expand All @@ -50,7 +48,7 @@ public void requestTest(DataStream expected, DataStream actual) {
} else {
assertEquals(expectedStr, actualStr);
}
} else {
} else if (expected.contentType() != null) { // Skip request compression tests since they do not have expected body
assertEquals(EMPTY_BODY, actualStr);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@
import software.amazon.smithy.java.protocoltests.harness.HttpClientRequestTests;
import software.amazon.smithy.java.protocoltests.harness.HttpClientResponseTests;
import software.amazon.smithy.java.protocoltests.harness.ProtocolTest;
import software.amazon.smithy.java.protocoltests.harness.ProtocolTestFilter;
import software.amazon.smithy.java.protocoltests.harness.StringBuildingSubscriber;
import software.amazon.smithy.java.protocoltests.harness.TestType;

Expand All @@ -34,11 +33,6 @@
testType = TestType.CLIENT)
public class RestXmlProtocolTests {
@HttpClientRequestTests
@ProtocolTestFilter(
skipTests = {
"SDKAppliedContentEncoding_restXml",
"SDKAppendedGzipAfterProvidedEncoding_restXml",
})
public void requestTest(DataStream expected, DataStream actual) {
if (expected.contentLength() != 0) {
var a = new String(ByteBufferUtils.getBytes(actual.asByteBuffer()), StandardCharsets.UTF_8);
Expand All @@ -51,7 +45,7 @@ public void requestTest(DataStream expected, DataStream actual) {
} else {
assertEquals(a, b);
}
} else {
} else if (expected.contentType() != null) { // Skip request compression tests since they do not have expected body
assertEquals("", new StringBuildingSubscriber(actual).getResult());
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
import software.amazon.smithy.java.client.http.mock.MockQueue;
import software.amazon.smithy.java.client.http.plugins.ApplyHttpRetryInfoPlugin;
import software.amazon.smithy.java.client.http.plugins.HttpChecksumPlugin;
import software.amazon.smithy.java.client.http.plugins.RequestCompressionPlugin;
import software.amazon.smithy.java.client.http.plugins.UserAgentPlugin;
import software.amazon.smithy.java.core.serde.document.Document;
import software.amazon.smithy.java.dynamicclient.DynamicClient;
Expand Down Expand Up @@ -83,6 +84,7 @@ public class ClientTest {
SimpleAuthDetectionPlugin.class,
UserAgentPlugin.class,
ApplyHttpRetryInfoPlugin.class,
RequestCompressionPlugin.class,
HttpChecksumPlugin.class,
FooPlugin.class);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,17 @@ public final class HttpContext {
public static final Context.Key<HttpHeaders> ENDPOINT_RESOLVER_HTTP_HEADERS = Context.key(
"HTTP headers to use with the request returned from an endpoint resolver");

/**
* The minimum length of bytes threshold for a request body to be compressed. Defaults to 10240 bytes if not set.
*/
public static final Context.Key<Integer> REQUEST_MIN_COMPRESSION_SIZE_BYTES =
Context.key("Minimum bytes size for request compression");

/**
* If request compression is disabled.
*/
public static final Context.Key<Boolean> DISABLE_REQUEST_COMPRESSION =
Context.key("If request compression is disabled");

private HttpContext() {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/

package software.amazon.smithy.java.client.http.compression;

import java.util.List;
import software.amazon.smithy.java.io.datastream.DataStream;
import software.amazon.smithy.utils.ListUtils;

/**
* Represents a compression algorithm that can be used to compress request
* bodies.
*/
public interface CompressionAlgorithm {
/**
* The ID of the compression algorithm. This is matched against the algorithm
* names used in the trait e.g. "gzip"
*/
String algorithmId();

/**
* Compresses content of fixed length
*/
DataStream compress(DataStream data);

static List<CompressionAlgorithm> supportedAlgorithms() {
return ListUtils.of(new Gzip());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/

package software.amazon.smithy.java.client.http.compression;

import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.zip.GZIPOutputStream;
import software.amazon.smithy.java.io.ByteBufferOutputStream;
import software.amazon.smithy.java.io.datastream.DataStream;

public final class Gzip implements CompressionAlgorithm {

@Override
public String algorithmId() {
return "gzip";
}

@Override
public DataStream compress(DataStream data) {
if (!data.hasKnownLength()) { // Using streaming
return DataStream.ofInputStream(
new GzipCompressingInputStream(data.asInputStream()),
data.contentType(),
-1);
}

try (var bos = new ByteBufferOutputStream();
var in = data.asInputStream()) {
var gzip = new GZIPOutputStream(bos);
in.transferTo(gzip);
gzip.close();
return DataStream.ofBytes(bos.toByteBuffer().array());
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/

package software.amazon.smithy.java.client.http.compression;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UncheckedIOException;
import java.util.zip.GZIPOutputStream;

/**
* An InputStream that compresses data from a source InputStream using GZIP compression.
* This implementation lazily compress from the source data on-demand as it's read.
*/
final class GzipCompressingInputStream extends InputStream {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add javadoc.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This implementation underneath still reads the whole input stream into memory to do the compression.

private static final int CHUNK_SIZE = 8192;
private final InputStream source;
private final ByteArrayOutputStream bufferStream;
private final GZIPOutputStream gzipStream;
private final byte[] chunk = new byte[CHUNK_SIZE];
private byte[] buffer;
private int bufferPos;
private int bufferLimit;
private boolean sourceExhausted;
private boolean closed;

public GzipCompressingInputStream(InputStream source) {
this.source = source;
this.bufferStream = new ByteArrayOutputStream();
this.gzipStream = createGzipOutputStream(bufferStream);
this.buffer = new byte[0];
this.bufferPos = 0;
this.bufferLimit = 0;
this.sourceExhausted = false;
this.closed = false;
}

@Override
public int read() throws IOException {
byte[] b = new byte[1];
int result = read(b, 0, 1);
return result == -1 ? -1 : (b[0] & 0xFF);
}

@Override
public int read(byte[] b, int off, int len) throws IOException {
if (closed) {
throw new IOException("Stream closed");
}

if (b == null) {
throw new NullPointerException("b");
} else if (off < 0 || len < 0 || len > b.length - off) {
throw new IndexOutOfBoundsException();
} else if (len == 0) {
return 0;
}

// Try to fill the output buffer if it's empty
while (bufferPos >= bufferLimit) {
if (!fillBuffer()) {
return -1; // End of stream
}
}

// Copy available data from buffer
int available = bufferLimit - bufferPos;
int toRead = Math.min(available, len);
System.arraycopy(buffer, bufferPos, b, off, toRead);
bufferPos += toRead;

return toRead;
}

/**
* Reads a chunk from the source, compresses it, and fills the internal buffer.
*
* @return true if data was added to buffer, false if end of stream reached
*/
private boolean fillBuffer() throws IOException {
if (sourceExhausted) {
return false;
}

// Read a chunk from source
int bytesRead = source.read(chunk);

if (bytesRead == -1) {
// Source is exhausted, finish compression
gzipStream.finish();
sourceExhausted = true;
} else {
// Compress the chunk
gzipStream.write(chunk, 0, bytesRead);
gzipStream.flush();
}

// Get compressed data from buffer stream
byte[] compressed = bufferStream.toByteArray();
if (compressed.length > 0) {
buffer = compressed;
bufferPos = 0;
bufferLimit = compressed.length;
bufferStream.reset();
return true;
}

if (sourceExhausted) {
return bufferPos >= bufferLimit;
}
return true;
}

@Override
public void close() throws IOException {
if (!closed) {
closed = true;
try {
gzipStream.close();
} finally {
source.close();
}
}
}

@Override
public int available() throws IOException {
if (closed) {
throw new IOException("Stream closed");
}
return bufferLimit - bufferPos;
}

/**
* Utility method to avoid having to throw the checked IOException exception.
*/
private GZIPOutputStream createGzipOutputStream(OutputStream bufferStream) {
try {
return new GZIPOutputStream(bufferStream);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
}
Loading