Last active
July 3, 2023 01:03
-
-
Save david-bakin/c50ae5f3c026856f0b11b624e79557fa to your computer and use it in GitHub Desktop.
Rewriting the message of a Throwable
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** Sometimes you might want to rethrow the same exception but with a little extra in the message. This allows | |
* you to "clone" an exception but change the message. (C# exceptions have the capability to hold arbitrary | |
* information via the `Data` property (a `Dictionary`).) | |
*/ | |
@NonNull | |
public static Throwable replaceMessageOf(@NonNull final Throwable ex, @NonNull final String msg) { | |
// First try using reflection to just stomp on the exception instance's message | |
if (false) { | |
final var rex = stompOnMessageOf(ex, msg); | |
if (rex != null) return rex; | |
} | |
// Not a perfect reconstruction. Original exception class might hold additional instance information that | |
// you set via constructor and access via getters and that we're ignoring here. E.g., | |
// `com.faster.jackson.core.JsonProcssingException`. | |
Throwable result = null; | |
Throwable suppressedHere = null; | |
final var constructors = getConstructorsOfThrowable(ex); | |
try { | |
if (constructors.msgAndThrowable() != null) { | |
result = constructors.msgAndThrowable().newInstance(msg, ex.getCause()); | |
} | |
if (constructors.msgOnly() != null) { | |
result = constructors.msgOnly().newInstance(msg); | |
result.initCause(ex.getCause()); | |
} | |
// If only nullary or throwableOnly - can't return a revised message, so fall-through ... | |
} catch (final ExceptionInInitializerError | |
| IllegalArgumentException | |
| IllegalAccessException | |
| InstantiationException | |
| InvocationTargetException iex) { | |
suppressedHere = iex; | |
} | |
// If no appropriate constructor _with_ message (as a Throwable has no way to add a detail message after | |
// construction) or can't create new instance for some reason, then simply wrap in a RuntimeException | |
if (result == null) result = new RuntimeException(msg, ex.getCause()); | |
// Fill in suppressed exceptions and stacktrace from original exception | |
for (@NonNull final var suppressed : ex.getSuppressed()) result.addSuppressed(suppressed); | |
if (suppressedHere != null) result.addSuppressed(suppressedHere); | |
result.setStackTrace(ex.getStackTrace()); | |
return result; | |
} | |
public record ThrowableConstructorsOf( | |
Throwable ex, | |
Constructor<Throwable> nullary, | |
Constructor<Throwable> msgOnly, | |
Constructor<Throwable> throwableOnly, | |
Constructor<Throwable> msgAndThrowable) {} | |
@SuppressWarnings("unchecked") | |
@NonNull | |
static ThrowableConstructorsOf getConstructorsOfThrowable(@NonNull Throwable ex) { | |
try { | |
final var allPublicConstructors = ex.getClass().getDeclaredConstructors(); | |
Constructor<Throwable> nullary = null; | |
Constructor<Throwable> msgOnly = null; | |
Constructor<Throwable> throwableOnly = null; | |
Constructor<Throwable> msgAndThrowable = null; | |
for (final var cUnrefined : allPublicConstructors) { | |
final var c = (Constructor<Throwable>) cUnrefined; | |
// Minimal argument type checking done here - just enough to distinguish cases - we know what Throwable | |
// should look like. But some exception classes might have a 1- or 2-arg constructor with unexpected | |
// parameters and it'll fail on those. | |
switch (c.getParameterCount()) { | |
case 0 -> nullary = c; | |
case 1 -> { | |
if (String.class.equals(c.getParameterTypes()[0])) msgOnly = c; | |
else throwableOnly = c; | |
} | |
case 2 -> msgAndThrowable = c; | |
default -> {} | |
} | |
} | |
return new ThrowableConstructorsOf(ex, nullary, msgOnly, throwableOnly, msgAndThrowable); | |
} catch (final SecurityException sex) { | |
System.err.printf( | |
"*** Cannot get constructors of %s: %s%n", ex.getClass().getName(), sex); | |
return new ThrowableConstructorsOf(ex, null, null, null, null); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Needs assertj, and `@ExtendsWith(SoftAssertionsExtension.class)` | |
@Test | |
void getConstructorsOfExceptionTest() { | |
{ // Exception with message only, no throwable | |
final var sut1 = new TimeoutException(); | |
final var actual1 = Utils.getConstructorsOfThrowable(sut1); | |
assertThat(actual1).isNotNull(); | |
softly.assertThat(actual1.ex()).isEqualTo(sut1); | |
softly.assertThat(actual1.nullary()).isNotNull(); | |
softly.assertThat(actual1.msgOnly()).isNotNull(); | |
softly.assertThat(actual1.throwableOnly()).isNull(); | |
softly.assertThat(actual1.msgAndThrowable()).isNull(); | |
} | |
{ | |
// Exception with no 0-arg constructor and no message only constructor | |
final var sut1 = new UncheckedIOException(new IOException()); | |
final var actual1 = Utils.getConstructorsOfThrowable(sut1); | |
assertThat(actual1).isNotNull(); | |
softly.assertThat(actual1.ex()).isEqualTo(sut1); | |
softly.assertThat(actual1.nullary()).isNull(); | |
softly.assertThat(actual1.msgOnly()).isNull(); | |
softly.assertThat(actual1.throwableOnly()).isNotNull(); | |
softly.assertThat(actual1.msgAndThrowable()).isNotNull(); | |
} | |
{ | |
// Exception with both message and throwable and others as well | |
final var sut1 = new URIReferenceException(); | |
final var actual1 = Utils.getConstructorsOfThrowable(sut1); | |
assertThat(actual1).isNotNull(); | |
softly.assertThat(actual1.ex()).isEqualTo(sut1); | |
softly.assertThat(actual1.nullary()).isNotNull(); | |
softly.assertThat(actual1.msgOnly()).isNotNull(); | |
softly.assertThat(actual1.throwableOnly()).isNotNull(); | |
softly.assertThat(actual1.msgAndThrowable()).isNotNull(); | |
} | |
} | |
private void n0(@NonNull final Runnable doit) { | |
doit.run(); | |
} | |
private void n1(@NonNull final Runnable doit) { | |
n0(doit); | |
} | |
private void n2(@NonNull final Runnable doit) { | |
n1(doit); | |
} | |
@Test | |
void replaceMessageOfTest() { | |
final var sutCause = new CancellationException("bar"); | |
final var sutThrowable = new IllegalStateException("foo", sutCause); | |
Throwable sut = null; | |
try { | |
n2(() -> { | |
throw sutThrowable; | |
}); | |
} catch (final Throwable t) { | |
sut = t; | |
} | |
assertThat(sut).isNotNull(); | |
sut.addSuppressed(new IllegalStateException()); | |
sut.addSuppressed(new IllegalArgumentException()); | |
final var actual = Utils.replaceMessageOf(sut, "zebra"); | |
assertThat(actual).isNotNull(); | |
softly.assertThat(actual).hasMessage("zebra").hasCause(sutCause); | |
softly.assertThat(actual.getSuppressed()).containsExactly(sut.getSuppressed()); | |
softly.assertThat(actual.getStackTrace()).containsExactly(sut.getStackTrace()); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
I tried to do it via reflection to stomp on the
detailMessage
field of theThrowable
but Java 17 has it locked down so hard that I couldn't even get--add-opens
to make it work.