Plug memory leak in ResettableEventBus, fix for
http://code.google.com/p/google-web-toolkit/issues/detail?id=5700

Review by rjrjr at http://gwt-code-reviews.appspot.com/1388804/

Review by: jlabanca@google.com

git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@9928 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/user/src/com/google/gwt/event/shared/ResettableEventBus.java b/user/src/com/google/gwt/event/shared/ResettableEventBus.java
index 5769a79..50055d6 100644
--- a/user/src/com/google/gwt/event/shared/ResettableEventBus.java
+++ b/user/src/com/google/gwt/event/shared/ResettableEventBus.java
@@ -18,6 +18,7 @@
 import com.google.gwt.event.shared.GwtEvent.Type;
 
 import java.util.HashSet;
+import java.util.Iterator;
 import java.util.Set;
 
 /**
@@ -25,6 +26,7 @@
  * easily all be cleared at once.
  */
 public class ResettableEventBus extends EventBus {
+
   private final EventBus wrapped;
   private final Set<HandlerRegistration> registrations = new HashSet<HandlerRegistration>();
 
@@ -33,20 +35,16 @@
   }
 
   @Override
-  public <H extends EventHandler> HandlerRegistration addHandler(Type<H> type,
-      H handler) {
+  public <H extends EventHandler> HandlerRegistration addHandler(Type<H> type, H handler) {
     HandlerRegistration rtn = wrapped.addHandler(type, handler);
-    registrations.add(rtn);
-    return rtn;
+    return doRegisterHandler(rtn);
   }
 
   @Override
-  public <H extends EventHandler> HandlerRegistration addHandlerToSource(
-      GwtEvent.Type<H> type, Object source, H handler) {
-    HandlerRegistration rtn = wrapped.addHandlerToSource(type, source,
-        handler);
-    registrations.add(rtn);
-    return rtn;
+  public <H extends EventHandler> HandlerRegistration addHandlerToSource(GwtEvent.Type<H> type,
+      Object source, H handler) {
+    HandlerRegistration rtn = wrapped.addHandlerToSource(type, source, handler);
+    return doRegisterHandler(rtn);
   }
 
   @Override
@@ -63,9 +61,38 @@
    * Remove all handlers that have been added through this wrapper.
    */
   public void removeHandlers() {
-    for (HandlerRegistration r : registrations) {
+    Iterator<HandlerRegistration> it = registrations.iterator();
+    while (it.hasNext()) {
+      HandlerRegistration r = it.next();
+
+      /*
+       * must remove before we call removeHandler. Might have come from nested
+       * ResettableEventBus
+       */
+      it.remove();
+
       r.removeHandler();
     }
-    registrations.clear();
+  }
+
+  // Visible for testing
+  int getRegistrationSize() {
+    return registrations.size();
+  }
+
+  private HandlerRegistration doRegisterHandler(final HandlerRegistration registration) {
+    registrations.add(registration);
+    return new HandlerRegistration() {
+      public void removeHandler() {
+        doUnregisterHandler(registration);
+      }
+    };
+  }
+
+  private void doUnregisterHandler(HandlerRegistration registration) {
+    if (registrations.contains(registration)) {
+      registration.removeHandler();
+      registrations.remove(registration);
+    }
   }
 }
diff --git a/user/test/com/google/gwt/event/shared/ResettableEventBusTest.java b/user/test/com/google/gwt/event/shared/ResettableEventBusTest.java
index de3fd36..9e14a8d 100644
--- a/user/test/com/google/gwt/event/shared/ResettableEventBusTest.java
+++ b/user/test/com/google/gwt/event/shared/ResettableEventBusTest.java
@@ -50,7 +50,7 @@
     assertFired(mouse1, mouse2, mouse3);
 
     reset();
-    
+
     subject.removeHandlers();
     assertEquals(0, wrapped.getCount(type));
 
@@ -58,30 +58,30 @@
     });
     assertNotFired(mouse1, mouse2, mouse3);
   }
