001/*
002 * MIT License
003 *
004 * Copyright (c) 2023-2024 Michael Cowan
005 *
006 * Permission is hereby granted, free of charge, to any person obtaining a copy
007 * of this software and associated documentation files (the "Software"), to deal
008 * in the Software without restriction, including without limitation the rights
009 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
010 * copies of the Software, and to permit persons to whom the Software is
011 * furnished to do so, subject to the following conditions:
012 *
013 * The above copyright notice and this permission notice shall be included in all
014 * copies or substantial portions of the Software.
015 *
016 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
017 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
018 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
019 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
020 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
021 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
022 * SOFTWARE.
023 */
024
025package io.blt.util;
026
027import io.blt.util.functional.ThrowingRunnable;
028import io.blt.util.functional.ThrowingSupplier;
029import java.util.function.Function;
030import java.util.function.Predicate;
031import java.util.function.Supplier;
032
033/**
034 * Static utility methods centred around {@code Exception} and {@code Throwable}.
035 * <p>Includes raising and handling exceptions.</p>
036 */
037public final class Ex {
038
039    private Ex() {
040        throw new IllegalAccessError("Utility class should be accessed statically and never constructed");
041    }
042
043    /**
044     * Executes a supplier, transforming any thrown {@link Exception} using a specified function.
045     * A thrown {@link RuntimeException} will bubble up unaltered.
046     *
047     * <p>e.g., a custom high-level {@code XmlProcessingException} that we want to raise when parsing XML:</p>
048     * <pre>{@code
049     * public Document parseXml(String pathname) throws XmlProcessingException {
050     *     return Ex.transformExceptions(
051     *             () -> DocumentBuilderFactory
052     *                     .newInstance()
053     *                     .newDocumentBuilder()       // throws ParserConfigurationException
054     *                     .parse(new File(pathname)), // throws SAXException, IOException
055     *             XmlProcessingException::new);
056     * }
057     * }</pre>
058     *
059     * @param <R>         The result type of the supplier
060     * @param <E>         The type of transformed exception
061     * @param supplier    The supplier that may throw an exception
062     * @param transformer Function to transform exceptions thrown by the supplier
063     * @return The result of the supplier if successful
064     * @throws E If the supplier throws an exception, transformed by the provided function
065     */
066    public static <R, E extends Throwable> R transformExceptions(
067            ThrowingSupplier<R, ? extends Exception> supplier, Function<? super Exception, E> transformer) throws E {
068        try {
069            return supplier.get();
070        } catch (RuntimeException e) {
071            throw e;
072        } catch (Exception e) {
073            throw transformer.apply(e);
074        }
075    }
076
077    /**
078     * Executes a runnable, transforming any thrown {@link Exception} using a specified function.
079     * A thrown {@link RuntimeException} will bubble up unaltered.
080     *
081     * <p>e.g.,</p>
082     * <pre>{@code
083     * public void appendFormattedDateToFile(String date, String fileName) throws LoggingException {
084     *     Ex.transformExceptions(
085     *             () -> {
086     *                 var formatted = new SimpleDateFormat()
087     *                         .parse(date)            // ParseException
088     *                         .toString();
089     *
090     *                 Files.write(                    // IOException
091     *                         Paths.get(fileName),
092     *                         formatted.getBytes(),
093     *                         StandardOpenOption.APPEND);
094     *             }, LoggingException::new
095     *     );
096     * }
097     * }</pre>
098     *
099     * @param <E>         The type of transformed exception
100     * @param runnable    The runnable that may throw an exception
101     * @param transformer Function to transform exceptions thrown by the supplier
102     * @throws E If the supplier throws an exception, transformed by the provided function
103     */
104    public static <E extends Throwable> void transformExceptions(
105            ThrowingRunnable<? extends Exception> runnable, Function<? super Exception, E> transformer) throws E {
106        transformExceptions(() -> {
107            runnable.run();
108            return null;
109        }, transformer);
110    }
111
112    /**
113     * Throws the specified {@code throwable} if the given {@code value} satisfies the provided {@code predicate}.
114     * For convenience, {@code value} is returned.
115     * e.g.,
116     * <pre>{@code
117     * public Map<String, String> loadProperties() {
118     *     return throwIf(Properties.loadFromJson(FILENAME), Map::isEmpty,
119     *             () -> new IllegalStateException("Properties must not be empty"));
120     * }
121     * }</pre>
122     *
123     * @param value     the value to be checked
124     * @param predicate the predicate to be evaluated
125     * @param throwable the supplier for the throwable to be thrown
126     * @param <T>       the type of the value
127     * @param <E>       the type of the throwable
128     * @return {@code value}
129     * @throws E if the given {@code value} satisfies the provided {@code predicate}
130     * @see Ex#throwUnless(Object, Predicate, Supplier)
131     */
132    public static <T, E extends Throwable> T throwIf(
133            T value, Predicate<? super T> predicate, Supplier<? extends E> throwable) throws E {
134        if (predicate.test(value)) {
135            throw throwable.get();
136        }
137        return value;
138    }
139
140    /**
141     * Throws the specified {@code throwable} if the given {@code value} does not satisfy the provided {@code predicate}.
142     * For convenience, {@code value} is returned.
143     * e.g.,
144     * <pre>{@code
145     * throwUnless(properties, p -> p.containsKey("host"),
146     *         () -> new IllegalStateException("Properties must contain a host"));
147     * }</pre>
148     *
149     * @param value     the value to be checked
150     * @param predicate the predicate to be evaluated
151     * @param throwable the supplier for the throwable to be thrown
152     * @param <T>       the type of the value
153     * @param <E>       the type of the throwable
154     * @return {@code value}
155     * @throws E if the given {@code value} does not satisfy the provided {@code predicate}
156     * @see Ex#throwIf(Object, Predicate, Supplier)
157     */
158    public static <T, E extends Throwable> T throwUnless(
159            T value, Predicate<? super T> predicate, Supplier<? extends E> throwable) throws E {
160        return throwIf(value, predicate.negate(), throwable);
161    }
162
163}