Fixes a hosted mode memory leak in HTMLTable by eliminating Java->JavaScript->Java reference cycles.

Found by: jat
Review by: knorton


git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@1040 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/user/src/com/google/gwt/user/client/ui/HTMLTable.java b/user/src/com/google/gwt/user/client/ui/HTMLTable.java
index 2b6e1dc..a3a2d4a 100644
--- a/user/src/com/google/gwt/user/client/ui/HTMLTable.java
+++ b/user/src/com/google/gwt/user/client/ui/HTMLTable.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2006 Google Inc.
+ * Copyright 2007 Google Inc.
  * 
  * Licensed under the Apache License, Version 2.0 (the "License"); you may not
  * use this file except in compliance with the License. You may obtain a copy of
@@ -16,14 +16,15 @@
 
 package com.google.gwt.user.client.ui;
 
-import com.google.gwt.core.client.JavaScriptObject;
 import com.google.gwt.user.client.DOM;
 import com.google.gwt.user.client.Element;
 import com.google.gwt.user.client.Event;
 import com.google.gwt.user.client.ui.HasHorizontalAlignment.HorizontalAlignmentConstant;
 import com.google.gwt.user.client.ui.HasVerticalAlignment.VerticalAlignmentConstant;
 
+import java.util.ArrayList;
 import java.util.Iterator;
+import java.util.NoSuchElementException;
 
 /**
  * HTMLTable contains the common table algorithms for
@@ -507,7 +508,7 @@
 
     protected native Element getRow(Element elem, int row)/*-{
       return elem.rows[row];
-     }-*/;
+    }-*/;
 
     /**
      * Convenience methods to set an attribute on a row.
@@ -526,21 +527,33 @@
   /**
    * Creates a mapping from elements to their associated widgets.
    */
