]> source.dussan.org Git - vaadin-framework.git/commitdiff
Introduce initial data source support for Grid (#12878)
authorLeif Åstrand <leif@vaadin.com>
Fri, 22 Nov 2013 13:40:34 +0000 (15:40 +0200)
committerLeif Åstrand <leif@vaadin.com>
Fri, 22 Nov 2013 13:40:34 +0000 (15:40 +0200)
Change-Id: I2d1b2e4a797b2dac9ee97c832fcd40fb472edc08

17 files changed:
client/src/com/vaadin/client/data/AbstractRemoteDataSource.java [new file with mode: 0644]
client/src/com/vaadin/client/data/DataChangeHandler.java [new file with mode: 0644]
client/src/com/vaadin/client/data/DataSource.java [new file with mode: 0644]
client/src/com/vaadin/client/data/RpcDataSourceConnector.java [new file with mode: 0644]
client/src/com/vaadin/client/ui/grid/Escalator.java
client/src/com/vaadin/client/ui/grid/Grid.java
client/src/com/vaadin/client/ui/grid/GridConnector.java
client/src/com/vaadin/client/ui/grid/Range.java
client/src/com/vaadin/client/ui/grid/RowVisibilityChangeEvent.java [new file with mode: 0644]
client/src/com/vaadin/client/ui/grid/RowVisibilityChangeHandler.java [new file with mode: 0644]
client/tests/src/com/vaadin/client/ui/grid/RangeTest.java
server/src/com/vaadin/data/RpcDataProviderExtension.java [new file with mode: 0644]
server/src/com/vaadin/ui/components/grid/Grid.java
shared/src/com/vaadin/shared/data/DataProviderRpc.java [new file with mode: 0644]
shared/src/com/vaadin/shared/data/DataProviderState.java [new file with mode: 0644]
shared/src/com/vaadin/shared/data/DataRequestRpc.java [new file with mode: 0644]
uitest/src/com/vaadin/tests/components/grid/GridBasicFeatures.java

diff --git a/client/src/com/vaadin/client/data/AbstractRemoteDataSource.java b/client/src/com/vaadin/client/data/AbstractRemoteDataSource.java
new file mode 100644 (file)
index 0000000..5790ada
--- /dev/null
@@ -0,0 +1,232 @@
+/*
+ * Copyright 2000-2013 Vaadin Ltd.
+ * 
+ * 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
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.vaadin.client.data;
+
+import java.util.HashMap;
+import java.util.List;
+
+import com.google.gwt.core.client.Scheduler;
+import com.google.gwt.core.client.Scheduler.ScheduledCommand;
+import com.vaadin.client.Profiler;
+import com.vaadin.client.ui.grid.Range;
+
+/**
+ * Base implementation for data sources that fetch data from a remote system.
+ * This class takes care of caching data and communicating with the data source
+ * user. An implementation of this class should override
+ * {@link #requestRows(int, int)} to trigger asynchronously loading of data.
+ * When data is received from the server, new row data should be passed to
+ * {@link #setRowData(int, List)}. {@link #setEstimatedSize(int)} should be used
+ * based on estimations of how many rows are available.
+ * 
+ * @since 7.2
+ * @author Vaadin Ltd
+ * @param <T>
+ *            the row type
+ */
+public abstract class AbstractRemoteDataSource<T> implements DataSource<T> {
+
+    private boolean requestPending = false;
+
+    private boolean coverageCheckPending = false;
+
+    private Range requestedAvailability = Range.between(0, 0);
+
+    private Range cached = Range.between(0, 0);
+
+    private final HashMap<Integer, T> rowCache = new HashMap<Integer, T>();
+
+    private DataChangeHandler dataChangeHandler;
+
+    private int estimatedSize;
+
+    private final ScheduledCommand coverageChecker = new ScheduledCommand() {
+        @Override
+        public void execute() {
+            coverageCheckPending = false;
+            checkCacheCoverage();
+        }
+    };
+
+    /**
+     * Sets the estimated number of rows in the data source.
+     * 
+     * @param estimatedSize
+     *            the estimated number of available rows
+     */
+    protected void setEstimatedSize(int estimatedSize) {
+        // TODO update dataChangeHandler if size changes
+        this.estimatedSize = estimatedSize;
+    }
+
+    private void ensureCoverageCheck() {
+        if (!coverageCheckPending) {
+            coverageCheckPending = true;
+            Scheduler.get().scheduleDeferred(coverageChecker);
+        }
+    }
+
+    @Override
+    public void ensureAvailability(int firstRowIndex, int numberOfRows) {
+        requestedAvailability = Range.withLength(firstRowIndex, numberOfRows);
+
+        /*
+         * Don't request any data right away since the data might be included in
+         * a message that has been received but not yet fully processed.
+         */
+        ensureCoverageCheck();
+    }
+
+    private void checkCacheCoverage() {
+        if (requestPending) {
+            // Anyone clearing requestPending should run this method again
+            return;
+        }
+
+        Profiler.enter("AbstractRemoteDataSource.checkCacheCoverage");
+
+        if (!requestedAvailability.intersects(cached)) {
+            /*
+             * Simple case: no overlap between cached data and needed data.
+             * Clear the cache and request new data
+             */
+            rowCache.clear();
+            cached = Range.between(0, 0);
+
+            handleMissingRows(requestedAvailability);
+        } else {
+            discardStaleCacheEntries();
+
+            // Might need more rows -> request them
+            Range[] availabilityPartition = requestedAvailability
+                    .partitionWith(cached);
+            handleMissingRows(availabilityPartition[0]);
+            handleMissingRows(availabilityPartition[2]);
+        }
+
+        Profiler.leave("AbstractRemoteDataSource.checkCacheCoverage");
+    }
+
+    private void discardStaleCacheEntries() {
+        Range[] cacheParition = cached.partitionWith(requestedAvailability);
+        dropFromCache(cacheParition[0]);
+        cached = cacheParition[1];
+        dropFromCache(cacheParition[2]);
+    }
+
+    private void dropFromCache(Range range) {
+        for (int i = range.getStart(); i < range.getEnd(); i++) {
+            rowCache.remove(Integer.valueOf(i));
+        }
+    }
+
+    private void handleMissingRows(Range range) {
+        if (range.isEmpty()) {
+            return;
+        }
+        requestPending = true;
+        requestRows(range.getStart(), range.length());
+    }
+
+    /**
+     * Triggers fetching rows from the remote data source.
+     * {@link #setRowData(int, List)} should be invoked with data for the
+     * requested rows when they have been received.
+     * 
+     * @param firstRowIndex
+     *            the index of the first row to fetch
+     * @param numberOfRows
+     *            the number of rows to fetch
+     */
+    protected abstract void requestRows(int firstRowIndex, int numberOfRows);
+
+    @Override
+    public int getEstimatedSize() {
+        return estimatedSize;
+    }
+
+    @Override
+    public T getRow(int rowIndex) {
+        return rowCache.get(Integer.valueOf(rowIndex));
+    }
+
+    @Override
+    public void setDataChangeHandler(DataChangeHandler dataChangeHandler) {
+        this.dataChangeHandler = dataChangeHandler;
+
+        if (dataChangeHandler != null && !cached.isEmpty()) {
+            // Push currently cached data to the implementation
+            dataChangeHandler.dataUpdated(cached.getStart(), cached.length());
+        }
+    }
+
+    /**
+     * Informs this data source that updated data has been sent from the server.
+     * 
+     * @param firstRowIndex
+     *            the index of the first received row
+     * @param rowData
+     *            a list of rows, starting from <code>firstRowIndex</code>
+     */
+    protected void setRowData(int firstRowIndex, List<T> rowData) {
+        requestPending = false;
+
+        Profiler.enter("AbstractRemoteDataSource.setRowData");
+
+        Range received = Range.withLength(firstRowIndex, rowData.size());
+
+        Range[] partition = received.partitionWith(requestedAvailability);
+
+        Range newUsefulData = partition[1];
+        if (!newUsefulData.isEmpty()) {
+            // Update the parts that are actually inside
+            for (int i = newUsefulData.getStart(); i < newUsefulData.getEnd(); i++) {
+                rowCache.put(Integer.valueOf(i), rowData.get(i - firstRowIndex));
+            }
+
+            if (dataChangeHandler != null) {
+                Profiler.enter("AbstractRemoteDataSource.setRowData notify dataChangeHandler");
+                dataChangeHandler.dataUpdated(newUsefulData.getStart(),
+                        newUsefulData.length());
+                Profiler.leave("AbstractRemoteDataSource.setRowData notify dataChangeHandler");
+            }
+
+            // Potentially extend the range
+            if (cached.isEmpty()) {
+                cached = newUsefulData;
+            } else {
+                discardStaleCacheEntries();
+                cached = cached.combineWith(newUsefulData);
+            }
+        }
+
+        if (!partition[0].isEmpty() || !partition[2].isEmpty()) {
+            /*
+             * FIXME
+             * 
+             * Got data that we might need in a moment if the container is
+             * updated before the widget settings. Support for this will be
+             * implemented later on.
+             */
+        }
+
+        // Eventually check whether all needed rows are now available
+        ensureCoverageCheck();
+
+        Profiler.leave("AbstractRemoteDataSource.setRowData");
+    }
+}
diff --git a/client/src/com/vaadin/client/data/DataChangeHandler.java b/client/src/com/vaadin/client/data/DataChangeHandler.java
new file mode 100644 (file)
index 0000000..4c4cc76
--- /dev/null
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2000-2013 Vaadin Ltd.
+ * 
+ * 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
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.vaadin.client.data;
+
+/**
+ * Callback interface used by {@link DataSource} to inform its user about
+ * updates to the data.
+ * 
+ * @since 7.2
+ * @author Vaadin Ltd
+ */
+public interface DataChangeHandler {
+    /**
+     * Called when the contents of the data source has changed. If the number of
+     * rows has changed or if rows have been moved around,
+     * {@link #dataAdded(int, int)} or {@link #dataRemoved(int, int)} should
+     * ideally be used instead.
+     * 
+     * @param firstRowIndex
+     *            the index of the first changed row
+     * @param numberOfRows
+     *            the number of changed rows
+     */
+    public void dataUpdated(int firstRowIndex, int numberOfRows);
+
+    /**
+     * Called when rows have been removed from the data source.
+     * 
+     * @param firstRowIndex
+     *            the index that the first removed row had prior to removal
+     * @param numberOfRows
+     *            the number of removed rows
+     */
+    public void dataRemoved(int firstRowIndex, int numberOfRows);
+
+    /**
+     * Called when the new rows have been added to the container.
+     * 
+     * @param firstRowIndex
+     *            the index of the first added row
+     * @param numberOfRows
+     *            the number of added rows
+     */
+    public void dataAdded(int firstRowIndex, int numberOfRows);
+}
diff --git a/client/src/com/vaadin/client/data/DataSource.java b/client/src/com/vaadin/client/data/DataSource.java
new file mode 100644 (file)
index 0000000..9179b6d
--- /dev/null
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2000-2013 Vaadin Ltd.
+ * 
+ * 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
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.vaadin.client.data;
+
+/**
+ * Source of data for widgets showing lazily loaded data based on indexable
+ * items (e.g. rows) of a specified type. The data source is a lazy view into a
+ * larger data set.
+ * 
+ * @since 7.2
+ * @author Vaadin Ltd
+ * @param <T>
+ *            the row type
+ */
+public interface DataSource<T> {
+    /**
+     * Informs the data source that data for the given range is needed. A data
+     * source only has one active region at a time, so calling this method
+     * discards the previously set range.
+     * <p>
+     * This method triggers lazy loading of data if necessary. The change
+     * handler registered using {@link #setDataChangeHandler(DataChangeHandler)}
+     * is informed when new data has been loaded.
+     * 
+     * @param firstRowIndex
+     *            the index of the first needed row
+     * @param numberOfRows
+     *            the number of needed rows
+     */
+    public void ensureAvailability(int firstRowIndex, int numberOfRows);
+
+    /**
+     * Retrieves the data for the row at the given index. If the row data is not
+     * available, returns <code>null</code>.
+     * <p>
+     * This method does not trigger loading of unavailable data.
+     * {@link #ensureAvailability(int, int)} should be used to signal what data
+     * will be needed.
+     * 
+     * @param rowIndex
+     *            the index of the row to retrieve data for
+     * @return data for the row; or <code>null</code> if no data is available
+     */
+    public T getRow(int rowIndex);
+
+    /**
+     * Returns the current best guess for the number of rows in the container.
+     * 
+     * @return the current estimation of the container size
+     */
+    public int getEstimatedSize();
+
+    /**
+     * Sets a data change handler to inform when data is updated, added or
+     * removed.
+     * 
+     * @param dataChangeHandler
+     *            the data change handler
+     */
+    public void setDataChangeHandler(DataChangeHandler dataChangeHandler);
+
+}
diff --git a/client/src/com/vaadin/client/data/RpcDataSourceConnector.java b/client/src/com/vaadin/client/data/RpcDataSourceConnector.java
new file mode 100644 (file)
index 0000000..1785fc6
--- /dev/null
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2000-2013 Vaadin Ltd.
+ * 
+ * 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
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.vaadin.client.data;
+
+import java.util.List;
+
+import com.vaadin.client.ServerConnector;
+import com.vaadin.client.extensions.AbstractExtensionConnector;
+import com.vaadin.client.ui.grid.GridConnector;
+import com.vaadin.shared.data.DataProviderRpc;
+import com.vaadin.shared.data.DataProviderState;
+import com.vaadin.shared.data.DataRequestRpc;
+import com.vaadin.shared.ui.Connect;
+
+/**
+ * Connects a Vaadin server-side container data source to a Grid. This is
+ * currently implemented as an Extension hardcoded to support a specific
+ * connector type. This will be changed once framework support for something
+ * more flexible has been implemented.
+ * 
+ * @since 7.2
+ * @author Vaadin Ltd
+ */
+@Connect(com.vaadin.data.RpcDataProviderExtension.class)
+public class RpcDataSourceConnector extends AbstractExtensionConnector {
+
+    private final AbstractRemoteDataSource<String[]> dataSource = new AbstractRemoteDataSource<String[]>() {
+        @Override
+        protected void requestRows(int firstRowIndex, int numberOfRows) {
+            getRpcProxy(DataRequestRpc.class).requestRows(firstRowIndex,
+                    numberOfRows);
+        }
+    };
+
+    @Override
+    protected void extend(ServerConnector target) {
+        dataSource.setEstimatedSize(getState().containerSize);
+        ((GridConnector) target).getWidget().setDataSource(dataSource);
+
+        registerRpc(DataProviderRpc.class, new DataProviderRpc() {
+            @Override
+            public void setRowData(int firstRow, List<String[]> rows) {
+                dataSource.setRowData(firstRow, rows);
+            }
+        });
+    }
+
+    /*
+     * (non-Javadoc)
+     * 
+     * @see com.vaadin.client.ui.AbstractConnector#getState()
+     */
+    @Override
+    public DataProviderState getState() {
+        return (DataProviderState) super.getState();
+    }
+}
index 769a109569e09d424ceffcf94c2a1e35735e57d1..540676653ee4f8dfea56cbd75506ac8412e44b56 100644 (file)
@@ -39,6 +39,7 @@ import com.google.gwt.user.client.DOM;
 import com.google.gwt.user.client.Element;
 import com.google.gwt.user.client.Window;
 import com.google.gwt.user.client.ui.Widget;
+import com.google.web.bindery.event.shared.HandlerRegistration;
 import com.vaadin.client.Profiler;
 import com.vaadin.client.Util;
 import com.vaadin.client.ui.grid.Escalator.JsniUtil.TouchHandlerBundle;
@@ -1522,6 +1523,8 @@ public class Escalator extends Widget {
                     moveAndUpdateEscalatorRows(strayRow, 0, topLogicalIndex);
                 }
             }
