Skip to content

Commit a14b9a8

Browse files
committed
Implement hook to render specific types in OnNextValue
1 parent 4c75853 commit a14b9a8

File tree

3 files changed

+157
-3
lines changed

3 files changed

+157
-3
lines changed

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

Lines changed: 18 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
@@ -131,11 +134,18 @@ public Object getValue() {
131134

132135
/**
133136
* 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.
137+
* or calls to toString() which may throw exceptions.
138+
*
139+
* If a specific behavior has been defined in the {@link RxJavaErrorHandler} plugin, some types
140+
* may also have a specific rendering. Non-primitive types not managed by the plugin are rendered
141+
* as the classname of the object.
142+
* <p>
143+
* See PR #1401 and Issue #2468 for details.
135144
*
136145
* @param value
137146
* 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
147+
* @return a string version of the object if primitive or managed through error plugin,
148+
* otherwise the classname of the object
139149
*/
140150
private static String renderValue(Object value){
141151
if (value == null) {
@@ -150,6 +160,12 @@ private static String renderValue(Object value){
150160
if (value instanceof Enum) {
151161
return ((Enum<?>) value).name();
152162
}
163+
164+
String pluggedRendering = RxJavaPlugins.getInstance().getErrorHandler().handleOnNextValueRendering(value);
165+
if (pluggedRendering != null) {
166+
return pluggedRendering;
167+
}
168+
153169
return value.getClass().getName() + ".class";
154170
}
155171
}

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

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

1818
import rx.Observable;
1919
import rx.Subscriber;
20+
import rx.exceptions.OnErrorThrowable;
2021

2122
/**
2223
* Abstract class for defining error handling logic in addition to the normal
@@ -25,6 +26,8 @@
2526
* For example, all {@code Exception}s can be logged using this handler even if
2627
* {@link Subscriber#onError(Throwable)} is ignored or not provided when an {@link Observable} is subscribed to.
2728
* <p>
29+
* This plugin is also responsible for augmenting rendering of {@link OnErrorThrowable.OnNextValue}.
30+
* <p>
2831
* See {@link RxJavaPlugins} or the RxJava GitHub Uncyclo for information on configuring plugins: <a
2932
* href="https://github.com/ReactiveX/RxJava/wiki/Plugins">https://github.com/ReactiveX/RxJava/wiki/Plugins</a>.
3033
*/
@@ -44,4 +47,45 @@ public void handleError(Throwable e) {
4447
// do nothing by default
4548
}
4649

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

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)