-  private class WidgetMapper {
-    // Using a native Javacript array to associate table cells with Widgets.
-    // Stamps each Widget's element with an id. If GWT ever decided to
-    // provide an int map, then this class should be modified to use it.
+  private static class WidgetMapper {
 
-    /**
-     * Stores the widgetArray, accessed via JSNI calls.
-     */
-    // Default access to supress warnings of unused fields.
-    JavaScriptObject widgetArray = createNativeArray();
+    private static class FreeNode {
+      int index;
+      FreeNode next;
+      public FreeNode(int index, FreeNode next) {
+        this.index = index;
+        this.next = next;
+      }
+    }
 
-    /**
-     * Counter to ensure each widget is given its own unique id.
-     */
-    private int widgetStamp = 1;
+    private static native void clearWidgetIndex(Element elem) /*-{
+      elem["__widgetID"] = null;
+    }-*/;
+
+    private static native int getWidgetIndex(Element elem) /*-{
+      var index = elem["__widgetID"];
+      return (index == null) ? -1 : index;
+    }-*/;
+
+    private static native void setWidgetIndex(Element elem, int index) /*-{
+      elem["__widgetID"] = index;
+    }-*/;
+
+    private FreeNode freeList = null;
+
+    private ArrayList widgetList = new ArrayList();
 
     /**
      * Returns the widget associated with the given element.
@@ -548,40 +561,12 @@
      * @param elem widget's element
      * @return the widget
      */
-    public native Widget getWidget(Element elem) /*-{
-      // Check to see if we have a widget associated with this cell.
-      // Using native reference to avoid parsing overhead.
-      var index = elem["__widgetID"];
-      if  (index == null) {
-        // Text or HTML node, so no widget associated with it.
+    public Widget getWidget(Element elem) {
+      int index = getWidgetIndex(elem);
+      if (index < 0) {
         return null;
       }
-    
-      // As we have a widget, find and return it.
-      var array = this.@com.google.gwt.user.client.ui.HTMLTable.WidgetMapper::widgetArray;
-      var result = array[index];
-      if (result == null)  {
-        return null;
-      } else {
-        return result;
-      }
-     }-*/;
-
-    /**
-     * Gets the Widget associated with the given cell.
-     * 
-     * @param row the cell's row
-     * @param column the cell's column
-     * @return the widget
-     */
-    public Widget getWidget(int row, int column) {
-      Element e = cellFormatter.getRawElement(row, column);
-      Element child = DOM.getFirstChild(e);
-      if (child == null) {
-        return null;
-      } else {
-        return getWidget(child);
-      }
+      return (Widget) widgetList.get(index);
     }
 
     /**
@@ -590,9 +575,16 @@
      * @param widget widget to add
      */
     public void putWidget(Widget widget) {
-      DOM.setElementPropertyInt(widget.getElement(), "__widgetID", widgetStamp);
-      putWidgetImpl(widgetStamp, widget);
-      ++widgetStamp;
+      int index;
+      if (freeList == null) {
+        index = widgetList.size();
+        widgetList.add(widget);
+      } else {
+        index = freeList.index;
+        widgetList.set(index, widget);
+        freeList = freeList.next;
+      }
+      setWidgetIndex(widget.getElement(), index);
     }
 
     /**
@@ -600,12 +592,10 @@
      * 
      * @param elem the widget's element
      */
-    public native void removeWidgetByElement(Element elem) /*-{
-      // Using native reference to avoid parsing overhead.
-      var index = elem["__widgetID"];
-      var array = this.@com.google.gwt.user.client.ui.HTMLTable.WidgetMapper::widgetArray;
-      delete array[index] ;
-    }-*/;
+    public void removeWidgetByElement(Element elem) {
+      int index = getWidgetIndex(elem);
+      removeImpl(elem, index);
+    }
 
     /**
      * Creates an iterator of widgets.
@@ -613,27 +603,52 @@
      * @return the iterator
      */
     public Iterator widgetIterator() {
-      WidgetCollection collection = new WidgetCollection(HTMLTable.this);
-      fillWidgets(collection);
-      return collection.iterator();
+      // TODO: look at using the WidgetIterators class!
+      return new Iterator() {
+        int lastIndex = -1;
+        int nextIndex = -1;
+        {
+          findNext();
+        }
+
+        public boolean hasNext() {
+          return nextIndex < widgetList.size();
+        }
+
+        public Object next() {
+          if (!hasNext()) {
+            throw new NoSuchElementException();
+          }
+          Object result = widgetList.get(nextIndex);
+          lastIndex = nextIndex;
+          findNext();
+          return result;
+        }
+
+        public void remove() {
+          if (lastIndex < 0) {
+            throw new IllegalStateException();
+          }
+          Widget w = (Widget) widgetList.get(lastIndex);
+          removeImpl(w.getElement(), lastIndex);
+          lastIndex = -1;
+        }
+
+        private void findNext() {
+          while (++nextIndex < widgetList.size()) {
+            if (widgetList.get(nextIndex) != null) {
+              return;
+            }
+          }
+        }
+      };
     }
 
-    private native JavaScriptObject createNativeArray() /*-{
-      return [];
-    }-*/;
-
-    private native void fillWidgets(WidgetCollection widgets) /*-{
-      var array = this.@com.google.gwt.user.client.ui.HTMLTable.WidgetMapper::widgetArray;
-      for (var index in array) { 
-        var widget = array[index];
-        widgets.@com.google.gwt.user.client.ui.WidgetCollection::add(Lcom/google/gwt/user/client/ui/Widget;)(widget);
-      }
-   }-*/;
-
-    private native void putWidgetImpl(int index, Widget widget) /*-{
-      var array = this.@com.google.gwt.user.client.ui.HTMLTable.WidgetMapper::widgetArray;
-      array[index] = widget;
-    }-*/;
+    private void removeImpl(Element elem, int index) {
+      clearWidgetIndex(elem);
+      widgetList.set(index, null);
+      freeList = new FreeNode(index, freeList);
+    }
   }
 
   /**
@@ -698,7 +713,7 @@
   public void clear() {
     for (int row = 0; row < getRowCount(); ++row) {
       for (int col = 0; col < getCellCount(row); ++col) {
-        Widget child = widgetMap.getWidget(row, col);
+        Widget child = getWidgetImpl(row, col);
         if (child != null) {
           remove(child);
         }
@@ -818,7 +833,7 @@
    */
   public Widget getWidget(int row, int column) {
     checkCellBounds(row, column);
-    return widgetMap.getWidget(row, column);
+    return getWidgetImpl(row, column);
   }
 
   /**
@@ -1062,7 +1077,7 @@
    */
   protected native int getDOMCellCount(Element tableBody, int row) /*-{
     return tableBody.rows[row].cells.length;
-   }-*/;
+  }-*/;
 
   /**
    * Directly ask the underlying DOM what the cell count on the given row is.
@@ -1085,7 +1100,7 @@
 
   protected native int getDOMRowCount(Element elem) /*-{
     return elem.rows.length;
-   }-*/;
+  }-*/;
 
   /**
    * Determines the TD associated with the specified event.
@@ -1287,4 +1302,21 @@
     internalClearCell(td, clearInnerHTML);
     return td;
   }
+
+  /**
+   * Gets the Widget associated with the given cell.
+   * 
+   * @param row the cell's row
+   * @param column the cell's column
+   * @return the widget
+   */
+  private Widget getWidgetImpl(int row, int column) {
+    Element e = cellFormatter.getRawElement(row, column);
+    Element child = DOM.getFirstChild(e);
+    if (child == null) {
+      return null;
+    } else {
+      return widgetMap.getWidget(child);
+    }
+  }
 }