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 );
+  }
+
+}