+
+            fireRowVisibilityChangeEvent();
         }
 
         @Override
@@ -1715,6 +1718,8 @@ public class Escalator extends Widget {
                     newRowTop += ROW_HEIGHT_PX;
                 }
             }
+
+            fireRowVisibilityChangeEvent();
         }
 
         /**
@@ -2101,6 +2106,8 @@ public class Escalator extends Widget {
              * or it won't work correctly (due to setScrollTop invocation)
              */
             scroller.recalculateScrollbarsForVirtualViewport();
+
+            fireRowVisibilityChangeEvent();
         }
 
         private void paintRemoveRowsAtMiddle(final Range removedLogicalInside,
@@ -2420,6 +2427,10 @@ public class Escalator extends Widget {
                 }
             }
 
+            if (neededEscalatorRowsDiff != 0) {
+                fireRowVisibilityChangeEvent();
+            }
+
             Profiler.leave("Escalator.BodyRowContainer.verifyEscalatorCount");
         }
     }
@@ -3116,4 +3127,30 @@ public class Escalator extends Widget {
 
         return array;
     }
+
+    /**
+     * Adds an event handler that gets notified when the range of visible rows
+     * changes e.g. because of scrolling.
+     * 
+     * @param rowVisibilityChangeHadler
+     *            the event handler
+     * @return a handler registration for the added handler
+     */
+    public HandlerRegistration addRowVisibilityChangeHandler(
+            RowVisibilityChangeHandler rowVisibilityChangeHadler) {
+        return addHandler(rowVisibilityChangeHadler,
+                RowVisibilityChangeEvent.TYPE);
+    }
+
+    private void fireRowVisibilityChangeEvent() {
+        int visibleRangeStart = body.getLogicalRowIndex(body.visualRowOrder
+                .getFirst());
+        int visibleRangeEnd = body.getLogicalRowIndex(body.visualRowOrder
+                .getLast()) + 1;
+
+        int visibleRowCount = visibleRangeEnd - visibleRangeStart;
+
+        fireEvent(new RowVisibilityChangeEvent(visibleRangeStart,
+                visibleRowCount));
+    }
 }
