ExceptionUtils.java

package com.bonitasoft.processbuilder.extension;

import java.util.function.Function;
import java.util.function.Supplier;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * A utility class for handling logging and exception throwing.
 * This class centralizes error management to ensure consistency.
 */
public final class ExceptionUtils {

    /**
     * A logger for this class, used to record log messages and provide debugging information.
     */
    private static final Logger LOGGER = LoggerFactory.getLogger(ExceptionUtils.class);

    /**
     * Private constructor to prevent instantiation of this utility class.
     *
     * @throws UnsupportedOperationException always, to enforce the utility pattern.
     */
    private ExceptionUtils() {
        throw new UnsupportedOperationException("This is a "+this.getClass().getSimpleName()+" class and cannot be instantiated.");
    }

    /**
     * Logs a formatted error message and then throws a new instance of a specified exception.
     * This method is a safe and generic way to handle exceptions by using a {@link java.util.function.Supplier},
     * which avoids the complexities and potential runtime errors of Reflection. It leverages the logging
     * framework's ability to format messages, which is more efficient because the message is only
     * constructed if the log level is active.
     *
     * @param <T> The type of the exception to be thrown. This must be a subclass of {@link java.lang.Exception}.
     * @param exceptionSupplier A {@link java.util.function.Supplier} that provides an instance of the exception to be thrown.
     * The supplier's {@code get()} method should create and return a new exception instance, typically
     * via a lambda or a constructor reference (e.g., {@code IllegalArgumentException::new}).
     * @param format A parameterized message format string compatible with the logging framework's
     * formatters (e.g., using `{}`). This format string will be logged to the error level.
     * @param args A variable-length argument list of objects to be substituted into the format string.
     * These objects correspond to the `{}` placeholders in the {@code format} string.
     * @throws T The exception instance provided by the {@code exceptionSupplier}.
     * @return The exception instance provided by the supplier.
     */
    public static <T extends Exception> T logAndThrow(
        Supplier<T> exceptionSupplier,
        String format,
        Object... args) throws T {
        
        // The logger is designed to handle message formatting lazily, which is more performant.
        // The message is only built if the ERROR log level is enabled.
        LOGGER.error(format, args);
        
        // Throw the exception provided by the supplier. This ensures we're throwing a new,
        // explicitly defined exception instance.
        // throw exceptionSupplier.get();
        throw exceptionSupplier.get();
    }

    /**
     * Utility method to safely log an error message and subsequently throw a new
     * exception instance containing that same formatted message.
     *
     * <p>This ensures that the exception thrown, even if generic (like RuntimeException),
     * always contains the context provided in the 'format' string, which is crucial
     * for troubleshooting in environments that might discard the original cause's details.</p>
     *
     * @param <T> The type of the Exception to be thrown, which must extend Exception.
     * @param exceptionFunction A function that accepts the final formatted message
     * (String) and returns a new exception of type T (e.g., {@code message -> new MyCustomException(message)}).
     * @param format The format string for the error message (compatible with String.format).
     * @param args Arguments referenced by the format specifiers in the format string.
     * @throws T The exception created by the provided function.
     */
    public static <T extends Exception> void logAndThrowWithMessage(
            Function<String, T> exceptionFunction, 
            String format,
            Object... args) throws T {
            
        // 1. EXPLICIT FORMATTING: Construct the complete, formatted error message string.
        // This is done once to ensure consistency between the log and the exception.
        String finalMessage = String.format(format, args);
        
        // 2. LOGGING: Log the error message using the fully formatted string.
        // The LOGGER variable must be defined elsewhere (e.g., LOGGER = LoggerFactory.getLogger(YourClass.class)).
        // Note: Use LOGGER.error, assuming the level is appropriately configured.
        LOGGER.error(finalMessage);
        
        // 3. SECURE THROW: Use the function to create and throw the new exception instance,
        // explicitly passing the 'finalMessage'. This guarantees the exception (T) 
        // always contains the detailed error message, avoiding "No message" errors.
        throw exceptionFunction.apply(finalMessage);
    }

    /**
     * Logs an error message and then securely throws a new instance of the specified exception class.
     * <p>
     * This method uses Java Reflection to reliably instantiate the exception by explicitly
     * calling the constructor that accepts a single {@code String} argument (the message).
     * This approach avoids the 'Ambiguous method overloading' errors often encountered 
     * in Groovy environments when using dynamic closures or constructors with {@code null} parameters.
     * </p>
     * <p>
     * The method ensures that the thrown exception always contains the detailed, formatted
     * error message, preventing common 'No message' issues in the calling environment.
     * </p>
     *
     * @param <T> The type of Exception to be thrown, which must extend {@link Exception}.
     * @param exceptionClass The {@link Class} object of the exception to instantiate (e.g., {@code IllegalArgumentException.class}).
     * @param format The format string for the error message, compliant with {@link String#format(String, Object...)}.
     * @param args The arguments referenced by the format specifiers in the format string.
     * @throws T The instantiated and thrown exception of type T, containing the formatted message.
     * @throws RuntimeException if the specified exception class does not have a public constructor
     * that accepts a single String argument, or if instantiation fails due to reflection errors.
     */
    public static <T extends Exception> void logAndThrowWithClass(
            Class<T> exceptionClass,
            String format,
            Object... args) throws T {

        // 1. FORMATTING AND LOGGING
        String finalMessage = String.format(format, args);
        LOGGER.error(finalMessage);

        // 2. SECURE THROW USING REFLECTION
        try {
            // Retrieve the constructor that accepts a single String (the message).
            // This explicitly resolves the constructor ambiguity issue.
            java.lang.reflect.Constructor<T> constructor = exceptionClass.getConstructor(String.class);

            // Create and instantiate the exception, passing the formatted message
            T exceptionInstance = constructor.newInstance(finalMessage);

            // Throw the generated exception instance
            throw exceptionInstance;

        } catch (NoSuchMethodException e) {
            // This occurs if the class T does not have the required String constructor.
            LOGGER.error("Fatal Error: Exception class '{}' does not have a public constructor "
                    + "that accepts a single String message.", exceptionClass.getName(), e);
            throw new RuntimeException("Error during exception construction for class: " + exceptionClass.getName(), e);
        } catch (ReflectiveOperationException e) {
            // Catches only reflection-related issues (IllegalAccessException, InstantiationException,
            // InvocationTargetException) but NOT the thrown exception T which escapes the try block.
            LOGGER.error("Fatal Error: Could not instantiate exception class '{}'.", exceptionClass.getName(), e);
            throw new RuntimeException("Could not instantiate exception class: " + exceptionClass.getName(), e);
        }
    }


}