Skip to content

Commit 020e843

Browse files
Merge pull request ReactiveX#2632 from simonbasle/plugErrorRenderer
Implement hook to render specific types in OnNextValue
2 parents 23a240b + c4b92ef commit 020e843

File tree

3 files changed

+167
-3
lines changed

3 files changed

+167
-3
lines changed

src/main/java/rx/exceptions/OnErrorThrowable.java

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@
1515
*/
1616
package rx.exceptions;
1717

18+
import rx.plugins.RxJavaErrorHandler;
19+
import rx.plugins.RxJavaPlugins;
20+
1821
/**
1922
* Represents a {@code Throwable} that an {@code Observable} might notify its subscribers of, but that then can
2023
* be handled by an operator that is designed to recover from or react appropriately to such an error. You can
@@ -106,6 +109,7 @@ public static Throwable addValueAsLastCause(Throwable e, Object value) {
106109
public static class OnNextValue extends RuntimeException {
107110

108111
private static final long serialVersionUID = -3454462756050397899L;
112+
109113
private final Object value;
110114

111115
/**
@@ -131,11 +135,18 @@ public Object getValue() {
131135

132136
/**
133137
* Render the object if it is a basic type. This avoids the library making potentially expensive
134-
* or calls to toString() which may throw exceptions. See PR #1401 for details.
138+
* or calls to toString() which may throw exceptions.
139+
*
140+
* If a specific behavior has been defined in the {@link RxJavaErrorHandler} plugin, some types
141+
* may also have a specific rendering. Non-primitive types not managed by the plugin are rendered
142+
* as the classname of the object.
143+
* <p>
144+
* See PR #1401 and Issue #2468 for details.
135145
*
136146
* @param value
137147
* the item that the Observable was trying to emit at the time of the exception
138-
* @return a string version of the object if primitive, otherwise the classname of the object
148+
* @return a string version of the object if primitive or managed through error plugin,
149+
* otherwise the classname of the object
139150
*/
140151
private static String renderValue(Object value){
141152
if (value == null) {
@@ -150,6 +161,12 @@ private static String renderValue(Object value){
150161
if (value instanceof Enum) {
151162
return ((Enum<?>) value).name();
152163
}
164+
165+
String pluggedRendering = RxJavaPlugins.getInstance().getErrorHandler().handleOnNextValueRendering(value);
166+
if (pluggedRendering != null) {
167+
return pluggedRendering;
168+
}
169+
153170
return value.getClass().getName() + ".class";
154171
}
155172
}

src/main/java/rx/plugins/RxJavaErrorHandler.java

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

1818
import rx.Observable;
1919
import rx.Subscriber;
20+
import rx.annotations.Experimental;
21+
import rx.exceptions.Exceptions;
22+
import rx.exceptions.OnErrorThrowable;
2023

2124
/**
2225
* Abstract class for defining error handling logic in addition to the normal
@@ -25,6 +28,8 @@
2528
* For example, all {@code Exception}s can be logged using this handler even if
2629
* {@link Subscriber#onError(Throwable)} is ignored or not provided when an {@link Observable} is subscribed to.
2730
* <p>
31+
* This plugin is also responsible for augmenting rendering of {@link OnErrorThrowable.OnNextValue}.
32+
* <p>
2833
* See {@link RxJavaPlugins} or the RxJava GitHub Wiki for information on configuring plugins: <a
2934
* href="https://github.com/ReactiveX/RxJava/wiki/Plugins">https://github.com/ReactiveX/RxJava/wiki/Plugins</a>.
3035
*/
@@ -44,4 +49,52 @@ public void handleError(Throwable e) {
4449
// do nothing by default
4550
}
4651

52+
protected static final String ERROR_IN_RENDERING_SUFFIX = ".errorRendering";
53+
54+
/**
55+
* Receives items causing {@link OnErrorThrowable.OnNextValue} and gives a chance to choose the String
56+
* representation of the item in the OnNextValue stacktrace rendering. Returns null if this type of item
57+
* is not managed and should use default rendering.
58+
* <p>
59+
* Note that primitive types are always rendered as their toString() value.
60+
* <p>
61+
* If a {@code Throwable} is caught when rendering, this will fallback to the item's classname suffixed by
62+
* {@value #ERROR_IN_RENDERING_SUFFIX}.
63+
*
64+
* @param item the last emitted item, that caused the exception wrapped in {@link OnErrorThrowable.OnNextValue}.
65+
* @return a short {@link String} representation of the item if one is known for its type, or null for default.
66+
*/
67+
@Experimental
68+
public final String handleOnNextValueRendering(Object item) {
69+
70+
try {
71+
return render(item);
72+
} catch (InterruptedException e) {
73+
Thread.currentThread().interrupt();
74+
} catch (Throwable t) {
75+
Exceptions.throwIfFatal(t);
76+
}
77+
return item.getClass().getName() + ERROR_IN_RENDERING_SUFFIX;
78+
}
79+
80+
/**
81+
* Override this method to provide rendering for specific types other than primitive types and null.
82+
* <p>
83+
* For performance and overhead reasons, this should should limit to a safe production of a short {@code String}
84+
* (as large renderings will bloat up the stacktrace). Prefer to try/catch({@code Throwable}) all code
85+
* inside this method implementation.
86+
* <p>
87+
* If a {@code Throwable} is caught when rendering, this will fallback to the item's classname suffixed by
88+
* {@value #ERROR_IN_RENDERING_SUFFIX}.
89+
*
90+
* @param item the last emitted item, that caused the exception wrapped in {@link OnErrorThrowable.OnNextValue}.
91+
* @return a short {@link String} representation of the item if one is known for its type, or null for default.
92+
* @throws InterruptedException if the rendering thread is interrupted
93+
*/
94+
@Experimental
95+
protected String render (Object item) throws InterruptedException {
96+
//do nothing by default
97+
return null;
98+
}
99+
47100
}

