garydgregory commented on code in PR #580: URL: https://github.com/apache/httpcomponents-client/pull/580#discussion_r1810552783
########## httpclient5/src/main/java/org/apache/hc/client5/http/entity/compress/CompressingFactory.java: ########## @@ -0,0 +1,277 @@ +/* + * ==================================================================== + * 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.compress; + +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 CompressingFactory { + + private static final Logger LOG = LoggerFactory.getLogger(CompressingFactory.class); + /** + * Singleton instance of the factory. + */ + public static final CompressingFactory INSTANCE = new CompressingFactory(); + + 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"); Review Comment: This API should not do any logging, especially at such a high level. This is just a string-to-string conversion API, it has no business rasing warnings IMO. ########## httpclient5/src/main/java/org/apache/hc/client5/http/entity/compress/CompressingFactory.java: ########## @@ -0,0 +1,277 @@ +/* + * ==================================================================== + * 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.compress; + +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 CompressingFactory { + + private static final Logger LOG = LoggerFactory.getLogger(CompressingFactory.class); + /** + * Singleton instance of the factory. + */ + public static final CompressingFactory INSTANCE = new CompressingFactory(); + + 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 DecompressingEntity(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() { Review Comment: The "fetch" method name prefix is unusual and surprising to me, I would follow the "get" naming convention. ########## httpclient5/src/main/java/org/apache/hc/client5/http/entity/compress/CompressingFactory.java: ########## @@ -0,0 +1,277 @@ +/* + * ==================================================================== + * 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.compress; + +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 CompressingFactory { + + private static final Logger LOG = LoggerFactory.getLogger(CompressingFactory.class); + /** + * Singleton instance of the factory. + */ + public static final CompressingFactory INSTANCE = new CompressingFactory(); + + 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); Review Comment: This class should not do any logging IMO, especially warnings, it should throw an illegal argument exception or let the call site deal with a null result. ########## httpclient5/src/main/java/org/apache/hc/client5/http/entity/compress/CompressingFactory.java: ########## @@ -0,0 +1,277 @@ +/* + * ==================================================================== + * 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.compress; + +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 CompressingFactory { + + private static final Logger LOG = LoggerFactory.getLogger(CompressingFactory.class); + /** + * Singleton instance of the factory. + */ + public static final CompressingFactory INSTANCE = new CompressingFactory(); + + 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. Review Comment: I don't understand what this Javadoc means. What's a "formatted" name? Maybe the API is misnamed? Is the job of the method to map from a content type value to a Commons Compress key? Is it something else? I think the comment should at least make it clear, which should guide us to a better API name. ########## httpclient5/src/main/java/org/apache/hc/client5/http/entity/compress/CompressingEntity.java: ########## @@ -0,0 +1,127 @@ +/* + * ==================================================================== + * 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.compress; + +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 CompressingFactory}, 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 = CompressingFactory.INSTANCE.getCompressorOutputStream(contentEncoding, outStream); + } catch (final CompressorException e) { + throw new IOException("Error initializing decompression stream", e); Review Comment: Don't you mean "Error initializing compression stream"? ########## httpclient5/src/main/java/org/apache/hc/client5/http/entity/compress/CompressingFactory.java: ########## @@ -0,0 +1,277 @@ +/* + * ==================================================================== + * 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.compress; + +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 CompressingFactory { + + private static final Logger LOG = LoggerFactory.getLogger(CompressingFactory.class); + /** + * Singleton instance of the factory. + */ + public static final CompressingFactory INSTANCE = new CompressingFactory(); + + 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; Review Comment: The Javadoc and implementation don't match: - Javadoc states decompression is performed - The code doesn't decompress if the format isn't supported. Either the Javadoc needs updating, or, more likely, we should throw an exception indicating the operation is not supported for that format type name. I must say I find the API and Javadoc confusing, we are creating a "Compressor" input stream in order to "decompress" with a "compression" format! What? - Maybe calling the API `getDecompressorInputStream` would help because it "decompresses". I expect a "Compressor" to compress, not the other way around. - Maybe using the term "format type name" instead "compression format" would remove the need for the reader to constantly parse "compress" vs. "decompress" in the text. - Maybe only using one term "decompress" or "compress" for one method (Javadoc and code) would alleviate the issue. ########## httpclient5/src/main/java/org/apache/hc/client5/http/entity/compress/CompressingFactory.java: ########## @@ -0,0 +1,277 @@ +/* + * ==================================================================== + * 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.compress; + +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 CompressingFactory { + + private static final Logger LOG = LoggerFactory.getLogger(CompressingFactory.class); + /** + * Singleton instance of the factory. + */ + public static final CompressingFactory INSTANCE = new CompressingFactory(); + + 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 DecompressingEntity(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) Review Comment: This might have some odd results depending on the locale context (https://garygregory.wordpress.com/2015/11/03/java-lowercase-conversion-turkey/) if you do not use a Locale like the PR does above using ROOT. ########## httpclient5/src/main/java/org/apache/hc/client5/http/entity/DecompressingEntity.java: ########## @@ -38,7 +38,9 @@ * Common base class for decompressing {@link HttpEntity} implementations. * * @since 4.4 + * @deprecated Review Comment: The Javadoc tag is missing its comment to redirect reader. ########## httpclient5/src/main/java/org/apache/hc/client5/http/entity/compress/DecompressingEntity.java: ########## @@ -0,0 +1,149 @@ +/* + * ==================================================================== + * 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.compress; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import org.apache.commons.io.IOUtils; +import org.apache.hc.core5.annotation.Contract; +import org.apache.hc.core5.annotation.ThreadingBehavior; +import org.apache.hc.core5.http.HttpEntity; +import org.apache.hc.core5.http.io.entity.HttpEntityWrapper; +import org.apache.hc.core5.util.Args; + +/** + * An {@link HttpEntity} wrapper that decompresses the content of the wrapped entity. + * This class supports different compression types and can handle both standard + * compression (e.g., gzip, deflate) and variations that require a custom handling (e.g., noWrap). + * + * <p>Decompression is performed using a {@link LazyDecompressingInputStream} that + * applies decompression lazily when content is requested.</p> + * + * <p> + * Note: This class uses lazy initialization for certain fields, making it <b>not thread-safe</b>. + * If multiple threads access an instance of this class concurrently, they must synchronize on the instance + * to ensure correct behavior. + * </p> + * + * @since 5.5 + */ +@Contract(threading = ThreadingBehavior.UNSAFE) +public class DecompressingEntity extends HttpEntityWrapper { + + /** + * The content input stream, initialized lazily during the first read. + */ + private InputStream content; + + /** + * The compression type used for decompression (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 DecompressingEntity} with the specified compression type and noWrap setting. + * + * @param wrapped the non-null {@link HttpEntity} to be wrapped. + * @param compressionType the compression type (e.g., "gzip", "deflate"). + * @param noWrap whether to decompress without headers for certain compression formats. + */ + public DecompressingEntity(final HttpEntity wrapped, final String compressionType, final boolean noWrap) { + super(wrapped); + this.compressionType = compressionType; + this.noWrap = noWrap; + } + + /** + * Constructs a new {@link DecompressingEntity} with the specified compression type, defaulting to no noWrap handling. + * + * @param wrapped the non-null {@link HttpEntity} to be wrapped. + * @param compressionType the compression type (e.g., "gzip", "deflate"). + */ + public DecompressingEntity(final HttpEntity wrapped, final String compressionType) { + this(wrapped, compressionType, false); + } + + /** + * Initializes and returns a stream for decompression. + * The decompression is applied lazily on the wrapped entity's content. + * + * @return a lazily initialized {@link InputStream} that decompresses the content. + * @throws IOException if an error occurs during decompression. + */ + private InputStream getDecompressingStream() throws IOException { + return new LazyDecompressingInputStream(super.getContent(), compressionType, noWrap); + } + + /** + * Returns the decompressed content stream. If the entity is streaming, + * the same {@link InputStream} is returned on subsequent calls. + * + * @return the decompressed {@link InputStream}. + * @throws IOException if an error occurs during decompression. + */ + @Override + public InputStream getContent() throws IOException { Review Comment: As I commented elsewhere in this PR, the thread safety or lack thereof needs to be documented if not addressed. ########## httpclient5/src/main/java/org/apache/hc/client5/http/entity/compress/CompressingFactory.java: ########## @@ -0,0 +1,277 @@ +/* + * ==================================================================== + * 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.compress; + +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 CompressingFactory { + + private static final Logger LOG = LoggerFactory.getLogger(CompressingFactory.class); + /** + * Singleton instance of the factory. + */ + public static final CompressingFactory INSTANCE = new CompressingFactory(); + + 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 DecompressingEntity(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) Review Comment: See above. ########## httpclient5/src/main/java/org/apache/hc/client5/http/entity/compress/CompressingEntity.java: ########## @@ -0,0 +1,127 @@ +/* + * ==================================================================== + * 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.compress; + +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 CompressingFactory}, 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 = CompressingFactory.INSTANCE.getCompressorOutputStream(contentEncoding, outStream); + } catch (final CompressorException e) { + throw new IOException("Error initializing decompression stream", e); + } + if (compressorStream != null) { + // Write compressed data + super.writeTo(compressorStream); + // Close the compressor stream after writing + compressorStream.close(); Review Comment: Could this method be rewritten to use a try-with-resources block? -- 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