001/*
002 * SPDX-FileCopyrightText: none
003 * SPDX-License-Identifier: CC0-1.0
004 */
005
006package dev.metaschema.core.util;
007
008import java.util.Arrays;
009import java.util.Objects;
010
011import edu.umd.cs.findbugs.annotations.NonNull;
012
013/**
014 * Provides a means for throwing important checked exceptions over non-checked
015 * methods, e.g. lambda invocations.
016 * <p>
017 * This capability should be used with care, and generally in limited
018 * circumstances.
019 */
020public final class ExceptionUtils {
021  /**
022   * Wrap a checked exception in an unchecked {@link WrappedException}.
023   *
024   * @param ex
025   *          the exception to wrap
026   * @return a new wrapped exception containing the provided exception
027   */
028  @NonNull
029  public static WrappedException wrap(@NonNull Throwable ex) {
030    return new WrappedException(Objects.requireNonNull(ex, "ex"));
031  }
032
033  /**
034   * Wrap a checked exception in an unchecked {@link WrappedException}.
035   * <p>
036   * This method is identical to {@link #wrap(Throwable)} but named to indicate
037   * intent when used in throw statements.
038   *
039   * @param ex
040   *          the exception to wrap
041   * @return a new wrapped exception containing the provided exception
042   */
043  @NonNull
044  public static WrappedException wrapAndThrow(@NonNull Throwable ex) {
045    return wrap(ex);
046  }
047
048  /**
049   * Unwrap a previously wrapped exception.
050   *
051   * @param ex
052   *          the wrapped exception to unwrap
053   * @return the original exception that was wrapped
054   */
055  @NonNull
056  public static Throwable unwrap(
057      @NonNull WrappedException ex) {
058    return ex.unwrap();
059  }
060
061  /**
062   * Unwrap a previously wrapped exception, casting it to the expected type.
063   *
064   * @param <E>
065   *          the expected exception type
066   * @param ex
067   *          the wrapped exception to unwrap
068   * @param wrappedExceptionClass
069   *          the class of the expected exception type
070   * @return the original exception cast to the expected type
071   * @throws IllegalArgumentException
072   *           if the wrapped exception is not of the expected type
073   */
074  @NonNull
075  public static <E extends Throwable> E unwrap(
076      @NonNull WrappedException ex,
077      @NonNull Class<E> wrappedExceptionClass) {
078    return ex.unwrap(wrappedExceptionClass);
079  }
080
081  /**
082   * A runtime exception that wraps a checked exception, allowing it to be thrown
083   * from contexts that do not allow checked exceptions (such as lambda
084   * expressions).
085   */
086  public static final class WrappedException
087      extends RuntimeException {
088
089    /**
090     * the serial version UID.
091     */
092    private static final long serialVersionUID = 2L;
093
094    /**
095     * Construct a new wrapped exception.
096     *
097     * @param cause
098     *          the exception to wrap
099     */
100    public WrappedException(@NonNull Throwable cause) {
101      super(Objects.requireNonNull(cause, "cause"));
102    }
103
104    @Override
105    public synchronized Throwable initCause(Throwable cause) {
106      throw new UnsupportedOperationException("must set cause in constructor");
107    }
108
109    /**
110     * Get the wrapped exception.
111     *
112     * @return the original exception that was wrapped
113     */
114    @NonNull
115    public Throwable unwrap() {
116      return ObjectUtils.notNull(getCause());
117    }
118
119    /**
120     * Get the wrapped exception, casting it to the expected type.
121     *
122     * @param <E>
123     *          the expected exception type
124     * @param wrappedExceptionClass
125     *          the class of the expected exception type
126     * @return the original exception cast to the expected type
127     * @throws IllegalArgumentException
128     *           if the wrapped exception is not of the expected type
129     */
130    @NonNull
131    public <E extends Throwable> E unwrap(@NonNull Class<E> wrappedExceptionClass) {
132      Throwable cause = unwrap();
133      if (wrappedExceptionClass.isInstance(cause)) {
134        E unwrappedEx = wrappedExceptionClass.cast(cause);
135        // Avoid adding duplicate suppressed exceptions on repeated unwraps
136        boolean alreadySuppressed = Arrays.stream(unwrappedEx.getSuppressed())
137            .anyMatch(s -> s == this);
138        if (!alreadySuppressed) {
139          unwrappedEx.addSuppressed(this);
140        }
141        return unwrappedEx;
142      }
143      throw new IllegalArgumentException(
144          String.format("Wrapped exception '%s' did not match expected type '%s'.",
145              cause.getClass().getName(),
146              wrappedExceptionClass.getName()));
147    }
148
149    /**
150     * Unwrap and throw the original exception.
151     *
152     * @throws Throwable
153     *           the original wrapped exception
154     */
155    public void unwrapAndThrow() throws Throwable {
156      throw unwrap();
157    }
158
159    /**
160     * Unwrap and throw the original exception, cast to the expected type.
161     *
162     * @param <E>
163     *          the expected exception type
164     * @param wrappedExceptionClass
165     *          the class of the expected exception type
166     * @throws E
167     *           the original wrapped exception cast to the expected type
168     */
169    public <E extends Throwable> void unwrapAndThrow(@NonNull Class<E> wrappedExceptionClass) throws E {
170      throw unwrap(wrappedExceptionClass);
171    }
172  }
173
174  private ExceptionUtils() {
175    // prevent construction
176  }
177}