src/test/java/rx/plugins/RxJavaPluginsTest.java

Lines changed: 95 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,25 @@
1616
package rx.plugins;
1717

1818
import static org.junit.Assert.assertEquals;
19+
import static org.junit.Assert.assertNotNull;
20+
import static org.junit.Assert.assertNull;
1921
import static org.junit.Assert.assertSame;
2022
import static org.junit.Assert.assertTrue;
2123
import static org.junit.Assert.fail;
2224

25+
import java.util.Calendar;
26+
import java.util.Collections;
27+
import java.util.Date;
28+
import java.util.concurrent.TimeUnit;
29+
2330
import org.junit.After;
2431
import org.junit.Before;
2532
import org.junit.Test;
2633

2734
import rx.Observable;
2835
import rx.Subscriber;
36+
import rx.exceptions.OnErrorThrowable;
37+
import rx.functions.Func1;
2938

3039
public class RxJavaPluginsTest {
3140

@@ -78,7 +87,18 @@ public void handleError(Throwable e) {
7887
this.e = e;
7988
count++;
8089
}
90+
}
8191

92+
public static class RxJavaErrorHandlerTestImplWithRender extends RxJavaErrorHandler {
93+
@Override
94+
protected String render(Object item) {
95+
if (item instanceof Calendar) {
96+
throw new IllegalArgumentException("calendar");
97+
} else if (item instanceof Date) {
98+
return String.valueOf(((Date) item).getTime());
99+
}
100+
return null;
101+
}
82102
}
83103

84104
@Test
@@ -149,12 +169,86 @@ public void testOnErrorWhenNotImplemented() {
149169
assertEquals(1, errorHandler.count);
150170
}
151171

