Skip to content

Commit 5889705

Browse files
committed
HTTPCLIENT-1843 Plug Commons-Compress into HttpClient’s automatic
content-decoding (optional) * New ContentDecoderRegistry discovers extra codecs (br, zstd, xz, lz4, …) via Commons-Compress when that jar is on the class-path; otherwise falls back to the built-ins (gzip, deflate) only. * No hard dependency added—projects that need the extra algorithms just add `commons-compress` (and helper jars like google-brotli, zstd-jni, xz-java) to their pom and HttpClient uses them automatically.
1 parent fd2870e commit 5889705

10 files changed

Lines changed: 473 additions & 24 deletions

File tree

httpclient5/pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,11 @@
108108
<artifactId>commons-io</artifactId>
109109
<scope>test</scope>
110110
</dependency>
111+
<dependency>
112+
<groupId>org.apache.commons</groupId>
113+
<artifactId>commons-compress</artifactId>
114+
<optional>true</optional>
115+
</dependency>
111116
</dependencies>
112117

113118
<build>

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,17 @@
4141
@Contract(threading = ThreadingBehavior.STATELESS)
4242
public class BrotliInputStreamFactory implements InputStreamFactory {
4343

44+
/**
45+
* Canonical token for the deflate content-coding.
46+
* @since 5.6
47+
*/
48+
public static final String ENCODING = "br";
49+
50+
@Override
51+
public String getContentEncoding() {
52+
return ENCODING;
53+
}
54+
4455
/**
4556
* Default instance of {@link BrotliInputStreamFactory}.
4657
*/

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,17 @@
4141
@Contract(threading = ThreadingBehavior.STATELESS)
4242
public class DeflateInputStreamFactory implements InputStreamFactory {
4343

44+
/**
45+
* Canonical token for the deflate content-coding.
46+
* @since 5.6
47+
*/
48+
public static final String ENCODING = "deflate";
49+
50+
@Override
51+
public String getContentEncoding() {
52+
return ENCODING;
53+
}
54+
4455
/**
4556
* Default instance of {@link DeflateInputStreamFactory}.
4657
*/

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,17 @@
4242
@Contract(threading = ThreadingBehavior.STATELESS)
4343
public class GZIPInputStreamFactory implements InputStreamFactory {
4444

45+
/**
46+
* Canonical token for the gzip content-coding.
47+
* @since 5.6
48+
*/
49+
public static final String ENCODING = "gzip";
50+
51+
@Override
52+
public String getContentEncoding() {
53+
return ENCODING;
54+
}
55+
4556
/**
4657
* Default instance of {@link GZIPInputStreamFactory}.
4758
*/

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,21 @@ public interface InputStreamFactory {
3838

3939
InputStream create(InputStream inputStream) throws IOException;
4040

41+
/**
42+
* Returns the canonical {@code Content-Encoding} token handled by this
43+
* factory (for example {@code "gzip"}, {@code "deflate"}, {@code "br"}).
44+
* <p>
45+
* Implementations that do <strong>not</strong> represent a HTTP
46+
* content-decoder should simply inherit the default implementation,
47+
* which returns an empty string.
48+
*
49+
* @return the lower-case encoding token, or an empty string when the
50+
* factory is not intended for HTTP content-decoding
51+
*
52+
* @since 5.6
53+
*/
54+
default String getContentEncoding() {
55+
return "";
56+
}
57+
4158
}
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
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.http.entity.compress;
29+
30+
import java.io.IOException;
31+
import java.io.InputStream;
32+
import java.util.Collections;
33+
import java.util.EnumMap;
34+
import java.util.Locale;
35+
import java.util.Map;
36+
37+
import org.apache.commons.compress.compressors.CompressorException;
38+
import org.apache.commons.compress.compressors.CompressorStreamFactory;
39+
import org.apache.hc.client5.http.entity.InputStreamFactory;
40+
import org.apache.hc.core5.annotation.Contract;
41+
import org.apache.hc.core5.annotation.Internal;
42+
import org.apache.hc.core5.annotation.ThreadingBehavior;
43+
44+
/**
45+
* A factory for creating InputStream instances, utilizing Apache Commons Compress.
46+
* This class is compiled with Commons Compress as an optional dependency, loading
47+
* only when the library is present at runtime, avoiding mandatory inclusion in
48+
* downstream builds.
49+
* <p>
50+
* <p>
51+
* Some encodings require native helper JARs; runtime availability is checked
52+
* using a lightweight Class.forName probe to register codecs only when helpers
53+
* are present.
54+
*
55+
* @since 5.6
56+
*/
57+
@Internal
58+
@Contract(threading = ThreadingBehavior.STATELESS)
59+
final class CommonsCompressDecoderFactory implements InputStreamFactory {
60+
61+
62+
/**
63+
* Map of codings that need extra JARs → the fully‐qualified class we test for
64+
*/
65+
private static final Map<ContentCoding, String> REQUIRED_CLASS_NAME;
66+
67+
static {
68+
final Map<ContentCoding, String> m = new EnumMap<>(ContentCoding.class);
69+
m.put(ContentCoding.BROTLI, "org.brotli.dec.BrotliInputStream");
70+
m.put(ContentCoding.ZSTD, "com.github.luben.zstd.ZstdInputStream");
71+
m.put(ContentCoding.XZ, "org.tukaani.xz.XZInputStream");
72+
m.put(ContentCoding.LZMA, "org.tukaani.xz.XZInputStream");
73+
REQUIRED_CLASS_NAME = Collections.unmodifiableMap(m);
74+
}
75+
76+
private final String encoding;
77+
78+
CommonsCompressDecoderFactory(final String encoding) {
79+
this.encoding = encoding.toLowerCase(Locale.ROOT);
80+
}
81+
82+
@Override
83+
public String getContentEncoding() {
84+
return encoding;
85+
}
86+
87+
@Override
88+
public InputStream create(final InputStream source) throws IOException {
89+
try {
90+
return new CompressorStreamFactory()
91+
.createCompressorInputStream(encoding, source);
92+
} catch (final CompressorException | LinkageError ex) {
93+
throw new IOException(
94+
"Unable to decode Content-Encoding '" + encoding + '\'', ex);
95+
}
96+
}
97+
98+
99+
static boolean runtimeAvailable(final String token) {
100+
final ContentCoding coding = ContentCoding.fromToken(token);
101+
if (coding == null) {
102+
return true;
103+
}
104+
final String helper = REQUIRED_CLASS_NAME.get(coding);
105+
if (helper == null) {
106+
// no extra JAR needed
107+
return true;
108+
}
109+
try {
110+
Class.forName(helper, false,
111+
CommonsCompressDecoderFactory.class.getClassLoader());
112+
return true;
113+
} catch (final ClassNotFoundException | LinkageError ex) {
114+
return false;
115+
}
116+
}
117+
}
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
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.http.entity.compress;
29+
30+
import java.util.Locale;
31+
32+
/**
33+
* Enumeration of the canonical IANA content-coding tokens supported by HttpClient for
34+
* HTTP request and response bodies.
35+
* <p>
36+
* Each constant corresponds to the standard token used in the {@code Content-Encoding}
37+
* and {@code Accept-Encoding} headers. Some codings (e.g. Brotli, Zstandard, XZ/LZMA)
38+
* may require additional helper libraries at runtime.
39+
*
40+
* @since 5.6
41+
*/
42+
public enum ContentCoding {
43+
44+
/**
45+
* GZIP compression format.
46+
*/
47+
GZIP("gzip"),
48+
/**
49+
* "deflate" compression format (zlib or raw).
50+
*/
51+
DEFLATE("deflate"),
52+
/**
53+
* Legacy alias for GZIP.
54+
*/
55+
X_GZIP("x-gzip"),
56+
57+
// Optional codecs requiring Commons-Compress or native helpers
58+
/**
59+
* Brotli compression format.
60+
*/
61+
BROTLI("br"),
62+
/**
63+
* Zstandard compression format.
64+
*/
65+
ZSTD("zstd"),
66+
/**
67+
* XZ compression format.
68+
*/
69+
XZ("xz"),
70+
/**
71+
* LZMA compression format.
72+
*/
73+
LZMA("lzma"),
74+
/**
75+
* Framed LZ4 compression format.
76+
*/
77+
LZ4_FRAMED("lz4-framed"),
78+
/**
79+
* Block LZ4 compression format.
80+
*/
81+
LZ4_BLOCK("lz4-block"),
82+
/**
83+
* BZIP2 compression format.
84+
*/
85+
BZIP2("bzip2"),
86+
/**
87+
* Pack200 compression format.
88+
*/
89+
PACK200("pack200"),
90+
/**
91+
* Deflate64 compression format.
92+
*/
93+
DEFLATE64("deflate64");
94+
95+
private final String token;
96+
97+
ContentCoding(final String token) {
98+
this.token = token;
99+
}
100+
101+
/**
102+
* Returns the standard IANA token string for this content-coding.
103+
*
104+
* @return the lowercase token used in HTTP headers
105+
*/
106+
public String token() {
107+
return token;
108+
}
109+
110+
/**
111+
* Lookup the enum by token (case-insensitive).
112+
*/
113+
public static ContentCoding fromToken(final String token) {
114+
final String copyToken = token.toLowerCase(Locale.ROOT);
115+
for (final ContentCoding coding : values()) {
116+
if (coding.token.equals(copyToken)) {
117+
return coding;
118+
}
119+
}
120+
return null;
121+
}
122+
}

0 commit comments

Comments
 (0)