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}