Skip to content

Commit 300da7f

Browse files
committed
Refactored compression and decompression logic to delegate stream handling to Apache Commons Compress. Removed redundant buffer management, simplified content encoding methods, and updated documentation. Addressed feedback regarding thread safety and alignment with existing API standards.
1 parent 48de43f commit 300da7f

15 files changed

Lines changed: 146 additions & 164 deletions

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

Lines changed: 2 additions & 2 deletions
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("snappy-raw, snappy-framed, xz, bzip2, lz4-framed, deflate64, br, lzma, zstd, lz4-block, deflate, gz, z, pack200"));
643-
assertThat(values.poll(), CoreMatchers.equalTo("snappy-raw, snappy-framed, xz, bzip2, lz4-framed, deflate64, br, lzma, zstd, lz4-block, deflate, gz, z, pack200"));
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

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
*
3535
* @see GzipDecompressingEntity
3636
* @since 5.2
37-
* @deprecated
37+
* @deprecated Use {@link CompressorFactory} for handling Brotli decompression.
3838
*/
3939
@Deprecated
4040
public class BrotliDecompressingEntity extends DecompressingEntity {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
* {@link InputStreamFactory} for handling Brotli Content Coded responses.
3838
*
3939
* @since 5.2
40-
* @deprecated
40+
* @deprecated Use {@link CompressorFactory} for handling Brotli compression.
4141
*/
4242
@Deprecated
4343
@Contract(threading = ThreadingBehavior.STATELESS)

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

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import java.io.InputStream;
3131
import java.io.OutputStream;
3232

33+
import org.apache.commons.compress.compressors.CompressorException;
3334
import org.apache.hc.core5.http.HttpEntity;
3435
import org.apache.hc.core5.http.io.entity.HttpEntityWrapper;
3536
import org.apache.hc.core5.util.Args;
@@ -45,7 +46,7 @@
4546
* reading the content directly through {@link #getContent()} as the content is always compressed
4647
* during write operations.</p>
4748
*
48-
* @since 5.4
49+
* @since 5.5
4950
*/
5051
public class CompressingEntity extends HttpEntityWrapper {
5152

@@ -76,16 +77,6 @@ public String getContentEncoding() {
7677
return contentEncoding;
7778
}
7879

79-
/**
80-
* Returns the length of the wrapped entity. As the content is compressed,
81-
* this will return the length of the wrapped entity's compressed data.
82-
*
83-
* @return the length of the compressed content, or {@code -1} if unknown.
84-
*/
85-
@Override
86-
public long getContentLength() {
87-
return super.getContentLength();
88-
}
8980

9081
/**
9182
* Returns whether the entity is chunked. This is determined by the wrapped entity.
@@ -97,6 +88,7 @@ public boolean isChunked() {
9788
return super.isChunked();
9889
}
9990

91+
10092
/**
10193
* This method is unsupported because the content is meant to be compressed during the
10294
* {@link #writeTo(OutputStream)} operation.
@@ -121,7 +113,12 @@ public void writeTo(final OutputStream outStream) throws IOException {
121113
Args.notNull(outStream, "Output stream");
122114

123115
// Get the compressor based on the specified content encoding
124-
final OutputStream compressorStream = CompressorFactory.INSTANCE.getCompressorOutputStream(contentEncoding, outStream);
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+
}
125122

126123
if (compressorStream != null) {
127124
// Write compressed data

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

Lines changed: 83 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -29,15 +29,14 @@
2929

3030
import java.io.InputStream;
3131
import java.io.OutputStream;
32-
import java.util.Collections;
33-
import java.util.HashMap;
3432
import java.util.Locale;
3533
import java.util.Map;
3634
import java.util.Set;
3735
import java.util.concurrent.ConcurrentHashMap;
3836
import java.util.concurrent.atomic.AtomicReference;
3937
import java.util.stream.Collectors;
4038

39+
import org.apache.commons.compress.compressors.CompressorException;
4140
import org.apache.commons.compress.compressors.CompressorStreamFactory;
4241
import org.apache.commons.compress.compressors.deflate.DeflateCompressorInputStream;
4342
import org.apache.commons.compress.compressors.deflate.DeflateParameters;
@@ -62,6 +61,8 @@
6261
* <p>
6362
* This class is thread-safe and uses {@link AtomicReference} to cache the available input and output stream providers.
6463
* </p>
64+
*
65+
* @since 5.5
6566
*/
6667
public class CompressorFactory {
6768

@@ -76,22 +77,13 @@ public class CompressorFactory {
7677
private final AtomicReference<Set<String>> outputProvidersCache = new AtomicReference<>();
7778
private final Map<String, String> formattedNameCache = new ConcurrentHashMap<>();
7879

79-
private static final Map<String, String> COMPRESSION_ALIASES;
80-
static {
81-
final Map<String, String> aliases = new HashMap<>();
82-
aliases.put("gzip", "gz");
83-
aliases.put("x-gzip", "gz");
84-
aliases.put("compress", "z");
85-
COMPRESSION_ALIASES = Collections.unmodifiableMap(aliases);
86-
}
87-
8880
/**
8981
* Returns a set of available input stream compression providers.
9082
*
9183
* @return a set of available input stream compression providers in lowercase.
9284
*/
9385
public Set<String> getAvailableInputProviders() {
94-
return getAvailableProviders(inputProvidersCache, false);
86+
return inputProvidersCache.updateAndGet(existing -> existing != null ? existing : fetchAvailableInputProviders());
9587
}
9688

9789
/**
@@ -100,7 +92,7 @@ public Set<String> getAvailableInputProviders() {
10092
* @return a set of available output stream compression providers in lowercase.
10193
*/
10294
public Set<String> getAvailableOutputProviders() {
103-
return getAvailableProviders(outputProvidersCache, true);
95+
return outputProvidersCache.updateAndGet(existing -> existing != null ? existing : fetchAvailableOutputProviders());
10496
}
10597

10698
/**
@@ -119,29 +111,35 @@ public String getFormattedName(final String name) {
119111
return null;
120112
}
121113
final String lowerCaseName = name.toLowerCase(Locale.ROOT);
122-
return formattedNameCache.computeIfAbsent(lowerCaseName, key -> COMPRESSION_ALIASES.getOrDefault(key, key));
114+
return formattedNameCache.computeIfAbsent(lowerCaseName, key -> {
115+
if ("gzip".equals(key) || "x-gzip".equals(key)) {
116+
return "gz";
117+
} else if ("compress".equals(key)) {
118+
return "z";
119+
}
120+
return key;
121+
});
123122
}
124123

125-
126124
/**
127125
* Creates an input stream for the specified compression format and decompresses the provided input stream.
128126
* <p>
129-
* This method uses the specified compression name to decompress the input stream and supports the "nowrap" option
127+
* This method uses the specified compression name to decompress the input stream and supports the "noWrap" option
130128
* for deflate streams.
131129
* </p>
132130
*
133131
* @param name the compression format.
134132
* @param inputStream the input stream to decompress.
135-
* @param nowrap if true, disables the zlib header and trailer for deflate streams.
133+
* @param noWrap if true, disables the zlib header and trailer for deflate streams.
136134
* @return the decompressed input stream, or the original input stream if the format is not supported.
137135
*/
138-
public InputStream getCompressorInputStream(final String name, final InputStream inputStream, final boolean nowrap) {
136+
public InputStream getCompressorInputStream(final String name, final InputStream inputStream, final boolean noWrap) throws CompressorException {
139137
Args.notNull(inputStream, "InputStream");
140138
Args.notNull(name, "name");
141139

142140
final String formattedName = getFormattedName(name);
143141
return isSupported(formattedName, false)
144-
? createCompressorInputStream(formattedName, inputStream, nowrap)
142+
? createCompressorInputStream(formattedName, inputStream, noWrap)
145143
: inputStream;
146144
}
147145

@@ -152,30 +150,15 @@ public InputStream getCompressorInputStream(final String name, final InputStream
152150
* @param outputStream the output stream to compress.
153151
* @return the compressed output stream, or the original output stream if the format is not supported.
154152
*/
155-
public OutputStream getCompressorOutputStream(final String name, final OutputStream outputStream) {
153+
public OutputStream getCompressorOutputStream(final String name, final OutputStream outputStream) throws CompressorException {
156154
final String formattedName = getFormattedName(name);
157155
return isSupported(formattedName, true)
158156
? createCompressorOutputStream(formattedName, outputStream)
159157
: outputStream;
160-
}
161158

162-
/**
163-
* Compresses the provided HTTP entity using the specified compression format.
164-
*
165-
* @param entity the HTTP entity to compress.
166-
* @param contentEncoding the compression format.
167-
* @return a compressed {@link HttpEntity}, or {@code null} if the compression format is unsupported.
168-
*/
169-
public HttpEntity compressEntity(final HttpEntity entity, final String contentEncoding) {
170-
Args.notNull(entity, "Entity");
171-
Args.notNull(contentEncoding, "Content Encoding");
172-
if (!isSupported(contentEncoding, true)) {
173-
LOG.warn("Unsupported compression type: {}", contentEncoding);
174-
return null;
175-
}
176-
return new CompressingEntity(entity, contentEncoding);
177159
}
178160

161+
179162
/**
180163
* Decompresses the provided HTTP entity using the specified compression format.
181164
*
@@ -192,103 +175,108 @@ public HttpEntity decompressEntity(final HttpEntity entity, final String content
192175
*
193176
* @param entity the HTTP entity to decompress.
194177
* @param contentEncoding the compression format.
195-
* @param nowrap if true, disables the zlib header and trailer for deflate streams.
178+
* @param noWrap if true, disables the zlib header and trailer for deflate streams.
196179
* @return a decompressed {@link HttpEntity}, or {@code null} if the compression format is unsupported.
197180
*/
198-
public HttpEntity decompressEntity(final HttpEntity entity, final String contentEncoding, final boolean nowrap) {
181+
public HttpEntity decompressEntity(final HttpEntity entity, final String contentEncoding, final boolean noWrap) {
199182
Args.notNull(entity, "Entity");
200183
Args.notNull(contentEncoding, "Content Encoding");
201184
if (!isSupported(contentEncoding, false)) {
202185
LOG.warn("Unsupported decompression type: {}", contentEncoding);
203186
return null;
204187
}
205-
return new DecompressEntity(entity, contentEncoding, nowrap);
188+
return new DecompressEntity(entity, contentEncoding, noWrap);
206189
}
207190

208191
/**
209-
* Creates a compressor input stream for the given compression format and input stream.
210-
* <p>
211-
* This method handles the special case for deflate compression where the zlib header can be skipped.
212-
* </p>
192+
* Compresses the provided HTTP entity using the specified compression format.
213193
*
214-
* @param name the compression format.
215-
* @param inputStream the input stream to decompress.
216-
* @param nowrap if true, disables the zlib header and trailer for deflate streams.
217-
* @return a decompressed input stream, or null if an error occurs.
194+
* @param entity the HTTP entity to compress.
195+
* @param contentEncoding the compression format.
196+
* @return a compressed {@link HttpEntity}, or {@code null} if the compression format is unsupported.
218197
*/
219-
private InputStream createCompressorInputStream(final String name, final InputStream inputStream, final boolean nowrap) {
220-
try {
221-
if ("deflate".equalsIgnoreCase(name)) {
222-
final DeflateParameters parameters = new DeflateParameters();
223-
parameters.setWithZlibHeader(nowrap);
224-
return new DeflateCompressorInputStream(inputStream, parameters);
225-
}
226-
return compressorStreamFactory.createCompressorInputStream(name, inputStream, true);
227-
} catch (final Exception ex) {
228-
LOG.warn("Could not create compressor {} input stream", name, ex);
198+
public HttpEntity compressEntity(final HttpEntity entity, final String contentEncoding) {
199+
Args.notNull(entity, "Entity");
200+
Args.notNull(contentEncoding, "Content Encoding");
201+
if (!isSupported(contentEncoding, true)) {
202+
LOG.warn("Unsupported compression type: {}", contentEncoding);
229203
return null;
230204
}
205+
return new CompressingEntity(entity, contentEncoding);
231206
}
232207

233208
/**
234-
* Determines if the specified compression format is supported for either input or output streams.
209+
* Fetches the available input stream compression providers from Commons Compress.
235210
*
236-
* @param name the compression format.
237-
* @param isOutput if true, checks if the format is supported for output; otherwise, checks for input support.
238-
* @return true if the format is supported, false otherwise.
211+
* @return a set of available input stream compression providers in lowercase.
239212
*/
240-
private boolean isSupported(final String name, final boolean isOutput) {
241-
final String formattedName = getFormattedName(name);
242-
return isOutput
243-
? getAvailableOutputProviders().contains(formattedName)
244-
: getAvailableInputProviders().contains(formattedName);
213+
private Set<String> fetchAvailableInputProviders() {
214+
final Set<String> inputNames = compressorStreamFactory.getInputStreamCompressorNames();
215+
return inputNames.stream()
216+
.map(String::toLowerCase)
217+
.collect(Collectors.toSet());
245218
}
246219

247220
/**
248-
* Creates a compressor output stream for the given compression format and output stream.
221+
* Fetches the available output stream compression providers from Commons Compress.
249222
*
250-
* @param name the compression format.
251-
* @param outputStream the output stream to compress.
252-
* @return a compressed output stream, or null if an error occurs.
223+
* @return a set of available output stream compression providers in lowercase.
253224
*/
254-
private OutputStream createCompressorOutputStream(final String name, final OutputStream outputStream) {
255-
try {
256-
return compressorStreamFactory.createCompressorOutputStream(name, outputStream);
257-
} catch (final Exception ex) {
258-
LOG.warn("Could not create compressor {} output stream", name, ex);
259-
260-
return null;
261-
}
225+
private Set<String> fetchAvailableOutputProviders() {
226+
final Set<String> outputNames = compressorStreamFactory.getOutputStreamCompressorNames();
227+
return outputNames.stream()
228+
.map(String::toLowerCase)
229+
.collect(Collectors.toSet());
262230
}
263231

264232
/**
265-
* Retrieves the available compression providers for input or output streams.
233+
* Creates a compressor input stream for the given compression format and input stream.
266234
* <p>
267-
* This method uses a cache to avoid redundant lookups and ensures the providers are formatted in lowercase.
235+
* This method handles the special case for deflate compression where the zlib header can be optionally included.
236+
* The noWrap parameter directly controls the behavior of the zlib header:
237+
* - If noWrap is {@code true}, the deflate stream is processed without zlib headers (raw Deflate).
238+
* - If noWrap is {@code false}, the deflate stream includes the zlib header.
268239
* </p>
269240
*
270-
* @param cache the cache that stores the available providers.
271-
* @param isOutput if true, retrieves available providers for output streams; otherwise, for input streams.
272-
* @return a set of available compression providers in lowercase.
241+
* @param name the compression format (e.g., "gzip", "deflate").
242+
* @param inputStream the input stream to decompress; must not be {@code null}.
243+
* @param noWrap if {@code true}, disables the zlib header and trailer for deflate streams (raw Deflate).
244+
* @return a decompressed input stream, or {@code null} if an error occurs during stream creation.
245+
* @throws CompressorException if an error occurs while creating the compressor input stream or if the compression format is unsupported.
273246
*/
274-
private Set<String> getAvailableProviders(final AtomicReference<Set<String>> cache, final boolean isOutput) {
275-
return cache.updateAndGet(existing -> existing != null ? existing : fetchAvailableProviders(isOutput));
247+
private InputStream createCompressorInputStream(final String name, final InputStream inputStream, final boolean noWrap) throws CompressorException {
248+
if ("deflate".equalsIgnoreCase(name)) {
249+
final DeflateParameters parameters = new DeflateParameters();
250+
parameters.setWithZlibHeader(noWrap);
251+
return new DeflateCompressorInputStream(inputStream, parameters);
252+
}
253+
return compressorStreamFactory.createCompressorInputStream(name, inputStream, true);
276254
}
277255

278256
/**
279-
* Fetches the available compression providers by querying the {@link CompressorStreamFactory}.
257+
* Creates a compressor output stream for the given compression format and output stream.
280258
*
281-
* @param isOutput if true, fetches available providers for output streams; otherwise, for input streams.
282-
* @return a set of available compression providers in lowercase.
259+
* @param name the compression format.
260+
* @param outputStream the output stream to compress.
261+
* @return a compressed output stream, or null if an error occurs.
262+
* @throws CompressorException if an error occurs while creating the compressor output stream.
283263
*/
284-
private Set<String> fetchAvailableProviders(final boolean isOutput) {
285-
return (isOutput
286-
? CompressorStreamFactory.findAvailableCompressorOutputStreamProviders()
287-
: CompressorStreamFactory.findAvailableCompressorInputStreamProviders())
288-
.keySet().stream()
289-
.map(String::toLowerCase)
290-
.collect(Collectors.collectingAndThen(Collectors.toSet(), Collections::unmodifiableSet));
264+
private OutputStream createCompressorOutputStream(final String name, final OutputStream outputStream) throws CompressorException {
265+
return compressorStreamFactory.createCompressorOutputStream(name, outputStream);
266+
}
267+
268+
/**
269+
* Determines if the specified compression format is supported for either input or output streams.
270+
*
271+
* @param name the compression format.
272+
* @param isOutput if true, checks if the format is supported for output; otherwise, checks for input support.
273+
* @return true if the format is supported, false otherwise.
274+
*/
275+
private boolean isSupported(final String name, final boolean isOutput) {
276+
final Set<String> availableProviders = isOutput ? getAvailableOutputProviders() : getAvailableInputProviders();
277+
return availableProviders.contains(name);
291278
}
292279
}
293280

294281

282+

0 commit comments

Comments
 (0)