Bug 581321 - [GroupBy] list order changed after grouping a filtered list

Fixed a regression that could causes a ClassCastException on loadState()
in case of GroupBy configurations for different columns of different
types.

Signed-off-by: Dirk Fauth <dirk.fauth@googlemail.com>

Change-Id: I2385c3d9e78cb9957d696c1ccf1c0e6cd06daec2
diff --git a/org.eclipse.nebula.widgets.nattable.extension.glazedlists.test/src/org/eclipse/nebula/widgets/nattable/extension/glazedlists/groupby/GroupByDataLayerTest.java b/org.eclipse.nebula.widgets.nattable.extension.glazedlists.test/src/org/eclipse/nebula/widgets/nattable/extension/glazedlists/groupby/GroupByDataLayerTest.java
index 77c21f3..a1e92b6 100644
--- a/org.eclipse.nebula.widgets.nattable.extension.glazedlists.test/src/org/eclipse/nebula/widgets/nattable/extension/glazedlists/groupby/GroupByDataLayerTest.java
+++ b/org.eclipse.nebula.widgets.nattable.extension.glazedlists.test/src/org/eclipse/nebula/widgets/nattable/extension/glazedlists/groupby/GroupByDataLayerTest.java
@@ -19,7 +19,9 @@
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Comparator;
+import java.util.Date;
 import java.util.List;
+import java.util.Properties;
 
 import org.eclipse.nebula.widgets.nattable.config.ConfigRegistry;
 import org.eclipse.nebula.widgets.nattable.config.DefaultComparator;
@@ -30,7 +32,7 @@
 import org.eclipse.nebula.widgets.nattable.dataset.person.Person;
 import org.eclipse.nebula.widgets.nattable.dataset.person.PersonService;
 import org.eclipse.nebula.widgets.nattable.extension.glazedlists.GlazedListsSortModel;
-import org.eclipse.nebula.widgets.nattable.extension.glazedlists.filterrow.DefaultGlazedListsFilterStrategy;
+import org.eclipse.nebula.widgets.nattable.extension.glazedlists.filterrow.DefaultGlazedListsStaticFilterStrategy;
 import org.eclipse.nebula.widgets.nattable.extension.glazedlists.groupBy.GroupByComparator;
 import org.eclipse.nebula.widgets.nattable.extension.glazedlists.groupBy.GroupByConfigAttributes;
 import org.eclipse.nebula.widgets.nattable.extension.glazedlists.groupBy.GroupByDataLayer;
