encoder(final String token) {
+ final String enc = token.toLowerCase(Locale.ROOT);
+ return out -> {
+ try {
+ final ClassLoader cl = CommonsCompressCodecFactory.class.getClassLoader();
+ final Class> factoryCls = Class.forName(FACTORY_CLASS, false, cl);
+ final Object factory = factoryCls.getConstructor().newInstance();
+ final Method m = factoryCls.getMethod("createCompressorOutputStream", String.class, OutputStream.class);
+ final Object cos = m.invoke(factory, enc, out);
+ return (OutputStream) cos;
+ } catch (final ClassNotFoundException e) {
+ throw new IOException("Apache Commons Compress is not on the classpath", e);
+ } catch (final ReflectiveOperationException | IllegalArgumentException | LinkageError e) {
+ throw new IOException("Unable to encode using Content-Encoding '" + enc + '\'', e);
+ }
+ };
+ }
+
+ /**
+ * Best-effort availability probe for optional Commons-Compress codecs.
+ *
+ * Returns {@code true} only if the CC factory and the codec-specific
+ * implementation (and helper classes if required) are present on the
+ * classpath. Built-in gzip/deflate are handled elsewhere and are not
+ * probed here.
+ *
+ */
+ static boolean runtimeAvailable(final ContentCoding coding) {
+ if (coding == null) {
+ return false;
+ }
+ if (!isPresent(FACTORY_CLASS)) {
+ return false;
+ }
+ switch (coding) {
+ case BROTLI:
+ return isPresent(CC_BROTLI) && isPresent(H_BROTLI);
+ case ZSTD:
+ return isPresent(CC_ZSTD) && isPresent(H_ZSTD);
+ case XZ:
+ return isPresent(CC_XZ) && isPresent(H_XZ);
+ case LZMA:
+ return isPresent(CC_LZMA) && isPresent(H_XZ);
+ case LZ4_FRAMED:
+ return isPresent(CC_LZ4_F);
+ case LZ4_BLOCK:
+ return isPresent(CC_LZ4_B);
+ case BZIP2:
+ return isPresent(CC_BZIP2);
+ case PACK200:
+ return isPresent(CC_PACK200) || isPresent("java.util.jar.Pack200");
+ case DEFLATE64:
+ return isPresent(CC_DEFLATE64);
+ default:
+ return false;
+ }
+ }
+}
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/entity/compress/CommonsCompressDecoderFactory.java b/httpclient5/src/main/java/org/apache/hc/client5/http/entity/compress/CommonsCompressDecoderFactory.java
deleted file mode 100644
index 9bc7d77708..0000000000
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/entity/compress/CommonsCompressDecoderFactory.java
+++ /dev/null
@@ -1,111 +0,0 @@
-/*
- * ====================================================================
- * 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
- * .
- *
- */
-
-package org.apache.hc.client5.http.entity.compress;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.util.Collections;
-import java.util.EnumMap;
-import java.util.Locale;
-import java.util.Map;
-
-import org.apache.commons.compress.compressors.CompressorException;
-import org.apache.commons.compress.compressors.CompressorStreamFactory;
-import org.apache.hc.core5.annotation.Contract;
-import org.apache.hc.core5.annotation.Internal;
-import org.apache.hc.core5.annotation.ThreadingBehavior;
-import org.apache.hc.core5.io.IOFunction;
-
-/**
- * A factory for creating InputStream instances, utilizing Apache Commons Compress.
- * This class is compiled with Commons Compress as an optional dependency, loading
- * only when the library is present at runtime, avoiding mandatory inclusion in
- * downstream builds.
- *
- *
- * Some encodings require native helper JARs; runtime availability is checked
- * using a lightweight Class.forName probe to register codecs only when helpers
- * are present.
- *
- * @since 5.6
- */
-@Internal
-@Contract(threading = ThreadingBehavior.STATELESS)
-final class CommonsCompressDecoderFactory {
-
- /**
- * Map of codings that need extra JARs → the fully‐qualified class we test for
- */
- private static final Map REQUIRED_CLASS_NAME;
-
- static {
- final Map m = new EnumMap<>(ContentCoding.class);
- m.put(ContentCoding.BROTLI, "org.brotli.dec.BrotliInputStream");
- m.put(ContentCoding.ZSTD, "com.github.luben.zstd.ZstdInputStream");
- m.put(ContentCoding.XZ, "org.tukaani.xz.XZInputStream");
- m.put(ContentCoding.LZMA, "org.tukaani.xz.XZInputStream");
- REQUIRED_CLASS_NAME = Collections.unmodifiableMap(m);
- }
-
- /**
- * @return lazy decoder for the given IANA token (lower-case).
- */
- static IOFunction decoder(final String token) {
- final String enc = token.toLowerCase(Locale.ROOT);
- final CompressorStreamFactory factory = new CompressorStreamFactory();
- return in -> {
- try {
- return factory.createCompressorInputStream(enc, in);
- } catch (final CompressorException | LinkageError ex) {
- throw new IOException("Unable to decode Content-Encoding '" + enc + '\'', ex);
- }
- };
- }
-
- /**
- * Tests that required helper classes are present for a coding token.
- */
- static boolean runtimeAvailable(final String token) {
- final ContentCoding coding = ContentCoding.fromToken(token);
- if (coding == null) {
- return true;
- }
- final String helper = REQUIRED_CLASS_NAME.get(coding);
- if (helper == null) {
- // no extra JAR needed
- return true;
- }
- try {
- Class.forName(helper, false,
- CommonsCompressDecoderFactory.class.getClassLoader());
- return true;
- } catch (final ClassNotFoundException | LinkageError ex) {
- return false;
- }
- }
-}
\ No newline at end of file
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/entity/compress/CommonsCompressSupport.java b/httpclient5/src/main/java/org/apache/hc/client5/http/entity/compress/CommonsCompressSupport.java
index 986f5c5683..2053bd59d4 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/entity/compress/CommonsCompressSupport.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/entity/compress/CommonsCompressSupport.java
@@ -33,9 +33,12 @@
import org.apache.hc.core5.annotation.ThreadingBehavior;
/**
- * Utility that answers the question “Is Apache Commons Compress
- * on the class-path and in a usable state?” Both the encoder and
- * decoder registries rely on this information.
+ * Lightweight guard that checks whether the Commons Compress factory
+ * class is loadable with the current class loader.
+ *
+ * Used by the codec registry to decide if reflective wiring of optional
+ * codecs should even be attempted.
+ *
*
* @since 5.6
*/
@@ -46,8 +49,11 @@ final class CommonsCompressSupport {
private static final String CCSF =
"org.apache.commons.compress.compressors.CompressorStreamFactory";
- /** Non-instantiable. */
- private CommonsCompressSupport() { }
+ /**
+ * Non-instantiable.
+ */
+ private CommonsCompressSupport() {
+ }
/**
* Returns {@code true} if the core Commons Compress class can be loaded
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/entity/compress/CommonsCompressingEntity.java b/httpclient5/src/main/java/org/apache/hc/client5/http/entity/compress/CompressingEntity.java
similarity index 56%
rename from httpclient5/src/main/java/org/apache/hc/client5/http/entity/compress/CommonsCompressingEntity.java
rename to httpclient5/src/main/java/org/apache/hc/client5/http/entity/compress/CompressingEntity.java
index 7580b1a34b..0b4ce098bd 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/entity/compress/CommonsCompressingEntity.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/entity/compress/CompressingEntity.java
@@ -30,36 +30,39 @@
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
-import java.util.Locale;
-import org.apache.commons.compress.compressors.CompressorException;
-import org.apache.commons.compress.compressors.CompressorStreamFactory;
-import org.apache.hc.core5.annotation.Contract;
-import org.apache.hc.core5.annotation.Internal;
-import org.apache.hc.core5.annotation.ThreadingBehavior;
import org.apache.hc.core5.http.HttpEntity;
import org.apache.hc.core5.http.io.entity.HttpEntityWrapper;
+import org.apache.hc.core5.io.IOFunction;
import org.apache.hc.core5.util.Args;
+
/**
- * Compresses the wrapped entity on-the-fly using Apache Commons Compress.
- *
- * The codec is chosen by its IANA token (for example {@code "br"} or
- * {@code "zstd"}). The helper JAR must be present at run-time; otherwise
- * {@link #writeTo(OutputStream)} will throw {@link IOException}.
+ * Streaming wrapper that compresses the enclosed {@link HttpEntity} on write.
+ *
+ * The actual compressor is supplied as an {@link IOFunction}<OutputStream,OutputStream>
+ * and is resolved by the caller (for example via reflective factories). This keeps
+ * compression back-ends fully optional and avoids hard classpath dependencies.
+ *
+ *
+ * The entity reports the configured {@code Content-Encoding} token and streams
+ * the content; length is unknown ({@code -1}), and the entity is chunked.
+ *
*
* @since 5.6
*/
-@Internal
-@Contract(threading = ThreadingBehavior.STATELESS)
-public final class CommonsCompressingEntity extends HttpEntityWrapper {
+public final class CompressingEntity extends HttpEntityWrapper {
- private final String coding; // lower-case
- private final CompressorStreamFactory factory = new CompressorStreamFactory();
+ private final IOFunction encoder;
+ private final String coding; // lower-case token for header reporting
- CommonsCompressingEntity(final HttpEntity src, final String coding) {
+ public CompressingEntity(
+ final HttpEntity src,
+ final String coding,
+ final IOFunction encoder) {
super(src);
- this.coding = coding.toLowerCase(Locale.ROOT);
+ this.encoder = Args.notNull(encoder, "Stream encoder");
+ this.coding = Args.notNull(coding, "Content coding").toLowerCase(java.util.Locale.ROOT);
}
@Override
@@ -69,8 +72,8 @@ public String getContentEncoding() {
@Override
public long getContentLength() {
- return -1;
- } // streaming
+ return -1; // streaming
+ }
@Override
public boolean isChunked() {
@@ -78,17 +81,23 @@ public boolean isChunked() {
}
@Override
- public InputStream getContent() { // Pull-mode is not supported
+ public InputStream getContent() {
throw new UnsupportedOperationException("Compressed entity is write-only");
}
@Override
public void writeTo(final OutputStream out) throws IOException {
Args.notNull(out, "Output stream");
- try (OutputStream cos = factory.createCompressorOutputStream(coding, out)) {
- super.writeTo(cos);
- } catch (final CompressorException | LinkageError ex) {
- throw new IOException("Unable to compress using coding '" + coding + '\'', ex);
+ final OutputStream wrapped = encoder.apply(out);
+ try {
+ super.writeTo(wrapped);
+ } finally {
+ // Close the wrapped stream to flush trailers/footers if any.
+ try {
+ wrapped.close();
+ } catch (final IOException ignore) {
+ // best effort
+ }
}
}
}
\ No newline at end of file
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/entity/compress/ContentCodecRegistry.java b/httpclient5/src/main/java/org/apache/hc/client5/http/entity/compress/ContentCodecRegistry.java
index 3d7ad9eaaa..76bceecbf5 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/entity/compress/ContentCodecRegistry.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/entity/compress/ContentCodecRegistry.java
@@ -27,6 +27,9 @@
package org.apache.hc.client5.http.entity.compress;
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.reflect.Constructor;
import java.util.Arrays;
import java.util.Collections;
import java.util.EnumMap;
@@ -34,20 +37,30 @@
import java.util.function.UnaryOperator;
import java.util.zip.GZIPInputStream;
+import org.apache.hc.client5.http.entity.DeflateCompressingEntity;
import org.apache.hc.client5.http.entity.DeflateInputStream;
+import org.apache.hc.client5.http.entity.GzipCompressingEntity;
import org.apache.hc.core5.annotation.Contract;
import org.apache.hc.core5.annotation.Internal;
import org.apache.hc.core5.annotation.ThreadingBehavior;
import org.apache.hc.core5.http.HttpEntity;
-import org.brotli.dec.BrotliInputStream;
+import org.apache.hc.core5.io.IOFunction;
/**
- * Run-time catalogue of built-in and Commons-Compress
- * {@linkplain java.util.function.UnaryOperator encoders} / {@linkplain java.util.function.UnaryOperator decoders}.
- *
- * Entries are wired once at class-load time and published through an
- * unmodifiable map, so lookups are lock-free and thread-safe.
+ * Registry of encode/decode transformations for HTTP content codings.
+ *
+ * Entries are wired once at class-load time and exposed via an unmodifiable map.
+ * Built-in gzip/deflate are always available. Additional codecs are discovered
+ * reflectively:
+ *
+ *
+ * - Commons Compress codecs, when the library and (if required) its helper JARs
+ * are present.
+ * - Decode-only Brotli via brotli4j, when present on the classpath. This does
+ * not affect the advertised {@code Accept-Encoding} unless an encoder is also
+ * registered.
+ *
*
* @since 5.6
*/
@@ -61,16 +74,10 @@ private static Map build() {
final Map m = new EnumMap<>(ContentCoding.class);
m.put(ContentCoding.GZIP,
- new Codec(
- // encoder
- org.apache.hc.client5.http.entity.GzipCompressingEntity::new,
- ent -> new DecompressingEntity(ent, GZIPInputStream::new)));
- m.put(ContentCoding.DEFLATE,
- new Codec(
- org.apache.hc.client5.http.entity.DeflateCompressingEntity::new,
- ent -> new DecompressingEntity(ent, DeflateInputStream::new)));
-
- /* 2. Commons-Compress extras ---------------------------------- */
+ new Codec(GzipCompressingEntity::new, ent -> new DecompressingEntity(ent, GZIPInputStream::new)));
+ m.put(ContentCoding.DEFLATE, new Codec(DeflateCompressingEntity::new, ent -> new DecompressingEntity(ent, DeflateInputStream::new)));
+
+ // 2) Commons-Compress (optional) — reflectively wired
if (CommonsCompressSupport.isPresent()) {
for (final ContentCoding c : Arrays.asList(
ContentCoding.BROTLI,
@@ -83,21 +90,21 @@ private static Map build() {
ContentCoding.PACK200,
ContentCoding.DEFLATE64)) {
- if (CommonsCompressDecoderFactory.runtimeAvailable(c.token())) {
+ if (CommonsCompressCodecFactory.runtimeAvailable(c)) {
m.put(c, new Codec(
- e -> new CommonsCompressingEntity(e, c.token()),
+ e -> new CompressingEntity(e, c.token(),
+ CommonsCompressCodecFactory.encoder(c.token())),
ent -> new DecompressingEntity(ent,
- CommonsCompressDecoderFactory.decoder(c.token()))));
+ CommonsCompressCodecFactory.decoder(c.token()))));
}
}
- }
- /* 3. Native Brotli fallback (decode-only) ---------------------- */
- if (!m.containsKey(ContentCoding.BROTLI)
- && CommonsCompressDecoderFactory.runtimeAvailable(ContentCoding.BROTLI.token())) {
- m.put(ContentCoding.BROTLI,
- Codec.decodeOnly(ent ->
- new DecompressingEntity(ent, BrotliInputStream::new)));
+ }
+ // 3) Native Brotli fallback (decode-only), no compile-time dep
+ if (isPresent("com.aayushatharva.brotli4j.decoder.BrotliInputStream")
+ && isPresent("com.aayushatharva.brotli4j.Brotli4jLoader")) {
+ m.put(ContentCoding.BROTLI, Codec.decodeOnly(ent ->
+ new DecompressingEntity(ent, brotli4jDecoder())));
}
return Collections.unmodifiableMap(m);
@@ -113,20 +120,11 @@ public static HttpEntity unwrap(final ContentCoding c, final HttpEntity src) {
return k != null && k.decoder != null ? k.decoder.apply(src) : null;
}
- private ContentCodecRegistry() {
- }
-
- /**
- * Returns the {@link java.util.function.UnaryOperator}<HttpEntity> for the given coding, or {@code null}.
- */
public static UnaryOperator decoder(final ContentCoding coding) {
final Codec c = REGISTRY.get(coding);
return c != null ? c.decoder : null;
}
- /**
- * Returns the {@link java.util.function.UnaryOperator}<HttpEntity> for the given coding, or {@code null}.
- */
public static UnaryOperator encoder(final ContentCoding coding) {
final Codec c = REGISTRY.get(coding);
return c != null ? c.encoder : null;
@@ -150,4 +148,30 @@ static Codec decodeOnly(final UnaryOperator d) {
}
}
+ private static boolean isPresent(final String className) {
+ try {
+ Class.forName(className, false, ContentCodecRegistry.class.getClassLoader());
+ return true;
+ } catch (final ClassNotFoundException | LinkageError ex) {
+ return false;
+ }
+ }
+
+ private static IOFunction brotli4jDecoder() {
+ return in -> {
+ try {
+ final ClassLoader cl = ContentCodecRegistry.class.getClassLoader();
+ final Class> loader = Class.forName("com.aayushatharva.brotli4j.Brotli4jLoader", false, cl);
+ loader.getMethod("ensureAvailability").invoke(null);
+ final Class> cls = Class.forName("com.aayushatharva.brotli4j.decoder.BrotliInputStream", false, cl);
+ final Constructor> ctor = cls.getConstructor(InputStream.class);
+ return (InputStream) ctor.newInstance(in);
+ } catch (final ReflectiveOperationException | LinkageError e) {
+ throw new IOException("Unable to decode brotli (brotli4j)", e);
+ }
+ };
+ }
+
+ private ContentCodecRegistry() {
+ }
}
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index 15f0627fe8..4e050498f0 100644
--- a/pom.xml
+++ b/pom.xml
@@ -145,11 +145,6 @@
log4j-core
${log4j.version}