1   /*
2    * SPDX-FileCopyrightText: none
3    * SPDX-License-Identifier: CC0-1.0
4    */
5   
6   package gov.nist.secauto.metaschema.core.util;
7   
8   import java.util.Arrays;
9   import java.util.Objects;
10  
11  import edu.umd.cs.findbugs.annotations.NonNull;
12  
13  /**
14   * Provides a means for throwing important checked exceptions over non-checked
15   * methods, e.g. lambda invocations.
16   * <p>
17   * This capability should be used with care, and generally in limited
18   * circumstances.
19   */
20  public final class ExceptionUtils {
21    /**
22     * Wrap a checked exception in an unchecked {@link WrappedException}.
23     *
24     * @param ex
25     *          the exception to wrap
26     * @return a new wrapped exception containing the provided exception
27     */
28    @NonNull
29    public static WrappedException wrap(@NonNull Throwable ex) {
30      return new WrappedException(Objects.requireNonNull(ex, "ex"));
31    }
32  
33    /**
34     * Wrap a checked exception in an unchecked {@link WrappedException}.
35     * <p>
36     * This method is identical to {@link #wrap(Throwable)} but named to indicate
37     * intent when used in throw statements.
38     *
39     * @param ex
40     *          the exception to wrap
41     * @return a new wrapped exception containing the provided exception
42     */
43    @NonNull
44    public static WrappedException wrapAndThrow(@NonNull Throwable ex) {
45      return wrap(ex);
46    }
47  
48    /**
49     * Unwrap a previously wrapped exception.
50     *
51     * @param ex
52     *          the wrapped exception to unwrap
53     * @return the original exception that was wrapped
54     */
55    @NonNull
56    public static Throwable unwrap(
57        @NonNull WrappedException ex) {
58      return ex.unwrap();
59    }
60  
61    /**
62     * Unwrap a previously wrapped exception, casting it to the expected type.
63     *
64     * @param <E>
65     *          the expected exception type
66     * @param ex
67     *          the wrapped exception to unwrap
68     * @param wrappedExceptionClass
69     *          the class of the expected exception type
70     * @return the original exception cast to the expected type
71     * @throws IllegalArgumentException
72     *           if the wrapped exception is not of the expected type
73     */
74    @NonNull
75    public static <E extends Throwable> E unwrap(
76        @NonNull WrappedException ex,
77        @NonNull Class<E> wrappedExceptionClass) {
78      return ex.unwrap(wrappedExceptionClass);
79    }
80  
81    /**
82     * A runtime exception that wraps a checked exception, allowing it to be thrown
83     * from contexts that do not allow checked exceptions (such as lambda
84     * expressions).
85     */
86    public static final class WrappedException
87        extends RuntimeException {
88  
89      /**
90       * the serial version UID.
91       */
92      private static final long serialVersionUID = 2L;
93  
94      /**
95       * Construct a new wrapped exception.
96       *
97       * @param cause
98       *          the exception to wrap
99       */
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 }