Skip to content

Commit 99d8034

Browse files
committed
Handle Throwable cause cycles when building the ErrorItem
1 parent f10293d commit 99d8034

2 files changed

Lines changed: 144 additions & 10 deletions

File tree

src/main/java/com/stackify/api/common/lang/Throwables.java

Lines changed: 58 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,69 @@
2828
*/
2929
public class Throwables {
3030

31+
/**
32+
* Returns the Throwable's cause chain as a list. The first entry is the Throwable followed by the cause chain.
33+
* @param throwable The Throwable
34+
* @return The Throwable and its cause chain
35+
*/
36+
public static List<Throwable> getCausalChain(final Throwable throwable) {
37+
if (throwable == null) {
38+
throw new NullPointerException("Throwable is null");
39+
}
40+
41+
List<Throwable> causes = new ArrayList<Throwable>();
42+
causes.add(throwable);
43+
44+
Throwable cause = throwable.getCause();
45+
46+
while ((cause != null) && (!causes.contains(cause))) {
47+
causes.add(cause);
48+
cause = cause.getCause();
49+
}
50+
51+
return causes;
52+
}
53+
3154
/**
3255
* Converts a Throwable to an ErrorItem
3356
* @param t The Throwable to be converted
3457
* @return The ErrorItem
3558
*/
3659
public static ErrorItem toErrorItem(final Throwable t) {
60+
61+
// get a flat list of the throwable and the causal chain
62+
63+
List<Throwable> throwables = Throwables.getCausalChain(t);
64+
65+
// create and populate builders for all throwables
66+
67+
List<ErrorItem.Builder> builders = new ArrayList<ErrorItem.Builder>(throwables.size());
68+
69+
for (Throwable throwable : throwables) {
70+
ErrorItem.Builder builder = toErrorItemBuilderWithoutCause(throwable);
71+
builders.add(builder);
72+
}
73+
74+
// attach child errors to their parent in reverse order
75+
76+
for (int i = builders.size() - 1; 0 < i; --i) {
77+
ErrorItem.Builder parent = builders.get(i - 1);
78+
ErrorItem.Builder child = builders.get(i);
79+
80+
parent.innerError(child.build());
81+
}
82+
83+
// return the assembled original error
84+
85+
return builders.get(0).build();
86+
}
87+
88+
/**
89+
* Converts a Throwable to an ErrorItem.Builder and ignores the cause
90+
* @param t The Throwable to be converted
91+
* @return The ErrorItem.Builder without the innerError populated
92+
*/
93+
private static ErrorItem.Builder toErrorItemBuilderWithoutCause(final Throwable t) {
3794
ErrorItem.Builder builder = ErrorItem.newBuilder();
3895
builder.message(t.getMessage());
3996
builder.errorType(t.getClass().getCanonicalName());
@@ -53,16 +110,8 @@ public static ErrorItem toErrorItem(final Throwable t) {
53110
}
54111

55112
builder.stackTrace(stackFrames);
56-
57-
Throwable cause = t.getCause();
58113

59-
if (cause != null) {
60-
if (!cause.equals(t)) {
61-
builder.innerError(Throwables.toErrorItem(cause));
62-
}
63-
}
64-
65-
return builder.build();
114+
return builder;
66115
}
67116

68117
/**

src/test/java/com/stackify/api/common/lang/ThrowablesTest.java

Lines changed: 86 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@
2323

2424
import com.stackify.api.ErrorItem;
2525
import com.stackify.api.TraceFrame;
26-
import com.stackify.api.common.lang.Throwables;
2726

2827
/**
2928
* Throwables JUnit Test
@@ -32,6 +31,60 @@
3231
*/
3332
public class ThrowablesTest {
3433

34+
/**
35+
* testGetCausalChainWithNull
36+
*/
37+
@Test(expected = NullPointerException.class)
38+
public void testGetCausalChainWithNull() {
39+
Throwables.getCausalChain(null);
40+
}
41+
42+
/**
43+
* testGetCausalChainWithoutCause
44+
*/
45+
@Test
46+
public void testGetCausalChainWithoutCause() {
47+
Throwable t = new NullPointerException();
48+
49+
List<Throwable> throwables = Throwables.getCausalChain(t);
50+
Assert.assertNotNull(throwables);
51+
Assert.assertEquals(1, throwables.size());
52+
Assert.assertEquals(t, throwables.get(0));
53+
}
54+
55+
/**
56+
* testGetCausalChainWithCause
57+
*/
58+
@Test
59+
public void testGetCausalChainWithCause() {
60+
Throwable c = new NullPointerException();
61+
Throwable t = new RuntimeException(c);
62+
63+
List<Throwable> throwables = Throwables.getCausalChain(t);
64+
Assert.assertNotNull(throwables);
65+
Assert.assertEquals(2, throwables.size());
66+
Assert.assertEquals(t, throwables.get(0));
67+
Assert.assertEquals(c, throwables.get(1));
68+
}
69+
70+
/**
71+
* testGetCausalChainWithSelfCausation
72+
*/
73+
@Test
74+
public void testGetCausalChainWithSelfCausation() {
75+
Throwable c2 = new RuntimeException();
76+
Throwable c1 = new RuntimeException(c2);
77+
c2.initCause(c1);
78+
Throwable t = new RuntimeException(c1);
79+
80+
List<Throwable> throwables = Throwables.getCausalChain(t);
81+
Assert.assertNotNull(throwables);
82+
Assert.assertEquals(3, throwables.size());
83+
Assert.assertEquals(t, throwables.get(0));
84+
Assert.assertEquals(c1, throwables.get(1));
85+
Assert.assertEquals(c2, throwables.get(2));
86+
}
87+
3588
/**
3689
* testToErrorDetail
3790
*/
@@ -127,4 +180,36 @@ public void testToErrorDetailWithRecursiveCause() {
127180
Assert.assertTrue(stackTrace.isEmpty());
128181
Assert.assertNull(errorDetail.getInnerError());
129182
}
183+
184+
/**
185+
* testToErrorDetailWithSubcause
186+
*/
187+
@Test
188+
public void testToErrorDetailWithSubcause() {
189+
String message = "message";
190+
String causeMessage = "causeMessage";
191+
String subcauseMessage = "subcauseMessage";
192+
Throwable subcause = new NullPointerException(subcauseMessage);
193+
Throwable cause = new UnsupportedOperationException(causeMessage, subcause);
194+
Throwable throwable = new RuntimeException(message, cause);
195+
196+
ErrorItem errorDetail = Throwables.toErrorItem(throwable);
197+
198+
Assert.assertNotNull(errorDetail);
199+
200+
Assert.assertEquals(message, errorDetail.getMessage());
201+
Assert.assertEquals("java.lang.RuntimeException", errorDetail.getErrorType());
202+
203+
List<TraceFrame> stackTrace = errorDetail.getStackTrace();
204+
Assert.assertNotNull(stackTrace);
205+
Assert.assertTrue(!stackTrace.isEmpty());
206+
207+
Assert.assertEquals(causeMessage, errorDetail.getInnerError().getMessage());
208+
Assert.assertEquals("java.lang.UnsupportedOperationException", errorDetail.getInnerError().getErrorType());
209+
210+
Assert.assertEquals(subcauseMessage, errorDetail.getInnerError().getInnerError().getMessage());
211+
Assert.assertEquals("java.lang.NullPointerException", errorDetail.getInnerError().getInnerError().getErrorType());
212+
213+
Assert.assertNull(errorDetail.getInnerError().getInnerError().getInnerError());
214+
}
130215
}

0 commit comments

Comments
 (0)