Skip to content

Commit ebfbd8c

Browse files
committed
[HTTPCLIENT-1843] - Delegate compression handling to Apache Commons Compress
* Integrated Apache Commons Compress into CompressorFactory to handle compression and decompression of HTTP entities using supported algorithms (gzip, deflate, etc.).
1 parent aad0e9a commit ebfbd8c

33 files changed

+1160
-111
lines changed

httpclient5-testing/src/test/java/org/apache/hc/client5/testing/extension/sync/StandardTestClientBuilder.java

+9
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ final class StandardTestClientBuilder implements TestClientBuilder {
5757

5858
private HttpClientConnectionManager connectionManager;
5959

60+
private boolean noWrap;
61+
6062
public StandardTestClientBuilder() {
6163
this.clientBuilder = HttpClientBuilder.create();
6264
}
@@ -72,6 +74,12 @@ public TestClientBuilder setTimeout(final Timeout timeout) {
7274
return this;
7375
}
7476

77+
@Override
78+
public TestClientBuilder setNoWrap(final boolean noWrap) {
79+
this.noWrap = noWrap;
80+
return this;
81+
}
82+
7583
@Override
7684
public TestClientBuilder setConnectionManager(final HttpClientConnectionManager connectionManager) {
7785
this.connectionManager = connectionManager;
@@ -165,6 +173,7 @@ public TestClient build() throws Exception {
165173

166174
final CloseableHttpClient client = clientBuilder
167175
.setConnectionManager(connectionManagerCopy)
176+
.setNoWrap(noWrap)
168177
.build();
169178
return new TestClient(client, connectionManagerCopy);
170179
}

httpclient5-testing/src/test/java/org/apache/hc/client5/testing/extension/sync/TestClientBuilder.java

+4
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,10 @@ default TestClientBuilder addExecInterceptorLast(String name, ExecChainHandler
9898
throw new UnsupportedOperationException("Operation not supported by " + getProtocolLevel());
9999
}
100100

101+
default TestClientBuilder setNoWrap(boolean noWrap){
102+
return this;
103+
}
104+
101105
TestClient build() throws Exception;
102106

103107
}

httpclient5-testing/src/test/java/org/apache/hc/client5/testing/extension/sync/TestClientResources.java

+5
Original file line numberDiff line numberDiff line change
@@ -118,4 +118,9 @@ public TestClient client() throws Exception {
118118
return client;
119119
}
120120

121+
public TestClient client(final boolean noWrap) throws Exception {
122+
clientBuilder.setNoWrap(noWrap);
123+
return client();
124+
}
125+
121126
}

httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/AbstractIntegrationTestBase.java

+4
Original file line numberDiff line numberDiff line change
@@ -78,4 +78,8 @@ public TestClient client() throws Exception {
7878
return testResources.client();
7979
}
8080

81+
public TestClient client(final boolean noWrap) throws Exception {
82+
return testResources.client(noWrap);
83+
}
84+
8185
}

httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestContentCodings.java

+3-3
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ void testDeflateSupportForServerReturningRfc1950Stream() throws Exception {
108108

109109
final HttpHost target = startServer();
110110

111-
final TestClient client = client();
111+
final TestClient client = client(true);
112112

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

134134
final HttpHost target = startServer();
135135

136-
final TestClient client = client();
136+
final TestClient client = client(false);
137137

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

290290
final HttpHost target = startServer();
291291

292-
final TestClient client = client();
292+
final TestClient client = client(true);
293293

294294
final HttpGet request = new HttpGet("/some-resource");
295295
final String response = client.execute(target, request, new BasicHttpClientResponseHandler());

httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestRedirects.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -639,8 +639,8 @@ public void handle(final ClassicHttpRequest request,
639639
Assertions.assertEquals(new URIBuilder().setHttpHost(target).setPath("/random/100").build(),
640640
reqWrapper.getUri());
641641

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

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
/*
2+
* ====================================================================
3+
* Licensed to the Apache Software Foundation (ASF) under one
4+
* or more contributor license agreements. See the NOTICE file
5+
* distributed with this work for additional information
6+
* regarding copyright ownership. The ASF licenses this file
7+
* to you under the Apache License, Version 2.0 (the
8+
* "License"); you may not use this file except in compliance
9+
* with the License. You may obtain a copy of the License at
10+
*
11+
* http://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing,
14+
* software distributed under the License is distributed on an
15+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16+
* KIND, either express or implied. See the License for the
17+
* specific language governing permissions and limitations
18+
* under the License.
19+
* ====================================================================
20+
*
21+
* This software consists of voluntary contributions made by many
22+
* individuals on behalf of the Apache Software Foundation. For more
23+
* information on the Apache Software Foundation, please see
24+
* <http://www.apache.org/>.
25+
*
26+
*/
27+
28+
package org.apache.hc.client5.testing.sync.compress;
29+
30+
import java.util.Arrays;
31+
import java.util.List;
32+
33+
import org.apache.hc.client5.http.entity.CompressorFactory;
34+
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
35+
import org.apache.hc.client5.http.impl.classic.HttpClients;
36+
import org.apache.hc.core5.http.ClassicHttpRequest;
37+
import org.apache.hc.core5.http.ContentType;
38+
import org.apache.hc.core5.http.HttpEntity;
39+
import org.apache.hc.core5.http.HttpHeaders;
40+
import org.apache.hc.core5.http.HttpHost;
41+
import org.apache.hc.core5.http.io.entity.EntityUtils;
42+
import org.apache.hc.core5.http.io.entity.StringEntity;
43+
import org.apache.hc.core5.http.io.support.ClassicRequestBuilder;
44+
import org.apache.hc.core5.io.CloseMode;
45+
import org.apache.hc.core5.testing.classic.ClassicTestServer;
46+
47+
48+
/**
49+
* Demonstrates handling of HTTP responses with content compression using Apache HttpClient.
50+
* <p>
51+
* This example sets up a local test server that simulates compressed HTTP responses. It then
52+
* creates a custom HttpClient configured to handle compression. The client makes a request to
53+
* the test server, receives a compressed response, and decompresses the content to verify the
54+
* process.
55+
* <p>
56+
* The main focus of this example is to illustrate the use of a custom HttpClient that can
57+
* handle compressed HTTP responses transparently, simulating a real-world scenario where
58+
* responses from a server might be compressed to reduce bandwidth usage.
59+
*/
60+
public class CompressedResponseHandlingExample {
61+
62+
public static void main(final String[] args) {
63+
64+
final ClassicTestServer server = new ClassicTestServer();
65+
try {
66+
server.register("/compressed", (request, response, context) -> {
67+
final String uncompressedContent = "This is the uncompressed response content";
68+
response.setEntity(compress(uncompressedContent, "gzip"));
69+
response.addHeader(HttpHeaders.CONTENT_ENCODING, "gzip");
70+
});
71+
72+
server.start();
73+
74+
final HttpHost target = new HttpHost("localhost", server.getPort());
75+
76+
final List<String> encodingList = Arrays.asList("gz", "deflate");
77+
78+
try (final CloseableHttpClient httpclient = HttpClients
79+
.custom()
80+
.setEncodings(encodingList)
81+
.build()) {
82+
final ClassicHttpRequest httpGet = ClassicRequestBuilder.get()
83+
.setHttpHost(target)
84+
.setPath("/compressed")
85+
.build();
86+
87+
System.out.println("Executing request " + httpGet.getMethod() + " " + httpGet.getUri());
88+
httpclient.execute(httpGet, response -> {
89+
System.out.println("----------------------------------------");
90+
System.out.println(httpGet + "->" + response.getCode() + " " + response.getReasonPhrase());
91+
92+
final HttpEntity responseEntity = response.getEntity();
93+
final String responseBody = EntityUtils.toString(responseEntity);
94+
System.out.println("Response content: " + responseBody);
95+
96+
return null;
97+
});
98+
}
99+
100+
} catch (final Exception e) {
101+
e.printStackTrace();
102+
} finally {
103+
server.shutdown(CloseMode.GRACEFUL);
104+
}
105+
}
106+
107+
108+
private static HttpEntity compress(final String data, final String name) {
109+
final StringEntity originalEntity = new StringEntity(data, ContentType.TEXT_PLAIN);
110+
return CompressorFactory.INSTANCE.compressEntity(originalEntity, name);
111+
}
112+
113+
}

httpclient5/pom.xml

+4
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,10 @@
9797
<artifactId>mockito-core</artifactId>
9898
<scope>test</scope>
9999
</dependency>
100+
<dependency>
101+
<groupId>org.apache.commons</groupId>
102+
<artifactId>commons-compress</artifactId>
103+
</dependency>
100104
</dependencies>
101105

102106
<build>

httpclient5/src/main/java/org/apache/hc/client5/http/entity/BrotliDecompressingEntity.java

+2
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,9 @@
3434
*
3535
* @see GzipDecompressingEntity
3636
* @since 5.2
37+
* @deprecated Use {@link CompressorFactory} for handling Brotli decompression.
3738
*/
39+
@Deprecated
3840
public class BrotliDecompressingEntity extends DecompressingEntity {
3941
/**
4042
* Creates a new {@link DecompressingEntity}.

httpclient5/src/main/java/org/apache/hc/client5/http/entity/BrotliInputStreamFactory.java

+2
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,9 @@
3737
* {@link InputStreamFactory} for handling Brotli Content Coded responses.
3838
*
3939
* @since 5.2
40+
* @deprecated Use {@link CompressorFactory} for handling Brotli compression.
4041
*/
42+
@Deprecated
4143
@Contract(threading = ThreadingBehavior.STATELESS)
4244
public class BrotliInputStreamFactory implements InputStreamFactory {
4345

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
/*
2+
* ====================================================================
3+
* Licensed to the Apache Software Foundation (ASF) under one
4+
* or more contributor license agreements. See the NOTICE file
5+
* distributed with this work for additional information
6+
* regarding copyright ownership. The ASF licenses this file
7+
* to you under the Apache License, Version 2.0 (the
8+
* "License"); you may not use this file except in compliance
9+
* with the License. You may obtain a copy of the License at
10+
*
11+
* http://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing,
14+
* software distributed under the License is distributed on an
15+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16+
* KIND, either express or implied. See the License for the
17+
* specific language governing permissions and limitations
18+
* under the License.
19+
* ====================================================================
20+
*
21+
* This software consists of voluntary contributions made by many
22+
* individuals on behalf of the Apache Software Foundation. For more
23+
* information on the Apache Software Foundation, please see
24+
* <http://www.apache.org/>.
25+
*
26+
*/
27+
package org.apache.hc.client5.http.entity;
28+
29+
import java.io.IOException;
30+
import java.io.InputStream;
31+
import java.io.OutputStream;
32+
33+
import org.apache.commons.compress.compressors.CompressorException;
34+
import org.apache.hc.core5.http.HttpEntity;
35+
import org.apache.hc.core5.http.io.entity.HttpEntityWrapper;
36+
import org.apache.hc.core5.util.Args;
37+
38+
39+
/**
40+
* An {@link HttpEntity} wrapper that applies compression to the content before writing it to
41+
* an output stream. This class supports various compression algorithms based on the
42+
* specified content encoding.
43+
*
44+
* <p>Compression is performed using {@link CompressorFactory}, which returns a corresponding
45+
* {@link OutputStream} for the requested compression type. This class does not support
46+
* reading the content directly through {@link #getContent()} as the content is always compressed
47+
* during write operations.</p>
48+
*
49+
* @since 5.5
50+
*/
51+
public class CompressingEntity extends HttpEntityWrapper {
52+
53+
/**
54+
* The content encoding type, e.g., "gzip", "deflate", etc.
55+
*/
56+
private final String contentEncoding;
57+
58+
/**
59+
* Creates a new {@link CompressingEntity} that compresses the wrapped entity's content
60+
* using the specified content encoding.
61+
*
62+
* @param entity the {@link HttpEntity} to wrap and compress; must not be {@code null}.
63+
* @param contentEncoding the content encoding to use for compression, e.g., "gzip".
64+
*/
65+
public CompressingEntity(final HttpEntity entity, final String contentEncoding) {
66+
super(entity);
67+
this.contentEncoding = Args.notNull(contentEncoding, "Content encoding");
68+
}
69+
70+
/**
71+
* Returns the content encoding used for compression.
72+
*
73+
* @return the content encoding (e.g., "gzip", "deflate").
74+
*/
75+
@Override
76+
public String getContentEncoding() {
77+
return contentEncoding;
78+
}
79+
80+
81+
/**
82+
* Returns whether the entity is chunked. This is determined by the wrapped entity.
83+
*
84+
* @return {@code true} if the entity is chunked, {@code false} otherwise.
85+
*/
86+
@Override
87+
public boolean isChunked() {
88+
return super.isChunked();
89+
}
90+
91+
92+
/**
93+
* This method is unsupported because the content is meant to be compressed during the
94+
* {@link #writeTo(OutputStream)} operation.
95+
*
96+
* @throws UnsupportedOperationException always, as this method is not supported.
97+
*/
98+
@Override
99+
public InputStream getContent() throws IOException {
100+
throw new UnsupportedOperationException("Reading content is not supported for CompressingEntity");
101+
}
102+
103+
/**
104+
* Writes the compressed content to the provided {@link OutputStream}. Compression is performed
105+
* using the content encoding provided during entity construction.
106+
*
107+
* @param outStream the {@link OutputStream} to which the compressed content will be written; must not be {@code null}.
108+
* @throws IOException if an I/O error occurs during compression or writing.
109+
* @throws UnsupportedOperationException if the specified compression type is not supported.
110+
*/
111+
@Override
112+
public void writeTo(final OutputStream outStream) throws IOException {
113+
Args.notNull(outStream, "Output stream");
114+
115+
// Get the compressor based on the specified content encoding
116+
final OutputStream compressorStream;
117+
try {
118+
compressorStream = CompressorFactory.INSTANCE.getCompressorOutputStream(contentEncoding, outStream);
119+
} catch (final CompressorException e) {
120+
throw new IOException("Error initializing decompression stream", e);
121+
}
122+
123+
if (compressorStream != null) {
124+
// Write compressed data
125+
super.writeTo(compressorStream);
126+
// Close the compressor stream after writing
127+
compressorStream.close();
128+
} else {
129+
throw new UnsupportedOperationException("Unsupported compression: " + contentEncoding);
130+
}
131+
}
132+
}

0 commit comments

Comments
 (0)