From a14b9a85fa57005697c91afdbbb8e6175a0cfc15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Basl=C3=A9?= Date: Fri, 6 Feb 2015 23:02:58 +0100 Subject: [PATCH] Implement hook to render specific types in OnNextValue --- .../java/rx/exceptions/OnErrorThrowable.java | 20 +++- .../java/rx/plugins/RxJavaErrorHandler.java | 44 +++++++++ .../java/rx/plugins/RxJavaPluginsTest.java | 96 ++++++++++++++++++- 3 files changed, 157 insertions(+), 3 deletions(-) diff --git a/src/main/java/rx/exceptions/OnErrorThrowable.java b/src/main/java/rx/exceptions/OnErrorThrowable.java index 3a38e74401..4e0969a6eb 100644 --- a/src/main/java/rx/exceptions/OnErrorThrowable.java +++ b/src/main/java/rx/exceptions/OnErrorThrowable.java @@ -15,6 +15,9 @@ */ package rx.exceptions; +import rx.plugins.RxJavaErrorHandler; +import rx.plugins.RxJavaPlugins; + /** * Represents a {@code Throwable} that an {@code Observable} might notify its subscribers of, but that then can * 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() { /** * Render the object if it is a basic type. This avoids the library making potentially expensive - * or calls to toString() which may throw exceptions. See PR #1401 for details. + * or calls to toString() which may throw exceptions. + * + * If a specific behavior has been defined in the {@link RxJavaErrorHandler} plugin, some types + * may also have a specific rendering. Non-primitive types not managed by the plugin are rendered + * as the classname of the object. + *

+ * See PR #1401 and Issue #2468 for details. * * @param value * the item that the Observable was trying to emit at the time of the exception - * @return a string version of the object if primitive, otherwise the classname of the object + * @return a string version of the object if primitive or managed through error plugin, + * otherwise the classname of the object */ private static String renderValue(Object value){ if (value == null) { @@ -150,6 +160,12 @@ private static String renderValue(Object value){ if (value instanceof Enum) { return ((Enum) value).name(); } + + String pluggedRendering = RxJavaPlugins.getInstance().getErrorHandler().handleOnNextValueRendering(value); + if (pluggedRendering != null) { + return pluggedRendering; + } + return value.getClass().getName() + ".class"; } } diff --git a/src/main/java/rx/plugins/RxJavaErrorHandler.java b/src/main/java/rx/plugins/RxJavaErrorHandler.java index b14b03f488..9dd52fdc3d 100644 --- a/src/main/java/rx/plugins/RxJavaErrorHandler.java +++ b/src/main/java/rx/plugins/RxJavaErrorHandler.java @@ -17,6 +17,7 @@ import rx.Observable; import rx.Subscriber; +import rx.exceptions.OnErrorThrowable; /** * Abstract class for defining error handling logic in addition to the normal @@ -25,6 +26,8 @@ * For example, all {@code Exception}s can be logged using this handler even if * {@link Subscriber#onError(Throwable)} is ignored or not provided when an {@link Observable} is subscribed to. *

+ * This plugin is also responsible for augmenting rendering of {@link OnErrorThrowable.OnNextValue}. + *

* See {@link RxJavaPlugins} or the RxJava GitHub Wiki for information on configuring plugins: https://github.com/ReactiveX/RxJava/wiki/Plugins. */ @@ -44,4 +47,45 @@ public void handleError(Throwable e) { // do nothing by default } + protected static final String ERROR_IN_RENDERING_SUFFIX = ".errorRendering"; + + /** + * Receives items causing {@link OnErrorThrowable.OnNextValue} and gives a chance to choose the String + * representation of the item in the OnNextValue stacktrace rendering. Returns null if this type of item + * is not managed and should use default rendering. + *

+ * Note that primitive types are always rendered as their toString() value. + *

+ * If a {@code Throwable} is caught when rendering, this will fallback to the item's classname suffixed by + * {@value #ERROR_IN_RENDERING_SUFFIX}. + * + * @param item the last emitted item, that caused the exception wrapped in {@link OnErrorThrowable.OnNextValue}. + * @return a short {@link String} representation of the item if one is known for its type, or null for default. + */ + public final String handleOnNextValueRendering(Object item) { + try { + return render(item); + } catch (Throwable t) { + return item.getClass().getName() + ERROR_IN_RENDERING_SUFFIX; + } + } + + /** + * Override this method to provide rendering for specific types other than primitive types and null. + *

+ * For performance and overhead reasons, this should should limit to a safe production of a short {@code String} + * (as large renderings will bloat up the stacktrace). Prefer to try/catch({@code Throwable}) all code + * inside this method implementation. + *

+ * If a {@code Throwable} is caught when rendering, this will fallback to the item's classname suffixed by + * {@value #ERROR_IN_RENDERING_SUFFIX}. + * + * @param item the last emitted item, that caused the exception wrapped in {@link OnErrorThrowable.OnNextValue}. + * @return a short {@link String} representation of the item if one is known for its type, or null for default. + */ + protected String render (Object item) { + //do nothing by default + return null; + } + } diff --git a/src/test/java/rx/plugins/RxJavaPluginsTest.java b/src/test/java/rx/plugins/RxJavaPluginsTest.java index 12b5b12665..3d18923915 100644 --- a/src/test/java/rx/plugins/RxJavaPluginsTest.java +++ b/src/test/java/rx/plugins/RxJavaPluginsTest.java @@ -16,16 +16,25 @@ package rx.plugins; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import java.util.Calendar; +import java.util.Collections; +import java.util.Date; +import java.util.concurrent.TimeUnit; + import org.junit.After; import org.junit.Before; import org.junit.Test; import rx.Observable; import rx.Subscriber; +import rx.exceptions.OnErrorThrowable; +import rx.functions.Func1; public class RxJavaPluginsTest { @@ -78,7 +87,18 @@ public void handleError(Throwable e) { this.e = e; count++; } + } + public static class RxJavaErrorHandlerTestImplWithRender extends RxJavaErrorHandler { + @Override + protected String render(Object item) { + if (item instanceof Calendar) { + throw new IllegalArgumentException("calendar"); + } else if (item instanceof Date) { + return String.valueOf(((Date) item).getTime()); + } + return null; + } } @Test @@ -149,12 +169,86 @@ public void testOnErrorWhenNotImplemented() { assertEquals(1, errorHandler.count); } + @Test + public void testOnNextValueRenderingWhenNotImplemented() { + RxJavaErrorHandlerTestImpl errorHandler = new RxJavaErrorHandlerTestImpl(); + RxJavaPlugins.getInstance().registerErrorHandler(errorHandler); + + String rendering = RxJavaPlugins.getInstance().getErrorHandler().handleOnNextValueRendering(new Date()); + + assertNull(rendering); + } + + @Test + public void testOnNextValueRenderingWhenImplementedAndNotManaged() { + RxJavaErrorHandlerTestImplWithRender errorHandler = new RxJavaErrorHandlerTestImplWithRender(); + RxJavaPlugins.getInstance().registerErrorHandler(errorHandler); + + String rendering = RxJavaPlugins.getInstance().getErrorHandler().handleOnNextValueRendering( + Collections.emptyList()); + + assertNull(rendering); + } + + @Test + public void testOnNextValueRenderingWhenImplementedAndManaged() { + RxJavaErrorHandlerTestImplWithRender errorHandler = new RxJavaErrorHandlerTestImplWithRender(); + RxJavaPlugins.getInstance().registerErrorHandler(errorHandler); + long time = 1234L; + Date date = new Date(time); + + String rendering = RxJavaPlugins.getInstance().getErrorHandler().handleOnNextValueRendering(date); + + assertNotNull(rendering); + assertEquals(String.valueOf(time), rendering); + } + + @Test + public void testOnNextValueRenderingWhenImplementedAndThrows() { + RxJavaErrorHandlerTestImplWithRender errorHandler = new RxJavaErrorHandlerTestImplWithRender(); + RxJavaPlugins.getInstance().registerErrorHandler(errorHandler); + Calendar cal = Calendar.getInstance(); + + String rendering = RxJavaPlugins.getInstance().getErrorHandler().handleOnNextValueRendering(cal); + + assertNotNull(rendering); + assertEquals(cal.getClass().getName() + RxJavaErrorHandler.ERROR_IN_RENDERING_SUFFIX, rendering); + } + + @Test + public void testOnNextValueCallsPlugin() { + RxJavaErrorHandlerTestImplWithRender errorHandler = new RxJavaErrorHandlerTestImplWithRender(); + RxJavaPlugins.getInstance().registerErrorHandler(errorHandler); + long time = 456L; + Date date = new Date(time); + + try { + Date notExpected = Observable.just(date) + .map(new Func1() { + @Override + public Date call(Date date) { + throw new IllegalStateException("Trigger OnNextValue"); + } + }) + .timeout(500, TimeUnit.MILLISECONDS) + .toBlocking().first(); + fail("Did not expect onNext/onCompleted, got " + notExpected); + } catch (IllegalStateException e) { + assertEquals("Trigger OnNextValue", e.getMessage()); + assertNotNull(e.getCause()); + assertTrue(e.getCause() instanceof OnErrorThrowable.OnNextValue); + assertEquals("OnError while emitting onNext value: " + time, e.getCause().getMessage()); + } + + } + // inside test so it is stripped from Javadocs public static class RxJavaObservableExecutionHookTestImpl extends RxJavaObservableExecutionHook { // just use defaults } private static String getFullClassNameForTestClass(Class cls) { - return RxJavaPlugins.class.getPackage().getName() + "." + RxJavaPluginsTest.class.getSimpleName() + "$" + cls.getSimpleName(); + return RxJavaPlugins.class.getPackage() + .getName() + "." + RxJavaPluginsTest.class.getSimpleName() + "$" + cls.getSimpleName(); } }