Skip to content

Commit 0661d00

Browse files
committed
Merge pull request #1431 from tomrozb/composite-exception
CompositeException fix for Android
2 parents 10e5b12 + eea2afa commit 0661d00

File tree

2 files changed

+106
-142
lines changed

2 files changed

+106
-142
lines changed

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

Lines changed: 71 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -15,44 +15,38 @@
1515
*/
1616
package rx.exceptions;
1717

18-
import java.io.PrintStream;
19-
import java.io.PrintWriter;
2018
import java.util.ArrayList;
2119
import java.util.Collection;
2220
import java.util.Collections;
23-
import java.util.LinkedHashSet;
21+
import java.util.HashSet;
2422
import java.util.List;
2523
import java.util.Set;
2624

2725
/**
2826
* Exception that is a composite of 1 or more other exceptions.
29-
* A CompositeException does not modify the structure of any exception it wraps, but at print-time
30-
* iterates through the list of contained Throwables to print them all.
31-
*
32-
* Its invariant is to contains an immutable, ordered (by insertion order), unique list of non-composite exceptions.
33-
* This list may be queried by {@code #getExceptions()}
27+
* <p>
28+
* Use <code>getMessage()</code> to retrieve a concatenation of the composite exceptions.
3429
*/
3530
public final class CompositeException extends RuntimeException {
3631

3732
private static final long serialVersionUID = 3026362227162912146L;
3833

3934
private final List<Throwable> exceptions;
4035
private final String message;
36+
private final Throwable cause;
4137

4238
public CompositeException(String messagePrefix, Collection<Throwable> errors) {
43-
Set<Throwable> deDupedExceptions = new LinkedHashSet<Throwable>();
4439
List<Throwable> _exceptions = new ArrayList<Throwable>();
45-
for (Throwable ex: errors) {
46-
if (ex instanceof CompositeException) {
47-
deDupedExceptions.addAll(((CompositeException) ex).getExceptions());
48-
} else {
49-
deDupedExceptions.add(ex);
50-
}
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);
5146
}
52-
53-
_exceptions.addAll(deDupedExceptions);
5447
this.exceptions = Collections.unmodifiableList(_exceptions);
55-
this.message = exceptions.size() + " exceptions occurred. See them in causal chain below.";
48+
this.message = count + " exceptions occurred. See them in causal chain below.";
49+
this.cause = _cause;
5650
}
5751