@@ -164,7 +166,7 @@
                 new DefaultColumnHeaderDataLayer(columnHeaderDataProvider);
 
         this.filterRowDataProvider = new FilterRowDataProvider<>(
-                new DefaultGlazedListsFilterStrategy<>(
+                new DefaultGlazedListsStaticFilterStrategy<>(
                         this.filterList,
                         this.columnPropertyAccessor,
                         this.configRegistry),
@@ -1155,4 +1157,107 @@
         assertTrue(stack.hasLabel("ROW_1"));
         assertFalse(stack.hasLabel(GroupByDataLayer.GROUP_BY_OBJECT));
     }
+
+    @Test
+    public void shouldSwitchGroupingTypesOnLoad() {
+        // change the birthday to get reliable results
+        @SuppressWarnings("deprecation")
+        Date temp1 = new Date(1978, 9, 13);
+        this.filterList.get(0).setBirthday(temp1);
+        this.filterList.get(1).setBirthday(temp1);
+        this.filterList.get(2).setBirthday(temp1);
+        this.filterList.get(3).setBirthday(temp1);
+        this.filterList.get(4).setBirthday(temp1);
+        this.filterList.get(5).setBirthday(temp1);
+
+        @SuppressWarnings("deprecation")
+        Date temp2 = new Date(1976, 1, 24);
+        this.filterList.get(6).setBirthday(temp2);
+        this.filterList.get(7).setBirthday(temp2);
+        this.filterList.get(8).setBirthday(temp2);
+        this.filterList.get(9).setBirthday(temp2);
+
+        @SuppressWarnings("deprecation")
+        Date temp3 = new Date(2012, 0, 19);
+        this.filterList.get(10).setBirthday(temp3);
+        this.filterList.get(11).setBirthday(temp3);
+        this.filterList.get(12).setBirthday(temp3);
+        this.filterList.get(13).setBirthday(temp3);
+        this.filterList.get(14).setBirthday(temp3);
+        this.filterList.get(15).setBirthday(temp3);
+        this.filterList.get(16).setBirthday(temp3);
+        this.filterList.get(17).setBirthday(temp3);
+
+        // add a static filter so a list change is propagated on update()
+        addFilterCapability();
+        ((DefaultGlazedListsStaticFilterStrategy<Person>) this.filterRowDataProvider.getFilterStrategy()).addStaticFilter(item -> !item.getFirstName().equals("Ned"));
+
+        assertEquals(16, this.filterList.size());
+
+        // groupBy lastname
+        this.groupByModel.addGroupByColumnIndex(1);
+        // 16 data rows + 2 GroupBy rows
+        assertEquals(18, this.dataLayer.getRowCount());
+
+        Object o = this.dataLayer.getTreeList().get(0);
+        assertTrue(o instanceof GroupByObject, "Object is not a GroupByObject");
+        assertEquals("Flanders", ((GroupByObject) o).getValue());
+        o = this.dataLayer.getTreeList().get(7);
+        assertTrue(o instanceof GroupByObject, "Object is not a GroupByObject");
+        assertEquals("Simpson", ((GroupByObject) o).getValue());
+
+        // save state
+        Properties props = new Properties();
+        this.groupByModel.saveState("lastname", props);
+
+        // clear grouping
+        this.groupByModel.clearGroupByColumnIndexes();
+
+        // group by birthday
+        this.groupByModel.addGroupByColumnIndex(5);
+        // 16 data rows + 3 GroupBy rows
+        assertEquals(19, this.dataLayer.getRowCount());
+
+        o = this.dataLayer.getTreeList().get(0);
+        assertTrue(o instanceof GroupByObject, "Object is not a GroupByObject");
+        assertEquals(temp2, ((GroupByObject) o).getValue());
+        o = this.dataLayer.getTreeList().get(5);
+        assertTrue(o instanceof GroupByObject, "Object is not a GroupByObject");
+        assertEquals(temp1, ((GroupByObject) o).getValue());
+        o = this.dataLayer.getTreeList().get(12);
+        assertTrue(o instanceof GroupByObject, "Object is not a GroupByObject");
+        assertEquals(temp3, ((GroupByObject) o).getValue());
+
+        // save state
+        this.groupByModel.saveState("birthday", props);
+
+        // clear grouping
+        this.groupByModel.clearGroupByColumnIndexes();
+
+        // load first state
+        this.groupByModel.loadState("lastname", props);
+
+        assertEquals(18, this.dataLayer.getRowCount());
+        o = this.dataLayer.getTreeList().get(0);
+        assertTrue(o instanceof GroupByObject, "Object is not a GroupByObject");
+        assertEquals("Flanders", ((GroupByObject) o).getValue());
+        o = this.dataLayer.getTreeList().get(7);
+        assertTrue(o instanceof GroupByObject, "Object is not a GroupByObject");
+        assertEquals("Simpson", ((GroupByObject) o).getValue());
+
+        // load second state
+        this.groupByModel.loadState("birthday", props);
+
+        assertEquals(19, this.dataLayer.getRowCount());
+        o = this.dataLayer.getTreeList().get(0);
+        assertTrue(o instanceof GroupByObject, "Object is not a GroupByObject");
+        assertEquals(temp2, ((GroupByObject) o).getValue());
+        o = this.dataLayer.getTreeList().get(5);
+        assertTrue(o instanceof GroupByObject, "Object is not a GroupByObject");
+        assertEquals(temp1, ((GroupByObject) o).getValue());
+        o = this.dataLayer.getTreeList().get(12);
+        assertTrue(o instanceof GroupByObject, "Object is not a GroupByObject");
+        assertEquals(temp3, ((GroupByObject) o).getValue());
+
+    }
 }
diff --git a/org.eclipse.nebula.widgets.nattable.extension.glazedlists/.settings/.api_filters b/org.eclipse.nebula.widgets.nattable.extension.glazedlists/.settings/.api_filters
new file mode 100644
index 0000000..29182a8
--- /dev/null
+++ b/org.eclipse.nebula.widgets.nattable.extension.glazedlists/.settings/.api_filters
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<component id="org.eclipse.nebula.widgets.nattable.extension.glazedlists" version="2">
+    <resource path="src/org/eclipse/nebula/widgets/nattable/extension/glazedlists/groupBy/GroupByModel.java" type="org.eclipse.nebula.widgets.nattable.extension.glazedlists.groupBy.GroupByModel">
+        <filter id="336658481">
+            <message_arguments>
+                <message_argument value="org.eclipse.nebula.widgets.nattable.extension.glazedlists.groupBy.GroupByModel"/>
+                <message_argument value="LOAD_STATE_INDICATOR"/>
+            </message_arguments>
+        </filter>
+    </resource>
+</component>
diff --git a/org.eclipse.nebula.widgets.nattable.extension.glazedlists/src/org/eclipse/nebula/widgets/nattable/extension/glazedlists/groupBy/GroupByDataLayer.java b/org.eclipse.nebula.widgets.nattable.extension.glazedlists/src/org/eclipse/nebula/widgets/nattable/extension/glazedlists/groupBy/GroupByDataLayer.java
index 488c73a..e9b9ccd 100644
--- a/org.eclipse.nebula.widgets.nattable.extension.glazedlists/src/org/eclipse/nebula/widgets/nattable/extension/glazedlists/groupBy/GroupByDataLayer.java
+++ b/org.eclipse.nebula.widgets.nattable.extension.glazedlists/src/org/eclipse/nebula/widgets/nattable/extension/glazedlists/groupBy/GroupByDataLayer.java
@@ -111,6 +111,10 @@
      */
     public static final String GROUP_BY_SUMMARY_COLUMN_PREFIX = "GROUP_BY_SUMMARY_COLUMN_"; //$NON-NLS-1$
     /**
+     * The {@link GroupByModel} used for grouping.
+     */
+    private final GroupByModel groupByModel;
+    /**
      * The underlying base EventList.
      */
     private final EventList<T> eventList;
