CompressionUtils.java

package com.bonitasoft.processbuilder.utils;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;

/**
 * Utility class for compressing and decompressing strings using GZIP compression and Base64 encoding.
 * <p>
 * This class provides methods to:
 * <ul>
 *   <li>Compress strings (typically JSON) to reduce size for transmission</li>
 *   <li>Decompress previously compressed strings back to original form</li>
 * </ul>
 * <p>
 * The compression process:
 * <ol>
 *   <li>Converts string to UTF-8 bytes</li>
 *   <li>Compresses using GZIP</li>
 *   <li>Encodes compressed bytes to Base64 for safe string transmission</li>
 * </ol>
 * <p>
 * Typical compression ratios for JSON data: 70-90% size reduction.
 * </p>
 *
 * @author Process-Builder Development Team
 * @version 1.0
 * @since 2026-02-12
 */
public final class CompressionUtils {

    private static final int BUFFER_SIZE = 1024;

    /**
     * Private constructor to prevent instantiation.
     * This is a utility class with only static methods.
     */
    private CompressionUtils() {
        throw new UnsupportedOperationException("Utility class cannot be instantiated");
    }

    /**
     * Compresses a string using GZIP compression and encodes it as Base64.
     * <p>
     * This method is particularly useful for compressing JSON strings or other text data
     * before sending them over the network or storing them in a database.
     * </p>
     * <p>
     * Example:
     * <pre>
     * String json = "{\"data\": \"large content...\"}";
     * String compressed = CompressionUtils.compress(json);
     * // compressed is now a Base64-encoded GZIP string, typically 70-90% smaller
     * </pre>
     *
     * @param input The string to compress (must not be null)
     * @return Base64-encoded GZIP-compressed string
     * @throws IllegalArgumentException if input is null
     * @throws IOException if compression fails
     */
    public static String compress(String input) throws IOException {
        if (input == null) {
            throw new IllegalArgumentException("Input string cannot be null");
        }

        if (input.isEmpty()) {
            return ""; // Empty string compresses to empty string
        }

        byte[] inputBytes = input.getBytes(StandardCharsets.UTF_8);

        try (ByteArrayOutputStream byteOutputStream = new ByteArrayOutputStream();
             GZIPOutputStream gzipOutputStream = new GZIPOutputStream(byteOutputStream)) {

            gzipOutputStream.write(inputBytes);
            gzipOutputStream.finish();

            byte[] compressedBytes = byteOutputStream.toByteArray();
            return Base64.getEncoder().encodeToString(compressedBytes);
        }
    }

    /**
     * Decompresses a Base64-encoded GZIP-compressed string back to its original form.
     * <p>
     * This method reverses the compression performed by {@link #compress(String)}.
     * The input must be a valid Base64-encoded GZIP string, otherwise decompression will fail.
     * </p>
     * <p>
     * Example:
     * <pre>
     * String compressed = "H4sIAAAAAAAA/..."; // Base64 GZIP string
     * String original = CompressionUtils.decompress(compressed);
     * // original is now the decompressed string
     * </pre>
     *
     * @param compressedBase64 The Base64-encoded GZIP-compressed string (must not be null)
     * @return The decompressed original string
     * @throws IllegalArgumentException if compressedBase64 is null or has invalid Base64 format
     * @throws IOException if decompression fails (corrupted data, invalid GZIP format)
     */
    public static String decompress(String compressedBase64) throws IOException {
        if (compressedBase64 == null) {
            throw new IllegalArgumentException("Compressed input cannot be null");
        }

        if (compressedBase64.isEmpty()) {
            return ""; // Empty string decompresses to empty string
        }

        try {
            byte[] compressedBytes = Base64.getDecoder().decode(compressedBase64);

            try (ByteArrayInputStream byteInputStream = new ByteArrayInputStream(compressedBytes);
                 GZIPInputStream gzipInputStream = new GZIPInputStream(byteInputStream);
                 ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {

                byte[] buffer = new byte[BUFFER_SIZE];
                int len;
                while ((len = gzipInputStream.read(buffer)) > 0) {
                    outputStream.write(buffer, 0, len);
                }

                return outputStream.toString(StandardCharsets.UTF_8.name());
            }
        } catch (IllegalArgumentException e) {
            throw new IllegalArgumentException("Invalid Base64 format: " + e.getMessage(), e);
        }
    }

    /**
     * Calculates the compression ratio as a percentage.
     * <p>
     * This is a utility method for monitoring and logging compression effectiveness.
     * </p>
     * <p>
     * Example:
     * <pre>
     * String original = "{\"data\": \"...\"}"; // 1000 bytes
     * String compressed = CompressionUtils.compress(original);
     * double ratio = CompressionUtils.getCompressionRatio(original, compressed);
     * // ratio might be 15.5 (meaning compressed is 15.5% of original size)
     * </pre>
     *
     * @param originalString The original uncompressed string
     * @param compressedBase64 The compressed Base64 string
     * @return Compression ratio as percentage (0-100), where lower is better
     * @throws IllegalArgumentException if either parameter is null
     */
    public static double getCompressionRatio(String originalString, String compressedBase64) {
        if (originalString == null || compressedBase64 == null) {
            throw new IllegalArgumentException("Parameters cannot be null");
        }

        if (originalString.isEmpty() || compressedBase64.isEmpty()) {
            return 0.0;
        }

        int originalSize = originalString.getBytes(StandardCharsets.UTF_8).length;
        int compressedSize = compressedBase64.getBytes(StandardCharsets.UTF_8).length;

        return (100.0 * compressedSize) / originalSize;
    }
}