- * Purely async/streaming: no {@code InputStream}/{@code OutputStream}. Back-pressure is - * honored via {@link #available()} and the I/O reactor’s calls into {@link #produce(DataStreamChannel)}. - * Trailers from the upstream producer are preserved and emitted once the compressed output - * has been fully drained. - *
- * - *- * Ensure {@link com.aayushatharva.brotli4j.Brotli4jLoader#ensureAvailability()} has been - * called once at startup; this class also invokes it in a static initializer as a safeguard. - *
- * - *{@code
- * AsyncEntityProducer plain = new StringAsyncEntityProducer("hello", ContentType.TEXT_PLAIN);
- * AsyncEntityProducer br = new DeflatingBrotliEntityProducer(plain); // defaults q=5, lgwin=22
- * client.execute(new BasicRequestProducer(post, br),
- * new BasicResponseConsumer<>(new StringAsyncEntityConsumer()),
- * null);
- * }
+ * Async Brotli deflater (reflection-based brotli4j). No compile-time dep.
*
- * @see org.apache.hc.core5.http.nio.AsyncEntityProducer
- * @see org.apache.hc.core5.http.nio.DataStreamChannel
- * @see com.aayushatharva.brotli4j.encoder.EncoderJNI
* @since 5.6
*/
public final class DeflatingBrotliEntityProducer implements AsyncEntityProducer {
@@ -83,56 +47,65 @@ public final class DeflatingBrotliEntityProducer implements AsyncEntityProducer
private enum State { STREAMING, FINISHING, DONE }
private final AsyncEntityProducer upstream;
- private final EncoderJNI.Wrapper encoder;
+ // Reflective encoder instance (brotli4j EncoderJNI.Wrapper)
+ private final Object encoder;
private ByteBuffer pendingOut;
private List extends Header> pendingTrailers;
private State state = State.STREAMING;
+ public enum BrotliMode {
+ GENERIC,
+ TEXT,
+ FONT;
+ }
+
+ /**
+ * Defaults: quality=5, lgwin=22, mode=GENERIC.
+ */
+ public DeflatingBrotliEntityProducer(final AsyncEntityProducer upstream) throws IOException {
+ this(upstream, 5, 22, BrotliMode.GENERIC);
+ }
+
/**
- * Create a producer with explicit Brotli params.
- *
- * @param upstream upstream entity producer whose bytes will be compressed
- * @param quality Brotli quality level (see brotli4j documentation)
- * @param lgwin Brotli window size log2 (see brotli4j documentation)
- * @param mode Brotli mode hint (GENERIC/TEXT/FONT)
- * @throws IOException if the native encoder cannot be created
- * @since 5.6
+ * Convenience: modeInt 0=GENERIC, 1=TEXT, 2=FONT.
*/
public DeflatingBrotliEntityProducer(
final AsyncEntityProducer upstream,
final int quality,
final int lgwin,
- final Encoder.Mode mode) throws IOException {
- this.upstream = Args.notNull(upstream, "upstream");
- this.encoder = new EncoderJNI.Wrapper(256 * 1024, quality, lgwin, mode);
+ final int modeInt) throws IOException {
+ this(upstream, quality, lgwin, modeInt == 1 ? BrotliMode.TEXT.name() : (modeInt == 2 ? BrotliMode.FONT.name() : BrotliMode.GENERIC.name()));
}
/**
- * Convenience constructor mapping {@code 0=GENERIC, 1=TEXT, 2=FONT}.
- *
- * @since 5.6
+ * Kept for compatibility with existing code/tests.
+ * Uses reflection under the hood; no EncoderJNI references here.
*/
public DeflatingBrotliEntityProducer(
final AsyncEntityProducer upstream,
final int quality,
final int lgwin,
- final int modeInt) throws IOException {
- this(upstream, quality, lgwin,
- modeInt == 1 ? Encoder.Mode.TEXT :
- modeInt == 2 ? Encoder.Mode.FONT : Encoder.Mode.GENERIC);
+ final BrotliMode mode) throws java.io.IOException {
+ this(upstream, quality, lgwin, mode != null ? mode.name() : "GENERIC");
}
/**
- * Create a producer with sensible defaults ({@code quality=5}, {@code lgwin=22}, {@code GENERIC}).
- *
- * @since 5.6
+ * Fully reflective constructor used by the others.
*/
- public DeflatingBrotliEntityProducer(final AsyncEntityProducer upstream) throws IOException {
- this(upstream, 5, 22, Encoder.Mode.GENERIC);
+ public DeflatingBrotliEntityProducer(
+ final AsyncEntityProducer upstream,
+ final int quality,
+ final int lgwin,
+ final String modeName) throws IOException {
+ this.upstream = Args.notNull(upstream, "upstream");
+ try {
+ this.encoder = AsyncBrotli.newEncoder(256 * 1024, quality, lgwin, modeName);
+ } catch (final Exception e) {
+ throw new IOException("Brotli (brotli4j) not available", e);
+ }
}
-
@Override
public String getContentType() {
return upstream.getContentType();
@@ -177,67 +150,13 @@ public int available() {
@Override
public void produce(final DataStreamChannel channel) throws IOException {
- if (flushPending(channel)) {
- return;
- }
-
- if (state == State.FINISHING) {
- encoder.push(EncoderJNI.Operation.FINISH, 0);
- if (drainEncoder(channel)) {
+ try {
+ if (flushPending(channel)) {
return;
}
- if (pendingTrailers == null) {
- pendingTrailers = Collections.emptyList();
- }
- channel.endStream(pendingTrailers);
- pendingTrailers = null;
- state = State.DONE;
- return;
- }
-
- upstream.produce(new DataStreamChannel() {
- @Override
- public void requestOutput() {
- channel.requestOutput();
- }
-
- @Override
- public int write(final ByteBuffer src) throws IOException {
- int accepted = 0;
- while (src.hasRemaining()) {
- final ByteBuffer in = encoder.getInputBuffer();
- if (!in.hasRemaining()) {
- encoder.push(EncoderJNI.Operation.PROCESS, 0);
- if (drainEncoder(channel)) {
- break;
- }
- continue;
- }
- final int xfer = Math.min(src.remaining(), in.remaining());
- final int lim = src.limit();
- src.limit(src.position() + xfer);
- in.put(src);
- src.limit(lim);
- accepted += xfer;
-
- encoder.push(EncoderJNI.Operation.PROCESS, xfer);
- if (drainEncoder(channel)) {
- break;
- }
- }
- return accepted;
- }
-
- @Override
- public void endStream() throws IOException {
- endStream(Collections.emptyList());
- }
- @Override
- public void endStream(final List extends Header> trailers) throws IOException {
- pendingTrailers = trailers;
- state = State.FINISHING;
- encoder.push(EncoderJNI.Operation.FINISH, 0);
+ if (state == State.FINISHING) {
+ AsyncBrotli.encPushFinish(encoder);
if (drainEncoder(channel)) {
return;
}
@@ -247,8 +166,76 @@ public void endStream(final List extends Header> trailers) throws IOException
channel.endStream(pendingTrailers);
pendingTrailers = null;
state = State.DONE;
+ return;
}
- });
+
+ upstream.produce(new DataStreamChannel() {
+ @Override
+ public void requestOutput() {
+ channel.requestOutput();
+ }
+
+ @Override
+ public int write(final ByteBuffer src) throws IOException {
+ int accepted = 0;
+ try {
+ while (src.hasRemaining()) {
+ final ByteBuffer in = AsyncBrotli.encInput(encoder);
+ if (!in.hasRemaining()) {
+ AsyncBrotli.encPushProcess(encoder, 0);
+ if (drainEncoder(channel)) {
+ break;
+ }
+ continue;
+ }
+ final int xfer = Math.min(src.remaining(), in.remaining());
+ final int lim = src.limit();
+ src.limit(src.position() + xfer);
+ in.put(src);
+ src.limit(lim);
+ accepted += xfer;
+
+ AsyncBrotli.encPushProcess(encoder, xfer);
+ if (drainEncoder(channel)) {
+ break;
+ }
+ }
+ return accepted;
+ } catch (final Exception ex) {
+ throw new IOException("Brotli encode failed", ex);
+ }
+ }
+
+ @Override
+ public void endStream() throws IOException {
+ endStream(Collections.emptyList());
+ }
+
+ @Override
+ public void endStream(final List extends Header> trailers) throws IOException {
+ try {
+ pendingTrailers = trailers;
+ state = State.FINISHING;
+ AsyncBrotli.encPushFinish(encoder);
+ if (drainEncoder(channel)) {
+ return;
+ }
+ if (pendingTrailers == null) {
+ pendingTrailers = Collections.+ * This class never links Commons Compress at compile time. At runtime, it + * reflectively locates {@code CompressorStreamFactory} and creates encoder / + * decoder streams for IANA tokens (e.g. {@code "zstd"}, {@code "xz"}) only + * when the relevant classes (and any helper JARs) are present. + *
+ *+ * Use {@link #runtimeAvailable(ContentCoding)} to probe whether a given coding + * can be provided by the current classpath configuration. Callers can then + * register codecs conditionally without hard dependencies. + *
+ * + * @since 5.6 + */ +@Internal +@Contract(threading = ThreadingBehavior.STATELESS) +final class CommonsCompressCodecFactory { + + private static final String FACTORY_CLASS = + "org.apache.commons.compress.compressors.CompressorStreamFactory"; + + // CC stream classes + private static final String CC_BROTLI = "org.apache.commons.compress.compressors.brotli.BrotliCompressorInputStream"; + private static final String CC_ZSTD = "org.apache.commons.compress.compressors.zstandard.ZstdCompressorInputStream"; + private static final String CC_XZ = "org.apache.commons.compress.compressors.xz.XZCompressorInputStream"; + private static final String CC_LZMA = "org.apache.commons.compress.compressors.lzma.LZMACompressorInputStream"; + private static final String CC_LZ4_F = "org.apache.commons.compress.compressors.lz4.FramedLZ4CompressorInputStream"; + private static final String CC_LZ4_B = "org.apache.commons.compress.compressors.lz4.BlockLZ4CompressorInputStream"; + private static final String CC_BZIP2 = "org.apache.commons.compress.compressors.bzip2.BZip2CompressorInputStream"; + private static final String CC_PACK200 = "org.apache.commons.compress.compressors.pack200.Pack200CompressorInputStream"; + private static final String CC_DEFLATE64 = "org.apache.commons.compress.compressors.deflate64.Deflate64CompressorInputStream"; + + // Helper libs + private static final String H_ZSTD = "com.github.luben.zstd.ZstdInputStream"; + private static final String H_XZ = "org.tukaani.xz.XZInputStream"; + + private CommonsCompressCodecFactory() { + } + + private static boolean isPresent(final String className) { + try { + Class.forName(className, false, CommonsCompressCodecFactory.class.getClassLoader()); + return true; + } catch (final ClassNotFoundException | LinkageError ex) { + return false; + } + } + + /** + * Creates a lazy decoder that instantiates the Commons Compress stream + * reflectively on first use. Throws {@link IOException} if Commons Compress + * is not available or the codec cannot be created. + */ + static IOFunction+ * 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); + 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 - *- *
- * 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
+ * Used by the codec registry to decide if reflective wiring of optional
+ * codecs should even be attempted.
+ * 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}.
+ * 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.
+ * Entries are wired once at class-load time and published through an
- * unmodifiable map, so lookups are lock-free and thread-safe.
+ * 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:
+ *
+ *
*
* @since 5.6
*/
@@ -61,19 +74,12 @@ private static Map