172+
@Test
173+
public void testOnNextValueRenderingWhenNotImplemented() {
174+
RxJavaErrorHandlerTestImpl errorHandler = new RxJavaErrorHandlerTestImpl();
175+
RxJavaPlugins.getInstance().registerErrorHandler(errorHandler);
176+
177+
String rendering = RxJavaPlugins.getInstance().getErrorHandler().handleOnNextValueRendering(new Date());
178+
179+
assertNull(rendering);
180+
}
181+
182+
@Test
183+
public void testOnNextValueRenderingWhenImplementedAndNotManaged() {
184+
RxJavaErrorHandlerTestImplWithRender errorHandler = new RxJavaErrorHandlerTestImplWithRender();
185+
RxJavaPlugins.getInstance().registerErrorHandler(errorHandler);
186+
187+
String rendering = RxJavaPlugins.getInstance().getErrorHandler().handleOnNextValueRendering(
188+
Collections.emptyList());
189+
190+
assertNull(rendering);
191+
}
192+
193+
@Test
194+
public void testOnNextValueRenderingWhenImplementedAndManaged() {
195+
RxJavaErrorHandlerTestImplWithRender errorHandler = new RxJavaErrorHandlerTestImplWithRender();
196+
RxJavaPlugins.getInstance().registerErrorHandler(errorHandler);
197+
long time = 1234L;
198+
Date date = new Date(time);
199+
200+
String rendering = RxJavaPlugins.getInstance().getErrorHandler().handleOnNextValueRendering(date);
201+
202+
assertNotNull(rendering);
203+
assertEquals(String.valueOf(time), rendering);
204+
}
205+
206+
@Test
207+
public void testOnNextValueRenderingWhenImplementedAndThrows() {
208+
RxJavaErrorHandlerTestImplWithRender errorHandler = new RxJavaErrorHandlerTestImplWithRender();
209+
RxJavaPlugins.getInstance().registerErrorHandler(errorHandler);
210+
Calendar cal = Calendar.getInstance();
211+
212+
String rendering = RxJavaPlugins.getInstance().getErrorHandler().handleOnNextValueRendering(cal);
213+
214+
assertNotNull(rendering);
215+
assertEquals(cal.getClass().getName() + RxJavaErrorHandler.ERROR_IN_RENDERING_SUFFIX, rendering);
216+
}
217+
218+
@Test
219+
public void testOnNextValueCallsPlugin() {
220+
RxJavaErrorHandlerTestImplWithRender errorHandler = new RxJavaErrorHandlerTestImplWithRender();
221+
RxJavaPlugins.getInstance().registerErrorHandler(errorHandler);
222+
long time = 456L;
223+
Date date = new Date(time);
224+
225+
try {
226+
Date notExpected = Observable.just(date)
227+
.map(new Func1<Date, Date>() {
228+
@Override
229+
public Date call(Date date) {
230+
throw new IllegalStateException("Trigger OnNextValue");
231+
}
232+
})
233+
.timeout(500, TimeUnit.MILLISECONDS)
234+
.toBlocking().first();
235+
fail("Did not expect onNext/onCompleted, got " + notExpected);
236+
} catch (IllegalStateException e) {
237+
assertEquals("Trigger OnNextValue", e.getMessage());
238+
assertNotNull(e.getCause());
239+
assertTrue(e.getCause() instanceof OnErrorThrowable.OnNextValue);
240+
assertEquals("OnError while emitting onNext value: " + time, e.getCause().getMessage());
241+
}
242+
243+
}
244+
152245
// inside test so it is stripped from Javadocs
153246
public static class RxJavaObservableExecutionHookTestImpl extends RxJavaObservableExecutionHook {
154247
// just use defaults
155248
}
156249

157250
private static String getFullClassNameForTestClass(Class<?> cls) {
158-
return RxJavaPlugins.class.getPackage().getName() + "." + RxJavaPluginsTest.class.getSimpleName() + "$" + cls.getSimpleName();
251+
return RxJavaPlugins.class.getPackage()
252+
.getName() + "." + RxJavaPluginsTest.class.getSimpleName() + "$" + cls.getSimpleName();
159253
}
160254
}

0 commit comments

Comments
 (0)