5852
public CompositeException(Collection<Throwable> errors) {
@@ -76,106 +70,80 @@ public String getMessage() {
7670

7771
@Override
7872
public synchronized Throwable getCause() {
79-
return null;
80-
}
81-
82-
/**
83-
* All of the following printStackTrace functionality is derived from JDK Throwable printStackTrace.
84-
* In particular, the PrintStreamOrWriter abstraction is copied wholesale.
85-
*
86-
* Changes from the official JDK implementation:
87-
* * No infinite loop detection
88-
* * Smaller critical section holding printStream lock
89-
* * Explicit knowledge about exceptions List that this loops through
90-
*/
91-
@Override
92-
public void printStackTrace() {
93-
printStackTrace(System.err);
73+
return cause;
9474
}
9575

96-
@Override
97-
public void printStackTrace(PrintStream s) {
98-
printStackTrace(new WrappedPrintStream(s));
99-
}
100-
101-
@Override
102-
public void printStackTrace(PrintWriter s) {
103-
printStackTrace(new WrappedPrintWriter(s));
104-
}
105-
106-
/**
107-
* Special handling for printing out a CompositeException
108-
* Loop through all inner exceptions and print them out
109-
* @param s stream to print to
110-
*/
111-
private void printStackTrace(PrintStreamOrWriter s) {
112-
StringBuilder bldr = new StringBuilder();
113-
bldr.append(this).append("\n");
114-
for (StackTraceElement myStackElement: getStackTrace()) {
115-
bldr.append("\tat ").append(myStackElement).append("\n");
116-
}
117-
int i = 1;
118-
for (Throwable ex: exceptions) {
119-
bldr.append(" ComposedException ").append(i).append(" :").append("\n");
120-
appendStackTrace(bldr, ex, "\t");
121-
i++;
76+
private Collection<Throwable> removeDuplicatedCauses(Collection<Throwable> errors) {
77+
Set<Throwable> duplicated = new HashSet<Throwable>();
78+
for (Throwable cause : errors) {
79+
for (Throwable error : errors) {
80+
if(cause == error || duplicated.contains(error)) {
81+
continue;
82+
}
83+
while (error.getCause() != null) {
84+
error = error.getCause();
85+
if (error == cause) {
86+
duplicated.add(cause);
87+
break;
88+
}
89+
}
90+
}
12291
}
123-
synchronized (s.lock()) {
124-
s.println(bldr.toString());
92+
if (!duplicated.isEmpty()) {
93+
errors = new ArrayList<Throwable>(errors);
94+
errors.removeAll(duplicated);
12595
}
96+
return errors;
12697
}
12798

128-
private void appendStackTrace(StringBuilder bldr, Throwable ex, String prefix) {
129-
bldr.append(prefix).append(ex).append("\n");
130-
for (StackTraceElement stackElement: ex.getStackTrace()) {
131-
bldr.append("\t\tat ").append(stackElement).append("\n");
132-
}
133-
if (ex.getCause() != null) {
134-
bldr.append("\tCaused by: ");
135-
appendStackTrace(bldr, ex.getCause(), "");
99+
@SuppressWarnings("unused")
100+
// useful when debugging but don't want to make part of publicly supported API
101+
private static String getStackTraceAsString(StackTraceElement[] stack) {
102+
StringBuilder s = new StringBuilder();
103+
boolean firstLine = true;
104+
for (StackTraceElement e : stack) {
105+
if (e.toString().startsWith("java.lang.Thread.getStackTrace")) {
106+
// we'll ignore this one
107+
continue;
108+
}
109+
if (!firstLine) {
110+
s.append("\n\t");
111+
}
112+
s.append(e.toString());
113+
firstLine = false;
136114
}
115+
return s.toString();
137116
}
138117

139-
private abstract static class PrintStreamOrWriter {
140-
/** Returns the object to be locked when using this StreamOrWriter */
141-
abstract Object lock();
142-
143-
/** Prints the specified string as a line on this StreamOrWriter */
144-
abstract void println(Object o);
145-
}
146-
147-
/**
148-
* Same abstraction and implementation as in JDK to allow PrintStream and PrintWriter to share implementation
149-
*/
150-
private static class WrappedPrintStream extends PrintStreamOrWriter {
151-
private final PrintStream printStream;
152-
153-
WrappedPrintStream(PrintStream printStream) {
154-
this.printStream = printStream;
155-
}
118+
/* package-private */ static void attachCallingThreadStack(Throwable e, Throwable cause) {
119+
Set<Throwable> seenCauses = new HashSet<Throwable>();
156120

157-
Object lock() {
158-
return printStream;
121+
while (e.getCause() != null) {
122+
e = e.getCause();
123+
if (seenCauses.contains(e.getCause())) {
124+
break;
125+
} else {
126+
seenCauses.add(e.getCause());
127+
}
159128
}
160-
161-
void println(Object o) {
162-
printStream.println(o);
129+
// we now have 'e' as the last in the chain
130+
try {
131+
e.initCause(cause);
132+
} catch (Throwable t) {
133+
// ignore
134+
// the javadocs say that some Throwables (depending on how they're made) will never
135+
// let me call initCause without blowing up even if it returns null
163136
}
164137
}
165138

166-
private static class WrappedPrintWriter extends PrintStreamOrWriter {
167-
private final PrintWriter printWriter;
139+
/* package-private */ final static class CompositeExceptionCausalChain extends RuntimeException {
140+
private static final long serialVersionUID = 3875212506787802066L;
141+
/* package-private */ static String MESSAGE = "Chain of Causes for CompositeException In Order Received =>";
168142

169-
WrappedPrintWriter(PrintWriter printWriter) {
170-
this.printWriter = printWriter;
171-
}
172-
173-
Object lock() {
174-
return printWriter;
175-
}
176-
177-
void println(Object o) {
178-
printWriter.println(o);
143+
@Override
144+
public String getMessage() {
145+
return MESSAGE;
179146
}
180147
}
148+
181149
}

rxjava-core/src/test/java/rx/exceptions/CompositeExceptionTest.java

Lines changed: 35 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,6 @@
1717

1818
import static org.junit.Assert.assertEquals;
1919

20-
import java.io.ByteArrayOutputStream;
21-
import java.io.PrintStream;
22-
import java.io.StringWriter;
2320
import java.util.ArrayList;
2421
import java.util.Arrays;
2522
import java.util.List;
@@ -33,6 +30,7 @@ public class CompositeExceptionTest {
3330
private final Throwable ex3 = new Throwable("Ex3", ex2);
3431

3532
public CompositeExceptionTest() {
33+
ex1.initCause(ex2);
3634
}
3735

3836
private CompositeException getNewCompositeExceptionWithEx123() {
@@ -52,58 +50,56 @@ public void testMultipleWithSameCause() {
5250
CompositeException ce = new CompositeException("3 failures with same root cause", Arrays.asList(e1, e2, e3));
5351

5452
assertEquals(3, ce.getExceptions().size());
55-
assertNoCircularReferences(ce);
53+
5654
}
5755

5856
@Test(timeout = 1000)
59-
public void testCompositeExceptionFromParentThenChild() {
60-
CompositeException cex = new CompositeException(Arrays.asList(ex1, ex2));
61-
assertNoCircularReferences(cex);
62-
assertEquals(2, cex.getExceptions().size());
57+
public void testOneIsCauseOfAnother() {
58+
Throwable rootCause = new Throwable("RootCause");
59+
Throwable throwable = new Throwable("1", rootCause);
60+
CompositeException ce = new CompositeException("One is the cause of another",
61+
Arrays.asList(rootCause, throwable));
62+
63+
assertEquals(1, ce.getExceptions().size());
6364
}
6465

6566
@Test(timeout = 1000)
66-
public void testCompositeExceptionFromChildThenParent() {
67-
CompositeException cex = new CompositeException(Arrays.asList(ex2, ex1));
68-
assertNoCircularReferences(cex);
69-
assertEquals(2, cex.getExceptions().size());
67+
public void testAttachCallingThreadStackParentThenChild() {
68+
CompositeException.attachCallingThreadStack(ex1, ex2);
69+
assertEquals("Ex2", ex1.getCause().getMessage());
7070
}
7171

7272
@Test(timeout = 1000)
73-
public void testCompositeExceptionFromChildAndComposite() {
74-
CompositeException cex = new CompositeException(Arrays.asList(ex1, getNewCompositeExceptionWithEx123()));
75-
assertNoCircularReferences(cex);
76-
assertEquals(3, cex.getExceptions().size());
73+
public void testAttachCallingThreadStackChildThenParent() {
74+
CompositeException.attachCallingThreadStack(ex2, ex1);
75+
assertEquals("Ex1", ex2.getCause().getMessage());
7776
}
7877

7978
@Test(timeout = 1000)
80-
public void testCompositeExceptionFromCompositeAndChild() {
81-
CompositeException cex = new CompositeException(Arrays.asList(getNewCompositeExceptionWithEx123(), ex1));
82-
assertNoCircularReferences(cex);
83-
assertEquals(3, cex.getExceptions().size());
79+
public void testAttachCallingThreadStackAddComposite() {
80+
CompositeException.attachCallingThreadStack(ex1, getNewCompositeExceptionWithEx123());
81+
assertEquals("Ex2", ex1.getCause().getMessage());
8482
}
8583

8684
@Test(timeout = 1000)
87-
public void testCompositeExceptionFromTwoDuplicateComposites() {
88-
List<Throwable> exs = new ArrayList<Throwable>();
89-
exs.add(getNewCompositeExceptionWithEx123());
90-
exs.add(getNewCompositeExceptionWithEx123());
91-
CompositeException cex = new CompositeException(exs);
92-
assertNoCircularReferences(cex);
93-
assertEquals(3, cex.getExceptions().size());
85+
public void testAttachCallingThreadStackAddToComposite() {
86+
CompositeException compositeEx = getNewCompositeExceptionWithEx123();
87+
CompositeException.attachCallingThreadStack(compositeEx, ex1);
88+
assertEquals(CompositeException.CompositeExceptionCausalChain.MESSAGE, compositeEx.getCause().getMessage());
9489
}
9590

96-
/**
97-
* This hijacks the Throwable.printStackTrace() output and puts it in a string, where we can look for
98-
* "CIRCULAR REFERENCE" (a String added by Throwable.printEnclosedStackTrace)
99-
*/
100-
private static void assertNoCircularReferences(Throwable ex) {
101-
ByteArrayOutputStream baos = new ByteArrayOutputStream();
102-
PrintStream printStream = new PrintStream(baos);
103-
StringWriter writer = new StringWriter();
104-
105-
ex.printStackTrace();
106-
//ex.printStackTrace(printStream);
107-
//assertFalse(baos.toString().contains("CIRCULAR REFERENCE"));
91+
@Test(timeout = 1000)
92+
public void testAttachCallingThreadStackAddCompositeToItself() {
93+
CompositeException compositeEx = getNewCompositeExceptionWithEx123();
94+
CompositeException.attachCallingThreadStack(compositeEx, compositeEx);
95+
assertEquals(CompositeException.CompositeExceptionCausalChain.MESSAGE, compositeEx.getCause().getMessage());
96+
}
97+
98+
@Test(timeout = 1000)
99+
public void testAttachCallingThreadStackAddExceptionsToEachOther() {
100+
CompositeException.attachCallingThreadStack(ex1, ex2);
101+
CompositeException.attachCallingThreadStack(ex2, ex1);
102+
assertEquals("Ex2", ex1.getCause().getMessage());
103+
assertEquals("Ex1", ex2.getCause().getMessage());
108104
}
109105
}

0 commit comments

Comments
 (0)