Skip to content

Commit

Permalink
[HTTPCLIENT-1843] - Delegate compression handling to Apache Commons C…
Browse files Browse the repository at this point in the history
…ompress

 * Integrated Apache Commons Compress into CompressorFactory to handle compression and decompression of HTTP entities using supported algorithms (gzip, deflate, etc.).
  • Loading branch information
arturobernalg committed Sep 21, 2024
1 parent aad0e9a commit ebfbd8c
Show file tree
Hide file tree
Showing 33 changed files with 1,160 additions and 111 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ final class StandardTestClientBuilder implements TestClientBuilder {

private HttpClientConnectionManager connectionManager;

private boolean noWrap;

public StandardTestClientBuilder() {
this.clientBuilder = HttpClientBuilder.create();
}
Expand All @@ -72,6 +74,12 @@ public TestClientBuilder setTimeout(final Timeout timeout) {
return this;
}

@Override
public TestClientBuilder setNoWrap(final boolean noWrap) {
this.noWrap = noWrap;
return this;
}

@Override
public TestClientBuilder setConnectionManager(final HttpClientConnectionManager connectionManager) {
this.connectionManager = connectionManager;
Expand Down Expand Up @@ -165,6 +173,7 @@ public TestClient build() throws Exception {

final CloseableHttpClient client = clientBuilder
.setConnectionManager(connectionManagerCopy)
.setNoWrap(noWrap)
.build();
return new TestClient(client, connectionManagerCopy);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,10 @@ default TestClientBuilder addExecInterceptorLast(String name, ExecChainHandler
throw new UnsupportedOperationException("Operation not supported by " + getProtocolLevel());
}

default TestClientBuilder setNoWrap(boolean noWrap){
return this;
}

TestClient build() throws Exception;

}
Original file line number Diff line number Diff line change
Expand Up @@ -118,4 +118,9 @@ public TestClient client() throws Exception {
return client;
}

public TestClient client(final boolean noWrap) throws Exception {
clientBuilder.setNoWrap(noWrap);
return client();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -78,4 +78,8 @@ public TestClient client() throws Exception {
return testResources.client();
}

public TestClient client(final boolean noWrap) throws Exception {
return testResources.client(noWrap);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ void testDeflateSupportForServerReturningRfc1950Stream() throws Exception {

final HttpHost target = startServer();

final TestClient client = client();
final TestClient client = client(true);

final HttpGet request = new HttpGet("/some-resource");
client.execute(target, request, response -> {
Expand All @@ -133,7 +133,7 @@ void testDeflateSupportForServerReturningRfc1951Stream() throws Exception {

final HttpHost target = startServer();

final TestClient client = client();
final TestClient client = client(false);

final HttpGet request = new HttpGet("/some-resource");
client.execute(target, request, response -> {
Expand Down Expand Up @@ -289,7 +289,7 @@ void deflateResponsesWorkWithBasicResponseHandler() throws Exception {

final HttpHost target = startServer();

final TestClient client = client();
final TestClient client = client(true);

final HttpGet request = new HttpGet("/some-resource");
final String response = client.execute(target, request, new BasicHttpClientResponseHandler());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -639,8 +639,8 @@ public void handle(final ClassicHttpRequest request,
Assertions.assertEquals(new URIBuilder().setHttpHost(target).setPath("/random/100").build(),
reqWrapper.getUri());

assertThat(values.poll(), CoreMatchers.equalTo("gzip, x-gzip, deflate"));
assertThat(values.poll(), CoreMatchers.equalTo("gzip, x-gzip, deflate"));
assertThat(values.poll(), CoreMatchers.equalTo("snappy-raw, xz, snappy-framed, bzip2, lz4-framed, deflate64, br, lzma, zstd, lz4-block, gz, deflate, z, pack200"));
assertThat(values.poll(), CoreMatchers.equalTo("snappy-raw, xz, snappy-framed, bzip2, lz4-framed, deflate64, br, lzma, zstd, lz4-block, gz, deflate, z, pack200"));
assertThat(values.poll(), CoreMatchers.nullValue());
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/*
* ====================================================================
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF 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
*
* http://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.
* ====================================================================
*
* This software consists of voluntary contributions made by many
* individuals on behalf of the Apache Software Foundation. For more
* information on the Apache Software Foundation, please see
* <http://www.apache.org/>.
*
*/

package org.apache.hc.client5.testing.sync.compress;

import java.util.Arrays;
import java.util.List;

import org.apache.hc.client5.http.entity.CompressorFactory;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.core5.http.ClassicHttpRequest;
import org.apache.hc.core5.http.ContentType;
import org.apache.hc.core5.http.HttpEntity;
import org.apache.hc.core5.http.HttpHeaders;
import org.apache.hc.core5.http.HttpHost;
import org.apache.hc.core5.http.io.entity.EntityUtils;
import org.apache.hc.core5.http.io.entity.StringEntity;
import org.apache.hc.core5.http.io.support.ClassicRequestBuilder;
import org.apache.hc.core5.io.CloseMode;
import org.apache.hc.core5.testing.classic.ClassicTestServer;


/**
* Demonstrates handling of HTTP responses with content compression using Apache HttpClient.
* <p>
* This example sets up a local test server that simulates compressed HTTP responses. It then
* creates a custom HttpClient configured to handle compression. The client makes a request to
* the test server, receives a compressed response, and decompresses the content to verify the
* process.
* <p>
* The main focus of this example is to illustrate the use of a custom HttpClient that can
* handle compressed HTTP responses transparently, simulating a real-world scenario where
* responses from a server might be compressed to reduce bandwidth usage.
*/
public class CompressedResponseHandlingExample {

public static void main(final String[] args) {

final ClassicTestServer server = new ClassicTestServer();
try {
server.register("/compressed", (request, response, context) -> {
final String uncompressedContent = "This is the uncompressed response content";
response.setEntity(compress(uncompressedContent, "gzip"));
response.addHeader(HttpHeaders.CONTENT_ENCODING, "gzip");
});

server.start();

final HttpHost target = new HttpHost("localhost", server.getPort());

final List<String> encodingList = Arrays.asList("gz", "deflate");

try (final CloseableHttpClient httpclient = HttpClients
.custom()
.setEncodings(encodingList)
.build()) {
final ClassicHttpRequest httpGet = ClassicRequestBuilder.get()
.setHttpHost(target)
.setPath("/compressed")
.build();

System.out.println("Executing request " + httpGet.getMethod() + " " + httpGet.getUri());
httpclient.execute(httpGet, response -> {
System.out.println("----------------------------------------");
System.out.println(httpGet + "->" + response.getCode() + " " + response.getReasonPhrase());

final HttpEntity responseEntity = response.getEntity();
final String responseBody = EntityUtils.toString(responseEntity);
System.out.println("Response content: " + responseBody);

return null;
});
}

} catch (final Exception e) {
e.printStackTrace();
} finally {
server.shutdown(CloseMode.GRACEFUL);
}
}


private static HttpEntity compress(final String data, final String name) {
final StringEntity originalEntity = new StringEntity(data, ContentType.TEXT_PLAIN);
return CompressorFactory.INSTANCE.compressEntity(originalEntity, name);
}

}
4 changes: 4 additions & 0 deletions httpclient5/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,10 @@
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-compress</artifactId>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@
*
* @see GzipDecompressingEntity
* @since 5.2
* @deprecated Use {@link CompressorFactory} for handling Brotli decompression.
*/
@Deprecated
public class BrotliDecompressingEntity extends DecompressingEntity {
/**
* Creates a new {@link DecompressingEntity}.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,9 @@
* {@link InputStreamFactory} for handling Brotli Content Coded responses.
*
* @since 5.2
* @deprecated Use {@link CompressorFactory} for handling Brotli compression.
*/
@Deprecated
@Contract(threading = ThreadingBehavior.STATELESS)
public class BrotliInputStreamFactory implements InputStreamFactory {

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
/*
* ====================================================================
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF 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
*
* http://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.
* ====================================================================
*
* This software consists of voluntary contributions made by many
* individuals on behalf of the Apache Software Foundation. For more
* information on the Apache Software Foundation, please see
* <http://www.apache.org/>.
*
*/
package org.apache.hc.client5.http.entity;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

import org.apache.commons.compress.compressors.CompressorException;
import org.apache.hc.core5.http.HttpEntity;
import org.apache.hc.core5.http.io.entity.HttpEntityWrapper;
import org.apache.hc.core5.util.Args;


/**
* An {@link HttpEntity} wrapper that applies compression to the content before writing it to
* an output stream. This class supports various compression algorithms based on the
* specified content encoding.
*
* <p>Compression is performed using {@link CompressorFactory}, which returns a corresponding
* {@link OutputStream} for the requested compression type. This class does not support
* reading the content directly through {@link #getContent()} as the content is always compressed
* during write operations.</p>
*
* @since 5.5
*/
public class CompressingEntity extends HttpEntityWrapper {

/**
* The content encoding type, e.g., "gzip", "deflate", etc.
*/
private final String contentEncoding;

/**
* Creates a new {@link CompressingEntity} that compresses the wrapped entity's content
* using the specified content encoding.
*
* @param entity the {@link HttpEntity} to wrap and compress; must not be {@code null}.
* @param contentEncoding the content encoding to use for compression, e.g., "gzip".
*/
public CompressingEntity(final HttpEntity entity, final String contentEncoding) {
super(entity);
this.contentEncoding = Args.notNull(contentEncoding, "Content encoding");
}

/**
* Returns the content encoding used for compression.
*
* @return the content encoding (e.g., "gzip", "deflate").
*/
@Override
public String getContentEncoding() {
return contentEncoding;
}


/**
* Returns whether the entity is chunked. This is determined by the wrapped entity.
*
* @return {@code true} if the entity is chunked, {@code false} otherwise.
*/
@Override
public boolean isChunked() {
return super.isChunked();
}


/**
* This method is unsupported because the content is meant to be compressed during the
* {@link #writeTo(OutputStream)} operation.
*
* @throws UnsupportedOperationException always, as this method is not supported.
*/
@Override
public InputStream getContent() throws IOException {
throw new UnsupportedOperationException("Reading content is not supported for CompressingEntity");
}

/**
* Writes the compressed content to the provided {@link OutputStream}. Compression is performed
* using the content encoding provided during entity construction.
*
* @param outStream the {@link OutputStream} to which the compressed content will be written; must not be {@code null}.
* @throws IOException if an I/O error occurs during compression or writing.
* @throws UnsupportedOperationException if the specified compression type is not supported.
*/
@Override
public void writeTo(final OutputStream outStream) throws IOException {
Args.notNull(outStream, "Output stream");

// Get the compressor based on the specified content encoding
final OutputStream compressorStream;
try {
compressorStream = CompressorFactory.INSTANCE.getCompressorOutputStream(contentEncoding, outStream);
} catch (final CompressorException e) {
throw new IOException("Error initializing decompression stream", e);
}

if (compressorStream != null) {
// Write compressed data
super.writeTo(compressorStream);
// Close the compressor stream after writing
compressorStream.close();
} else {
throw new UnsupportedOperationException("Unsupported compression: " + contentEncoding);
}
}
}
Loading

0 comments on commit ebfbd8c

Please sign in to comment.