Implement DataSource.dispose()

DataSources need to be destroyable to free up memory on the client,
especially if the nature of the data prevents it from being re-used.
diff --git a/bundles/org.eclipse.rap.addons.autosuggest/src/org/eclipse/rap/addons/autosuggest/AutoSuggest.java b/bundles/org.eclipse.rap.addons.autosuggest/src/org/eclipse/rap/addons/autosuggest/AutoSuggest.java
index 5bc1f6a..db9dcf6 100644
--- a/bundles/org.eclipse.rap.addons.autosuggest/src/org/eclipse/rap/addons/autosuggest/AutoSuggest.java
+++ b/bundles/org.eclipse.rap.addons.autosuggest/src/org/eclipse/rap/addons/autosuggest/AutoSuggest.java
@@ -125,6 +125,7 @@
    * @param dataSource the DataSource (can be null)
    *
    * @exception IllegalStateException when the receiver is disposed
+   * @exception IllegalArgumentException when the argument is disposed
    *
    * <p>
    * NOTE: The dataSource may be changed at any time
@@ -132,6 +133,9 @@
    */
   public void setDataSource( DataSource dataSource ) {
     checkDisposed();
+    if( dataSource != null && dataSource.isDisposed() ) {
+      throw new IllegalArgumentException( "DataSource is disposed" );
+    }
     remoteObject.set( "dataSourceId", dataSource != null ? dataSource.getId() : null );
     if( dataSource != null ) {
       ColumnTemplate template = dataSource.getTemplate();
diff --git a/bundles/org.eclipse.rap.addons.autosuggest/src/org/eclipse/rap/addons/autosuggest/DataSource.java b/bundles/org.eclipse.rap.addons.autosuggest/src/org/eclipse/rap/addons/autosuggest/DataSource.java
index 79300ae..b182ac1 100644
--- a/bundles/org.eclipse.rap.addons.autosuggest/src/org/eclipse/rap/addons/autosuggest/DataSource.java
+++ b/bundles/org.eclipse.rap.addons.autosuggest/src/org/eclipse/rap/addons/autosuggest/DataSource.java
@@ -21,8 +21,8 @@
  *
  * <p>
  *   A single instance can be used by multiple <code>AutoSuggest</code> instances simultaneously.
- *   Each new DataSource is linked to the lifecycle of the UISession,
- *   therefore no duplicates should be created.
+ *   DataSources should be disposed when no longer needed. Disposing the <code>AutoSuggest</code>
+ *   has no effect on the <code>DataSource</code>.
  * </p>
  *
  * <p>
@@ -43,6 +43,7 @@
   private final RemoteObject remoteObject;
   private DataProvider<Object> dataProvider;
   private ColumnTemplate template;
+  private boolean isDisposed;
 
   /**
    * Constructs a new instance of this class. A {@link DataProvider} has to be set before it can be
@@ -67,12 +68,14 @@
    * @param dataProvider the DataProvider instance (may not be null)
    *
    * @exception NullPointerException when dataProvider is null
+   * @exception IllegalStateException when the receiver is disposed
    *
    * @see DataSource#setTemplate(ColumnTemplate)
    * @see DataSource#setFilterScript(String)
    */
   @SuppressWarnings( "unchecked" )
   public void setDataProvider( DataProvider<?> dataProvider ) {
+    checkDisposed();
     if( dataProvider == null ) {
       throw new NullPointerException( "Parameter must not be null: dataProvider" );
     }
@@ -95,9 +98,12 @@
    *
    * @param script the filterScript, or <code>null</code> to use default script
    *
+   * @exception IllegalStateException when the receiver is disposed
+   *
    * @see DataSource#setDataProvider(DataProvider)
    */
   public void setFilterScript( String script ) {
+    checkDisposed();
     remoteObject.set( "filterScript", script );
   }
 
@@ -114,12 +120,35 @@
    *
    * @param template the template (may be null)
    *
+   * @exception IllegalStateException when the receiver is disposed
+   *
    * @see DataSource#setDataProvider(DataProvider)
    */
   public void setTemplate( ColumnTemplate template ) {
+    checkDisposed();
     this.template = template;
   }
 
+  /**
+   * Disposes the receiver with all resources it created, but not the <code>DataProvider</code>
+   * that may be attached to it. If the instance is already disposed, nothing happens.
+   */
+  public void dispose() {
+    if( !isDisposed ) {
+      isDisposed = true;
+      remoteObject.destroy();
+    }
+  }
+
+  /**
+   * Indicates whether the receiver has been disposed.
+   *
+   * @return true if the receiver is disposed
+   */
+  public boolean isDisposed() {
+    return isDisposed;
+  }
+
   String getId() {
     return remoteObject.getId();
   }
@@ -128,6 +157,12 @@
     return template;
   }
 
+  private void checkDisposed() {
+    if( isDisposed ) {
+      throw new IllegalStateException( "AutoSuggest is disposed" );
+    }
+  }
+
   private void setInitialData() {
     boolean hasColumns = dataProvider instanceof ColumnDataProvider;
     remoteObject.set( "data", hasColumns ? getColumnData() : getStringData() );
diff --git a/tests/org.eclipse.rap.addons.autosuggest.test/src/org/eclipse/rap/addons/autosuggest/AutoSuggest_Test.java b/tests/org.eclipse.rap.addons.autosuggest.test/src/org/eclipse/rap/addons/autosuggest/AutoSuggest_Test.java
index e6103c0..6fdb971 100644
--- a/tests/org.eclipse.rap.addons.autosuggest.test/src/org/eclipse/rap/addons/autosuggest/AutoSuggest_Test.java
+++ b/tests/org.eclipse.rap.addons.autosuggest.test/src/org/eclipse/rap/addons/autosuggest/AutoSuggest_Test.java
@@ -284,6 +284,15 @@
     autoSuggest.setDataSource( mock( DataSource.class ) );
   }
 
+  @Test( expected = IllegalArgumentException.class )
+  public void testSetDataSource_failsIfDataSourceIsDisposed() {
+    AutoSuggest autoSuggest = new AutoSuggest( text );
+    DataSource dataSource = mock( DataSource.class );
+    when( new Boolean( dataSource.isDisposed() ) ).thenReturn( Boolean.TRUE );
+
+    autoSuggest.setDataSource( dataSource );
+  }
+
   @Test
   public void testSetDataSource_setsDataSourceOnRemoteObject() {
     AutoSuggest autoSuggest = new AutoSuggest( text );
diff --git a/tests/org.eclipse.rap.addons.autosuggest.test/src/org/eclipse/rap/addons/autosuggest/DataSource_Test.java b/tests/org.eclipse.rap.addons.autosuggest.test/src/org/eclipse/rap/addons/autosuggest/DataSource_Test.java
index 25b917a..b00a098 100644
--- a/tests/org.eclipse.rap.addons.autosuggest.test/src/org/eclipse/rap/addons/autosuggest/DataSource_Test.java
+++ b/tests/org.eclipse.rap.addons.autosuggest.test/src/org/eclipse/rap/addons/autosuggest/DataSource_Test.java
@@ -11,10 +11,13 @@
 package org.eclipse.rap.addons.autosuggest;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
 import static org.mockito.Matchers.anyString;
 import static org.mockito.Matchers.eq;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
@@ -32,10 +35,9 @@
 public class DataSource_Test {
 
   private static final String REMOTE_TYPE = "rwt.remote.Model";
-
   private Connection connection;
-
   private RemoteObject remoteObject;
+  private DataSource dataSource;
 
   @Before
   public void setUp() {
@@ -45,6 +47,7 @@
     when( connection.createRemoteObject( anyString() ) ).thenReturn( remoteObject );
     when( remoteObject.getId() ).thenReturn( "idFoo" );
     Fixture.fakeConnection( connection );
+    dataSource = new DataSource();
   }
 
   @After
@@ -54,38 +57,36 @@
 
   @Test
   public void testConstructor_createsRemoteObject() {
-    new DataSource();
-
     verify( connection ).createRemoteObject( eq( REMOTE_TYPE ) );
   }
 
   @Test
   public void testGetId() {
-    DataSource dataSource = new DataSource();
     assertEquals( "idFoo", dataSource.getId() );
   }
 
   @Test ( expected = NullPointerException.class )
   public void testSetDataProvider_failsWithNullArgument() {
-    DataSource dataSource = new DataSource();
-
     dataSource.setDataProvider( null );
   }
 
   @Test
   public void testSetDataProvider_setsDataOnRemoteObject() {
-    DataSource dataSource = new DataSource();
-
     dataSource.setDataProvider( new ArrayDataProvider( "foo", "bar" ) );
 
     JsonArray array = new JsonArray().add( "foo" ).add( "bar" );
     verify( remoteObject ).set( eq( "data" ), eq( array ) );
   }
 
+  @Test( expected = IllegalStateException.class )
+  public void testSetDataProvider_failsIfDisposed() {
+    dataSource.dispose();
+
+    dataSource.setDataProvider( new ArrayDataProvider( "foo", "bar" ) );
+  }
+
   @Test
   public void testSetDataProvider_setsDataOnRemoteObject_forColumnDataProvider() {
-    DataSource dataSource = new DataSource();
-
     dataSource.setDataProvider( new ColumnDataProvider() {
       public Iterable<?> getSuggestions() {
         return Arrays.asList( "foo", "bar" );
@@ -106,16 +107,20 @@
 
   @Test
   public void testSetFilterScript_setsFilterScriptOnRemoteObject() {
-    DataSource dataSource = new DataSource();
-
     dataSource.setFilterScript( "foobar" );
 
     verify( remoteObject ).set( eq( "filterScript" ), eq( "foobar" ) );
   }
 
+  @Test( expected = IllegalStateException.class )
+  public void testSetFilterScript_failsIfDisposed() {
+    dataSource.dispose();
+
+    dataSource.setFilterScript( "foobar" );
+  }
+
   @Test
   public void testSetTemplate() {
-    DataSource dataSource = new DataSource();
     ColumnTemplate template = mock( ColumnTemplate.class );
 
     dataSource.setTemplate( template );
@@ -123,4 +128,37 @@
     assertSame( template, dataSource.getTemplate() );
   }
 
+  @Test( expected = IllegalStateException.class )
+  public void testSetTemplate_failsIfDisposed() {
+    dataSource.dispose();
+
+    dataSource.setTemplate( mock( ColumnTemplate.class ) );
+  }
+
+  @Test
+  public void testIsDisposed_returnsFalse() {
+    assertFalse( dataSource.isDisposed() );
+  }
+
+  @Test
+  public void testIsDisposed_returnsTrueAfterDispose() {
+    dataSource.dispose();
+
+    assertTrue( dataSource.isDisposed() );
+  }
+
+  @Test
+  public void testDispose_destroyRemoteObject() {
+    dataSource.dispose();
+
+    verify( remoteObject ).destroy();
+  }
+
+  @Test
+  public void testDispose_callingTwicedestroysRemoteObjectOnce() {
+    dataSource.dispose();
+    dataSource.dispose();
+
+    verify( remoteObject, times( 1 ) ).destroy();
+  }
 }