diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/entity/BrotliDecompressingEntity.java b/httpclient5/src/main/java/org/apache/hc/client5/http/entity/BrotliDecompressingEntity.java index 9b8c7bb33c..216fd27c60 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/entity/BrotliDecompressingEntity.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/entity/BrotliDecompressingEntity.java @@ -31,10 +31,12 @@ /** * {@link org.apache.hc.core5.http.io.entity.HttpEntityWrapper} responsible for * handling br Content Coded responses. + * @deprecated See {@link org.apache.hc.client5.http.entity.compress.ContentCodecRegistry#decoder(org.apache.hc.client5.http.entity.compress.ContentCoding)} * * @see GzipDecompressingEntity * @since 5.2 */ +@Deprecated public class BrotliDecompressingEntity extends DecompressingEntity { /** * Creates a new {@link DecompressingEntity}. diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/entity/BrotliInputStreamFactory.java b/httpclient5/src/main/java/org/apache/hc/client5/http/entity/BrotliInputStreamFactory.java index 427b5d27ed..369f914fc5 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/entity/BrotliInputStreamFactory.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/entity/BrotliInputStreamFactory.java @@ -35,10 +35,11 @@ /** * {@link InputStreamFactory} for handling Brotli Content Coded responses. - * + * @deprecated See {@link org.apache.hc.client5.http.entity.compress.ContentCodecRegistry#decoder(org.apache.hc.client5.http.entity.compress.ContentCoding)} * @since 5.2 */ @Contract(threading = ThreadingBehavior.STATELESS) +@Deprecated public class BrotliInputStreamFactory implements InputStreamFactory { /** diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/entity/DecompressingEntity.java b/httpclient5/src/main/java/org/apache/hc/client5/http/entity/DecompressingEntity.java index dd164371c1..e313108142 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/entity/DecompressingEntity.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/entity/DecompressingEntity.java @@ -36,9 +36,9 @@ /** * Common base class for decompressing {@link HttpEntity} implementations. - * - * @since 4.4 + * @deprecated See {@link org.apache.hc.client5.http.entity.compress.ContentCodecRegistry#decoder(org.apache.hc.client5.http.entity.compress.ContentCoding)} */ +@Deprecated public class DecompressingEntity extends HttpEntityWrapper { /** diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/entity/DeflateDecompressingEntity.java b/httpclient5/src/main/java/org/apache/hc/client5/http/entity/DeflateDecompressingEntity.java index 3f20536368..8735571f8c 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/entity/DeflateDecompressingEntity.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/entity/DeflateDecompressingEntity.java @@ -39,11 +39,13 @@ * rather than {@code deflate} streams. We handle both types in here, * since that's what is seen on the internet. Moral - prefer * {@code gzip}! + * @deprecated See {@link org.apache.hc.client5.http.entity.compress.ContentCodecRegistry#decoder(org.apache.hc.client5.http.entity.compress.ContentCoding)} * * @see GzipDecompressingEntity * * @since 4.1 */ +@Deprecated public class DeflateDecompressingEntity extends DecompressingEntity { /** diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/entity/DeflateInputStreamFactory.java b/httpclient5/src/main/java/org/apache/hc/client5/http/entity/DeflateInputStreamFactory.java index ce56d8cb5d..ad48e927da 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/entity/DeflateInputStreamFactory.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/entity/DeflateInputStreamFactory.java @@ -35,9 +35,10 @@ /** * {@link InputStreamFactory} for handling Deflate Content Coded responses. - * + * @deprecated Use {@link org.apache.hc.client5.http.entity.compress.ContentCodecRegistry#decoder(org.apache.hc.client5.http.entity.compress.ContentCoding)} * @since 5.0 */ +@Deprecated @Contract(threading = ThreadingBehavior.STATELESS) public class DeflateInputStreamFactory implements InputStreamFactory { diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/entity/EntityBuilder.java b/httpclient5/src/main/java/org/apache/hc/client5/http/entity/EntityBuilder.java index 342c2fa60a..a770594b69 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/entity/EntityBuilder.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/entity/EntityBuilder.java @@ -33,8 +33,8 @@ import java.util.Arrays; import java.util.List; +import org.apache.hc.client5.http.entity.compress.ContentCodecRegistry; import org.apache.hc.client5.http.entity.compress.ContentCoding; -import org.apache.hc.client5.http.entity.compress.ContentEncoderRegistry; import org.apache.hc.core5.http.ContentType; import org.apache.hc.core5.http.HttpEntity; import org.apache.hc.core5.http.NameValuePair; @@ -439,12 +439,12 @@ public HttpEntity build() { throw new IllegalStateException("No entity set"); } if (compressWith != null) { - final ContentEncoderRegistry.EncoderFactory f = ContentEncoderRegistry.lookup(compressWith); - if (f == null) { + final HttpEntity compressed = ContentCodecRegistry.wrap(compressWith, e); + if (compressed == null) { throw new UnsupportedOperationException( "No encoder available for content-coding '" + compressWith.token() + '\''); } - return f.wrap(e); + return compressed; } return e; } diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/entity/GZIPInputStreamFactory.java b/httpclient5/src/main/java/org/apache/hc/client5/http/entity/GZIPInputStreamFactory.java index cda94373b7..80debfd8bf 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/entity/GZIPInputStreamFactory.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/entity/GZIPInputStreamFactory.java @@ -36,9 +36,14 @@ /** * {@link InputStreamFactory} for handling GZIPContent Coded responses. - * + * @deprecated – the public extension point has moved to + * {@link org.apache.hc.client5.http.entity.compress.Decoder}. + * For built-in gzip support use + * {@code ContentCodecRegistry.decoder(ContentCoding.GZIP)} or + * wrap a stream directly with * @since 5.0 */ +@Deprecated @Contract(threading = ThreadingBehavior.STATELESS) public class GZIPInputStreamFactory implements InputStreamFactory { diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/entity/GzipDecompressingEntity.java b/httpclient5/src/main/java/org/apache/hc/client5/http/entity/GzipDecompressingEntity.java index ca32f70e90..7bc2a34e6f 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/entity/GzipDecompressingEntity.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/entity/GzipDecompressingEntity.java @@ -32,8 +32,11 @@ * {@link org.apache.hc.core5.http.io.entity.HttpEntityWrapper} for handling * gzip Content Coded responses. * + * @deprecated See {@link org.apache.hc.client5.http.entity.compress.ContentCodecRegistry#decoder(org.apache.hc.client5.http.entity.compress.ContentCoding)} + * * @since 4.1 */ +@Deprecated public class GzipDecompressingEntity extends DecompressingEntity { /** diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/entity/InputStreamFactory.java b/httpclient5/src/main/java/org/apache/hc/client5/http/entity/InputStreamFactory.java index a6689435ef..a7b8ce43bd 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/entity/InputStreamFactory.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/entity/InputStreamFactory.java @@ -31,9 +31,10 @@ /** * Factory for decorated {@link InputStream}s. - * + * @deprecated Replaced by {@link org.apache.hc.client5.http.entity.compress.Decoder}. * @since 4.4 */ +@Deprecated public interface InputStreamFactory { InputStream create(InputStream inputStream) throws IOException; diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/entity/LazyDecompressingInputStream.java b/httpclient5/src/main/java/org/apache/hc/client5/http/entity/LazyDecompressingInputStream.java index 84150dbc02..c98a3001c1 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/entity/LazyDecompressingInputStream.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/entity/LazyDecompressingInputStream.java @@ -35,7 +35,9 @@ /** * Lazy initializes from an {@link InputStream} wrapper. + * @deprecated Superseded by {@link org.apache.hc.client5.http.entity.compress.DecompressingEntity}. */ +@Deprecated class LazyDecompressingInputStream extends FilterInputStream { private final InputStreamFactory inputStreamFactory; 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 index 1dae2c8e6a..043c64735c 100644 --- 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 @@ -36,7 +36,6 @@ import org.apache.commons.compress.compressors.CompressorException; import org.apache.commons.compress.compressors.CompressorStreamFactory; -import org.apache.hc.client5.http.entity.InputStreamFactory; import org.apache.hc.core5.annotation.Contract; import org.apache.hc.core5.annotation.Internal; import org.apache.hc.core5.annotation.ThreadingBehavior; @@ -56,8 +55,7 @@ */ @Internal @Contract(threading = ThreadingBehavior.STATELESS) -final class CommonsCompressDecoderFactory implements InputStreamFactory { - +final class CommonsCompressDecoderFactory { /** * Map of codings that need extra JARs → the fully‐qualified class we test for @@ -73,28 +71,24 @@ final class CommonsCompressDecoderFactory implements InputStreamFactory { REQUIRED_CLASS_NAME = Collections.unmodifiableMap(m); } - private final String encoding; - - CommonsCompressDecoderFactory(final String encoding) { - this.encoding = encoding.toLowerCase(Locale.ROOT); - } - - public String getContentEncoding() { - return encoding; - } - - @Override - public InputStream create(final InputStream source) throws IOException { - try { - return new CompressorStreamFactory() - .createCompressorInputStream(encoding, source); - } catch (final CompressorException | LinkageError ex) { - throw new IOException( - "Unable to decode Content-Encoding '" + encoding + '\'', ex); - } + /** + * @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) { 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 new file mode 100644 index 0000000000..3d7ad9eaaa --- /dev/null +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/entity/compress/ContentCodecRegistry.java @@ -0,0 +1,153 @@ +/* + * ==================================================================== + * 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.util.Arrays; +import java.util.Collections; +import java.util.EnumMap; +import java.util.Map; +import java.util.function.UnaryOperator; +import java.util.zip.GZIPInputStream; + +import org.apache.hc.client5.http.entity.DeflateInputStream; +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; + + +/** + * 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.

+ * + * @since 5.6 + */ +@Internal +@Contract(threading = ThreadingBehavior.STATELESS) +public final class ContentCodecRegistry { + + private static final Map REGISTRY = build(); + + 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 ---------------------------------- */ + if (CommonsCompressSupport.isPresent()) { + for (final ContentCoding c : Arrays.asList( + ContentCoding.BROTLI, + ContentCoding.ZSTD, + ContentCoding.XZ, + ContentCoding.LZMA, + ContentCoding.LZ4_FRAMED, + ContentCoding.LZ4_BLOCK, + ContentCoding.BZIP2, + ContentCoding.PACK200, + ContentCoding.DEFLATE64)) { + + if (CommonsCompressDecoderFactory.runtimeAvailable(c.token())) { + m.put(c, new Codec( + e -> new CommonsCompressingEntity(e, c.token()), + ent -> new DecompressingEntity(ent, + CommonsCompressDecoderFactory.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))); + } + + return Collections.unmodifiableMap(m); + } + + public static HttpEntity wrap(final ContentCoding c, final HttpEntity src) { + final Codec k = REGISTRY.get(c); + return k != null && k.encoder != null ? k.encoder.apply(src) : null; + } + + public static HttpEntity unwrap(final ContentCoding c, final HttpEntity src) { + final Codec k = REGISTRY.get(c); + 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; + } + + static final class Codec { + final UnaryOperator encoder; + final UnaryOperator decoder; + + Codec(final UnaryOperator enc, final UnaryOperator dec) { + this.encoder = enc; + this.decoder = dec; + } + + static Codec encodeOnly(final UnaryOperator e) { + return new Codec(e, null); + } + + static Codec decodeOnly(final UnaryOperator d) { + return new Codec(null, d); + } + } + +} \ No newline at end of file diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/entity/compress/ContentDecoderRegistry.java b/httpclient5/src/main/java/org/apache/hc/client5/http/entity/compress/ContentDecoderRegistry.java deleted file mode 100644 index c3c5ced5f7..0000000000 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/entity/compress/ContentDecoderRegistry.java +++ /dev/null @@ -1,131 +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.util.Arrays; -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.Map; - -import org.apache.hc.client5.http.entity.BrotliDecompressingEntity; -import org.apache.hc.client5.http.entity.BrotliInputStreamFactory; -import org.apache.hc.client5.http.entity.DeflateInputStreamFactory; -import org.apache.hc.client5.http.entity.GZIPInputStreamFactory; -import org.apache.hc.client5.http.entity.InputStreamFactory; -import org.apache.hc.core5.annotation.Contract; -import org.apache.hc.core5.annotation.Internal; -import org.apache.hc.core5.annotation.ThreadingBehavior; - -/** - * Immutable run-time catalogue of {@link InputStreamFactory} instances - * capable of decoding HTTP entity bodies. - * - *

The map is populated once during class initialisation:

- *
    - *
  1. Built-ins: {@code gzip} and {@code deflate} are always present.
  2. - *
  3. If Commons-Compress is on the class-path we register a configurable - * list of codecs (br, zstd, xz, …) via - * {@link CommonsCompressDecoderFactory} – guarded by a cheap - * presence check.
  4. - *
  5. If Commons was absent or could not supply br, - * we fall back to the pure native singleton - * {@link BrotliInputStreamFactory} (when the org.brotli - * decoder JAR is available).
  6. - *
- * - *

The resulting {@code Map} is wrapped in - * {@link Collections#unmodifiableMap(Map)} and published through - * {@link #getRegistry()} for safe, lock-free concurrent reads.

- * - * @since 5.6 - */ -@Internal -@Contract(threading = ThreadingBehavior.STATELESS) -public final class ContentDecoderRegistry { - - - private static final Map REGISTRY = buildRegistry(); - - - /** - * Returns the unmodifiable codec map (key = canonical token, value = factory). - */ - public static Map getRegistry() { - return REGISTRY; - } - - - private static Map buildRegistry() { - final LinkedHashMap m = new LinkedHashMap<>(); - - // 1. Built-ins - register(m, ContentCoding.GZIP, new GZIPInputStreamFactory()); - register(m, ContentCoding.DEFLATE, new DeflateInputStreamFactory()); - - // 2. Commons-Compress (optional) - if (CommonsCompressSupport.isPresent()) { - for (final ContentCoding coding : Arrays.asList( - ContentCoding.BROTLI, // note: will be skipped until CC ships an encoder - ContentCoding.ZSTD, - ContentCoding.XZ, - ContentCoding.LZMA, - ContentCoding.LZ4_FRAMED, - ContentCoding.LZ4_BLOCK, - ContentCoding.BZIP2, - ContentCoding.PACK200, - ContentCoding.DEFLATE64)) { - addCommons(m, coding); - } - } - - // 3. Native Brotli fallback if Commons did not register it - if (!m.containsKey(ContentCoding.BROTLI) - && BrotliDecompressingEntity.isAvailable()) { - register(m, ContentCoding.BROTLI, new BrotliInputStreamFactory()); - } - - return Collections.unmodifiableMap(m); - } - - private static void register(final Map map, - final ContentCoding coding, - final InputStreamFactory factory) { - map.put(coding, factory); - } - - private static void addCommons(final Map map, - final ContentCoding coding) { - if (CommonsCompressDecoderFactory.runtimeAvailable(coding.token())) { - register(map, coding, new CommonsCompressDecoderFactory(coding.token())); - } - } - - private ContentDecoderRegistry() { - } -} - diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/entity/compress/ContentEncoderRegistry.java b/httpclient5/src/main/java/org/apache/hc/client5/http/entity/compress/ContentEncoderRegistry.java deleted file mode 100644 index c6b8a9a2f8..0000000000 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/entity/compress/ContentEncoderRegistry.java +++ /dev/null @@ -1,95 +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.util.Arrays; -import java.util.Collections; -import java.util.EnumMap; -import java.util.Map; - -import org.apache.hc.client5.http.entity.DeflateCompressingEntity; -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; - -@Internal -@Contract(threading = ThreadingBehavior.STATELESS) -public final class ContentEncoderRegistry { - - /** - * Map token → factory (immutable, thread-safe). - */ - private static final Map REGISTRY = build(); - - public static EncoderFactory lookup(final ContentCoding coding) { - return REGISTRY.get(coding); - } - - - @FunctionalInterface - public interface EncoderFactory { - /** - * Wraps the source entity in its compressing counterpart. - */ - HttpEntity wrap(HttpEntity src); - } - - private static Map build() { - final Map m = - new EnumMap<>(ContentCoding.class); - - /* 1. Built-ins – gzip + deflate use the existing wrappers */ - m.put(ContentCoding.GZIP, GzipCompressingEntity::new); - m.put(ContentCoding.DEFLATE, DeflateCompressingEntity::new); - - /* 2. Commons-Compress – only if the helper class is present */ - if (CommonsCompressSupport.isPresent()) { - for (final ContentCoding c : Arrays.asList( - ContentCoding.BROTLI, - ContentCoding.ZSTD, - ContentCoding.XZ, - ContentCoding.LZMA, - ContentCoding.LZ4_FRAMED, - ContentCoding.LZ4_BLOCK, - ContentCoding.BZIP2, - ContentCoding.PACK200, - ContentCoding.DEFLATE64)) { - - if (CommonsCompressDecoderFactory.runtimeAvailable(c.token())) { - m.put(c, e -> new CommonsCompressingEntity(e, c.token())); - } - } - } - return Collections.unmodifiableMap(m); - } - - private ContentEncoderRegistry() { - } // no-instantiation -} diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/entity/compress/DecompressingEntity.java b/httpclient5/src/main/java/org/apache/hc/client5/http/entity/compress/DecompressingEntity.java new file mode 100644 index 0000000000..a5c8af0cb0 --- /dev/null +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/entity/compress/DecompressingEntity.java @@ -0,0 +1,109 @@ +/* + * ==================================================================== + * 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.io.OutputStream; +import java.util.concurrent.locks.ReentrantLock; + +import org.apache.hc.core5.http.HttpEntity; +import org.apache.hc.core5.http.io.entity.HttpEntityWrapper; +import org.apache.hc.core5.util.Args; + +public class DecompressingEntity extends HttpEntityWrapper { + + private static final int BUF_SIZE = 8 * 1024; // 8 KiB buffer + private final IOFunction decoder; + private final ReentrantLock lock = new ReentrantLock(); + private volatile InputStream cached; + + public DecompressingEntity( + final HttpEntity src, + final IOFunction decoder) { + super(src); + this.decoder = Args.notNull(decoder, "Stream decoder"); + } + + /** + * Returns the cached decoded stream, creating it once if necessary. + */ + @Override + public InputStream getContent() throws IOException { + if (!isStreaming()) { + return decoder.apply(super.getContent()); + } + + InputStream local = cached; + if (local == null) { + lock.lock(); + try { + if (cached == null) { + cached = decoder.apply(super.getContent()); + } + local = cached; + } finally { + lock.unlock(); + } + } + return local; + } + + /** + * Length is unknown after decompression. + */ + @Override + public long getContentLength() { + return -1; + } + + @Override + public boolean isRepeatable() { + return super.isRepeatable(); + } + + @Override + public boolean isStreaming() { + return super.isStreaming(); + } + + /** + * Streams the decoded bytes directly to {@code out}. + */ + @Override + public void writeTo(final OutputStream out) throws IOException { + Args.notNull(out, "Output stream"); + try (InputStream in = getContent()) { + final byte[] buf = new byte[BUF_SIZE]; + int len; + while ((len = in.read(buf)) != -1) { + out.write(buf, 0, len); + } + } + } +} diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/entity/compress/IOFunction.java b/httpclient5/src/main/java/org/apache/hc/client5/http/entity/compress/IOFunction.java new file mode 100644 index 0000000000..de5716570c --- /dev/null +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/entity/compress/IOFunction.java @@ -0,0 +1,59 @@ +/* + * ==================================================================== + * 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 org.apache.hc.core5.annotation.Internal; + + +/** + * Minimal equivalent of {@link java.util.function.Function} whose + * {@link #apply(Object)} method is allowed to throw {@link IOException}. + *

+ * Used internally by the content-coding layer to pass lambdas that wrap / + * unwrap streams without forcing boiler-plate try/catch blocks. + *

+ * + * @param input type + * @param result type + * @since 5.6 + */ +@Internal +@FunctionalInterface +public interface IOFunction { + + /** + * Applies the transformation. + * + * @param value source value (never {@code null}) + * @return transformed value + * @throws IOException if the transformation cannot be performed + */ + R apply(T value) throws IOException; +} \ No newline at end of file diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/ContentCompressionExec.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/ContentCompressionExec.java index 4a58972ae4..06d5b114e8 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/ContentCompressionExec.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/ContentCompressionExec.java @@ -29,17 +29,17 @@ import java.io.IOException; import java.util.ArrayList; +import java.util.EnumMap; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.function.UnaryOperator; import org.apache.hc.client5.http.classic.ExecChain; import org.apache.hc.client5.http.classic.ExecChainHandler; import org.apache.hc.client5.http.config.RequestConfig; -import org.apache.hc.client5.http.entity.DecompressingEntity; -import org.apache.hc.client5.http.entity.InputStreamFactory; +import org.apache.hc.client5.http.entity.compress.ContentCodecRegistry; import org.apache.hc.client5.http.entity.compress.ContentCoding; -import org.apache.hc.client5.http.entity.compress.ContentDecoderRegistry; import org.apache.hc.client5.http.protocol.HttpClientContext; import org.apache.hc.core5.annotation.Contract; import org.apache.hc.core5.annotation.Internal; @@ -74,12 +74,12 @@ public final class ContentCompressionExec implements ExecChainHandler { private final Header acceptEncoding; - private final Lookup decoderRegistry; + private final Lookup> decoderRegistry; private final boolean ignoreUnknown; public ContentCompressionExec( final List acceptEncoding, - final Lookup decoderRegistry, + final Lookup> decoderRegistry, final boolean ignoreUnknown) { this.acceptEncoding = MessageSupport.headerOfTokens(HttpHeaders.ACCEPT_ENCODING, Args.notEmpty(acceptEncoding, "Encoding list")); @@ -88,19 +88,26 @@ public ContentCompressionExec( } public ContentCompressionExec(final boolean ignoreUnknown) { - final Map decoderMap = ContentDecoderRegistry.getRegistry(); - final RegistryBuilder builder = RegistryBuilder.create(); - final List acceptEncodingList = new ArrayList<>(decoderMap.size() + 1); - decoderMap.forEach((coding, factory) -> { - acceptEncodingList.add(coding.token()); - builder.register(coding.token(), factory); + final Map> decoderMap = new EnumMap<>(ContentCoding.class); + for (final ContentCoding c : ContentCoding.values()) { + final UnaryOperator d = ContentCodecRegistry.decoder(c); + if (d != null) { + decoderMap.put(c, d); + } + } + + final RegistryBuilder> builder = RegistryBuilder.create(); + final List acceptList = new ArrayList<>(decoderMap.size() + 1); + decoderMap.forEach((coding, decoder) -> { + acceptList.add(coding.token()); + builder.register(coding.token(), decoder); }); - // register the x-gzip alias again + /* x-gzip alias */ if (decoderMap.containsKey(ContentCoding.GZIP)) { - acceptEncodingList.add(ContentCoding.X_GZIP.token()); + acceptList.add(ContentCoding.X_GZIP.token()); builder.register(ContentCoding.X_GZIP.token(), decoderMap.get(ContentCoding.GZIP)); } - this.acceptEncoding = MessageSupport.headerOfTokens(HttpHeaders.ACCEPT_ENCODING, acceptEncodingList); + this.acceptEncoding = MessageSupport.headerOfTokens(HttpHeaders.ACCEPT_ENCODING, acceptList); this.decoderRegistry = builder.build(); this.ignoreUnknown = ignoreUnknown; } @@ -139,16 +146,14 @@ public ClassicHttpResponse execute( final HeaderElement[] codecs = BasicHeaderValueParser.INSTANCE.parseElements(contentEncoding, cursor); for (final HeaderElement codec : codecs) { final String codecname = codec.getName().toLowerCase(Locale.ROOT); - final InputStreamFactory decoderFactory = decoderRegistry.lookup(codecname); - if (decoderFactory != null) { - response.setEntity(new DecompressingEntity(response.getEntity(), decoderFactory)); + final UnaryOperator decoder = decoderRegistry.lookup(codecname); + if (decoder != null) { + response.setEntity(decoder.apply(response.getEntity())); response.removeHeaders(HttpHeaders.CONTENT_LENGTH); response.removeHeaders(HttpHeaders.CONTENT_ENCODING); response.removeHeaders(HttpHeaders.CONTENT_MD5); - } else { - if (!"identity".equals(codecname) && !ignoreUnknown) { - throw new HttpException("Unsupported Content-Encoding: " + codec.getName()); - } + } else if (!"identity".equals(codecname) && !ignoreUnknown) { + throw new HttpException("Unsupported Content-Encoding: " + codec.getName()); } } } diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/HttpClientBuilder.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/HttpClientBuilder.java index 406ffd0f80..d3c8b05edd 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/HttpClientBuilder.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/HttpClientBuilder.java @@ -38,6 +38,7 @@ import java.util.List; import java.util.Map; import java.util.function.Function; +import java.util.function.UnaryOperator; import org.apache.hc.client5.http.AuthenticationStrategy; import org.apache.hc.client5.http.ConnectionKeepAliveStrategy; @@ -55,6 +56,8 @@ import org.apache.hc.client5.http.cookie.CookieSpecFactory; import org.apache.hc.client5.http.cookie.CookieStore; import org.apache.hc.client5.http.entity.InputStreamFactory; +import org.apache.hc.client5.http.entity.compress.ContentCodecRegistry; +import org.apache.hc.client5.http.entity.compress.ContentCoding; import org.apache.hc.client5.http.impl.ChainElement; import org.apache.hc.client5.http.impl.CookieSpecSupport; import org.apache.hc.client5.http.impl.DefaultAuthenticationStrategy; @@ -89,6 +92,7 @@ import org.apache.hc.core5.annotation.Internal; import org.apache.hc.core5.http.ConnectionReuseStrategy; import org.apache.hc.core5.http.Header; +import org.apache.hc.core5.http.HttpEntity; import org.apache.hc.core5.http.HttpHost; import org.apache.hc.core5.http.HttpRequestInterceptor; import org.apache.hc.core5.http.HttpResponseInterceptor; @@ -211,6 +215,7 @@ private ExecInterceptorEntry( private BackoffManager backoffManager; private Lookup authSchemeRegistry; private Lookup cookieSpecRegistry; + @Deprecated private LinkedHashMap contentDecoderMap; private CookieStore cookieStore; private CredentialsProvider credentialsProvider; @@ -234,6 +239,12 @@ private ExecInterceptorEntry( private List closeables; + /** + * Custom decoders keyed by {@link ContentCoding}. + * + */ + private LinkedHashMap> contentDecoder; + public static HttpClientBuilder create() { return new HttpClientBuilder(); } @@ -703,6 +714,23 @@ public final HttpClientBuilder setContentDecoderRegistry( return this; } + /** + * Sets a map of {@linkplain java.util.function.UnaryOperator}<HttpEntity> decoders, + * keyed by {@link ContentCoding}, to be used for automatic response decompression. + * + * @param contentDecoder decoder map, or {@code null} to fall back to the + * defaults from {@link ContentCodecRegistry}. + * @return this builder. + * + * @since 5.6 + */ + public final HttpClientBuilder setContentDecoder( + final LinkedHashMap> contentDecoder) { + this.contentDecoder = contentDecoder; + return this; + } + + /** * Sets default {@link RequestConfig} instance which will be used * for request execution if not explicitly set in the client execution @@ -963,18 +991,26 @@ public CloseableHttpClient build() { ChainElement.PROTOCOL.name()); if (!contentCompressionDisabled) { - if (contentDecoderMap != null) { - final List encodings = new ArrayList<>(contentDecoderMap.keySet()); - final RegistryBuilder b2 = RegistryBuilder.create(); - for (final Map.Entry entry: contentDecoderMap.entrySet()) { - b2.register(entry.getKey(), entry.getValue()); + // Custom decoder map supplied by the caller + if (contentDecoder != null) { + final List encodings = new ArrayList<>(contentDecoder.size()); + final RegistryBuilder> b2 = RegistryBuilder.create(); + for (final Map.Entry> entry : contentDecoder.entrySet()) { + final String token = entry.getKey().token(); + encodings.add(token); + b2.register(token, entry.getValue()); } - final Registry decoderRegistry = b2.build(); + final Registry> decoderRegistry = b2.build(); + execChainDefinition.addFirst( new ContentCompressionExec(encodings, decoderRegistry, true), ChainElement.COMPRESS.name()); + } else { - execChainDefinition.addFirst(new ContentCompressionExec(true), ChainElement.COMPRESS.name()); + // Use the default decoders from ContentCodecRegistry + execChainDefinition.addFirst( + new ContentCompressionExec(true), + ChainElement.COMPRESS.name()); } } diff --git a/httpclient5/src/test/java/org/apache/hc/client5/http/entity/TestBrotli.java b/httpclient5/src/test/java/org/apache/hc/client5/http/entity/TestBrotli.java index 735a623991..d297ec1f1f 100644 --- a/httpclient5/src/test/java/org/apache/hc/client5/http/entity/TestBrotli.java +++ b/httpclient5/src/test/java/org/apache/hc/client5/http/entity/TestBrotli.java @@ -27,6 +27,8 @@ package org.apache.hc.client5.http.entity; +import org.apache.hc.client5.http.entity.compress.ContentCodecRegistry; +import org.apache.hc.client5.http.entity.compress.ContentCoding; import org.apache.hc.core5.http.HttpEntity; import org.apache.hc.core5.http.io.entity.ByteArrayEntity; import org.apache.hc.core5.http.io.entity.EntityUtils; @@ -45,7 +47,10 @@ void testDecompressionWithBrotli() throws Exception { final byte[] bytes = new byte[] {33, 44, 0, 4, 116, 101, 115, 116, 32, 98, 114, 111, 116, 108, 105, 10, 3}; - final HttpEntity entity = new BrotliDecompressingEntity(new ByteArrayEntity(bytes, null)); + final HttpEntity entity = ContentCodecRegistry.unwrap( + ContentCoding.BROTLI, + new ByteArrayEntity(bytes, null)); + Assertions.assertEquals("test brotli\n", EntityUtils.toString(entity)); } diff --git a/httpclient5/src/test/java/org/apache/hc/client5/http/entity/TestDecompressingEntity.java b/httpclient5/src/test/java/org/apache/hc/client5/http/entity/TestDecompressingEntity.java index a3959d02d6..e19b61aca7 100644 --- a/httpclient5/src/test/java/org/apache/hc/client5/http/entity/TestDecompressingEntity.java +++ b/httpclient5/src/test/java/org/apache/hc/client5/http/entity/TestDecompressingEntity.java @@ -108,7 +108,7 @@ void testWriteToStream() throws Exception { } } - static class ChecksumEntity extends DecompressingEntity { + static class ChecksumEntity extends org.apache.hc.client5.http.entity.compress.DecompressingEntity { public ChecksumEntity(final HttpEntity wrapped, final Checksum checksum) { super(wrapped, inStream -> new CheckedInputStream(inStream, checksum)); diff --git a/httpclient5/src/test/java/org/apache/hc/client5/http/entity/TestDeflate.java b/httpclient5/src/test/java/org/apache/hc/client5/http/entity/TestDeflate.java index 0859a8bd11..2e40a1b73a 100644 --- a/httpclient5/src/test/java/org/apache/hc/client5/http/entity/TestDeflate.java +++ b/httpclient5/src/test/java/org/apache/hc/client5/http/entity/TestDeflate.java @@ -27,13 +27,18 @@ package org.apache.hc.client5.http.entity; +import java.io.ByteArrayOutputStream; import java.nio.charset.StandardCharsets; +import java.util.function.UnaryOperator; import java.util.zip.Deflater; +import org.apache.hc.client5.http.entity.compress.ContentCodecRegistry; +import org.apache.hc.client5.http.entity.compress.ContentCoding; import org.apache.hc.core5.http.ContentType; import org.apache.hc.core5.http.HttpEntity; import org.apache.hc.core5.http.io.entity.ByteArrayEntity; import org.apache.hc.core5.http.io.entity.EntityUtils; +import org.apache.hc.core5.http.io.entity.StringEntity; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -52,8 +57,33 @@ void testCompressDecompress() throws Exception { compresser.finish(); final int len = compresser.deflate(compressed); - final HttpEntity entity = new DeflateDecompressingEntity(new ByteArrayEntity(compressed, 0, len, ContentType.APPLICATION_OCTET_STREAM)); + final HttpEntity entity = ContentCodecRegistry + .decoder(ContentCoding.DEFLATE) + .apply(new ByteArrayEntity(compressed, 0, len, ContentType.APPLICATION_OCTET_STREAM)); + Assertions.assertEquals(s, EntityUtils.toString(entity)); } + @Test + void testEncodeThenDecode() throws Exception { + + final String text = "some kind of text"; + + final HttpEntity plain = new StringEntity(text, ContentType.TEXT_PLAIN); + final UnaryOperator encoder = ContentCodecRegistry.encoder(ContentCoding.DEFLATE); + Assertions.assertNotNull(encoder, "deflate encoder must exist"); + + final HttpEntity deflated = encoder.apply(plain); + + final ByteArrayOutputStream buf = new ByteArrayOutputStream(); + deflated.writeTo(buf); + + final HttpEntity decoded = ContentCodecRegistry + .decoder(ContentCoding.DEFLATE) + .apply(new ByteArrayEntity(buf.toByteArray(), ContentType.APPLICATION_OCTET_STREAM)); + + Assertions.assertEquals(text, EntityUtils.toString(decoded, StandardCharsets.US_ASCII)); + } + + } diff --git a/httpclient5/src/test/java/org/apache/hc/client5/http/entity/TestGZip.java b/httpclient5/src/test/java/org/apache/hc/client5/http/entity/TestGZip.java index 7ca13caf4e..95d3aac7b8 100644 --- a/httpclient5/src/test/java/org/apache/hc/client5/http/entity/TestGZip.java +++ b/httpclient5/src/test/java/org/apache/hc/client5/http/entity/TestGZip.java @@ -32,7 +32,10 @@ import java.io.IOException; import java.io.OutputStream; import java.nio.charset.StandardCharsets; +import java.util.function.UnaryOperator; +import org.apache.hc.client5.http.entity.compress.ContentCodecRegistry; +import org.apache.hc.client5.http.entity.compress.ContentCoding; import org.apache.hc.core5.http.ContentType; import org.apache.hc.core5.http.HttpEntity; import org.apache.hc.core5.http.io.entity.ByteArrayEntity; @@ -65,11 +68,15 @@ void testCompressionDecompression() throws Exception { final ByteArrayOutputStream buf = new ByteArrayOutputStream(); gzipe.writeTo(buf); final ByteArrayEntity out = new ByteArrayEntity(buf.toByteArray(), ContentType.APPLICATION_OCTET_STREAM); - final GzipDecompressingEntity gunzipe = new GzipDecompressingEntity(out); - Assertions.assertEquals("some kind of text", EntityUtils.toString(gunzipe, StandardCharsets.US_ASCII)); + final HttpEntity gunzipe = ContentCodecRegistry + .decoder(ContentCoding.GZIP) + .apply(out); + Assertions.assertEquals("some kind of text", + EntityUtils.toString(gunzipe, StandardCharsets.US_ASCII)); } } + @Test void testCompressionIOExceptionLeavesOutputStreamOpen() throws Exception { final HttpEntity in = Mockito.mock(HttpEntity.class); @@ -98,9 +105,53 @@ void testDecompressionWithMultipleGZipStream() throws Exception { bytes[i] = (byte) (data[i] & 0xff); } - try (final GzipDecompressingEntity entity = new GzipDecompressingEntity(new InputStreamEntity(new ByteArrayInputStream(bytes), ContentType.APPLICATION_OCTET_STREAM))) { - Assertions.assertEquals("stream-1\nstream-2\n", EntityUtils.toString(entity, StandardCharsets.US_ASCII)); + try (final HttpEntity entity = ContentCodecRegistry + .decoder(ContentCoding.GZIP) + .apply(new InputStreamEntity(new ByteArrayInputStream(bytes), + ContentType.APPLICATION_OCTET_STREAM))) { + Assertions.assertEquals("stream-1\nstream-2\n", + EntityUtils.toString(entity, StandardCharsets.US_ASCII)); } } + @Test + void testEncodeThenDecode() throws Exception { + + final String txt = "some kind of text"; + + final HttpEntity plain = new StringEntity(txt, ContentType.TEXT_PLAIN); + final UnaryOperator gzipEn = ContentCodecRegistry.encoder(ContentCoding.GZIP); + Assertions.assertNotNull(gzipEn, "gzip encoder must exist"); + + final HttpEntity gzipped = gzipEn.apply(plain); + + final ByteArrayOutputStream buf = new ByteArrayOutputStream(); + gzipped.writeTo(buf); + + final HttpEntity ungzip = ContentCodecRegistry + .decoder(ContentCoding.GZIP) + .apply(new ByteArrayEntity(buf.toByteArray(), + ContentType.APPLICATION_OCTET_STREAM)); + + Assertions.assertEquals(txt, EntityUtils.toString(ungzip, StandardCharsets.US_ASCII)); + } + + @Test + void testUnwrapHelper() throws Exception { + + final ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try (java.util.zip.GZIPOutputStream gout = new java.util.zip.GZIPOutputStream(baos)) { + gout.write("unwrap check".getBytes(StandardCharsets.US_ASCII)); + } + final byte[] gzBytes = baos.toByteArray(); + + final HttpEntity decodedEntity = ContentCodecRegistry.unwrap( + ContentCoding.GZIP, + new ByteArrayEntity(gzBytes, ContentType.APPLICATION_OCTET_STREAM)); + + Assertions.assertNotNull(decodedEntity, "unwrap returned null"); + Assertions.assertEquals("unwrap check", + EntityUtils.toString(decodedEntity, StandardCharsets.US_ASCII)); + } + } diff --git a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/classic/TestContentCompressionExec.java b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/classic/TestContentCompressionExec.java index 60eaa27f6f..e6084952c9 100644 --- a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/classic/TestContentCompressionExec.java +++ b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/classic/TestContentCompressionExec.java @@ -30,9 +30,9 @@ import org.apache.hc.client5.http.classic.ExecChain; import org.apache.hc.client5.http.classic.ExecRuntime; import org.apache.hc.client5.http.config.RequestConfig; -import org.apache.hc.client5.http.entity.DecompressingEntity; import org.apache.hc.client5.http.entity.EntityBuilder; import org.apache.hc.client5.http.entity.GzipDecompressingEntity; +import org.apache.hc.client5.http.entity.compress.DecompressingEntity; import org.apache.hc.client5.http.protocol.HttpClientContext; import org.apache.hc.core5.http.ClassicHttpRequest; import org.apache.hc.core5.http.ClassicHttpResponse;