@@ -467,7 +471,8 @@
         this.eventList = eventList;
         this.columnAccessor = columnAccessor;
 
-        groupByModel.addObserver(this);
+        this.groupByModel = groupByModel;
+        this.groupByModel.addObserver(this);
 
         this.groupByColumnAccessor = new GroupByColumnAccessor(columnAccessor);
 
@@ -630,6 +635,20 @@
 
     @Override
     public void update(Observable o, Object arg) {
+        List<Integer> indexes = new ArrayList<>();
+        if (GroupByModel.LOAD_STATE_INDICATOR.equals(arg)) {
+            // if the GroupByModel was loaded and not simply updated, there can
+            // be situations where the groupby indexes and the corresponding
+            // types per column are totally different. In this case we first
+            // need to perform a clear operation without the new groupby
+            // configuration, and then re-apply everything. Otherwise the filter
+            // operation might trigger a list update which then leads to
+            // ClassCastExceptions as the old GroupByObjects are still in the
+            // TreeList.
+            indexes.addAll(this.groupByModel.getGroupByColumnIndexes());
+            this.groupByModel.getGroupByColumnIndexes().clear();
+        }
+
         // if we know the sort model, we need to clear the sort model to avoid
         // strange side effects while updating the tree structure (e.g. not
         // applied sorting although showing the sort indicator)
@@ -663,6 +682,10 @@
             this.filterRowDataProvider.getFilterStrategy().applyFilter(original);
         }
 
+        if (GroupByModel.LOAD_STATE_INDICATOR.equals(arg)) {
+            this.groupByModel.getGroupByColumnIndexes().addAll(indexes);
+        }
+
         updateTree();
 
         // re-apply the filter after the tree update
diff --git a/org.eclipse.nebula.widgets.nattable.extension.glazedlists/src/org/eclipse/nebula/widgets/nattable/extension/glazedlists/groupBy/GroupByModel.java b/org.eclipse.nebula.widgets.nattable.extension.glazedlists/src/org/eclipse/nebula/widgets/nattable/extension/glazedlists/groupBy/GroupByModel.java
index 4e26266..f34c9de 100644
--- a/org.eclipse.nebula.widgets.nattable.extension.glazedlists/src/org/eclipse/nebula/widgets/nattable/extension/glazedlists/groupBy/GroupByModel.java
+++ b/org.eclipse.nebula.widgets.nattable.extension.glazedlists/src/org/eclipse/nebula/widgets/nattable/extension/glazedlists/groupBy/GroupByModel.java
@@ -1,5 +1,5 @@
 /*******************************************************************************
- * Copyright (c) 2012, 2020 Original authors and others.
+ * Copyright (c) 2012, 2023 Original authors and others.
  *
  * This program and the accompanying materials are made
  * available under the terms of the Eclipse Public License 2.0
@@ -29,6 +29,15 @@
 
     public static final String PERSISTENCE_KEY_GROUP_BY_COLUMN_INDEXES = ".groupByColumnIndexes"; //$NON-NLS-1$
 
+    /**
+     * Argument that is passed to {@link #notifyObservers(Object)} to inform
+     * about a complete change. Needed to handle the tree update slightly
+     * different.
+     *
+     * @since 2.1
+     */
+    public static final String LOAD_STATE_INDICATOR = "LOAD_STATE"; //$NON-NLS-1$
+
     private List<Integer> groupByColumnIndexes = new ArrayList<>();
 
     /**
@@ -112,7 +121,8 @@
             }
         }
 
-        update();
+        setChanged();
+        notifyObservers(LOAD_STATE_INDICATOR);
     }
 
     /**