diff options
author | Henrik Paul <henrik@vaadin.com> | 2014-10-01 15:19:27 +0300 |
---|---|---|
committer | Johannes Dahlström <johannesd@vaadin.com> | 2014-10-20 15:06:25 +0000 |
commit | 07c1bdfa889fe14048e8dcfd103d424d15c75bb3 (patch) | |
tree | 0fde4cd3dde27e5ba6ff6a63e85901ca7baca5a1 | |
parent | c1de8966d4b9bc7fa50eb27d19cc5142205a167d (diff) | |
download | vaadin-framework-07c1bdfa889fe14048e8dcfd103d424d15c75bb3.tar.gz vaadin-framework-07c1bdfa889fe14048e8dcfd103d424d15c75bb3.zip |
REST-like data source use case. (#13334)
Change-Id: Ib708f3d18ff38c2c293f179640b85baebaf69550
7 files changed, 489 insertions, 19 deletions
diff --git a/client/src/com/vaadin/client/data/AbstractRemoteDataSource.java b/client/src/com/vaadin/client/data/AbstractRemoteDataSource.java index cec956817b..45e1533662 100644 --- a/client/src/com/vaadin/client/data/AbstractRemoteDataSource.java +++ b/client/src/com/vaadin/client/data/AbstractRemoteDataSource.java @@ -399,6 +399,7 @@ public abstract class AbstractRemoteDataSource<T> implements DataSource<T> { cached = remainsBefore.combineWith(transposedRemainsAfter); } + assertDataChangeHandlerIsInjected(); dataChangeHandler.dataRemoved(firstRowIndex, count); checkCacheCoverage(); @@ -445,6 +446,7 @@ public abstract class AbstractRemoteDataSource<T> implements DataSource<T> { } } + assertDataChangeHandlerIsInjected(); dataChangeHandler.dataAdded(firstRowIndex, count); checkCacheCoverage(); @@ -586,6 +588,15 @@ public abstract class AbstractRemoteDataSource<T> implements DataSource<T> { protected void resetDataAndSize(int newSize) { dropFromCache(getCachedRange()); cached = Range.withLength(0, 0); + assertDataChangeHandlerIsInjected(); dataChangeHandler.resetDataAndSize(newSize); } + + private void assertDataChangeHandlerIsInjected() { + assert dataChangeHandler != null : "The dataChangeHandler was " + + "called before it was injected. Maybe you tried " + + "to manipulate the data in the DataSource's " + + "constructor instead of in overriding onAttach() " + + "and doing it there?"; + } } diff --git a/client/src/com/vaadin/client/ui/grid/Grid.java b/client/src/com/vaadin/client/ui/grid/Grid.java index 04e050fb2f..c19f5a7c09 100644 --- a/client/src/com/vaadin/client/ui/grid/Grid.java +++ b/client/src/com/vaadin/client/ui/grid/Grid.java @@ -1889,7 +1889,7 @@ public class Grid<T> extends ResizeComposite implements * @throws IllegalArgumentException * if <code>dataSource</code> is <code>null</code> */ - public void setDataSource(DataSource<T> dataSource) + public void setDataSource(final DataSource<T> dataSource) throws IllegalArgumentException { if (dataSource == null) { throw new IllegalArgumentException("dataSource can't be null."); @@ -1932,23 +1932,19 @@ public class Grid<T> extends ResizeComposite implements @Override public void resetDataAndSize(int newSize) { RowContainer body = escalator.getBody(); + int oldSize = body.getRowCount(); - /* - * Because the data has simply changed and we don't really know - * what, we'll simply remove everything and redraw everything. - */ + if (newSize > oldSize) { + body.insertRows(oldSize, newSize - oldSize); + } else if (newSize < oldSize) { + body.removeRows(newSize, oldSize - newSize); + } - double prevScroll = escalator.getScrollTop(); - body.removeRows(0, body.getRowCount()); - body.insertRows(0, newSize); + Range visibleRowRange = escalator.getVisibleRowRange(); + dataSource.ensureAvailability(visibleRowRange.getStart(), + visibleRowRange.length()); - /* - * If data was removed or inserted above the scroll top, the - * scroll position is kept locked, leading to data - * "sliding under us". But we can't do anything about that, - * since simply _something_ happened. - */ - escalator.setScrollTop(prevScroll); + assert body.getRowCount() == newSize; } }); diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridClientDataSources.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridClientDataSources.java new file mode 100644 index 0000000000..a307d4ce07 --- /dev/null +++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridClientDataSources.java @@ -0,0 +1,36 @@ +/* + * Copyright 2000-2014 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.tests.components.grid.basicfeatures; + +import com.vaadin.annotations.Widgetset; +import com.vaadin.server.VaadinRequest; +import com.vaadin.tests.widgetset.TestingWidgetSet; +import com.vaadin.ui.AbstractComponent; +import com.vaadin.ui.UI; + +@Widgetset(TestingWidgetSet.NAME) +public class GridClientDataSources extends UI { + + public static class GridClientDataSourcesComponent extends + AbstractComponent { + // empty + } + + @Override + protected void init(VaadinRequest request) { + setContent(new GridClientDataSourcesComponent()); + } +} diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridClientDataSourcesTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridClientDataSourcesTest.java new file mode 100644 index 0000000000..71173c3a4a --- /dev/null +++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridClientDataSourcesTest.java @@ -0,0 +1,176 @@ +/* + * Copyright 2000-2014 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.tests.components.grid.basicfeatures; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + +import org.junit.Before; +import org.junit.Test; +import org.openqa.selenium.By; +import org.openqa.selenium.Dimension; +import org.openqa.selenium.JavascriptExecutor; +import org.openqa.selenium.NoSuchElementException; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.interactions.Actions; + +import com.vaadin.tests.tb3.MultiBrowserTest; + +public class GridClientDataSourcesTest extends MultiBrowserTest { + + @Before + public void before() { + openTestURL(); + } + + @Test + public void normalRestishDatasource() throws Exception { + selectMenuPath("DataSources", "RESTish", "Use"); + assertCellPresent("cell 0 #0"); + + scrollToBottom(); + assertCellPresent("cell 99 #0"); + assertCellNotPresent("cell 100 #0"); + } + + @Test + public void growOnRequestRestishDatasource() throws Exception { + selectMenuPath("DataSources", "RESTish", "Use"); + selectMenuPath("DataSources", "RESTish", "Next request +10"); + + scrollToBottom(); + /* second scroll needed because of scrollsize change after scrolling */ + scrollToBottom(); + + assertCellPresent("cell 109 #1"); + assertCellNotPresent("cell 110 #1"); + } + + @Test + public void shrinkOnRequestRestishDatasource() throws Exception { + selectMenuPath("DataSources", "RESTish", "Use"); + scrollToBottom(); + + selectMenuPath("DataSources", "RESTish", "Next request -10"); + scrollToTop(); + + assertCellPresent("cell 0 #1"); + } + + @Test + public void pushChangeRestishDatasource() throws Exception { + selectMenuPath("DataSources", "RESTish", "Use"); + selectMenuPath("DataSources", "RESTish", "Push data change"); + assertCellPresent("cell 0 #1"); + assertCellNotPresent("cell 0 #0"); + } + + @Test + public void growOnPushRestishDatasource() throws Exception { + selectMenuPath("DataSources", "RESTish", "Use"); + selectMenuPath("DataSources", "RESTish", "Push data change +10"); + assertCellPresent("cell 0 #1"); + assertCellNotPresent("cell 0 #0"); + scrollToBottom(); + assertCellPresent("cell 109 #1"); + } + + @Test + public void shrinkOnPushRestishDatasource() throws Exception { + selectMenuPath("DataSources", "RESTish", "Use"); + scrollToBottom(); + + selectMenuPath("DataSources", "RESTish", "Push data change -10"); + assertCellPresent("cell 89 #1"); + assertCellNotPresent("cell 89 #0"); + assertCellNotPresent("cell 99 #1"); + assertCellNotPresent("cell 99 #0"); + } + + private void assertCellPresent(String content) { + assertNotNull(findByXPath("//td[text()='" + content + "']")); + } + + private void assertCellNotPresent(String content) { + assertNull(findByXPath("//td[text()='" + content + "']")); + } + + private void scrollToTop() { + scrollVerticallyTo(0); + } + + private void scrollToBottom() { + scrollVerticallyTo(9999); + } + + private WebElement findByXPath(String string) { + try { + return findElement(By.xpath(string)); + } catch (NoSuchElementException e) { + return null; + } + } + + private void scrollVerticallyTo(int px) { + executeScript("arguments[0].scrollTop = " + px, findVerticalScrollbar()); + } + + private Object executeScript(String script, Object args) { + @SuppressWarnings("hiding") + final WebDriver driver = getDriver(); + if (driver instanceof JavascriptExecutor) { + final JavascriptExecutor je = (JavascriptExecutor) driver; + return je.executeScript(script, args); + } else { + throw new IllegalStateException("current driver " + + getDriver().getClass().getName() + " is not a " + + JavascriptExecutor.class.getSimpleName()); + } + } + + private WebElement findVerticalScrollbar() { + return getDriver().findElement( + By.xpath("//div[contains(@class, " + + "\"v-grid-scroller-vertical\")]")); + } + + private void selectMenu(String menuCaption) { + WebElement menuElement = getMenuElement(menuCaption); + Dimension size = menuElement.getSize(); + new Actions(getDriver()).moveToElement(menuElement, size.width - 10, + size.height / 2).perform(); + } + + private WebElement getMenuElement(String menuCaption) { + return getDriver().findElement( + By.xpath("//td[text() = '" + menuCaption + "']")); + } + + private void selectMenuPath(String... menuCaptions) { + new Actions(getDriver()).moveToElement(getMenuElement(menuCaptions[0])) + .click().perform(); + for (int i = 1; i < menuCaptions.length - 1; ++i) { + selectMenu(menuCaptions[i]); + new Actions(getDriver()).moveByOffset(20, 0).perform(); + } + new Actions(getDriver()) + .moveToElement( + getMenuElement(menuCaptions[menuCaptions.length - 1])) + .click().perform(); + } + +} diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridSortingTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridSortingTest.java index e66b8b36d6..7b3e0b0dd4 100644 --- a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridSortingTest.java +++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridSortingTest.java @@ -128,7 +128,6 @@ public class GridSortingTest extends GridBasicFeaturesTest { // Sorting by column 9 is sorting by row index that is represented as a // String. - // First cells for first 3 rows are (9, 0), (99, 0) and (999, 0) // Click header twice to sort descending grid.getHeaderCell(0, 9).click(); @@ -136,12 +135,14 @@ public class GridSortingTest extends GridBasicFeaturesTest { grid.getHeaderCell(0, 9).click(); assertColumnsAreSortedAs(_(9, 1, SortDirection.DESCENDING)); + // First cells for first 3 rows are (9, 0), (99, 0) and (999, 0) String row = ""; for (int i = 0; i < 3; ++i) { row += "9"; - assertEquals( - "Grid is not sorted by Column 9 using descending direction.", - "(" + row + ", 0)", grid.getCell(i, 0).getText()); + String expected = "(" + row + ", 0)"; + String actual = grid.getCell(i, 0).getText(); + assertEquals("Grid is not sorted by Column 9" + + " using descending direction.", expected, actual); } // Column 10 is random numbers from Random with seed 13334 diff --git a/uitest/src/com/vaadin/tests/widgetset/client/grid/GridClientDataSourcesConnector.java b/uitest/src/com/vaadin/tests/widgetset/client/grid/GridClientDataSourcesConnector.java new file mode 100644 index 0000000000..157fa18b0e --- /dev/null +++ b/uitest/src/com/vaadin/tests/widgetset/client/grid/GridClientDataSourcesConnector.java @@ -0,0 +1,28 @@ +/* + * Copyright 2000-2014 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.tests.widgetset.client.grid; + +import com.vaadin.client.ui.AbstractComponentConnector; +import com.vaadin.shared.ui.Connect; +import com.vaadin.tests.components.grid.basicfeatures.GridClientDataSources.GridClientDataSourcesComponent; + +@Connect(GridClientDataSourcesComponent.class) +public class GridClientDataSourcesConnector extends AbstractComponentConnector { + @Override + public GridClientDataSourcesWidget getWidget() { + return (GridClientDataSourcesWidget) super.getWidget(); + } +} diff --git a/uitest/src/com/vaadin/tests/widgetset/client/grid/GridClientDataSourcesWidget.java b/uitest/src/com/vaadin/tests/widgetset/client/grid/GridClientDataSourcesWidget.java new file mode 100644 index 0000000000..2126d2d335 --- /dev/null +++ b/uitest/src/com/vaadin/tests/widgetset/client/grid/GridClientDataSourcesWidget.java @@ -0,0 +1,222 @@ +/* + * Copyright 2000-2014 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.tests.widgetset.client.grid; + +import java.util.ArrayList; +import java.util.List; + +import com.google.gwt.core.client.Scheduler.ScheduledCommand; +import com.vaadin.client.data.AbstractRemoteDataSource; +import com.vaadin.client.ui.grid.Grid; +import com.vaadin.client.ui.grid.Grid.SelectionMode; +import com.vaadin.client.ui.grid.GridColumn; +import com.vaadin.client.ui.grid.renderers.TextRenderer; + +public class GridClientDataSourcesWidget extends + PureGWTTestApplication<Grid<String[]>> { + + /** + * This is an emulated datasource that has a back-end that changes size + * constantly. The back-end is unable to actively push data to Grid. + * Instead, with each row request, in addition to its row payload it tells + * how many rows it contains in total. + * + * A plausible response from this REST-like api would be: + * + * <pre> + * <code> + * GET /foos/4..8 + * + * { + * "resultsize": 4, + * "data": [ + * [4, "foo IV"], + * [5, "foo V"], + * [6, "foo VI"] + * [7, "foo VII"] + * ], + * "totalrows": 100 + * } + * </code> + * </pre> + * + * In this case, the size of Grid needs to be updated to be able to show 100 + * rows in total (no more, no less). + * + * This class + * <ol> + * <li>gets initialized + * <li>asks for its size + * <li>updates Grid once the reply is received + * <li>as the Grid fetches more data, the total row count is dynamically + * updated. + * </ol> + */ + private class RestishDataSource extends AbstractRemoteDataSource<String[]> { + private int currentSize = 0; + + /** + * Pretend like this class doesn't exist. It just simulates a backend + * somewhere. + * <p> + * It's scoped inside the RDS class only because it's tied to that. + * */ + private class Backend { + public class Result { + public int size; + public List<String[]> rows; + } + + private int size = 100; + private int modCount = 0; + + public Result query(int firstRowIndex, int numberOfRows) { + Result result = new Result(); + result.size = size; + result.rows = getRows(firstRowIndex, numberOfRows); + return result; + } + + private List<String[]> getRows(int firstRowIndex, int numberOfRows) { + List<String[]> rows = new ArrayList<String[]>(); + for (int i = 0; i < numberOfRows; i++) { + String id = String.valueOf(firstRowIndex + i); + rows.add(new String[] { id, "cell " + id + " #" + modCount }); + } + return rows; + } + + public void pushRowChanges(int rows) { + size += rows; + pushRowChanges(); + } + + public void pushRowChanges() { + modCount++; + + // push "something happened" to datasource "over the wire": + resetDataAndSize(size); + } + + public void addRows(int rowcount) { + modCount++; + size += rowcount; + } + } + + final Backend backend; + + public RestishDataSource() { + backend = new Backend(); + currentSize = backend.size; + } + + @Override + public int size() { + return currentSize; + } + + @Override + protected void requestRows(int firstRowIndex, int numberOfRows) { + Backend.Result result = backend.query(firstRowIndex, numberOfRows); + final List<String[]> newRows = result.rows; + + // order matters: first set row data, only then modify size. + + /* Here's the requested data. Process it, please. */ + setRowData(firstRowIndex, newRows); + + /* Let's check whether the backend size changed. */ + if (currentSize != result.size) { + currentSize = result.size; + resetDataAndSize(currentSize); + } + } + + @Override + public Object getRowKey(String[] row) { + return row[0]; + } + } + + private final Grid<String[]> grid; + + private RestishDataSource restishDataSource; + + private final ScheduledCommand setRestishCommand = new ScheduledCommand() { + @Override + public void execute() { + for (GridColumn<?, String[]> column : grid.getColumns()) { + grid.removeColumn(column); + } + + restishDataSource = new RestishDataSource(); + grid.setDataSource(restishDataSource); + grid.addColumn(new GridColumn<String, String[]>(new TextRenderer()) { + { + setHeader("column"); + } + + @Override + public String getValue(String[] row) { + return row[1]; + } + }); + } + }; + + public GridClientDataSourcesWidget() { + super(new Grid<String[]>()); + grid = getTestedWidget(); + + grid.getElement().getStyle().setZIndex(0); + grid.setHeight("400px"); + grid.setSelectionMode(SelectionMode.NONE); + addNorth(grid, 400); + + addMenuCommand("Use", setRestishCommand, "DataSources", "RESTish"); + addMenuCommand("Next request +10", new ScheduledCommand() { + @Override + public void execute() { + restishDataSource.backend.addRows(10); + } + }, "DataSources", "RESTish"); + addMenuCommand("Next request -10", new ScheduledCommand() { + @Override + public void execute() { + restishDataSource.backend.addRows(-10); + } + }, "DataSources", "RESTish"); + addMenuCommand("Push data change", new ScheduledCommand() { + @Override + public void execute() { + restishDataSource.backend.pushRowChanges(); + } + }, "DataSources", "RESTish"); + addMenuCommand("Push data change +10", new ScheduledCommand() { + @Override + public void execute() { + restishDataSource.backend.pushRowChanges(10); + } + }, "DataSources", "RESTish"); + addMenuCommand("Push data change -10", new ScheduledCommand() { + @Override + public void execute() { + restishDataSource.backend.pushRowChanges(-10); + } + }, "DataSources", "RESTish"); + } +} |