Skip to content

Commit f9744e9

Browse files
Merge pull request #1632 from benjchristensen/composite-exception
Composite Exception - Circular Reference Handling
2 parents 1693324 + d32ad88 commit f9744e9

File tree

2 files changed

+269
-104
lines changed

2 files changed

+269
-104
lines changed

rxjava-core/src/main/java/rx/exceptions/CompositeException.java

+171-71
Original file line numberDiff line numberDiff line change
@@ -15,54 +15,61 @@
1515
*/
1616
package rx.exceptions;
1717

18+
import java.io.PrintStream;
19+
import java.io.PrintWriter;
1820
import java.util.ArrayList;
1921
import java.util.Collection;
2022
import java.util.Collections;
2123
import java.util.HashSet;
24+
import java.util.LinkedHashSet;
2225
import java.util.List;
2326
import java.util.Set;
2427

2528
/**
2629
* Exception that is a composite of 1 or more other exceptions.
27-
* <p>
28-
* Use <code>getMessage()</code> to retrieve a concatenation of the composite exceptions.
30+
* A CompositeException does not modify the structure of any exception it wraps, but at print-time
31+
* iterates through the list of contained Throwables to print them all.
32+
*
33+
* Its invariant is to contains an immutable, ordered (by insertion order), unique list of non-composite exceptions.
34+
* This list may be queried by {@code #getExceptions()}
35+
*
36+
* The `printStackTrace()` implementation does custom handling of the StackTrace instead of using `getCause()` so it
37+
* can avoid circular references.
38+
*
39+
* If `getCause()` is invoked, it will lazily create the causal chain but stop if it finds any Throwable in the chain
40+
* that it has already seen.
2941
*/
3042
public final class CompositeException extends RuntimeException {
3143

3244
private static final long serialVersionUID = 3026362227162912146L;
3345

3446
private final List<Throwable> exceptions;
3547
private final String message;
36-
private final Throwable cause;
3748

38-
public CompositeException(String messagePrefix, Collection<Throwable> errors) {
49+
public CompositeException(String messagePrefix, Collection<? extends Throwable> errors) {
50+
Set<Throwable> deDupedExceptions = new LinkedHashSet<Throwable>();
3951
List<Throwable> _exceptions = new ArrayList<Throwable>();
40-
CompositeExceptionCausalChain _cause = new CompositeExceptionCausalChain();
41-
int count = errors.size();
42-
errors = removeDuplicatedCauses(errors);
43-
for (Throwable e : errors) {
44-
attachCallingThreadStack(_cause, e);
45-
_exceptions.add(e);
52+
for (Throwable ex : errors) {
53+
if (ex instanceof CompositeException) {
54+
deDupedExceptions.addAll(((CompositeException) ex).getExceptions());
55+
} else {
56+
deDupedExceptions.add(ex);
57+
}
4658
}
59+
60+
_exceptions.addAll(deDupedExceptions);
4761
this.exceptions = Collections.unmodifiableList(_exceptions);
48-
49-
String msg = count + " exceptions occurred. See them in causal chain below.";
50-
if(messagePrefix != null) {
51-
msg = messagePrefix + " " + msg;
52-
}
53-
this.message = msg;
54-
this.cause = _cause;
62+
this.message = exceptions.size() + " exceptions occurred. ";
5563
}
5664

57-
public CompositeException(Collection<Throwable> errors) {
65+
public CompositeException(Collection<? extends Throwable> errors) {
5866
this(null, errors);
5967
}
6068

6169
/**
6270
* Retrieves the list of exceptions that make up the {@code CompositeException}
6371
*
64-
* @return the exceptions that make up the {@code CompositeException}, as a {@link List} of
65-
* {@link Throwable}s
72+
* @return the exceptions that make up the {@code CompositeException}, as a {@link List} of {@link Throwable}s
6673
*/
6774
public List<Throwable> getExceptions() {
6875
return exceptions;
@@ -73,82 +80,175 @@ public String getMessage() {
7380
return message;
7481
}
7582

83+
private Throwable cause = null;
84+
7685
@Override
7786
public synchronized Throwable getCause() {
78-
return cause;
79-
}
80-
81-
private Collection<Throwable> removeDuplicatedCauses(Collection<Throwable> errors) {
82-
Set<Throwable> duplicated = new HashSet<Throwable>();
83-
for (Throwable cause : errors) {
84-
for (Throwable error : errors) {
85-
if(cause == error || duplicated.contains(error)) {
87+
if (cause == null) {
88+
// we lazily generate this causal chain if this is called
89+
CompositeExceptionCausalChain _cause = new CompositeExceptionCausalChain();
90+
Set<Throwable> seenCauses = new HashSet<Throwable>();
91+
92+
Throwable chain = _cause;
93+
for (Throwable e : exceptions) {
94+
if (seenCauses.contains(e)) {
95+
// already seen this outer Throwable so skip
8696
continue;
8797
}
88-
while (error.getCause() != null) {
89-
error = error.getCause();
90-
if (error == cause) {
91-
duplicated.add(cause);
92-
break;
98+
seenCauses.add(e);
99+
100+
List<Throwable> listOfCauses = getListOfCauses(e);
101+
// check if any of them have been seen before
102+
for(Throwable child : listOfCauses) {
103+
if (seenCauses.contains(child)) {
104+
// already seen this outer Throwable so skip
105+
e = new RuntimeException("Duplicate found in causal chain so cropping to prevent loop ...");
106+
continue;
93107
}
108+
seenCauses.add(child);
94109
}
110+
111+
// we now have 'e' as the last in the chain
112+
try {
113+
chain.initCause(e);
114+
} catch (Throwable t) {
115+
// ignore
116+
// the javadocs say that some Throwables (depending on how they're made) will never
117+
// let me call initCause without blowing up even if it returns null
118+
}
119+
chain = chain.getCause();
95120
}
121+
cause = _cause;
122+
}
123+
return cause;
124+
}
125+
126+
/**
127+
* All of the following printStackTrace functionality is derived from JDK Throwable printStackTrace.
128+
* In particular, the PrintStreamOrWriter abstraction is copied wholesale.
129+
*
130+
* Changes from the official JDK implementation:
131+
* * No infinite loop detection
132+
* * Smaller critical section holding printStream lock
133+
* * Explicit knowledge about exceptions List that this loops through
134+
*/
135+
@Override
136+
public void printStackTrace() {
137+
printStackTrace(System.err);
138+
}
139+
140+
@Override
141+
public void printStackTrace(PrintStream s) {
142+
printStackTrace(new WrappedPrintStream(s));
143+
}
144+
145+
@Override
146+
public void printStackTrace(PrintWriter s) {
147+
printStackTrace(new WrappedPrintWriter(s));
148+
}
149+
150+
/**
151+
* Special handling for printing out a CompositeException
152+
* Loop through all inner exceptions and print them out
153+
*
154+
* @param s
155+
* stream to print to
156+
*/
157+
private void printStackTrace(PrintStreamOrWriter s) {
158+
StringBuilder bldr = new StringBuilder();
159+
bldr.append(this).append("\n");
160+
for (StackTraceElement myStackElement : getStackTrace()) {
161+
bldr.append("\tat ").append(myStackElement).append("\n");
96162
}
97-
if (!duplicated.isEmpty()) {
98-
errors = new ArrayList<Throwable>(errors);
99-
errors.removeAll(duplicated);
163+
int i = 1;
164+
for (Throwable ex : exceptions) {
165+
bldr.append(" ComposedException ").append(i).append(" :").append("\n");
166+
appendStackTrace(bldr, ex, "\t");
167+
i++;
168+
}
169+
synchronized (s.lock()) {
170+
s.println(bldr.toString());
100171
}
101-
return errors;
102172
}
103173

104-
@SuppressWarnings("unused")
105-
// useful when debugging but don't want to make part of publicly supported API
106-
private static String getStackTraceAsString(StackTraceElement[] stack) {
107-
StringBuilder s = new StringBuilder();
108-
boolean firstLine = true;
109-
for (StackTraceElement e : stack) {
110-
if (e.toString().startsWith("java.lang.Thread.getStackTrace")) {
111-
// we'll ignore this one
112-
continue;
113-
}
114-
if (!firstLine) {
115-
s.append("\n\t");
116-
}
117-
s.append(e.toString());
118-
firstLine = false;
174+
private void appendStackTrace(StringBuilder bldr, Throwable ex, String prefix) {
175+
bldr.append(prefix).append(ex).append("\n");
176+
for (StackTraceElement stackElement : ex.getStackTrace()) {
177+
bldr.append("\t\tat ").append(stackElement).append("\n");
178+
}
179+
if (ex.getCause() != null) {
180+
bldr.append("\tCaused by: ");
181+
appendStackTrace(bldr, ex.getCause(), "");
119182
}
120-
return s.toString();
121183
}
122184

123-
/* package-private */ static void attachCallingThreadStack(Throwable e, Throwable cause) {
124-
Set<Throwable> seenCauses = new HashSet<Throwable>();
185+
private abstract static class PrintStreamOrWriter {
186+
/** Returns the object to be locked when using this StreamOrWriter */
187+
abstract Object lock();
125188

126-
while (e.getCause() != null) {
127-
e = e.getCause();
128-
if (seenCauses.contains(e.getCause())) {
129-
break;
130-
} else {
131-
seenCauses.add(e.getCause());
132-
}
189+
/** Prints the specified string as a line on this StreamOrWriter */
190+
abstract void println(Object o);
191+
}
192+
193+
/**
194+
* Same abstraction and implementation as in JDK to allow PrintStream and PrintWriter to share implementation
195+
*/
196+
private static class WrappedPrintStream extends PrintStreamOrWriter {
197+
private final PrintStream printStream;
198+
199+
WrappedPrintStream(PrintStream printStream) {
200+
this.printStream = printStream;
201+
}
202+
203+
Object lock() {
204+
return printStream;
205+
}
206+
207+
void println(Object o) {
208+
printStream.println(o);
209+
}
210+
}
211+
212+
private static class WrappedPrintWriter extends PrintStreamOrWriter {
213+
private final PrintWriter printWriter;
214+
215+
WrappedPrintWriter(PrintWriter printWriter) {
216+
this.printWriter = printWriter;
133217
}
134-
// we now have 'e' as the last in the chain
135-
try {
136-
e.initCause(cause);
137-
} catch (Throwable t) {
138-
// ignore
139-
// the javadocs say that some Throwables (depending on how they're made) will never
140-
// let me call initCause without blowing up even if it returns null
218+
219+
Object lock() {
220+
return printWriter;
221+
}
222+
223+
void println(Object o) {
224+
printWriter.println(o);
141225
}
142226
}
143227

144-
/* package-private */ final static class CompositeExceptionCausalChain extends RuntimeException {
228+
/* package-private */final static class CompositeExceptionCausalChain extends RuntimeException {
145229
private static final long serialVersionUID = 3875212506787802066L;
146-
/* package-private */ static String MESSAGE = "Chain of Causes for CompositeException In Order Received =>";
230+
/* package-private */static String MESSAGE = "Chain of Causes for CompositeException In Order Received =>";
147231

148232
@Override
149233
public String getMessage() {
150234
return MESSAGE;
151235
}
152236
}
153237

238+
private final List<Throwable> getListOfCauses(Throwable ex) {
239+
List<Throwable> list = new ArrayList<Throwable>();
240+
Throwable root = ex.getCause();
241+
if (root == null) {
242+
return list;
243+
} else {
244+
while(true) {
245+
list.add(root);
246+
if (root.getCause() == null) {
247+
return list;
248+
} else {
249+
root = root.getCause();
250+
}
251+
}
252+
}
253+
}
154254
}

0 commit comments

Comments
 (0)