garydgregory commented on code in PR #580: URL: https://github.com/apache/httpcomponents-client/pull/580#discussion_r1769261744
########## httpclient5-testing/src/test/java/org/apache/hc/client5/testing/extension/sync/StandardTestClientBuilder.java: ########## @@ -72,6 +74,12 @@ public TestClientBuilder setTimeout(final Timeout timeout) { return this; } + @Override + public TestClientBuilder setnowrap(final boolean nowrap) { Review Comment: Camel case "noWrap". ########## httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/AbstractIntegrationTestBase.java: ########## @@ -78,4 +78,8 @@ public TestClient client() throws Exception { return testResources.client(); } + public TestClient client(final boolean nowrap) throws Exception { Review Comment: Camel case "noWrap". ########## httpclient5-testing/src/test/java/org/apache/hc/client5/testing/extension/sync/StandardTestClientBuilder.java: ########## @@ -57,6 +57,8 @@ final class StandardTestClientBuilder implements TestClientBuilder { private HttpClientConnectionManager connectionManager; + private boolean nowrap; Review Comment: Camel case "noWrap". ########## httpclient5/src/main/java/org/apache/hc/client5/http/entity/CompressorFactory.java: ########## @@ -0,0 +1,282 @@ +/* + * ==================================================================== + * 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 + * <http://www.apache.org/>. + * + */ + +package org.apache.hc.client5.http.entity; + +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; + +import org.apache.commons.compress.compressors.CompressorException; +import org.apache.commons.compress.compressors.CompressorStreamFactory; +import org.apache.commons.compress.compressors.deflate.DeflateCompressorInputStream; +import org.apache.commons.compress.compressors.deflate.DeflateParameters; +import org.apache.hc.core5.http.HttpEntity; +import org.apache.hc.core5.util.Args; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A factory class for managing compression and decompression of HTTP entities using different compression formats. + * <p> + * This factory uses a cache to optimize access to available input and output stream providers for compression formats. + * It also allows the use of aliases (e.g., "gzip" and "x-gzip") and automatically formats the compression names + * to ensure consistency. + * </p> + * + * <p> + * Supported compression formats include gzip, deflate, and other available formats provided by the + * {@link CompressorStreamFactory}. + * </p> + * + * <p> + * This class is thread-safe and uses {@link AtomicReference} to cache the available input and output stream providers. + * </p> + * + * @since 5.5 + */ +public class CompressorFactory { + + private static final Logger LOG = LoggerFactory.getLogger(CompressorFactory.class); + /** + * Singleton instance of the factory. + */ + public static final CompressorFactory INSTANCE = new CompressorFactory(); + + private final CompressorStreamFactory compressorStreamFactory = new CompressorStreamFactory(); + private final AtomicReference<Set<String>> inputProvidersCache = new AtomicReference<>(); + private final AtomicReference<Set<String>> outputProvidersCache = new AtomicReference<>(); + private final Map<String, String> formattedNameCache = new ConcurrentHashMap<>(); + + /** + * Returns a set of available input stream compression providers. + * + * @return a set of available input stream compression providers in lowercase. + */ + public Set<String> getAvailableInputProviders() { + return inputProvidersCache.updateAndGet(existing -> existing != null ? existing : fetchAvailableInputProviders()); + } + + /** + * Returns a set of available output stream compression providers. + * + * @return a set of available output stream compression providers in lowercase. + */ + public Set<String> getAvailableOutputProviders() { + return outputProvidersCache.updateAndGet(existing -> existing != null ? existing : fetchAvailableOutputProviders()); + } + + /** + * Returns the formatted name of the provided compression format. + * <p> + * If the provided name matches an alias (e.g., "gzip" or "x-gzip"), the method will return the standard name. + * </p> + * + * @param name the compression format name. + * @return the formatted name, or the original name if no alias is found. + * @throws IllegalArgumentException if the name is null or empty. + */ + public String getFormattedName(final String name) { + if (name == null || name.isEmpty()) { + LOG.warn("Compression name is null or empty"); + return null; + } + final String lowerCaseName = name.toLowerCase(Locale.ROOT); + return formattedNameCache.computeIfAbsent(lowerCaseName, key -> { + if ("gzip".equals(key) || "x-gzip".equals(key)) { Review Comment: Use constants instead of magic strings. ########## httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/HttpClientBuilder.java: ########## @@ -696,13 +698,62 @@ public final HttpClientBuilder setDefaultCookieSpecRegistry( * to be used for automatic content decompression. * * @return this instance. + * @deprecated Use {@link HttpClientBuilder#setEncodings(List)} */ + @Deprecated public final HttpClientBuilder setContentDecoderRegistry( final LinkedHashMap<String, InputStreamFactory> contentDecoderMap) { this.contentDecoderMap = contentDecoderMap; + setEncodings(new ArrayList<>(contentDecoderMap.keySet())); + return this; + } + + + /** + * Sets the list of content encodings to be supported for automatic content compression and decompression. + * <p> + * This method allows the user to provide a list of content encodings that the client will support during HTTP + * requests and responses. It enables automatic handling of compressed data streams for specified encodings. + * Supported encodings could include algorithms like "gzip", "deflate", "br" (Brotli), or custom encodings + * depending on the available compression implementations. + * </p> + * + * <pre> + * Example: + * {@code + * List<String> supportedEncodings = Arrays.asList("gzip", "deflate", "br"); + * HttpClientBuilder builder = HttpClientBuilder.create() + * .setEncodings(supportedEncodings); + * } + * </pre> + * + * @param encodings a list of encoding names to support for automatic content compression and decompression + * @return this {@code HttpClientBuilder} instance for method chaining + * @since 5.0 + */ + public final HttpClientBuilder setEncodings(final List<String> encodings) { + this.encodings = encodings; + return this; + } + + /** + * Sets the "nowrap" option for the HTTP client. + * <p> + * When enabled, this option disables the zlib header and trailer in deflate compression streams. + * This is useful when working with servers that require or expect raw deflate streams without + * the standard zlib header and trailer. + * </p> + * + * @param nowrap if {@code true}, disables the zlib header and trailer in deflate streams. + * @return the updated {@link HttpClientBuilder} instance. + * @since 5.4 + */ + public final HttpClientBuilder setNowrap(final boolean nowrap) { Review Comment: Camel case "noWrap". ########## httpclient5/src/main/java/org/apache/hc/client5/http/entity/CompressorFactory.java: ########## @@ -0,0 +1,282 @@ +/* + * ==================================================================== + * 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 + * <http://www.apache.org/>. + * + */ + +package org.apache.hc.client5.http.entity; + +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; + +import org.apache.commons.compress.compressors.CompressorException; +import org.apache.commons.compress.compressors.CompressorStreamFactory; +import org.apache.commons.compress.compressors.deflate.DeflateCompressorInputStream; +import org.apache.commons.compress.compressors.deflate.DeflateParameters; +import org.apache.hc.core5.http.HttpEntity; +import org.apache.hc.core5.util.Args; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A factory class for managing compression and decompression of HTTP entities using different compression formats. + * <p> + * This factory uses a cache to optimize access to available input and output stream providers for compression formats. + * It also allows the use of aliases (e.g., "gzip" and "x-gzip") and automatically formats the compression names + * to ensure consistency. + * </p> + * + * <p> + * Supported compression formats include gzip, deflate, and other available formats provided by the + * {@link CompressorStreamFactory}. + * </p> + * + * <p> + * This class is thread-safe and uses {@link AtomicReference} to cache the available input and output stream providers. + * </p> + * + * @since 5.5 + */ +public class CompressorFactory { + + private static final Logger LOG = LoggerFactory.getLogger(CompressorFactory.class); + /** + * Singleton instance of the factory. + */ + public static final CompressorFactory INSTANCE = new CompressorFactory(); + + private final CompressorStreamFactory compressorStreamFactory = new CompressorStreamFactory(); + private final AtomicReference<Set<String>> inputProvidersCache = new AtomicReference<>(); + private final AtomicReference<Set<String>> outputProvidersCache = new AtomicReference<>(); + private final Map<String, String> formattedNameCache = new ConcurrentHashMap<>(); + + /** + * Returns a set of available input stream compression providers. + * + * @return a set of available input stream compression providers in lowercase. + */ + public Set<String> getAvailableInputProviders() { + return inputProvidersCache.updateAndGet(existing -> existing != null ? existing : fetchAvailableInputProviders()); + } + + /** + * Returns a set of available output stream compression providers. + * + * @return a set of available output stream compression providers in lowercase. + */ + public Set<String> getAvailableOutputProviders() { + return outputProvidersCache.updateAndGet(existing -> existing != null ? existing : fetchAvailableOutputProviders()); + } + + /** + * Returns the formatted name of the provided compression format. + * <p> + * If the provided name matches an alias (e.g., "gzip" or "x-gzip"), the method will return the standard name. + * </p> + * + * @param name the compression format name. + * @return the formatted name, or the original name if no alias is found. + * @throws IllegalArgumentException if the name is null or empty. + */ + public String getFormattedName(final String name) { + if (name == null || name.isEmpty()) { + LOG.warn("Compression name is null or empty"); + return null; + } + final String lowerCaseName = name.toLowerCase(Locale.ROOT); + return formattedNameCache.computeIfAbsent(lowerCaseName, key -> { + if ("gzip".equals(key) || "x-gzip".equals(key)) { + return "gz"; + } else if ("compress".equals(key)) { + return "z"; + } + return key; + }); + } + + /** + * Creates an input stream for the specified compression format and decompresses the provided input stream. + * <p> + * This method uses the specified compression name to decompress the input stream and supports the "noWrap" option + * for deflate streams. + * </p> + * + * @param name the compression format. + * @param inputStream the input stream to decompress. + * @param noWrap if true, disables the zlib header and trailer for deflate streams. + * @return the decompressed input stream, or the original input stream if the format is not supported. + */ + public InputStream getCompressorInputStream(final String name, final InputStream inputStream, final boolean noWrap) throws CompressorException { + Args.notNull(inputStream, "InputStream"); + Args.notNull(name, "name"); + + final String formattedName = getFormattedName(name); + return isSupported(formattedName, false) + ? createCompressorInputStream(formattedName, inputStream, noWrap) + : inputStream; + } + + /** + * Creates an output stream for the specified compression format and compresses the provided output stream. + * + * @param name the compression format. + * @param outputStream the output stream to compress. + * @return the compressed output stream, or the original output stream if the format is not supported. + */ + public OutputStream getCompressorOutputStream(final String name, final OutputStream outputStream) throws CompressorException { + final String formattedName = getFormattedName(name); + return isSupported(formattedName, true) + ? createCompressorOutputStream(formattedName, outputStream) + : outputStream; + + } + + + /** + * Decompresses the provided HTTP entity using the specified compression format. + * + * @param entity the HTTP entity to decompress. + * @param contentEncoding the compression format. + * @return a decompressed {@link HttpEntity}, or {@code null} if the compression format is unsupported. + */ + public HttpEntity decompressEntity(final HttpEntity entity, final String contentEncoding) { + return decompressEntity(entity, contentEncoding, false); + } + + /** + * Decompresses the provided HTTP entity using the specified compression format with the option for deflate streams. + * + * @param entity the HTTP entity to decompress. + * @param contentEncoding the compression format. + * @param noWrap if true, disables the zlib header and trailer for deflate streams. + * @return a decompressed {@link HttpEntity}, or {@code null} if the compression format is unsupported. + */ + public HttpEntity decompressEntity(final HttpEntity entity, final String contentEncoding, final boolean noWrap) { + Args.notNull(entity, "Entity"); + Args.notNull(contentEncoding, "Content Encoding"); + if (!isSupported(contentEncoding, false)) { + LOG.warn("Unsupported decompression type: {}", contentEncoding); + return null; + } + return new DecompressEntity(entity, contentEncoding, noWrap); + } + + /** + * Compresses the provided HTTP entity using the specified compression format. + * + * @param entity the HTTP entity to compress. + * @param contentEncoding the compression format. + * @return a compressed {@link HttpEntity}, or {@code null} if the compression format is unsupported. + */ + public HttpEntity compressEntity(final HttpEntity entity, final String contentEncoding) { + Args.notNull(entity, "Entity"); + Args.notNull(contentEncoding, "Content Encoding"); + if (!isSupported(contentEncoding, true)) { + LOG.warn("Unsupported compression type: {}", contentEncoding); + return null; + } + return new CompressingEntity(entity, contentEncoding); + } + + /** + * Fetches the available input stream compression providers from Commons Compress. + * + * @return a set of available input stream compression providers in lowercase. + */ + private Set<String> fetchAvailableInputProviders() { + final Set<String> inputNames = compressorStreamFactory.getInputStreamCompressorNames(); Review Comment: Inline single use "inputNames". ########## httpclient5-testing/src/test/java/org/apache/hc/client5/testing/extension/sync/TestClientBuilder.java: ########## @@ -98,6 +98,10 @@ default TestClientBuilder addExecInterceptorLast(String name, ExecChainHandler throw new UnsupportedOperationException("Operation not supported by " + getProtocolLevel()); } + default TestClientBuilder setnowrap(boolean nowrap){ Review Comment: - Extra space after "default" - Camel case "noWrap". ########## httpclient5/src/main/java/org/apache/hc/client5/http/entity/CompressingEntity.java: ########## @@ -0,0 +1,132 @@ +/* + * ==================================================================== + * 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 + * <http://www.apache.org/>. + * + */ +package org.apache.hc.client5.http.entity; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import org.apache.commons.compress.compressors.CompressorException; +import org.apache.hc.core5.http.HttpEntity; +import org.apache.hc.core5.http.io.entity.HttpEntityWrapper; +import org.apache.hc.core5.util.Args; + + Review Comment: Remove extra blank line. ########## httpclient5-testing/src/test/java/org/apache/hc/client5/testing/extension/sync/TestClientResources.java: ########## @@ -118,4 +118,9 @@ public TestClient client() throws Exception { return client; } + public TestClient client(final boolean nowrap) throws Exception { Review Comment: Camel case "noWrap", "setNoWrap()". ########## httpclient5/src/main/java/org/apache/hc/client5/http/entity/CompressingEntity.java: ########## @@ -0,0 +1,132 @@ +/* + * ==================================================================== + * 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 + * <http://www.apache.org/>. + * + */ +package org.apache.hc.client5.http.entity; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import org.apache.commons.compress.compressors.CompressorException; +import org.apache.hc.core5.http.HttpEntity; +import org.apache.hc.core5.http.io.entity.HttpEntityWrapper; +import org.apache.hc.core5.util.Args; + + +/** + * An {@link HttpEntity} wrapper that applies compression to the content before writing it to + * an output stream. This class supports various compression algorithms based on the + * specified content encoding. + * + * <p>Compression is performed using {@link CompressorFactory}, which returns a corresponding + * {@link OutputStream} for the requested compression type. This class does not support + * reading the content directly through {@link #getContent()} as the content is always compressed + * during write operations.</p> + * + * @since 5.5 + */ +public class CompressingEntity extends HttpEntityWrapper { + + /** + * The content encoding type, e.g., "gzip", "deflate", etc. + */ + private final String contentEncoding; + + /** + * Creates a new {@link CompressingEntity} that compresses the wrapped entity's content + * using the specified content encoding. + * + * @param entity the {@link HttpEntity} to wrap and compress; must not be {@code null}. + * @param contentEncoding the content encoding to use for compression, e.g., "gzip". + */ + public CompressingEntity(final HttpEntity entity, final String contentEncoding) { + super(entity); + this.contentEncoding = Args.notNull(contentEncoding, "Content encoding"); + } + + /** + * Returns the content encoding used for compression. + * + * @return the content encoding (e.g., "gzip", "deflate"). + */ + @Override + public String getContentEncoding() { + return contentEncoding; + } + + + /** + * Returns whether the entity is chunked. This is determined by the wrapped entity. + * + * @return {@code true} if the entity is chunked, {@code false} otherwise. + */ + @Override + public boolean isChunked() { + return super.isChunked(); + } + + + /** + * This method is unsupported because the content is meant to be compressed during the Review Comment: First sentence should be "Throws UnsupportedOperationException." ########## httpclient5/src/main/java/org/apache/hc/client5/http/entity/CompressingEntity.java: ########## @@ -0,0 +1,132 @@ +/* + * ==================================================================== + * 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 + * <http://www.apache.org/>. + * + */ +package org.apache.hc.client5.http.entity; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import org.apache.commons.compress.compressors.CompressorException; +import org.apache.hc.core5.http.HttpEntity; +import org.apache.hc.core5.http.io.entity.HttpEntityWrapper; +import org.apache.hc.core5.util.Args; + + +/** + * An {@link HttpEntity} wrapper that applies compression to the content before writing it to + * an output stream. This class supports various compression algorithms based on the + * specified content encoding. + * + * <p>Compression is performed using {@link CompressorFactory}, which returns a corresponding + * {@link OutputStream} for the requested compression type. This class does not support + * reading the content directly through {@link #getContent()} as the content is always compressed + * during write operations.</p> + * + * @since 5.5 + */ +public class CompressingEntity extends HttpEntityWrapper { + + /** + * The content encoding type, e.g., "gzip", "deflate", etc. + */ + private final String contentEncoding; + + /** + * Creates a new {@link CompressingEntity} that compresses the wrapped entity's content + * using the specified content encoding. + * + * @param entity the {@link HttpEntity} to wrap and compress; must not be {@code null}. + * @param contentEncoding the content encoding to use for compression, e.g., "gzip". + */ + public CompressingEntity(final HttpEntity entity, final String contentEncoding) { + super(entity); + this.contentEncoding = Args.notNull(contentEncoding, "Content encoding"); + } + + /** + * Returns the content encoding used for compression. + * + * @return the content encoding (e.g., "gzip", "deflate"). + */ + @Override + public String getContentEncoding() { + return contentEncoding; + } + + + /** + * Returns whether the entity is chunked. This is determined by the wrapped entity. + * + * @return {@code true} if the entity is chunked, {@code false} otherwise. + */ + @Override + public boolean isChunked() { + return super.isChunked(); + } + + + /** + * This method is unsupported because the content is meant to be compressed during the + * {@link #writeTo(OutputStream)} operation. + * + * @throws UnsupportedOperationException always, as this method is not supported. + */ + @Override + public InputStream getContent() throws IOException { + throw new UnsupportedOperationException("Reading content is not supported for CompressingEntity"); + } + + /** + * Writes the compressed content to the provided {@link OutputStream}. Compression is performed + * using the content encoding provided during entity construction. + * + * @param outStream the {@link OutputStream} to which the compressed content will be written; must not be {@code null}. + * @throws IOException if an I/O error occurs during compression or writing. + * @throws UnsupportedOperationException if the specified compression type is not supported. + */ + @Override + public void writeTo(final OutputStream outStream) throws IOException { + Args.notNull(outStream, "Output stream"); + Review Comment: Remove extra blank line. ########## httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/compress/CompressedResponseHandlingExample.java: ########## @@ -0,0 +1,113 @@ +/* + * ==================================================================== + * 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 + * <http://www.apache.org/>. + * + */ + +package org.apache.hc.client5.testing.sync.compress; + +import java.util.Arrays; +import java.util.List; + +import org.apache.hc.client5.http.entity.CompressorFactory; +import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; +import org.apache.hc.client5.http.impl.classic.HttpClients; +import org.apache.hc.core5.http.ClassicHttpRequest; +import org.apache.hc.core5.http.ContentType; +import org.apache.hc.core5.http.HttpEntity; +import org.apache.hc.core5.http.HttpHeaders; +import org.apache.hc.core5.http.HttpHost; +import org.apache.hc.core5.http.io.entity.EntityUtils; +import org.apache.hc.core5.http.io.entity.StringEntity; +import org.apache.hc.core5.http.io.support.ClassicRequestBuilder; +import org.apache.hc.core5.io.CloseMode; +import org.apache.hc.core5.testing.classic.ClassicTestServer; + + Review Comment: Remove extra blank line. ########## httpclient5/src/main/java/org/apache/hc/client5/http/entity/CompressingEntity.java: ########## @@ -0,0 +1,132 @@ +/* + * ==================================================================== + * 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 + * <http://www.apache.org/>. + * + */ +package org.apache.hc.client5.http.entity; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import org.apache.commons.compress.compressors.CompressorException; +import org.apache.hc.core5.http.HttpEntity; +import org.apache.hc.core5.http.io.entity.HttpEntityWrapper; +import org.apache.hc.core5.util.Args; + + +/** + * An {@link HttpEntity} wrapper that applies compression to the content before writing it to + * an output stream. This class supports various compression algorithms based on the + * specified content encoding. + * + * <p>Compression is performed using {@link CompressorFactory}, which returns a corresponding + * {@link OutputStream} for the requested compression type. This class does not support + * reading the content directly through {@link #getContent()} as the content is always compressed + * during write operations.</p> + * + * @since 5.5 + */ +public class CompressingEntity extends HttpEntityWrapper { + + /** + * The content encoding type, e.g., "gzip", "deflate", etc. + */ + private final String contentEncoding; + + /** + * Creates a new {@link CompressingEntity} that compresses the wrapped entity's content + * using the specified content encoding. + * + * @param entity the {@link HttpEntity} to wrap and compress; must not be {@code null}. + * @param contentEncoding the content encoding to use for compression, e.g., "gzip". + */ + public CompressingEntity(final HttpEntity entity, final String contentEncoding) { + super(entity); + this.contentEncoding = Args.notNull(contentEncoding, "Content encoding"); + } + + /** + * Returns the content encoding used for compression. + * + * @return the content encoding (e.g., "gzip", "deflate"). + */ + @Override + public String getContentEncoding() { + return contentEncoding; + } + + + /** + * Returns whether the entity is chunked. This is determined by the wrapped entity. + * + * @return {@code true} if the entity is chunked, {@code false} otherwise. + */ + @Override + public boolean isChunked() { + return super.isChunked(); + } + + + /** + * This method is unsupported because the content is meant to be compressed during the + * {@link #writeTo(OutputStream)} operation. + * + * @throws UnsupportedOperationException always, as this method is not supported. + */ + @Override + public InputStream getContent() throws IOException { + throw new UnsupportedOperationException("Reading content is not supported for CompressingEntity"); + } + + /** + * Writes the compressed content to the provided {@link OutputStream}. Compression is performed + * using the content encoding provided during entity construction. + * + * @param outStream the {@link OutputStream} to which the compressed content will be written; must not be {@code null}. + * @throws IOException if an I/O error occurs during compression or writing. + * @throws UnsupportedOperationException if the specified compression type is not supported. + */ + @Override + public void writeTo(final OutputStream outStream) throws IOException { + Args.notNull(outStream, "Output stream"); + + // Get the compressor based on the specified content encoding + final OutputStream compressorStream; + try { + compressorStream = CompressorFactory.INSTANCE.getCompressorOutputStream(contentEncoding, outStream); + } catch (final CompressorException e) { + throw new IOException("Error initializing decompression stream", e); + } + Review Comment: Remove extra blank line. ########## httpclient5/src/main/java/org/apache/hc/client5/http/entity/CompressingEntity.java: ########## @@ -0,0 +1,132 @@ +/* + * ==================================================================== + * 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 + * <http://www.apache.org/>. + * + */ +package org.apache.hc.client5.http.entity; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import org.apache.commons.compress.compressors.CompressorException; +import org.apache.hc.core5.http.HttpEntity; +import org.apache.hc.core5.http.io.entity.HttpEntityWrapper; +import org.apache.hc.core5.util.Args; + + +/** + * An {@link HttpEntity} wrapper that applies compression to the content before writing it to + * an output stream. This class supports various compression algorithms based on the + * specified content encoding. + * + * <p>Compression is performed using {@link CompressorFactory}, which returns a corresponding + * {@link OutputStream} for the requested compression type. This class does not support + * reading the content directly through {@link #getContent()} as the content is always compressed + * during write operations.</p> + * + * @since 5.5 + */ +public class CompressingEntity extends HttpEntityWrapper { + + /** + * The content encoding type, e.g., "gzip", "deflate", etc. + */ + private final String contentEncoding; + + /** + * Creates a new {@link CompressingEntity} that compresses the wrapped entity's content + * using the specified content encoding. + * + * @param entity the {@link HttpEntity} to wrap and compress; must not be {@code null}. + * @param contentEncoding the content encoding to use for compression, e.g., "gzip". + */ + public CompressingEntity(final HttpEntity entity, final String contentEncoding) { + super(entity); + this.contentEncoding = Args.notNull(contentEncoding, "Content encoding"); + } + + /** + * Returns the content encoding used for compression. + * + * @return the content encoding (e.g., "gzip", "deflate"). + */ + @Override + public String getContentEncoding() { + return contentEncoding; + } + + + /** + * Returns whether the entity is chunked. This is determined by the wrapped entity. + * + * @return {@code true} if the entity is chunked, {@code false} otherwise. + */ + @Override Review Comment: Remove method, all it does is call super(); anything to document can go in the class comment. ########## httpclient5/src/main/java/org/apache/hc/client5/http/entity/CompressorFactory.java: ########## @@ -0,0 +1,282 @@ +/* + * ==================================================================== + * 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 + * <http://www.apache.org/>. + * + */ + +package org.apache.hc.client5.http.entity; + +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; + +import org.apache.commons.compress.compressors.CompressorException; +import org.apache.commons.compress.compressors.CompressorStreamFactory; +import org.apache.commons.compress.compressors.deflate.DeflateCompressorInputStream; +import org.apache.commons.compress.compressors.deflate.DeflateParameters; +import org.apache.hc.core5.http.HttpEntity; +import org.apache.hc.core5.util.Args; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A factory class for managing compression and decompression of HTTP entities using different compression formats. + * <p> + * This factory uses a cache to optimize access to available input and output stream providers for compression formats. + * It also allows the use of aliases (e.g., "gzip" and "x-gzip") and automatically formats the compression names + * to ensure consistency. + * </p> + * + * <p> + * Supported compression formats include gzip, deflate, and other available formats provided by the + * {@link CompressorStreamFactory}. + * </p> + * + * <p> + * This class is thread-safe and uses {@link AtomicReference} to cache the available input and output stream providers. + * </p> + * + * @since 5.5 + */ +public class CompressorFactory { + + private static final Logger LOG = LoggerFactory.getLogger(CompressorFactory.class); + /** + * Singleton instance of the factory. + */ + public static final CompressorFactory INSTANCE = new CompressorFactory(); + + private final CompressorStreamFactory compressorStreamFactory = new CompressorStreamFactory(); + private final AtomicReference<Set<String>> inputProvidersCache = new AtomicReference<>(); + private final AtomicReference<Set<String>> outputProvidersCache = new AtomicReference<>(); + private final Map<String, String> formattedNameCache = new ConcurrentHashMap<>(); + + /** + * Returns a set of available input stream compression providers. + * + * @return a set of available input stream compression providers in lowercase. + */ + public Set<String> getAvailableInputProviders() { + return inputProvidersCache.updateAndGet(existing -> existing != null ? existing : fetchAvailableInputProviders()); + } + + /** + * Returns a set of available output stream compression providers. + * + * @return a set of available output stream compression providers in lowercase. + */ + public Set<String> getAvailableOutputProviders() { + return outputProvidersCache.updateAndGet(existing -> existing != null ? existing : fetchAvailableOutputProviders()); + } + + /** + * Returns the formatted name of the provided compression format. + * <p> + * If the provided name matches an alias (e.g., "gzip" or "x-gzip"), the method will return the standard name. + * </p> + * + * @param name the compression format name. + * @return the formatted name, or the original name if no alias is found. + * @throws IllegalArgumentException if the name is null or empty. + */ + public String getFormattedName(final String name) { + if (name == null || name.isEmpty()) { + LOG.warn("Compression name is null or empty"); + return null; + } + final String lowerCaseName = name.toLowerCase(Locale.ROOT); + return formattedNameCache.computeIfAbsent(lowerCaseName, key -> { + if ("gzip".equals(key) || "x-gzip".equals(key)) { + return "gz"; + } else if ("compress".equals(key)) { + return "z"; + } + return key; + }); + } + + /** + * Creates an input stream for the specified compression format and decompresses the provided input stream. + * <p> + * This method uses the specified compression name to decompress the input stream and supports the "noWrap" option + * for deflate streams. + * </p> + * + * @param name the compression format. + * @param inputStream the input stream to decompress. + * @param noWrap if true, disables the zlib header and trailer for deflate streams. + * @return the decompressed input stream, or the original input stream if the format is not supported. + */ + public InputStream getCompressorInputStream(final String name, final InputStream inputStream, final boolean noWrap) throws CompressorException { + Args.notNull(inputStream, "InputStream"); + Args.notNull(name, "name"); + Review Comment: Remove extra blank line. ########## httpclient5/src/main/java/org/apache/hc/client5/http/entity/CompressorFactory.java: ########## @@ -0,0 +1,282 @@ +/* + * ==================================================================== + * 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 + * <http://www.apache.org/>. + * + */ + +package org.apache.hc.client5.http.entity; + +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; + +import org.apache.commons.compress.compressors.CompressorException; +import org.apache.commons.compress.compressors.CompressorStreamFactory; +import org.apache.commons.compress.compressors.deflate.DeflateCompressorInputStream; +import org.apache.commons.compress.compressors.deflate.DeflateParameters; +import org.apache.hc.core5.http.HttpEntity; +import org.apache.hc.core5.util.Args; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A factory class for managing compression and decompression of HTTP entities using different compression formats. + * <p> + * This factory uses a cache to optimize access to available input and output stream providers for compression formats. + * It also allows the use of aliases (e.g., "gzip" and "x-gzip") and automatically formats the compression names + * to ensure consistency. + * </p> + * + * <p> + * Supported compression formats include gzip, deflate, and other available formats provided by the + * {@link CompressorStreamFactory}. + * </p> + * + * <p> + * This class is thread-safe and uses {@link AtomicReference} to cache the available input and output stream providers. + * </p> + * + * @since 5.5 + */ +public class CompressorFactory { + + private static final Logger LOG = LoggerFactory.getLogger(CompressorFactory.class); + /** + * Singleton instance of the factory. + */ + public static final CompressorFactory INSTANCE = new CompressorFactory(); + + private final CompressorStreamFactory compressorStreamFactory = new CompressorStreamFactory(); + private final AtomicReference<Set<String>> inputProvidersCache = new AtomicReference<>(); + private final AtomicReference<Set<String>> outputProvidersCache = new AtomicReference<>(); + private final Map<String, String> formattedNameCache = new ConcurrentHashMap<>(); + + /** + * Returns a set of available input stream compression providers. + * + * @return a set of available input stream compression providers in lowercase. + */ + public Set<String> getAvailableInputProviders() { + return inputProvidersCache.updateAndGet(existing -> existing != null ? existing : fetchAvailableInputProviders()); + } + + /** + * Returns a set of available output stream compression providers. + * + * @return a set of available output stream compression providers in lowercase. + */ + public Set<String> getAvailableOutputProviders() { + return outputProvidersCache.updateAndGet(existing -> existing != null ? existing : fetchAvailableOutputProviders()); + } + + /** + * Returns the formatted name of the provided compression format. + * <p> + * If the provided name matches an alias (e.g., "gzip" or "x-gzip"), the method will return the standard name. + * </p> + * + * @param name the compression format name. + * @return the formatted name, or the original name if no alias is found. + * @throws IllegalArgumentException if the name is null or empty. + */ + public String getFormattedName(final String name) { + if (name == null || name.isEmpty()) { + LOG.warn("Compression name is null or empty"); + return null; + } + final String lowerCaseName = name.toLowerCase(Locale.ROOT); + return formattedNameCache.computeIfAbsent(lowerCaseName, key -> { + if ("gzip".equals(key) || "x-gzip".equals(key)) { + return "gz"; + } else if ("compress".equals(key)) { + return "z"; + } + return key; + }); + } + + /** + * Creates an input stream for the specified compression format and decompresses the provided input stream. + * <p> + * This method uses the specified compression name to decompress the input stream and supports the "noWrap" option + * for deflate streams. + * </p> + * + * @param name the compression format. + * @param inputStream the input stream to decompress. + * @param noWrap if true, disables the zlib header and trailer for deflate streams. + * @return the decompressed input stream, or the original input stream if the format is not supported. + */ + public InputStream getCompressorInputStream(final String name, final InputStream inputStream, final boolean noWrap) throws CompressorException { + Args.notNull(inputStream, "InputStream"); + Args.notNull(name, "name"); + + final String formattedName = getFormattedName(name); + return isSupported(formattedName, false) + ? createCompressorInputStream(formattedName, inputStream, noWrap) + : inputStream; + } + + /** + * Creates an output stream for the specified compression format and compresses the provided output stream. + * + * @param name the compression format. + * @param outputStream the output stream to compress. + * @return the compressed output stream, or the original output stream if the format is not supported. + */ + public OutputStream getCompressorOutputStream(final String name, final OutputStream outputStream) throws CompressorException { + final String formattedName = getFormattedName(name); + return isSupported(formattedName, true) + ? createCompressorOutputStream(formattedName, outputStream) + : outputStream; + + } + + + /** + * Decompresses the provided HTTP entity using the specified compression format. + * + * @param entity the HTTP entity to decompress. + * @param contentEncoding the compression format. + * @return a decompressed {@link HttpEntity}, or {@code null} if the compression format is unsupported. + */ + public HttpEntity decompressEntity(final HttpEntity entity, final String contentEncoding) { + return decompressEntity(entity, contentEncoding, false); + } + + /** + * Decompresses the provided HTTP entity using the specified compression format with the option for deflate streams. + * + * @param entity the HTTP entity to decompress. + * @param contentEncoding the compression format. + * @param noWrap if true, disables the zlib header and trailer for deflate streams. + * @return a decompressed {@link HttpEntity}, or {@code null} if the compression format is unsupported. + */ + public HttpEntity decompressEntity(final HttpEntity entity, final String contentEncoding, final boolean noWrap) { + Args.notNull(entity, "Entity"); + Args.notNull(contentEncoding, "Content Encoding"); + if (!isSupported(contentEncoding, false)) { + LOG.warn("Unsupported decompression type: {}", contentEncoding); + return null; + } + return new DecompressEntity(entity, contentEncoding, noWrap); + } + + /** + * Compresses the provided HTTP entity using the specified compression format. + * + * @param entity the HTTP entity to compress. + * @param contentEncoding the compression format. + * @return a compressed {@link HttpEntity}, or {@code null} if the compression format is unsupported. + */ + public HttpEntity compressEntity(final HttpEntity entity, final String contentEncoding) { + Args.notNull(entity, "Entity"); + Args.notNull(contentEncoding, "Content Encoding"); + if (!isSupported(contentEncoding, true)) { + LOG.warn("Unsupported compression type: {}", contentEncoding); + return null; + } + return new CompressingEntity(entity, contentEncoding); + } + + /** + * Fetches the available input stream compression providers from Commons Compress. + * + * @return a set of available input stream compression providers in lowercase. + */ + private Set<String> fetchAvailableInputProviders() { + final Set<String> inputNames = compressorStreamFactory.getInputStreamCompressorNames(); + return inputNames.stream() + .map(String::toLowerCase) + .collect(Collectors.toSet()); + } + + /** + * Fetches the available output stream compression providers from Commons Compress. + * + * @return a set of available output stream compression providers in lowercase. + */ + private Set<String> fetchAvailableOutputProviders() { + final Set<String> outputNames = compressorStreamFactory.getOutputStreamCompressorNames(); + return outputNames.stream() + .map(String::toLowerCase) + .collect(Collectors.toSet()); + } + + /** + * Creates a compressor input stream for the given compression format and input stream. + * <p> + * This method handles the special case for deflate compression where the zlib header can be optionally included. + * The noWrap parameter directly controls the behavior of the zlib header: + * - If noWrap is {@code true}, the deflate stream is processed without zlib headers (raw Deflate). + * - If noWrap is {@code false}, the deflate stream includes the zlib header. + * </p> + * + * @param name the compression format (e.g., "gzip", "deflate"). + * @param inputStream the input stream to decompress; must not be {@code null}. + * @param noWrap if {@code true}, disables the zlib header and trailer for deflate streams (raw Deflate). + * @return a decompressed input stream, or {@code null} if an error occurs during stream creation. + * @throws CompressorException if an error occurs while creating the compressor input stream or if the compression format is unsupported. + */ + private InputStream createCompressorInputStream(final String name, final InputStream inputStream, final boolean noWrap) throws CompressorException { + if ("deflate".equalsIgnoreCase(name)) { Review Comment: Use constant instead of magic string. ########## httpclient5/src/main/java/org/apache/hc/client5/http/entity/CompressorFactory.java: ########## @@ -0,0 +1,282 @@ +/* + * ==================================================================== + * 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 + * <http://www.apache.org/>. + * + */ + +package org.apache.hc.client5.http.entity; + +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; + +import org.apache.commons.compress.compressors.CompressorException; +import org.apache.commons.compress.compressors.CompressorStreamFactory; +import org.apache.commons.compress.compressors.deflate.DeflateCompressorInputStream; +import org.apache.commons.compress.compressors.deflate.DeflateParameters; +import org.apache.hc.core5.http.HttpEntity; +import org.apache.hc.core5.util.Args; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A factory class for managing compression and decompression of HTTP entities using different compression formats. + * <p> + * This factory uses a cache to optimize access to available input and output stream providers for compression formats. + * It also allows the use of aliases (e.g., "gzip" and "x-gzip") and automatically formats the compression names + * to ensure consistency. + * </p> + * + * <p> + * Supported compression formats include gzip, deflate, and other available formats provided by the + * {@link CompressorStreamFactory}. + * </p> + * + * <p> + * This class is thread-safe and uses {@link AtomicReference} to cache the available input and output stream providers. + * </p> + * + * @since 5.5 + */ +public class CompressorFactory { + + private static final Logger LOG = LoggerFactory.getLogger(CompressorFactory.class); + /** + * Singleton instance of the factory. + */ + public static final CompressorFactory INSTANCE = new CompressorFactory(); + + private final CompressorStreamFactory compressorStreamFactory = new CompressorStreamFactory(); + private final AtomicReference<Set<String>> inputProvidersCache = new AtomicReference<>(); + private final AtomicReference<Set<String>> outputProvidersCache = new AtomicReference<>(); + private final Map<String, String> formattedNameCache = new ConcurrentHashMap<>(); + + /** + * Returns a set of available input stream compression providers. + * + * @return a set of available input stream compression providers in lowercase. + */ + public Set<String> getAvailableInputProviders() { + return inputProvidersCache.updateAndGet(existing -> existing != null ? existing : fetchAvailableInputProviders()); + } + + /** + * Returns a set of available output stream compression providers. + * + * @return a set of available output stream compression providers in lowercase. + */ + public Set<String> getAvailableOutputProviders() { + return outputProvidersCache.updateAndGet(existing -> existing != null ? existing : fetchAvailableOutputProviders()); + } + + /** + * Returns the formatted name of the provided compression format. + * <p> + * If the provided name matches an alias (e.g., "gzip" or "x-gzip"), the method will return the standard name. + * </p> + * + * @param name the compression format name. + * @return the formatted name, or the original name if no alias is found. + * @throws IllegalArgumentException if the name is null or empty. + */ + public String getFormattedName(final String name) { + if (name == null || name.isEmpty()) { + LOG.warn("Compression name is null or empty"); + return null; + } + final String lowerCaseName = name.toLowerCase(Locale.ROOT); + return formattedNameCache.computeIfAbsent(lowerCaseName, key -> { + if ("gzip".equals(key) || "x-gzip".equals(key)) { + return "gz"; + } else if ("compress".equals(key)) { + return "z"; + } + return key; + }); + } + + /** + * Creates an input stream for the specified compression format and decompresses the provided input stream. + * <p> + * This method uses the specified compression name to decompress the input stream and supports the "noWrap" option + * for deflate streams. + * </p> + * + * @param name the compression format. + * @param inputStream the input stream to decompress. + * @param noWrap if true, disables the zlib header and trailer for deflate streams. + * @return the decompressed input stream, or the original input stream if the format is not supported. + */ + public InputStream getCompressorInputStream(final String name, final InputStream inputStream, final boolean noWrap) throws CompressorException { + Args.notNull(inputStream, "InputStream"); + Args.notNull(name, "name"); + + final String formattedName = getFormattedName(name); + return isSupported(formattedName, false) + ? createCompressorInputStream(formattedName, inputStream, noWrap) + : inputStream; + } + + /** + * Creates an output stream for the specified compression format and compresses the provided output stream. + * + * @param name the compression format. + * @param outputStream the output stream to compress. + * @return the compressed output stream, or the original output stream if the format is not supported. + */ + public OutputStream getCompressorOutputStream(final String name, final OutputStream outputStream) throws CompressorException { + final String formattedName = getFormattedName(name); + return isSupported(formattedName, true) + ? createCompressorOutputStream(formattedName, outputStream) + : outputStream; + Review Comment: Remove extra blank line. ########## httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/HttpClientBuilder.java: ########## @@ -234,6 +232,10 @@ private ExecInterceptorEntry( private List<Closeable> closeables; + private List<String> encodings; + + private boolean nowrap; Review Comment: Camel case "noWrap". ########## httpclient5/src/main/java/org/apache/hc/client5/http/entity/LazyDecompressInputStream.java: ########## @@ -0,0 +1,214 @@ +/* + * ==================================================================== + * 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 + * <http://www.apache.org/>. + * + */ +package org.apache.hc.client5.http.entity; + +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; + +import org.apache.commons.compress.compressors.CompressorException; +import org.apache.hc.core5.io.Closer; + + +/** + * A {@link FilterInputStream} that lazily initializes and applies decompression on the underlying input stream. + * This class supports multiple compression types and uses {@link CompressorFactory} to obtain the appropriate + * decompression stream when the first read operation occurs. + * + * <p>This implementation delays the creation of the decompression stream until it is required, optimizing + * the performance when the stream may not be read immediately or at all.</p> + * + * @since 5.5 + */ +public class LazyDecompressInputStream extends FilterInputStream { + + /** + * The lazily initialized decompression stream. + */ + private InputStream wrapperStream; + + /** + * The compression type used to determine which decompression algorithm to apply (e.g., "gzip", "deflate"). + */ + private final String compressionType; + + /** + * The flag indicating if decompression should skip certain headers (noWrap). + */ + private final boolean noWrap; + + /** + * Constructs a new {@link LazyDecompressInputStream} that applies the specified compression type and noWrap setting. + * + * @param wrappedStream the non-null {@link InputStream} to be wrapped and decompressed. + * @param compressionType the compression type (e.g., "gzip", "deflate"). + * @param noWrap whether to decompress without headers for certain compression formats. + */ + public LazyDecompressInputStream(final InputStream wrappedStream, final String compressionType, final boolean noWrap) { + super(wrappedStream); + this.compressionType = compressionType; + this.noWrap = noWrap; + } + + /** + * Constructs a new {@link LazyDecompressInputStream} that applies the specified compression type, + * defaulting to no noWrap handling. + * + * @param wrappedStream the non-null {@link InputStream} to be wrapped and decompressed. + * @param compressionType the compression type (e.g., "gzip", "deflate"). + */ + public LazyDecompressInputStream(final InputStream wrappedStream, final String compressionType) { + this(wrappedStream, compressionType, false); + } + + /** + * Initializes the decompression wrapper stream lazily, based on the compression type and noWrap flag. + * + * @return the initialized decompression stream. + * @throws IOException if an error occurs during initialization. + */ + private InputStream initWrapper() throws IOException { + if (wrapperStream == null) { Review Comment: It does not look like there is any locking here, so the class should be Javadoc'd as thread unsafe. ########## httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/HttpClientBuilder.java: ########## @@ -696,13 +698,62 @@ public final HttpClientBuilder setDefaultCookieSpecRegistry( * to be used for automatic content decompression. * * @return this instance. + * @deprecated Use {@link HttpClientBuilder#setEncodings(List)} */ + @Deprecated public final HttpClientBuilder setContentDecoderRegistry( final LinkedHashMap<String, InputStreamFactory> contentDecoderMap) { this.contentDecoderMap = contentDecoderMap; + setEncodings(new ArrayList<>(contentDecoderMap.keySet())); + return this; + } + Review Comment: Remove extra blank line. ########## httpclient5/src/test/java/org/apache/hc/client5/http/entity/TestGZip.java: ########## @@ -47,46 +47,56 @@ class TestGZip { @Test - void testBasic() throws Exception { + void testBasic() { final String s = "some kind of text"; - final StringEntity e = new StringEntity(s, ContentType.TEXT_PLAIN, false); - try (final GzipCompressingEntity gzipe = new GzipCompressingEntity(e)) { - Assertions.assertTrue(gzipe.isChunked()); - Assertions.assertEquals(-1, gzipe.getContentLength()); - Assertions.assertNotNull(gzipe.getContentEncoding()); - Assertions.assertEquals("gzip", gzipe.getContentEncoding()); - } + final HttpEntity entity = CompressorFactory.INSTANCE.decompressEntity(new StringEntity(s, ContentType.TEXT_PLAIN, false), "gz"); + Assertions.assertEquals(17, entity.getContentLength()); + Assertions.assertNotNull(entity.getContentEncoding()); + Assertions.assertEquals("gz", entity.getContentEncoding()); } @Test void testCompressionDecompression() throws Exception { final StringEntity in = new StringEntity("some kind of text", ContentType.TEXT_PLAIN); - try (final GzipCompressingEntity gzipe = new GzipCompressingEntity(in)) { - 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)); - } + Review Comment: Remove extra blank lines, the comments suffice. ########## httpclient5/src/test/java/org/apache/hc/client5/http/entity/TestEntityBuilder.java: ########## @@ -116,12 +120,32 @@ void testBuildChunked() { @Test void testBuildGZipped() { - final HttpEntity entity = EntityBuilder.create().setText("stuff").gzipCompressed().build(); + final HttpEntity entity = EntityBuilder.create().setText("stuff").setCompressed(true).setContentEncoding("gzip").build(); Assertions.assertNotNull(entity); Assertions.assertNotNull(entity.getContentType()); Assertions.assertEquals("text/plain; charset=UTF-8", entity.getContentType()); Assertions.assertNotNull(entity.getContentEncoding()); - Assertions.assertEquals("gzip", entity.getContentEncoding()); + Assertions.assertEquals("gz", entity.getContentEncoding()); + } + + @Test + public void testCompressionDecompression() throws Exception { + Review Comment: Remove all blank like, if something needs to be called out, `// say it`. ########## httpclient5/src/main/java/org/apache/hc/client5/http/entity/CompressorFactory.java: ########## @@ -0,0 +1,282 @@ +/* + * ==================================================================== + * 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 + * <http://www.apache.org/>. + * + */ + +package org.apache.hc.client5.http.entity; + +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; + +import org.apache.commons.compress.compressors.CompressorException; +import org.apache.commons.compress.compressors.CompressorStreamFactory; +import org.apache.commons.compress.compressors.deflate.DeflateCompressorInputStream; +import org.apache.commons.compress.compressors.deflate.DeflateParameters; +import org.apache.hc.core5.http.HttpEntity; +import org.apache.hc.core5.util.Args; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A factory class for managing compression and decompression of HTTP entities using different compression formats. + * <p> + * This factory uses a cache to optimize access to available input and output stream providers for compression formats. + * It also allows the use of aliases (e.g., "gzip" and "x-gzip") and automatically formats the compression names + * to ensure consistency. + * </p> + * + * <p> + * Supported compression formats include gzip, deflate, and other available formats provided by the + * {@link CompressorStreamFactory}. + * </p> + * + * <p> + * This class is thread-safe and uses {@link AtomicReference} to cache the available input and output stream providers. + * </p> + * + * @since 5.5 + */ +public class CompressorFactory { + + private static final Logger LOG = LoggerFactory.getLogger(CompressorFactory.class); + /** + * Singleton instance of the factory. + */ + public static final CompressorFactory INSTANCE = new CompressorFactory(); + + private final CompressorStreamFactory compressorStreamFactory = new CompressorStreamFactory(); + private final AtomicReference<Set<String>> inputProvidersCache = new AtomicReference<>(); + private final AtomicReference<Set<String>> outputProvidersCache = new AtomicReference<>(); + private final Map<String, String> formattedNameCache = new ConcurrentHashMap<>(); + + /** + * Returns a set of available input stream compression providers. + * + * @return a set of available input stream compression providers in lowercase. + */ + public Set<String> getAvailableInputProviders() { + return inputProvidersCache.updateAndGet(existing -> existing != null ? existing : fetchAvailableInputProviders()); + } + + /** + * Returns a set of available output stream compression providers. + * + * @return a set of available output stream compression providers in lowercase. + */ + public Set<String> getAvailableOutputProviders() { + return outputProvidersCache.updateAndGet(existing -> existing != null ? existing : fetchAvailableOutputProviders()); + } + + /** + * Returns the formatted name of the provided compression format. + * <p> + * If the provided name matches an alias (e.g., "gzip" or "x-gzip"), the method will return the standard name. + * </p> + * + * @param name the compression format name. + * @return the formatted name, or the original name if no alias is found. + * @throws IllegalArgumentException if the name is null or empty. + */ + public String getFormattedName(final String name) { + if (name == null || name.isEmpty()) { + LOG.warn("Compression name is null or empty"); + return null; + } + final String lowerCaseName = name.toLowerCase(Locale.ROOT); + return formattedNameCache.computeIfAbsent(lowerCaseName, key -> { + if ("gzip".equals(key) || "x-gzip".equals(key)) { + return "gz"; + } else if ("compress".equals(key)) { + return "z"; + } + return key; + }); + } + + /** + * Creates an input stream for the specified compression format and decompresses the provided input stream. + * <p> + * This method uses the specified compression name to decompress the input stream and supports the "noWrap" option + * for deflate streams. + * </p> + * + * @param name the compression format. + * @param inputStream the input stream to decompress. + * @param noWrap if true, disables the zlib header and trailer for deflate streams. + * @return the decompressed input stream, or the original input stream if the format is not supported. + */ + public InputStream getCompressorInputStream(final String name, final InputStream inputStream, final boolean noWrap) throws CompressorException { + Args.notNull(inputStream, "InputStream"); + Args.notNull(name, "name"); + + final String formattedName = getFormattedName(name); + return isSupported(formattedName, false) + ? createCompressorInputStream(formattedName, inputStream, noWrap) + : inputStream; + } + + /** + * Creates an output stream for the specified compression format and compresses the provided output stream. + * + * @param name the compression format. + * @param outputStream the output stream to compress. + * @return the compressed output stream, or the original output stream if the format is not supported. + */ + public OutputStream getCompressorOutputStream(final String name, final OutputStream outputStream) throws CompressorException { + final String formattedName = getFormattedName(name); + return isSupported(formattedName, true) + ? createCompressorOutputStream(formattedName, outputStream) + : outputStream; + + } + + + /** + * Decompresses the provided HTTP entity using the specified compression format. + * + * @param entity the HTTP entity to decompress. + * @param contentEncoding the compression format. + * @return a decompressed {@link HttpEntity}, or {@code null} if the compression format is unsupported. + */ + public HttpEntity decompressEntity(final HttpEntity entity, final String contentEncoding) { + return decompressEntity(entity, contentEncoding, false); + } + + /** + * Decompresses the provided HTTP entity using the specified compression format with the option for deflate streams. + * + * @param entity the HTTP entity to decompress. + * @param contentEncoding the compression format. + * @param noWrap if true, disables the zlib header and trailer for deflate streams. + * @return a decompressed {@link HttpEntity}, or {@code null} if the compression format is unsupported. + */ + public HttpEntity decompressEntity(final HttpEntity entity, final String contentEncoding, final boolean noWrap) { + Args.notNull(entity, "Entity"); + Args.notNull(contentEncoding, "Content Encoding"); + if (!isSupported(contentEncoding, false)) { + LOG.warn("Unsupported decompression type: {}", contentEncoding); + return null; + } + return new DecompressEntity(entity, contentEncoding, noWrap); + } + + /** + * Compresses the provided HTTP entity using the specified compression format. + * + * @param entity the HTTP entity to compress. + * @param contentEncoding the compression format. + * @return a compressed {@link HttpEntity}, or {@code null} if the compression format is unsupported. + */ + public HttpEntity compressEntity(final HttpEntity entity, final String contentEncoding) { + Args.notNull(entity, "Entity"); + Args.notNull(contentEncoding, "Content Encoding"); + if (!isSupported(contentEncoding, true)) { + LOG.warn("Unsupported compression type: {}", contentEncoding); + return null; + } + return new CompressingEntity(entity, contentEncoding); + } + + /** + * Fetches the available input stream compression providers from Commons Compress. + * + * @return a set of available input stream compression providers in lowercase. + */ + private Set<String> fetchAvailableInputProviders() { + final Set<String> inputNames = compressorStreamFactory.getInputStreamCompressorNames(); + return inputNames.stream() + .map(String::toLowerCase) + .collect(Collectors.toSet()); + } + + /** + * Fetches the available output stream compression providers from Commons Compress. + * + * @return a set of available output stream compression providers in lowercase. + */ + private Set<String> fetchAvailableOutputProviders() { + final Set<String> outputNames = compressorStreamFactory.getOutputStreamCompressorNames(); Review Comment: Inline single use "outputNames". -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: dev-unsubscr...@hc.apache.org For queries about this service, please contact Infrastructure at: us...@infra.apache.org --------------------------------------------------------------------- To unsubscribe, e-mail: dev-unsubscr...@hc.apache.org For additional commands, e-mail: dev-h...@hc.apache.org