Skip to content

Instantly share code, notes, and snippets.

@david-bakin
Last active July 3, 2023 01:03
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save david-bakin/c50ae5f3c026856f0b11b624e79557fa to your computer and use it in GitHub Desktop.
Save david-bakin/c50ae5f3c026856f0b11b624e79557fa to your computer and use it in GitHub Desktop.
Rewriting the message of a Throwable
/** 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);
}
}
// 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());
}
@david-bakin
Copy link
Author

david-bakin commented Jul 3, 2023

I tried to do it via reflection to stomp on the detailMessage field of the Throwable but Java 17 has it locked down so hard that I couldn't even get --add-opens to make it work.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment