Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[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.).
arturobernalg committed Sep 20, 2024
1 parent aad0e9a commit 0c68966
Showing 33 changed files with 1,171 additions and 111 deletions.
Original file line number Diff line number Diff line change
@@ -57,6 +57,8 @@ final class StandardTestClientBuilder implements TestClientBuilder {

private HttpClientConnectionManager connectionManager;

private boolean nowrap;

public StandardTestClientBuilder() {
this.clientBuilder = HttpClientBuilder.create();
}
@@ -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;
@@ -165,6 +173,7 @@ public TestClient build() throws Exception {

final CloseableHttpClient client = clientBuilder
.setConnectionManager(connectionManagerCopy)
.setNowrap(nowrap)
.build();
return new TestClient(client, connectionManagerCopy);
}
Original file line number Diff line number Diff line change
@@ -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
@@ -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
@@ -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
@@ -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 -> {
@@ -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 -> {
@@ -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());
Original file line number Diff line number Diff line change
@@ -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());
}

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
@@ -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>
Original file line number Diff line number Diff line change
@@ -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}.
Original file line number Diff line number Diff line change
@@ -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 {

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 0c68966

Please sign in to comment.