Provide exception handler
Bug 367773: The JEE_COMPATIBILITY mode is missing central exceptions handling
diff --git a/bundles/org.eclipse.rap.rwt/src/org/eclipse/rap/rwt/application/Application.java b/bundles/org.eclipse.rap.rwt/src/org/eclipse/rap/rwt/application/Application.java
index a5c5e70..7fef8b3 100644
--- a/bundles/org.eclipse.rap.rwt/src/org/eclipse/rap/rwt/application/Application.java
+++ b/bundles/org.eclipse.rap.rwt/src/org/eclipse/rap/rwt/application/Application.java
@@ -8,6 +8,7 @@
* Contributors:
* Frank Appel - initial API and implementation
* EclipseSource - ongoing development
+ * Rüdiger Herrmann - exception handler (bug 367773)
******************************************************************************/
package org.eclipse.rap.rwt.application;
@@ -185,10 +186,27 @@
/**
* Configure this application to use a custom setting store implementation.
*
- * @param the setting store implementation to use
+ * @param the setting store implementation to use, must not be <code>null</code>
* @see SettingStore
*/
void setSettingStoreFactory( SettingStoreFactory settingStoreFactory );
+
+ /**
+ * The exception handler to which exceptions should be forwarded that occur while running
+ * the event loop.
+ * <p>
+ * To give an exception handler the chance to log errors it called for all classes of exceptions.
+ * <code>Error</code>s however are re-thrown after the handler was called so that they cannot be
+ * swallowed.
+ * </p>
+ * <p>
+ * The default implementation throws the given exception, resulting in a HTTP 500 response.
+ * </p>
+ *
+ * @param the exception handler to use, must not be <code>null</code>
+ * @see ExceptionHandler
+ */
+ void setExceptionHandler( ExceptionHandler exceptionHandler );
/**
* Register a themeable widget for this application. A themeable widget is a
diff --git a/bundles/org.eclipse.rap.rwt/src/org/eclipse/rap/rwt/application/ExceptionHandler.java b/bundles/org.eclipse.rap.rwt/src/org/eclipse/rap/rwt/application/ExceptionHandler.java
new file mode 100644
index 0000000..12ff126
--- /dev/null
+++ b/bundles/org.eclipse.rap.rwt/src/org/eclipse/rap/rwt/application/ExceptionHandler.java
@@ -0,0 +1,29 @@
+/*******************************************************************************
+ * Copyright (c) 2013 Rüdiger Herrmann and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Rüdiger Herrmann - initial API and implementation
+ ******************************************************************************/
+package org.eclipse.rap.rwt.application;
+
+
+/**
+ * This interface allows application code to be informed of exceptions that occurr while
+ * running the event loop.
+ *
+ * @see Application#setExceptionHandler(ExceptionHandler)
+ * @since 2.1
+ */
+public interface ExceptionHandler {
+
+ /**
+ * Called if an exception occured.
+ *
+ * @param throwable the exception that occured, never <code>null</code>
+ */
+ void handleException( Throwable throwable );
+}
diff --git a/bundles/org.eclipse.rap.rwt/src/org/eclipse/rap/rwt/internal/application/ApplicationContextImpl.java b/bundles/org.eclipse.rap.rwt/src/org/eclipse/rap/rwt/internal/application/ApplicationContextImpl.java
index 043cc59..58c39c4 100644
--- a/bundles/org.eclipse.rap.rwt/src/org/eclipse/rap/rwt/internal/application/ApplicationContextImpl.java
+++ b/bundles/org.eclipse.rap.rwt/src/org/eclipse/rap/rwt/internal/application/ApplicationContextImpl.java
@@ -14,6 +14,7 @@
import javax.servlet.ServletContext;
import org.eclipse.rap.rwt.application.ApplicationConfiguration;
+import org.eclipse.rap.rwt.application.ExceptionHandler;
import org.eclipse.rap.rwt.internal.client.ClientSelector;
import org.eclipse.rap.rwt.internal.lifecycle.EntryPointManager;
import org.eclipse.rap.rwt.internal.lifecycle.LifeCycleAdapterFactory;
@@ -77,6 +78,7 @@
private final ServletContext servletContext;
private final ApplicationContextActivator contextActivator;
private final ClientSelector clientSelector;
+ private ExceptionHandler exceptionHandler;
private boolean active;
public ApplicationContextImpl( ApplicationConfiguration applicationConfiguration,
@@ -107,7 +109,7 @@
contextActivator = new ApplicationContextActivator( this );
clientSelector = new ClientSelector();
}
-
+
public void setAttribute( String name, Object value ) {
applicationStore.setAttribute( name, value );
}
@@ -235,6 +237,14 @@
public ClientSelector getClientSelector() {
return clientSelector;
}
+
+ public ExceptionHandler getExceptionHandler() {
+ return exceptionHandler;
+ }
+
+ public void setExceptionHandler( ExceptionHandler exceptionHandler ) {
+ this.exceptionHandler = exceptionHandler;
+ }
private void checkIsNotActivated() {
if( !active ) {
diff --git a/bundles/org.eclipse.rap.rwt/src/org/eclipse/rap/rwt/internal/application/ApplicationImpl.java b/bundles/org.eclipse.rap.rwt/src/org/eclipse/rap/rwt/internal/application/ApplicationImpl.java
index d56ab5a..4af0244 100644
--- a/bundles/org.eclipse.rap.rwt/src/org/eclipse/rap/rwt/internal/application/ApplicationImpl.java
+++ b/bundles/org.eclipse.rap.rwt/src/org/eclipse/rap/rwt/internal/application/ApplicationImpl.java
@@ -20,6 +20,7 @@
import org.eclipse.rap.rwt.application.ApplicationConfiguration;
import org.eclipse.rap.rwt.application.EntryPoint;
import org.eclipse.rap.rwt.application.EntryPointFactory;
+import org.eclipse.rap.rwt.application.ExceptionHandler;
import org.eclipse.rap.rwt.internal.client.ClientProvider;
import org.eclipse.rap.rwt.internal.lifecycle.RWTLifeCycle;
import org.eclipse.rap.rwt.internal.theme.Theme;
@@ -74,6 +75,11 @@
applicationContext.getSettingStoreManager().register( settingStoreFactory );
}
+
+ public void setExceptionHandler( ExceptionHandler exceptionHandler ) {
+ ParamCheck.notNull( exceptionHandler, "exceptionHandler" );
+ applicationContext.setExceptionHandler( exceptionHandler );
+ }
public void addEntryPoint( String path,
Class<? extends EntryPoint> entryPointType,
@@ -148,6 +154,10 @@
applicationContext.setAttribute( name, value );
}
+ public ApplicationContextImpl getApplicationContext() {
+ return applicationContext;
+ }
+
private ClassLoader getClassLoader() {
return configuration.getClass().getClassLoader();
}
@@ -165,10 +175,6 @@
return result;
}
- public ApplicationContextImpl getApplicationContext() {
- return applicationContext;
- }
-
static class ResourceLoaderImpl implements ResourceLoader {
private final ClassLoader loader;
diff --git a/bundles/org.eclipse.rap.rwt/src/org/eclipse/swt/widgets/Display.java b/bundles/org.eclipse.rap.rwt/src/org/eclipse/swt/widgets/Display.java
index 536a64a..72e2dce 100644
--- a/bundles/org.eclipse.rap.rwt/src/org/eclipse/swt/widgets/Display.java
+++ b/bundles/org.eclipse.rap.rwt/src/org/eclipse/swt/widgets/Display.java
@@ -9,6 +9,7 @@
* Innoopract Informationssysteme GmbH - initial API and implementation
* EclipseSource - ongoing development
* Frank Appel - replaced singletons and static fields (Bug 337787)
+ * Rüdiger Herrmann - exception handler (bug 367773)
******************************************************************************/
package org.eclipse.swt.widgets;
@@ -25,6 +26,7 @@
import java.util.Set;
import org.eclipse.rap.rwt.Adaptable;
+import org.eclipse.rap.rwt.application.ExceptionHandler;
import org.eclipse.rap.rwt.internal.application.ApplicationContextImpl;
import org.eclipse.rap.rwt.internal.application.ApplicationContextUtil;
import org.eclipse.rap.rwt.internal.lifecycle.CurrentPhase;
@@ -153,14 +155,21 @@
= DisplayAdapter.class.getName() + "#invalidateFocus";
private static final String APP_NAME = Display.class.getName() + "#appName";
private static final String APP_VERSION = Display.class.getName() + "#appVersion";
-
- /* Package Name */
+ private static final int DOUBLE_CLICK_TIME = 500; // Keep in sync with client-side (EventUtil.js)
+ private static final int GROW_SIZE = 1024;
+
static final String PACKAGE_PREFIX = "org.eclipse.swt.widgets.";
- // Keep in sync with client-side (EventUtil.js)
- private static final int DOUBLE_CLICK_TIME = 500;
-
- private static final int GROW_SIZE = 1024;
+ private static final ExceptionHandler DEFAULT_EXCEPTION_HANDLER = new ExceptionHandler() {
+ public void handleException( Throwable throwable ) {
+ if( throwable instanceof RuntimeException ) {
+ throw ( RuntimeException )throwable;
+ }
+ if( throwable instanceof Error ) {
+ throw ( Error )throwable;
+ }
+ }
+ };
/**
* Returns the display which the currently running thread is
@@ -1138,9 +1147,22 @@
*/
public boolean readAndDispatch() {
checkDevice();
- runSkin();
- runDeferredLayouts();
- return runPendingMessages();
+ return safeReadAndDispatch();
+ }
+
+ private boolean safeReadAndDispatch() {
+ boolean result = false;
+ try {
+ runSkin();
+ runDeferredLayouts();
+ result = runPendingMessages();
+ } catch( RuntimeException runtimeException ) {
+ handleException( runtimeException );
+ } catch( Error error ) {
+ handleException( error );
+ throw error;
+ }
+ return result;
}
private boolean runPendingMessages() {
@@ -1178,6 +1200,19 @@
return result;
}
+ private void handleException( Throwable throwable ) {
+ ExceptionHandler exceptionHandler = getExceptionHandler();
+ exceptionHandler.handleException( throwable );
+ }
+
+ private ExceptionHandler getExceptionHandler() {
+ ExceptionHandler result = getApplicationContext().getExceptionHandler();
+ if( result == null ) {
+ result = DEFAULT_EXCEPTION_HANDLER;
+ }
+ return result;
+ }
+
/**
* Causes the user-interface thread to <em>sleep</em> (that is,
* to be put in a state where it does not consume CPU cycles)
@@ -2397,5 +2432,5 @@
beep = false;
}
}
-
+
}
diff --git a/tests/org.eclipse.rap.rwt.test/src/org/eclipse/rap/rwt/internal/application/ApplicationImpl_Test.java b/tests/org.eclipse.rap.rwt.test/src/org/eclipse/rap/rwt/internal/application/ApplicationImpl_Test.java
index 50df237..a1c541f 100644
--- a/tests/org.eclipse.rap.rwt.test/src/org/eclipse/rap/rwt/internal/application/ApplicationImpl_Test.java
+++ b/tests/org.eclipse.rap.rwt.test/src/org/eclipse/rap/rwt/internal/application/ApplicationImpl_Test.java
@@ -21,6 +21,7 @@
import org.eclipse.rap.rwt.application.Application.OperationMode;
import org.eclipse.rap.rwt.application.ApplicationConfiguration;
+import org.eclipse.rap.rwt.application.ExceptionHandler;
import org.eclipse.rap.rwt.engine.RWTServlet;
import org.eclipse.rap.rwt.internal.engine.RWTClusterSupport;
import org.eclipse.rap.rwt.internal.lifecycle.LifeCycle;
@@ -148,7 +149,25 @@
} catch( NullPointerException expected ) {
}
}
+
+ @Test
+ public void testSetExceptionHandler() {
+ ExceptionHandler exceptionHandler = mock( ExceptionHandler.class );
+
+ application.setExceptionHandler( exceptionHandler );
+
+ assertSame( exceptionHandler, applicationContext.getExceptionHandler() );
+ }
+ @Test
+ public void testSetExceptionHandlerWithNullArgument() {
+ try {
+ application.setExceptionHandler( null );
+ fail();
+ } catch( NullPointerException expected ) {
+ }
+ }
+
private void assertFilterRegistered( Class<RWTClusterSupport> filterClass ) {
FilterRegistration[] filterRegistrations = getFilterRegistrations();
boolean found = false;
diff --git a/tests/org.eclipse.rap.rwt.test/src/org/eclipse/swt/widgets/DisplayExceptionHandler_Test.java b/tests/org.eclipse.rap.rwt.test/src/org/eclipse/swt/widgets/DisplayExceptionHandler_Test.java
new file mode 100644
index 0000000..4d96ab8
--- /dev/null
+++ b/tests/org.eclipse.rap.rwt.test/src/org/eclipse/swt/widgets/DisplayExceptionHandler_Test.java
@@ -0,0 +1,176 @@
+/*******************************************************************************
+ * Copyright (c) 2013 Rüdiger Herrmann and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Rüdiger Herrmann - initial API and implementation
+ ******************************************************************************/
+package org.eclipse.swt.widgets;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyBoolean;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import junit.framework.TestCase;
+
+import org.eclipse.rap.rwt.application.ExceptionHandler;
+import org.eclipse.rap.rwt.internal.application.ApplicationContextUtil;
+import org.eclipse.rap.rwt.lifecycle.PhaseId;
+import org.eclipse.rap.rwt.lifecycle.ProcessActionRunner;
+import org.eclipse.rap.rwt.testfixture.Fixture;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.internal.events.EventList;
+import org.junit.After;
+import org.junit.Before;
+
+
+public class DisplayExceptionHandler_Test extends TestCase {
+
+ private ExceptionHandler exceptionHandler;
+ private Display display;
+ private Shell shell;
+
+ public void testRuntimeExceptionInListenerWithExceptionHandler() {
+ ApplicationContextUtil.getInstance().setExceptionHandler( exceptionHandler );
+ RuntimeException exception = new RuntimeException();
+ addMaliciousListener( SWT.Resize, exception );
+ generateEvent( shell, SWT.Resize );
+
+ display.readAndDispatch();
+
+ verify( exceptionHandler ).handleException( exception );
+ }
+
+ public void testRuntimeExceptionInListenerWithoutExceptionHandler() {
+ RuntimeException exception = new RuntimeException();
+ addMaliciousListener( SWT.Resize, exception );
+ generateEvent( shell, SWT.Resize );
+
+ try {
+ display.readAndDispatch();
+ fail();
+ } catch( RuntimeException expected ) {
+ assertSame( exception, expected );
+ }
+ }
+
+ public void testErrorInListenerWithExceptionHandler() {
+ ApplicationContextUtil.getInstance().setExceptionHandler( exceptionHandler );
+ Error error = new Error();
+ addMaliciousListener( SWT.Resize, error );
+ generateEvent( shell, SWT.Resize );
+
+ try {
+ display.readAndDispatch();
+ fail();
+ } catch( Error expected ) {
+ assertSame( error, expected );
+ }
+
+ verify( exceptionHandler ).handleException( error );
+ }
+
+ public void testErrorInListenerWithoutExceptionHandler() {
+ Error error = new Error();
+ addMaliciousListener( SWT.Resize, error );
+ generateEvent( shell, SWT.Resize );
+
+ try {
+ display.readAndDispatch();
+ fail();
+ } catch( Error expected ) {
+ assertSame( error, expected );
+ }
+ }
+
+ public void testExceptionInExceptionHandler() {
+ ApplicationContextUtil.getInstance().setExceptionHandler( exceptionHandler );
+ RuntimeException exceptionInHandler = new RuntimeException();
+ doThrow( exceptionInHandler ).when( exceptionHandler ).handleException( any( Throwable.class ) );
+ addMaliciousListener( SWT.Resize, exceptionInHandler );
+ generateEvent( shell, SWT.Resize );
+
+ try {
+ display.readAndDispatch();
+ fail();
+ } catch( Exception expected ) {
+ assertSame( exceptionInHandler, expected );
+ }
+ }
+
+ public void testReSkinningIsRunWithinExceptionHandler() {
+ ApplicationContextUtil.getInstance().setExceptionHandler( exceptionHandler );
+ RuntimeException exception = new RuntimeException();
+ Listener listener = mock( Listener.class );
+ doThrow( exception ).when( listener ).handleEvent( any( Event.class ) );
+ display.addListener( SWT.Skin, listener );
+ display.addSkinnableWidget( shell );
+
+ display.readAndDispatch();
+
+ verify( exceptionHandler ).handleException( exception );
+ }
+
+ public void testDeferredLayoutIsRunWithinExceptionHandler() {
+ shell = spy( shell );
+ ApplicationContextUtil.getInstance().setExceptionHandler( exceptionHandler );
+ RuntimeException exception = new RuntimeException();
+ doThrow( exception ).when( shell ).setLayoutDeferred( anyBoolean() );
+ display.addLayoutDeferred( shell );
+
+ display.readAndDispatch();
+
+ verify( exceptionHandler ).handleException( exception );
+ }
+
+ public void testProcessActionRunnableIsRunWithinExceptionHandler() {
+ ApplicationContextUtil.getInstance().setExceptionHandler( exceptionHandler );
+ RuntimeException exception = new RuntimeException();
+ Runnable runnable = mock( Runnable.class );
+ doThrow( exception ).when( runnable ).run();
+ addProcessActionRunnable( runnable );
+
+ display.readAndDispatch();
+
+ verify( exceptionHandler ).handleException( exception );
+ }
+
+ @Before
+ public void setUp() {
+ Fixture.setUp();
+ Fixture.fakePhase( PhaseId.PROCESS_ACTION );
+ exceptionHandler = mock( ExceptionHandler.class );
+ display = new Display();
+ shell = new Shell( display );
+ }
+
+ @After
+ public void tearDown() {
+ Fixture.tearDown();
+ }
+
+ private void addMaliciousListener( int eventType, Throwable throwable ) {
+ Listener listener = mock( Listener.class );
+ doThrow( throwable ).when( listener ).handleEvent( any( Event.class ) );
+ shell.addListener( eventType, listener );
+ }
+
+ private void generateEvent( Widget widget, int eventType ) {
+ Event event = new Event();
+ event.type = eventType;
+ event.widget = widget;
+ EventList.getInstance().add( event );
+ }
+
+ private static void addProcessActionRunnable( Runnable runnable ) {
+ Fixture.fakePhase( PhaseId.READ_DATA );
+ ProcessActionRunner.add( runnable );
+ Fixture.fakePhase( PhaseId.PROCESS_ACTION );
+ }
+
+}