index 67f14301f01c11fe54b56d089034a9694eda4a73..d76424ae31b169450a9eb29fdd16754fb2b3de91 100644 (file)
@@ -21,6 +21,8 @@ import java.util.List;
 
 import com.google.gwt.core.shared.GWT;
 import com.google.gwt.user.client.ui.Composite;
+import com.vaadin.client.data.DataChangeHandler;
+import com.vaadin.client.data.DataSource;
 import com.vaadin.shared.util.SharedUtil;
 
 /**
@@ -64,6 +66,8 @@ public class Grid<T> extends Composite {
      */
     private final List<GridColumn<T>> columns = new ArrayList<GridColumn<T>>();
 
+    private DataSource<T> dataSource;
+
     /**
      * The column groups rows added to the grid
      */
@@ -366,6 +370,20 @@ public class Grid<T> extends Composite {
 
         refreshHeader();
         refreshFooter();
+
+        escalator
+                .addRowVisibilityChangeHandler(new RowVisibilityChangeHandler() {
+                    @Override
+                    public void onRowVisibilityChange(
+                            RowVisibilityChangeEvent event) {
+                        if (dataSource != null) {
+                            dataSource.ensureAvailability(
+                                    event.getFirstVisibleRow(),
+                                    event.getVisibleRowCount());
+                        }
+                    }
+                });
+
     }
 
     /**
@@ -399,15 +417,33 @@ public class Grid<T> extends Composite {
         };
     }
 
-    // TODO Should be implemented by the data sources
-    @SuppressWarnings("static-method")
     private EscalatorUpdater createBodyUpdater() {
         return new EscalatorUpdater() {
 
             @Override
             public void updateCells(Row row, List<Cell> cellsToUpdate) {
+                int rowIndex = row.getRow();
+                if (dataSource == null) {
+                    setCellsLoading(cellsToUpdate);
+                    return;
+                }
+
+                T rowData = dataSource.getRow(rowIndex);
+                if (rowData == null) {
+                    setCellsLoading(cellsToUpdate);
+                    return;
+                }
+
                 for (Cell cell : cellsToUpdate) {
-                    cell.getElement().setInnerHTML("-");
+                    String value = getColumn(cell.getColumn())
+                            .getValue(rowData);
+                    cell.getElement().setInnerText(value);
+                }
+            }
+
+            private void setCellsLoading(List<Cell> cellsToUpdate) {
+                for (Cell cell : cellsToUpdate) {
+                    cell.getElement().setInnerText("...");
                 }
             }
         };
@@ -778,4 +814,51 @@ public class Grid<T> extends Composite {
     public void setWidth(String width) {
         escalator.setWidth(width);
     }
+
+    /**
+     * Sets the data source used by this grid.
+     * 
+     * @param dataSource
+     *            the data source to use, not null
+     * @throws IllegalArgumentException
+     *             if <code>dataSource</code> is <code>null</code>
+     */
+    public void setDataSource(DataSource<T> dataSource)
+            throws IllegalArgumentException {
+        if (dataSource == null) {
+            throw new IllegalArgumentException("dataSource can't be null.");
+        }
+
+        if (this.dataSource != null) {
+            this.dataSource.setDataChangeHandler(null);
+        }
+
+        this.dataSource = dataSource;
+        dataSource.setDataChangeHandler(new DataChangeHandler() {
+            @Override
+            public void dataUpdated(int firstIndex, int numberOfItems) {
+                escalator.getBody().refreshRows(firstIndex, numberOfItems);
+            }
+
+            @Override
+            public void dataRemoved(int firstIndex, int numberOfItems) {
+                escalator.getBody().removeRows(firstIndex, numberOfItems);
+            }
+
+            @Override
+            public void dataAdded(int firstIndex, int numberOfItems) {
+                escalator.getBody().insertRows(firstIndex, numberOfItems);
+            }
+        });
+
+        int previousRowCount = escalator.getBody().getRowCount();
+        if (previousRowCount != 0) {
+            escalator.getBody().removeRows(0, previousRowCount);
+        }
+
+        int estimatedSize = dataSource.getEstimatedSize();
+        if (estimatedSize > 0) {
+            escalator.getBody().insertRows(0, estimatedSize);
+        }
+    }
 }