-  
+
   public void testNestedResetInnerFirst() {
     CountingEventBus wrapped = new CountingEventBus();
     ResettableEventBus wideScope = new ResettableEventBus(wrapped);
     ResettableEventBus narrowScope = new ResettableEventBus(wideScope);
-    
+
     Type<MouseDownHandler> type = MouseDownEvent.getType();
-    
+
     wideScope.addHandler(type, mouse1);
     narrowScope.addHandler(type, mouse2);
-    
+
     wrapped.fireEvent(new MouseDownEvent() {
     });
     assertFired(mouse1, mouse2);
-    
+
     reset();
 
     /*
-     * When I remove handlers from the narrow resettable, it should have no effect
-     * on handlers registered with the wider instance.
+     * When I remove handlers from the narrow resettable, it should have no
+     * effect on handlers registered with the wider instance.
      */
-    
+
     narrowScope.removeHandlers();
-    
+
     wrapped.fireEvent(new MouseDownEvent() {
     });
     assertFired(mouse1);
@@ -92,28 +92,90 @@
     CountingEventBus wrapped = new CountingEventBus();
     ResettableEventBus wideScope = new ResettableEventBus(wrapped);
     ResettableEventBus narrowScope = new ResettableEventBus(wideScope);
-    
+
     Type<MouseDownHandler> type = MouseDownEvent.getType();
-    
+
     wideScope.addHandler(type, mouse1);
     narrowScope.addHandler(type, mouse2);
-    
+
     wrapped.fireEvent(new MouseDownEvent() {
     });
     assertFired(mouse1, mouse2);
-    
+
     reset();
-    
+
     /*
-     * When I remove handlers from the first resettable, handlers registered
-     * by the narrower scoped one that wraps it should also be severed.
+     * When I remove handlers from the first resettable, handlers registered by
+     * the narrower scoped one that wraps it should also be severed.
      */
-    
+
     wideScope.removeHandlers();
-    
+
     wrapped.fireEvent(new MouseDownEvent() {
     });
     assertNotFired(mouse1);
     assertNotFired(mouse2);
   }
+
+  public void testManualRemoveMemory() {
+    SimpleEventBus eventBus = new SimpleEventBus();
+    ResettableEventBus subject = new ResettableEventBus(eventBus);
+
+    Type<MouseDownHandler> type = MouseDownEvent.getType();
+
+    HandlerRegistration registration1 = subject.addHandler(type, mouse1);
+    HandlerRegistration registration2 = subject.addHandler(type, mouse2);
+    HandlerRegistration registration3 = subject.addHandler(type, mouse3);
+
+    registration1.removeHandler();
+    registration2.removeHandler();
+    registration3.removeHandler();
+
+    /*
+     * removing handlers manually should remove registration from the internal
+     * set.
+     */
+
+    assertEquals(0, subject.getRegistrationSize());
+
+    subject.removeHandlers();
+    
+    // Expect nothing to happen. Especially no exceptions.
+    registration1.removeHandler();
+  }
+
+  public void testNestedRemoveMemory() {
+    SimpleEventBus eventBus = new SimpleEventBus();
+    ResettableEventBus wideScope = new ResettableEventBus(eventBus);
+    ResettableEventBus narrowScope = new ResettableEventBus(wideScope);
+
+    Type<MouseDownHandler> type = MouseDownEvent.getType();
+    wideScope.addHandler(type, mouse1);
+    narrowScope.addHandler(type, mouse2);
+    narrowScope.addHandler(type, mouse3);
+
+    narrowScope.removeHandlers();
+    wideScope.removeHandlers();
+
+    /*
+     * Internal registeration should be empty after calling removeHandlers
+     */
+
+    assertEquals(0, wideScope.getRegistrationSize());
+    assertEquals(0, narrowScope.getRegistrationSize());
+
+    wideScope.addHandler(type, mouse1);
+    narrowScope.addHandler(type, mouse2);
+
+    /*
+     * Reverse remove order
+     */
+
+    wideScope.removeHandlers();
+    narrowScope.removeHandlers();
+
+    assertEquals(0, wideScope.getRegistrationSize());
+    assertEquals(0, narrowScope.getRegistrationSize());
+  }
+
 }