Add server-side support for footer spanning

- handle footerSpan set by GridColumn#setData
- render footerSpan property in protocol

Change-Id: I155d4e7db3c4db6364337a7b3e8a235374aa539f
Signed-off-by: Ivan Furnadjiev <ivan@eclipsesource.com>
diff --git a/bundles/org.eclipse.rap.nebula.widgets.grid/src/org/eclipse/nebula/widgets/grid/GridColumn.java b/bundles/org.eclipse.rap.nebula.widgets.grid/src/org/eclipse/nebula/widgets/grid/GridColumn.java
index 846d08f..cb89212 100644
--- a/bundles/org.eclipse.rap.nebula.widgets.grid/src/org/eclipse/nebula/widgets/grid/GridColumn.java
+++ b/bundles/org.eclipse.rap.nebula.widgets.grid/src/org/eclipse/nebula/widgets/grid/GridColumn.java
@@ -44,6 +44,7 @@
 @SuppressWarnings( "restriction" )
 public class GridColumn extends Item {
 
+  static final String FOOTER_SPAN = "footerSpan";
   private static final int SORT_INDICATOR_WIDTH = 10;
   private static final int MARGIN_IMAGE = 3;
   private static final int DEFAULT_WIDTH = 10;
@@ -1086,11 +1087,24 @@
 
   @Override
   public void setData( String key, Object value ) {
+    checkFooterSpan( key, value );
     if( !RWT.TOOLTIP_MARKUP_ENABLED.equals( key ) || !isToolTipMarkupEnabledFor( this ) ) {
       super.setData( key, value );
     }
   }
 
+  private void checkFooterSpan( String key, Object value ) {
+    if( FOOTER_SPAN.equals( key ) ) {
+      if( !( value instanceof Integer ) ) {
+        SWT.error( SWT.ERROR_INVALID_ARGUMENT );
+      }
+      int footerSpan = ( ( Integer )value ).intValue();
+      if( footerSpan < 1 || parent.indexOf( this ) + footerSpan > parent.getColumnCount() ) {
+        SWT.error( SWT.ERROR_INVALID_RANGE );
+      }
+    }
+  }
+
   void repack() {
     if( packed ) {
       pack();
diff --git a/bundles/org.eclipse.rap.nebula.widgets.grid/src/org/eclipse/nebula/widgets/grid/internal/gridcolumnkit/GridColumnLCA.java b/bundles/org.eclipse.rap.nebula.widgets.grid/src/org/eclipse/nebula/widgets/grid/internal/gridcolumnkit/GridColumnLCA.java
index 79fb605..14bd758 100644
--- a/bundles/org.eclipse.rap.nebula.widgets.grid/src/org/eclipse/nebula/widgets/grid/internal/gridcolumnkit/GridColumnLCA.java
+++ b/bundles/org.eclipse.rap.nebula.widgets.grid/src/org/eclipse/nebula/widgets/grid/internal/gridcolumnkit/GridColumnLCA.java
@@ -51,6 +51,7 @@
   private static final String PROP_FOOTER_FONT = "footerFont";
   private static final String PROP_FOOTER_TEXT = "footerText";
   private static final String PROP_FOOTER_IMAGE = "footerImage";
+  private static final String PROP_FOOTER_SPAN = "footerSpan";
   private static final String PROP_WORD_WRAP = "wordWrap";
   private static final String PROP_HEADER_WORD_WRAP = "headerWordWrap";
   private static final String PROP_SELECTION_LISTENER = "Selection";
@@ -88,6 +89,7 @@
     preserveProperty( column, PROP_FOOTER_FONT, column.getFooterFont() );
     preserveProperty( column, PROP_FOOTER_TEXT, column.getFooterText() );
     preserveProperty( column, PROP_FOOTER_IMAGE, column.getFooterImage() );
+    preserveProperty( column, PROP_FOOTER_SPAN, getFooterSpan( column ) );
     preserveProperty( column, PROP_WORD_WRAP, column.getWordWrap() );
     preserveProperty( column, PROP_HEADER_WORD_WRAP, column.getHeaderWordWrap() );
     preserveListener( column, PROP_SELECTION_LISTENER, isListening( column, SWT.Selection ) );
@@ -111,6 +113,7 @@
     renderFont( column, PROP_FOOTER_FONT, column.getFooterFont() );
     renderProperty( column, PROP_FOOTER_TEXT, column.getFooterText(), "" );
     renderProperty( column, PROP_FOOTER_IMAGE, column.getFooterImage(), null );
+    renderProperty( column, PROP_FOOTER_SPAN, getFooterSpan( column ), 1 );
     renderProperty( column, PROP_WORD_WRAP, column.getWordWrap(), false );
     renderProperty( column, PROP_HEADER_WORD_WRAP, column.getHeaderWordWrap(), false );
     renderListener( column, PROP_SELECTION_LISTENER, isListening( column, SWT.Selection ), false );
@@ -148,6 +151,11 @@
     return column.getParent().indexOf( column );
   }
 
+  private static int getFooterSpan( GridColumn column ) {
+    Integer value = ( Integer )column.getData( PROP_FOOTER_SPAN );
+    return value == null ? 1 : value.intValue();
+  }
+
   private static IGridAdapter getGridAdapter( GridColumn column ) {
     return column.getParent().getAdapter( IGridAdapter.class );
   }
diff --git a/tests/org.eclipse.rap.nebula.widgets.grid.test/src/org/eclipse/nebula/widgets/grid/GridColumn_Test.java b/tests/org.eclipse.rap.nebula.widgets.grid.test/src/org/eclipse/nebula/widgets/grid/GridColumn_Test.java
index de1587c..5b63cb3 100644
--- a/tests/org.eclipse.rap.nebula.widgets.grid.test/src/org/eclipse/nebula/widgets/grid/GridColumn_Test.java
+++ b/tests/org.eclipse.rap.nebula.widgets.grid.test/src/org/eclipse/nebula/widgets/grid/GridColumn_Test.java
@@ -10,6 +10,7 @@
  ******************************************************************************/
 package org.eclipse.nebula.widgets.grid;
 
+import static org.eclipse.nebula.widgets.grid.GridColumn.FOOTER_SPAN;
 import static org.eclipse.nebula.widgets.grid.GridTestUtil.createGridColumns;
 import static org.eclipse.nebula.widgets.grid.GridTestUtil.createGridItems;
 import static org.eclipse.nebula.widgets.grid.GridTestUtil.loadImage;
@@ -969,6 +970,50 @@
     assertEquals( "bar", column.getData( "foo" ) );
   }
 
+  @Test
+  public void testFooterSpan_initial() {
+    GridColumn column = new GridColumn( grid, SWT.NONE );
+
+    assertNull( column.getData( FOOTER_SPAN ) );
+  }
+
+  @Test( expected = IllegalArgumentException.class )
+  public void testFooterSpan_nullValue() {
+    GridColumn column = new GridColumn( grid, SWT.NONE );
+
+    column.setData( FOOTER_SPAN, null );
+  }
+
+  @Test( expected = IllegalArgumentException.class )
+  public void testFooterSpan_notIntegerValue() {
+    GridColumn column = new GridColumn( grid, SWT.NONE );
+
+    column.setData( FOOTER_SPAN, "foo" );
+  }
+
+  @Test( expected = IllegalArgumentException.class )
+  public void testFooterSpan_zeroOrLessValue() {
+    GridColumn column = new GridColumn( grid, SWT.NONE );
+
+    column.setData( FOOTER_SPAN, Integer.valueOf( 0 ) );
+  }
+
+  @Test( expected = IllegalArgumentException.class )
+  public void testFooterSpan_outOfRangeValue() {
+    GridColumn[] columns = createGridColumns( grid, 5, SWT.NONE);
+
+    columns[ 1 ].setData( FOOTER_SPAN, Integer.valueOf( 5 ) );
+  }
+
+  @Test
+  public void testFooterSpan() {
+    GridColumn[] columns = createGridColumns( grid, 5, SWT.NONE);
+
+    columns[ 1 ].setData( FOOTER_SPAN, Integer.valueOf( 3 ) );
+
+    assertEquals( Integer.valueOf( 3 ), columns[ 1 ].getData( FOOTER_SPAN ) );
+  }
+
   //////////////////
   // Helping methods
 
diff --git a/tests/org.eclipse.rap.nebula.widgets.grid.test/src/org/eclipse/nebula/widgets/grid/internal/gridcolumnkit/GridColumnLCA_Test.java b/tests/org.eclipse.rap.nebula.widgets.grid.test/src/org/eclipse/nebula/widgets/grid/internal/gridcolumnkit/GridColumnLCA_Test.java
index 5954f67..4e6bc6e 100644
--- a/tests/org.eclipse.rap.nebula.widgets.grid.test/src/org/eclipse/nebula/widgets/grid/internal/gridcolumnkit/GridColumnLCA_Test.java
+++ b/tests/org.eclipse.rap.nebula.widgets.grid.test/src/org/eclipse/nebula/widgets/grid/internal/gridcolumnkit/GridColumnLCA_Test.java
@@ -801,6 +801,39 @@
   }
 
   @Test