index 32907e1e29059c9b7da89518c0737d4912574b59..896a9998fb1ad5c994b8d1afc9ceccd270cf9d3e 100644 (file)
@@ -48,10 +48,15 @@ public class GridConnector extends AbstractComponentConnector {
      */
     private class CustomGridColumn extends GridColumn<String[]> {
 
+        private final int columnIndex;
+
+        public CustomGridColumn(int columnIndex) {
+            this.columnIndex = columnIndex;
+        }
+
         @Override
         public String getValue(String[] obj) {
-            // FIXME Should return something from the data source.
-            return null;
+            return obj[columnIndex];
         }
     }
 
@@ -142,7 +147,7 @@ public class GridConnector extends AbstractComponentConnector {
      */
     private void addColumnFromStateChangeEvent(int columnIndex) {
         GridColumnState state = getState().columns.get(columnIndex);
-        CustomGridColumn column = new CustomGridColumn();
+        CustomGridColumn column = new CustomGridColumn(columnIndex);
         updateColumnFromState(column, state);
 
         columnIdToColumn.put(state.id, column);
index 6dbb287e57261fa2b1bd5121f1e542903ede9a29..d3ae3c37535ebdbcd1724304db557578ce5ea820 100644 (file)
@@ -359,4 +359,26 @@ public final class Range {
     public Range[] splitAtFromStart(final int length) {
         return splitAt(getStart() + length);
     }
+
+    /**
+     * Combines two ranges to create a range containing all values in both
+     * ranges, provided there are no gaps between the ranges.
+     * 
+     * @param other
+     *            the range to combine with this range
+     * 
+     * @return the combined range
+     * 
+     * @throws IllegalArgumentException
+     *             if the two ranges aren't connected
+     */
+    public Range combineWith(Range other) throws IllegalArgumentException {
+        if (getStart() > other.getEnd() || other.getStart() > getEnd()) {
+            throw new IllegalArgumentException("There is a gap between " + this
+                    + " and " + other);
+        }
+
+        return Range.between(Math.min(getStart(), other.getStart()),
+                Math.max(getEnd(), other.getEnd()));
+    }
 }
diff --git a/client/src/com/vaadin/client/ui/grid/RowVisibilityChangeEvent.java b/client/src/com/vaadin/client/ui/grid/RowVisibilityChangeEvent.java
new file mode 100644 (file)
index 0000000..0e9652e
--- /dev/null
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2000-2013 Vaadin Ltd.
+ * 
+ * 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
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.vaadin.client.ui.grid;
+
+import com.google.gwt.event.shared.GwtEvent;
+
+/**
+ * Event fired when the range of visible rows changes e.g. because of scrolling.
+ * 
+ * @since 7.2
+ * @author Vaadin Ltd
+ */
+public class RowVisibilityChangeEvent extends
+        GwtEvent<RowVisibilityChangeHandler> {
+    /**
+     * The type of this event.
+     */
+    public static final Type<RowVisibilityChangeHandler> TYPE = new Type<RowVisibilityChangeHandler>();
+
+    private final int firstVisibleRow;
+    private final int visibleRowCount;
+
+    /**
+     * Creates a new row visibility change event
+     * 
+     * @param firstVisibleRow
+     *            the index of the first visible row
+     * @param visibleRowCount
+     *            the number of visible rows
+     */
+    public RowVisibilityChangeEvent(int firstVisibleRow, int visibleRowCount) {
+        this.firstVisibleRow = firstVisibleRow;
+        this.visibleRowCount = visibleRowCount;
+    }
+
+    /**
+     * Gets the index of the first row that is at least partially visible.
+     * 
+     * @return the index of the first visible row
+     */
+    public int getFirstVisibleRow() {
+        return firstVisibleRow;
+    }
+
+    /**
+     * Gets the number of at least partially visible rows.
+     * 
+     * @return the number of visible rows
+     */
+    public int getVisibleRowCount() {
+        return visibleRowCount;
+    }
+
+    /*
+     * (non-Javadoc)
+     * 
+     * @see com.google.gwt.event.shared.GwtEvent#getAssociatedType()
+     */
+    @Override
+    public Type<RowVisibilityChangeHandler> getAssociatedType() {
+        return TYPE;
+    }
+
+    /*
+     * (non-Javadoc)
+     * 
+     * @see
+     * com.google.gwt.event.shared.GwtEvent#dispatch(com.google.gwt.event.shared
+     * .EventHandler)
+     */
+    @Override
+    protected void dispatch(RowVisibilityChangeHandler handler) {
+        handler.onRowVisibilityChange(this);
+    }
+
+}
diff --git a/client/src/com/vaadin/client/ui/grid/RowVisibilityChangeHandler.java b/client/src/com/vaadin/client/ui/grid/RowVisibilityChangeHandler.java
new file mode 100644 (file)
index 0000000..dd24521
--- /dev/null
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2000-2013 Vaadin Ltd.
+ * 
+ * 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
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.vaadin.client.ui.grid;
+
+import com.google.gwt.event.shared.EventHandler;
+
+/**
+ * Event handler that gets notified when the range of visible rows changes e.g.
+ * because of scrolling.
+ * 
+ * @since 7.2
+ * @author Vaadin Ltd
+ */
+public interface RowVisibilityChangeHandler extends EventHandler {
+
+    /**
+     * Called when the range of visible rows changes e.g. because of scrolling.
+     * 
+     * @param event
+     *            the row visibility change event describing the change
+     */
+    void onRowVisibilityChange(RowVisibilityChangeEvent event);
+
+}
index a4715924b4af1c76498bed80ea136a5e5d22b6f4..4441ee901d3637019b0643b3e8e92464a59907ac 100644 (file)
@@ -209,4 +209,80 @@ public class RangeTest {
         assertTrue("no overlap allowed",
                 !Range.between(0, 10).endsAfter(Range.between(5, 10)));
     }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void combine_notOverlappingFirstSmaller() {
+        Range.between(0, 10).combineWith(Range.between(11, 20));
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void combine_notOverlappingSecondLarger() {
+        Range.between(11, 20).combineWith(Range.between(0, 10));
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void combine_firstEmptyNotOverlapping() {
+        Range.between(15, 15).combineWith(Range.between(0, 10));
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void combine_secondEmptyNotOverlapping() {
+        Range.between(0, 10).combineWith(Range.between(15, 15));
+    }
+
+    @Test
+    public void combine_barelyOverlapping() {
+        Range r1 = Range.between(0, 10);
+        Range r2 = Range.between(10, 20);
+
+        // Test both ways, should give the same result
+        Range combined1 = r1.combineWith(r2);
+        Range combined2 = r2.combineWith(r1);
+        assertEquals(combined1, combined2);
+
+        assertEquals(0, combined1.getStart());
+        assertEquals(20, combined1.getEnd());
+    }
+
+    @Test
+    public void combine_subRange() {
+        Range r1 = Range.between(0, 10);
+        Range r2 = Range.between(2, 8);
+
+        // Test both ways, should give the same result
+        Range combined1 = r1.combineWith(r2);
+        Range combined2 = r2.combineWith(r1);
+        assertEquals(combined1, combined2);
+
+        assertEquals(r1, combined1);
+    }
+
+    @Test
+    public void combine_intersecting() {
+        Range r1 = Range.between(0, 10);
+        Range r2 = Range.between(5, 15);
+
+        // Test both ways, should give the same result
+        Range combined1 = r1.combineWith(r2);
+        Range combined2 = r2.combineWith(r1);
+        assertEquals(combined1, combined2);
+
+        assertEquals(0, combined1.getStart());
+        assertEquals(15, combined1.getEnd());
+
+    }
+
+    @Test
+    public void combine_emptyInside() {
+        Range r1 = Range.between(0, 10);
+        Range r2 = Range.between(5, 5);
+
+        // Test both ways, should give the same result
+        Range combined1 = r1.combineWith(r2);
+        Range combined2 = r2.combineWith(r1);
+        assertEquals(combined1, combined2);
+
+        assertEquals(r1, combined1);
+    }
+
 }
diff --git a/server/src/com/vaadin/data/RpcDataProviderExtension.java b/server/src/com/vaadin/data/RpcDataProviderExtension.java
new file mode 100644 (file)
index 0000000..48f03b9
--- /dev/null
@@ -0,0 +1,101 @@
+/*
+ * Copyright 2000-2013 Vaadin Ltd.
+ * 
+ * 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
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.vaadin.data;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+import com.vaadin.data.Container.Indexed;
+import com.vaadin.server.AbstractExtension;
+import com.vaadin.shared.data.DataProviderRpc;
+import com.vaadin.shared.data.DataProviderState;
+import com.vaadin.shared.data.DataRequestRpc;
+import com.vaadin.ui.components.grid.Grid;
+
+/**
+ * Provides Vaadin server-side container data source to a
+ * {@link com.vaadin.client.ui.grid.GridConnector}. This is currently
+ * implemented as an Extension hardcoded to support a specific connector type.
+ * This will be changed once framework support for something more flexible has
+ * been implemented.
+ * 
+ * @since 7.2
+ * @author Vaadin Ltd
+ */
+public class RpcDataProviderExtension extends AbstractExtension {
+
+    private final Indexed container;
+
+    /**
+     * Creates a new data provider using the given container.
+     * 
+     * @param container
+     *            the container to make available
+     */
+    public RpcDataProviderExtension(Indexed container) {
+        this.container = container;
+
+        // TODO support for reacting to events from the container added later
+
+        registerRpc(new DataRequestRpc() {
+            @Override
+            public void requestRows(int firstRow, int numberOfRows) {
+                pushRows(firstRow, numberOfRows);
+            }
+        });
+
+        getState().containerSize = container.size();
+    }
+
+    private void pushRows(int firstRow, int numberOfRows) {
+        List<?> itemIds = container.getItemIds(firstRow, numberOfRows);
+        Collection<?> propertyIds = container.getContainerPropertyIds();
+        List<String[]> rows = new ArrayList<String[]>(itemIds.size());
+        for (Object itemId : itemIds) {
+            Item item = container.getItem(itemId);
+            String[] row = new String[propertyIds.size()];
+
+            int i = 0;
+            for (Object propertyId : propertyIds) {
+                Object value = item.getItemProperty(propertyId).getValue();
+                String stringValue = String.valueOf(value);
+                row[i++] = stringValue;
+            }
+
+            rows.add(row);
+        }
+
+        getRpcProxy(DataProviderRpc.class).setRowData(firstRow, rows);
+    }
+
+    @Override
+    protected DataProviderState getState() {
+        return (DataProviderState) super.getState();
+    }
+
+    /**
+     * Makes the data source available to the given {@link Grid} component.
+     * 
+     * @param component
+     *            the remote data grid component to extend
+     */
+    public void extend(Grid component) {
+        super.extend(component);
+    }
+
+}
index 2b19043d93d9c65663b3b97e7737ccfc0eda623e..79cc05e1a0078a406c41c1b494e9a2a039369864 100644 (file)
@@ -29,6 +29,7 @@ import com.vaadin.data.Container;
 import com.vaadin.data.Container.PropertySetChangeEvent;
 import com.vaadin.data.Container.PropertySetChangeListener;
 import com.vaadin.data.Container.PropertySetChangeNotifier;
+import com.vaadin.data.RpcDataProviderExtension;
 import com.vaadin.server.KeyMapper;
 import com.vaadin.shared.ui.grid.ColumnGroupRowState;
 import com.vaadin.shared.ui.grid.GridColumnState;
@@ -107,6 +108,8 @@ public class Grid extends AbstractComponent {
         }
     };
 
+    private RpcDataProviderExtension datasourceExtension;
+
     /**
      * Creates a new Grid using the given datasource.
      * 
@@ -140,7 +143,13 @@ public class Grid extends AbstractComponent {
                     .removePropertySetChangeListener(propertyListener);
         }
 
+        if (datasourceExtension != null) {
+            removeExtension(datasourceExtension);
+        }
+
         datasource = container;
+        datasourceExtension = new RpcDataProviderExtension(container);
+        datasourceExtension.extend(this);
 
         // Listen to changes in properties and remove columns if needed
         if (datasource instanceof PropertySetChangeNotifier) {
diff --git a/shared/src/com/vaadin/shared/data/DataProviderRpc.java b/shared/src/com/vaadin/shared/data/DataProviderRpc.java
new file mode 100644 (file)
index 0000000..7d82ecc
--- /dev/null
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2000-2013 Vaadin Ltd.
+ * 
+ * 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
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.vaadin.shared.data;
+
+import java.util.List;
+
+import com.vaadin.shared.communication.ClientRpc;
+
+/**
+ * RPC interface used for pushing container data to the client.
+ * 
+ * @since 7.2
+ * @author Vaadin Ltd
+ */
+public interface DataProviderRpc extends ClientRpc {
+
+    /**
+     * Sends updated row data to a client.
+     * 
+     * @param firstRowIndex
+     *            the index of the first updated row
+     * @param rowData
+     *            the updated row data
+     */
+    public void setRowData(int firstRowIndex, List<String[]> rowData);
+}
diff --git a/shared/src/com/vaadin/shared/data/DataProviderState.java b/shared/src/com/vaadin/shared/data/DataProviderState.java
new file mode 100644 (file)
index 0000000..2eabe0b
--- /dev/null
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2000-2013 Vaadin Ltd.
+ * 
+ * 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
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.vaadin.shared.data;
+
+import com.vaadin.shared.communication.SharedState;
+
+/**
+ * Shared state used by client-side data sources.
+ * 
+ * @since 7.2
+ * @author Vaadin Ltd
+ */
+public class DataProviderState extends SharedState {
+    /**
+     * The size of the container.
+     */
+    public int containerSize;
+}
diff --git a/shared/src/com/vaadin/shared/data/DataRequestRpc.java b/shared/src/com/vaadin/shared/data/DataRequestRpc.java
new file mode 100644 (file)
index 0000000..eaf17df
--- /dev/null
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2000-2013 Vaadin Ltd.
+ * 
+ * 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
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.vaadin.shared.data;
+
+import com.vaadin.shared.communication.ServerRpc;
+
+/**
+ * RPC interface used for requesting container data to the client.
+ * 
+ * @since 7.2
+ * @author Vaadin Ltd
+ */
+public interface DataRequestRpc extends ServerRpc {
+
+    /**
+     * Request rows from the server.
+     * 
+     * @param firstRowIndex
+     *            the index of the first requested row
+     * @param numberOfRows
+     *            the number of requested rows
+     */
+    public void requestRows(int firstRowIndex, int numberOfRows);
+}
index bd3e96f84a1c8704e6d4301e87bc7cae26f8f94f..7bf5d65e8be67607085c3b77ce41233816a75106 100644 (file)
@@ -17,6 +17,7 @@ package com.vaadin.tests.components.grid;
 
 import java.util.ArrayList;
 
+import com.vaadin.data.Item;
 import com.vaadin.data.util.IndexedContainer;
 import com.vaadin.tests.components.AbstractComponentTest;
 import com.vaadin.ui.components.grid.ColumnGroup;
@@ -36,6 +37,8 @@ public class GridBasicFeatures extends AbstractComponentTest<Grid> {
 
     private int columnGroupRows = 0;
 
+    private final int ROWS = 1000;
+
     @Override
     protected Grid constructComponent() {
 
@@ -46,6 +49,14 @@ public class GridBasicFeatures extends AbstractComponentTest<Grid> {
             ds.addContainerProperty("Column" + col, String.class, "");
         }
 
+        for (int row = 0; row < ROWS; row++) {
+            Item item = ds.addItem(Integer.valueOf(row));
+            for (int col = 0; col < COLUMNS; col++) {
+                item.getItemProperty("Column" + col).setValue(
+                        "(" + row + ", " + col + ")");
+            }
+        }
+
         // Create grid
         Grid grid = new Grid(ds);