+  public void testRenderInitialFooterSpan() throws IOException {
+    lca.renderChanges( column );
+
+    TestMessage message = Fixture.getProtocolMessage();
+    assertNull( message.findSetOperation( column, "footerSpan" ) );
+  }
+
+  @Test
+  public void testRenderFooterSpan() throws IOException {
+    createGridColumns( grid, 3, SWT.NONE );
+    column.setData( "footerSpan", Integer.valueOf( 2 ) );
+
+    lca.renderChanges( column );
+
+    TestMessage message = Fixture.getProtocolMessage();
+    assertEquals( 2, message.findSetProperty( column, "footerSpan" ).asInt() );
+  }
+
+  @Test
+  public void testRenderFooterSpanUnchanged() throws IOException {
+    Fixture.markInitialized( display );
+    Fixture.markInitialized( column );
+    createGridColumns( grid, 3, SWT.NONE );
+    column.setData( "footerSpan", Integer.valueOf( 2 ) );
+
+    Fixture.preserveWidgets();
+    lca.renderChanges( column );
+
+    TestMessage message = Fixture.getProtocolMessage();
+    assertNull( message.findSetOperation( column, "footerSpan" ) );
+  }
+
+  @Test
   public void testRenderInitialWordWrap() throws IOException {
     lca.render( column );