From 2c7a984542c70641db954f197d3ba1f2f0d7488c Mon Sep 17 00:00:00 2001 From: Teemu Suo-Anttila Date: Wed, 7 Oct 2015 13:40:29 +0300 Subject: Move RpcDataProviderExtension and DataGenerator to correct package New location is com.vaadin.server.communication.data as the extension only manages the communication of container data to a client-side data source. Change-Id: I7aeefe23c9d771d70bc1dd389bc7f0c3357f0a17 --- server/src/com/vaadin/data/DataGenerator.java | 57 -- .../com/vaadin/data/RpcDataProviderExtension.java | 590 -------------------- .../server/communication/data/DataGenerator.java | 58 ++ .../data/RpcDataProviderExtension.java | 593 +++++++++++++++++++++ server/src/com/vaadin/ui/Grid.java | 4 +- .../tests/server/component/grid/TestGrid.java | 2 +- 6 files changed, 654 insertions(+), 650 deletions(-) delete mode 100644 server/src/com/vaadin/data/DataGenerator.java delete mode 100644 server/src/com/vaadin/data/RpcDataProviderExtension.java create mode 100644 server/src/com/vaadin/server/communication/data/DataGenerator.java create mode 100644 server/src/com/vaadin/server/communication/data/RpcDataProviderExtension.java (limited to 'server') diff --git a/server/src/com/vaadin/data/DataGenerator.java b/server/src/com/vaadin/data/DataGenerator.java deleted file mode 100644 index fd64a389b8..0000000000 --- a/server/src/com/vaadin/data/DataGenerator.java +++ /dev/null @@ -1,57 +0,0 @@ -/* - * 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.data; - -import java.io.Serializable; - -import com.vaadin.ui.Grid.AbstractGridExtension; - -import elemental.json.JsonObject; - -/** - * Interface for {@link AbstractGridExtension}s that allows adding data to row - * objects being sent to client by the {@link RpcDataProviderExtension}. - *

- * This class also provides a way to remove any unneeded data once the data - * object is no longer used on the client-side. - * - * @since 7.6 - * @author Vaadin Ltd - */ -public interface DataGenerator extends Serializable { - - /** - * Adds data to row object for given item and item id being sent to client. - * - * @param itemId - * item id of item - * @param item - * item being sent to client - * @param rowData - * row object being sent to client - */ - public void generateData(Object itemId, Item item, JsonObject rowData); - - /** - * Informs the DataGenerator that an item id has been dropped and is no - * longer needed. This method should clean up any unneeded stored data - * related to the item. - * - * @param itemId - * removed item id - */ - public void destroyData(Object itemId); -} diff --git a/server/src/com/vaadin/data/RpcDataProviderExtension.java b/server/src/com/vaadin/data/RpcDataProviderExtension.java deleted file mode 100644 index 293940aec7..0000000000 --- a/server/src/com/vaadin/data/RpcDataProviderExtension.java +++ /dev/null @@ -1,590 +0,0 @@ -/* - * 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.data; - -import java.io.Serializable; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; -import java.util.HashSet; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; - -import com.vaadin.data.Container.Indexed; -import com.vaadin.data.Container.Indexed.ItemAddEvent; -import com.vaadin.data.Container.Indexed.ItemRemoveEvent; -import com.vaadin.data.Container.ItemSetChangeEvent; -import com.vaadin.data.Container.ItemSetChangeListener; -import com.vaadin.data.Container.ItemSetChangeNotifier; -import com.vaadin.data.Property.ValueChangeEvent; -import com.vaadin.data.Property.ValueChangeListener; -import com.vaadin.data.Property.ValueChangeNotifier; -import com.vaadin.server.AbstractExtension; -import com.vaadin.server.ClientConnector; -import com.vaadin.server.KeyMapper; -import com.vaadin.shared.data.DataProviderRpc; -import com.vaadin.shared.data.DataRequestRpc; -import com.vaadin.shared.ui.grid.GridState; -import com.vaadin.shared.ui.grid.Range; -import com.vaadin.ui.Grid; -import com.vaadin.ui.Grid.Column; - -import elemental.json.Json; -import elemental.json.JsonArray; -import elemental.json.JsonObject; - -/** - * 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.4 - * @author Vaadin Ltd - */ -public class RpcDataProviderExtension extends AbstractExtension { - - /** - * Class for keeping track of current items and ValueChangeListeners. - * - * @since 7.6 - */ - private class ActiveItemHandler implements Serializable, DataGenerator { - - private final Map activeItemMap = new HashMap(); - private final KeyMapper keyMapper = new KeyMapper(); - private final Set droppedItems = new HashSet(); - - /** - * Registers ValueChangeListeners for given item ids. - *

- * Note: This method will clean up any unneeded listeners and key - * mappings - * - * @param itemIds - * collection of new active item ids - */ - public void addActiveItems(Collection itemIds) { - for (Object itemId : itemIds) { - if (!activeItemMap.containsKey(itemId)) { - activeItemMap.put(itemId, new GridValueChangeListener( - itemId, container.getItem(itemId))); - } - } - - // Remove still active rows that were "dropped" - droppedItems.removeAll(itemIds); - internalDropItems(droppedItems); - droppedItems.clear(); - } - - /** - * Marks given item id as dropped. Dropped items are cleared when adding - * new active items. - * - * @param itemId - * dropped item id - */ - public void dropActiveItem(Object itemId) { - if (activeItemMap.containsKey(itemId)) { - droppedItems.add(itemId); - } - } - - /** - * Gets a collection copy of currently active item ids. - * - * @return collection of item ids - */ - public Collection getActiveItemIds() { - return new HashSet(activeItemMap.keySet()); - } - - /** - * Gets a collection copy of currently active ValueChangeListeners. - * - * @return collection of value change listeners - */ - public Collection getValueChangeListeners() { - return new HashSet(activeItemMap.values()); - } - - @Override - public void generateData(Object itemId, Item item, JsonObject rowData) { - rowData.put(GridState.JSONKEY_ROWKEY, keyMapper.key(itemId)); - } - - @Override - public void destroyData(Object itemId) { - keyMapper.remove(itemId); - GridValueChangeListener removed = activeItemMap.remove(itemId); - - if (removed != null) { - removed.removeListener(); - } - } - } - - /** - * A class to listen to changes in property values in the Container added - * with {@link Grid#setContainerDatasource(Container.Indexed)}, and notifies - * the data source to update the client-side representation of the modified - * item. - *

- * One instance of this class can (and should) be reused for all the - * properties in an item, since this class will inform that the entire row - * needs to be re-evaluated (in contrast to a property-based change - * management) - *

- * Since there's no Container-wide possibility to listen to any kind of - * value changes, an instance of this class needs to be attached to each and - * every Item's Property in the container. - * - * @see Grid#addValueChangeListener(Container, Object, Object) - * @see Grid#valueChangeListeners - */ - private class GridValueChangeListener implements ValueChangeListener { - private final Object itemId; - private final Item item; - - public GridValueChangeListener(Object itemId, Item item) { - /* - * Using an assert instead of an exception throw, just to optimize - * prematurely - */ - assert itemId != null : "null itemId not accepted"; - this.itemId = itemId; - this.item = item; - - internalAddColumns(getGrid().getColumns()); - } - - @Override - public void valueChange(ValueChangeEvent event) { - updateRowData(itemId); - } - - public void removeListener() { - removeColumns(getGrid().getColumns()); - } - - public void addColumns(Collection addedColumns) { - internalAddColumns(addedColumns); - updateRowData(itemId); - } - - private void internalAddColumns(Collection addedColumns) { - for (final Column column : addedColumns) { - final Property property = item.getItemProperty(column - .getPropertyId()); - if (property instanceof ValueChangeNotifier) { - ((ValueChangeNotifier) property) - .addValueChangeListener(this); - } - } - } - - public void removeColumns(Collection removedColumns) { - for (final Column column : removedColumns) { - final Property property = item.getItemProperty(column - .getPropertyId()); - if (property instanceof ValueChangeNotifier) { - ((ValueChangeNotifier) property) - .removeValueChangeListener(this); - } - } - } - } - - private final Indexed container; - - private DataProviderRpc rpc; - - private final ItemSetChangeListener itemListener = new ItemSetChangeListener() { - @Override - public void containerItemSetChange(ItemSetChangeEvent event) { - - if (event instanceof ItemAddEvent) { - ItemAddEvent addEvent = (ItemAddEvent) event; - int firstIndex = addEvent.getFirstIndex(); - int count = addEvent.getAddedItemsCount(); - insertRowData(firstIndex, count); - } - - else if (event instanceof ItemRemoveEvent) { - ItemRemoveEvent removeEvent = (ItemRemoveEvent) event; - int firstIndex = removeEvent.getFirstIndex(); - int count = removeEvent.getRemovedItemsCount(); - removeRowData(firstIndex, count); - } - - else { - /* Mark as dirty to push changes in beforeClientResponse */ - bareItemSetTriggeredSizeChange = true; - markAsDirty(); - } - } - }; - - /** RpcDataProvider should send the current cache again. */ - private boolean refreshCache = false; - - /** Set of updated item ids */ - private Set updatedItemIds = new LinkedHashSet(); - - /** - * Queued RPC calls for adding and removing rows. Queue will be handled in - * {@link beforeClientResponse} - */ - private List rowChanges = new ArrayList(); - - /** Size possibly changed with a bare ItemSetChangeEvent */ - private boolean bareItemSetTriggeredSizeChange = false; - - private final Set dataGenerators = new LinkedHashSet(); - - private final ActiveItemHandler activeItemHandler = new ActiveItemHandler(); - - /** - * Creates a new data provider using the given container. - * - * @param container - * the container to make available - */ - public RpcDataProviderExtension(Indexed container) { - this.container = container; - rpc = getRpcProxy(DataProviderRpc.class); - - registerRpc(new DataRequestRpc() { - @Override - public void requestRows(int firstRow, int numberOfRows, - int firstCachedRowIndex, int cacheSize) { - pushRowData(firstRow, numberOfRows, firstCachedRowIndex, - cacheSize); - } - - @Override - public void dropRows(JsonArray rowKeys) { - for (int i = 0; i < rowKeys.length(); ++i) { - activeItemHandler.dropActiveItem(getKeyMapper().get( - rowKeys.getString(i))); - } - } - }); - - if (container instanceof ItemSetChangeNotifier) { - ((ItemSetChangeNotifier) container) - .addItemSetChangeListener(itemListener); - } - - addDataGenerator(activeItemHandler); - } - - /** - * {@inheritDoc} - *

- * RpcDataProviderExtension makes all actual RPC calls from this function - * based on changes in the container. - */ - @Override - public void beforeClientResponse(boolean initial) { - if (initial || bareItemSetTriggeredSizeChange) { - /* - * Push initial set of rows, assuming Grid will initially be - * rendered scrolled to the top and with a decent amount of rows - * visible. If this guess is right, initial data can be shown - * without a round-trip and if it's wrong, the data will simply be - * discarded. - */ - int size = container.size(); - rpc.resetDataAndSize(size); - - int numberOfRows = Math.min(40, size); - pushRowData(0, numberOfRows, 0, 0); - } else { - // Only do row changes if not initial response. - for (Runnable r : rowChanges) { - r.run(); - } - - // Send current rows again if needed. - if (refreshCache) { - updatedItemIds.addAll(activeItemHandler.getActiveItemIds()); - } - } - - internalUpdateRows(updatedItemIds); - - // Clear all changes. - rowChanges.clear(); - refreshCache = false; - updatedItemIds.clear(); - bareItemSetTriggeredSizeChange = false; - - super.beforeClientResponse(initial); - } - - private void pushRowData(int firstRowToPush, int numberOfRows, - int firstCachedRowIndex, int cacheSize) { - Range newRange = Range.withLength(firstRowToPush, numberOfRows); - Range cached = Range.withLength(firstCachedRowIndex, cacheSize); - Range fullRange = newRange; - if (!cached.isEmpty()) { - fullRange = newRange.combineWith(cached); - } - - List itemIds = container.getItemIds(fullRange.getStart(), - fullRange.length()); - - JsonArray rows = Json.createArray(); - - // Offset the index to match the wanted range. - int diff = 0; - if (!cached.isEmpty() && newRange.getStart() > cached.getStart()) { - diff = cached.length(); - } - - for (int i = 0; i < newRange.length() && i + diff < itemIds.size(); ++i) { - Object itemId = itemIds.get(i + diff); - - rows.set(i, getRowData(getGrid().getColumns(), itemId)); - } - rpc.setRowData(firstRowToPush, rows); - - activeItemHandler.addActiveItems(itemIds); - } - - private JsonObject getRowData(Collection columns, Object itemId) { - Item item = container.getItem(itemId); - - final JsonObject rowObject = Json.createObject(); - for (DataGenerator dg : dataGenerators) { - dg.generateData(itemId, item, rowObject); - } - - return rowObject; - } - - /** - * Makes the data source available to the given {@link Grid} component. - * - * @param component - * the remote data grid component to extend - * @param columnKeys - * the key mapper for columns - */ - public void extend(Grid component) { - super.extend(component); - } - - /** - * Adds a {@link DataGenerator} for this {@code RpcDataProviderExtension}. - * DataGenerators are called when sending row data to client. If given - * DataGenerator is already added, this method does nothing. - * - * @since 7.6 - * @param generator - * generator to add - */ - public void addDataGenerator(DataGenerator generator) { - dataGenerators.add(generator); - } - - /** - * Removes a {@link DataGenerator} from this - * {@code RpcDataProviderExtension}. If given DataGenerator is not added to - * this data provider, this method does nothing. - * - * @since 7.6 - * @param generator - * generator to remove - */ - public void removeDataGenerator(DataGenerator generator) { - dataGenerators.remove(generator); - } - - /** - * Informs the client side that new rows have been inserted into the data - * source. - * - * @param index - * the index at which new rows have been inserted - * @param count - * the number of rows inserted at index - */ - private void insertRowData(final int index, final int count) { - if (rowChanges.isEmpty()) { - markAsDirty(); - } - - /* - * Since all changes should be processed in a consistent order, we don't - * send the RPC call immediately. beforeClientResponse will decide - * whether to send these or not. Valid situation to not send these is - * initial response or bare ItemSetChange event. - */ - rowChanges.add(new Runnable() { - @Override - public void run() { - rpc.insertRowData(index, count); - } - }); - } - - /** - * Informs the client side that rows have been removed from the data source. - * - * @param index - * the index of the first row removed - * @param count - * the number of rows removed - * @param firstItemId - * the item id of the first removed item - */ - private void removeRowData(final int index, final int count) { - if (rowChanges.isEmpty()) { - markAsDirty(); - } - - /* See comment in insertRowData */ - rowChanges.add(new Runnable() { - @Override - public void run() { - rpc.removeRowData(index, count); - } - }); - } - - /** - * Informs the client side that data of a row has been modified in the data - * source. - * - * @param itemId - * the item Id the row that was updated - */ - public void updateRowData(Object itemId) { - if (updatedItemIds.isEmpty()) { - // At least one new item will be updated. Mark as dirty to actually - // update before response to client. - markAsDirty(); - } - - updatedItemIds.add(itemId); - } - - private void internalUpdateRows(Set itemIds) { - if (itemIds.isEmpty()) { - return; - } - - JsonArray rowData = Json.createArray(); - int i = 0; - for (Object itemId : itemIds) { - if (activeItemHandler.getActiveItemIds().contains(itemId)) { - JsonObject row = getRowData(getGrid().getColumns(), itemId); - rowData.set(i++, row); - } - } - rpc.updateRowData(rowData); - } - - /** - * Pushes a new version of all the rows in the active cache range. - */ - public void refreshCache() { - if (!refreshCache) { - refreshCache = true; - markAsDirty(); - } - } - - @Override - public void setParent(ClientConnector parent) { - if (parent == null) { - // We're being detached, release various listeners - internalDropItems(activeItemHandler.getActiveItemIds()); - - if (container instanceof ItemSetChangeNotifier) { - ((ItemSetChangeNotifier) container) - .removeItemSetChangeListener(itemListener); - } - - } else if (!(parent instanceof Grid)) { - throw new IllegalStateException( - "Grid is the only accepted parent type"); - } - super.setParent(parent); - } - - /** - * Informs all DataGenerators than an item id has been dropped. - * - * @param droppedItemIds - * collection of dropped item ids - */ - private void internalDropItems(Collection droppedItemIds) { - for (Object itemId : droppedItemIds) { - for (DataGenerator generator : dataGenerators) { - generator.destroyData(itemId); - } - } - } - - /** - * Informs this data provider that given columns have been removed from - * grid. - * - * @param removedColumns - * a list of removed columns - */ - public void columnsRemoved(List removedColumns) { - for (GridValueChangeListener l : activeItemHandler - .getValueChangeListeners()) { - l.removeColumns(removedColumns); - } - - // No need to resend unchanged data. Client will remember the old - // columns until next set of rows is sent. - } - - /** - * Informs this data provider that given columns have been added to grid. - * - * @param addedColumns - * a list of added columns - */ - public void columnsAdded(List addedColumns) { - for (GridValueChangeListener l : activeItemHandler - .getValueChangeListeners()) { - l.addColumns(addedColumns); - } - - // Resend all rows to contain new data. - refreshCache(); - } - - public KeyMapper getKeyMapper() { - return activeItemHandler.keyMapper; - } - - protected Grid getGrid() { - return (Grid) getParent(); - } -} diff --git a/server/src/com/vaadin/server/communication/data/DataGenerator.java b/server/src/com/vaadin/server/communication/data/DataGenerator.java new file mode 100644 index 0000000000..58ba1b3b00 --- /dev/null +++ b/server/src/com/vaadin/server/communication/data/DataGenerator.java @@ -0,0 +1,58 @@ +/* + * 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.server.communication.data; + +import java.io.Serializable; + +import com.vaadin.data.Item; +import com.vaadin.ui.Grid.AbstractGridExtension; + +import elemental.json.JsonObject; + +/** + * Interface for {@link AbstractGridExtension}s that allows adding data to row + * objects being sent to client by the {@link RpcDataProviderExtension}. + *

+ * This class also provides a way to remove any unneeded data once the data + * object is no longer used on the client-side. + * + * @since 7.6 + * @author Vaadin Ltd + */ +public interface DataGenerator extends Serializable { + + /** + * Adds data to row object for given item and item id being sent to client. + * + * @param itemId + * item id of item + * @param item + * item being sent to client + * @param rowData + * row object being sent to client + */ + public void generateData(Object itemId, Item item, JsonObject rowData); + + /** + * Informs the DataGenerator that an item id has been dropped and is no + * longer needed. This method should clean up any unneeded stored data + * related to the item. + * + * @param itemId + * removed item id + */ + public void destroyData(Object itemId); +} diff --git a/server/src/com/vaadin/server/communication/data/RpcDataProviderExtension.java b/server/src/com/vaadin/server/communication/data/RpcDataProviderExtension.java new file mode 100644 index 0000000000..e3c48f1ec0 --- /dev/null +++ b/server/src/com/vaadin/server/communication/data/RpcDataProviderExtension.java @@ -0,0 +1,593 @@ +/* + * 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.server.communication.data; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import com.vaadin.data.Container; +import com.vaadin.data.Container.Indexed; +import com.vaadin.data.Container.Indexed.ItemAddEvent; +import com.vaadin.data.Container.Indexed.ItemRemoveEvent; +import com.vaadin.data.Container.ItemSetChangeEvent; +import com.vaadin.data.Container.ItemSetChangeListener; +import com.vaadin.data.Container.ItemSetChangeNotifier; +import com.vaadin.data.Item; +import com.vaadin.data.Property; +import com.vaadin.data.Property.ValueChangeEvent; +import com.vaadin.data.Property.ValueChangeListener; +import com.vaadin.data.Property.ValueChangeNotifier; +import com.vaadin.server.AbstractExtension; +import com.vaadin.server.ClientConnector; +import com.vaadin.server.KeyMapper; +import com.vaadin.shared.data.DataProviderRpc; +import com.vaadin.shared.data.DataRequestRpc; +import com.vaadin.shared.ui.grid.GridState; +import com.vaadin.shared.ui.grid.Range; +import com.vaadin.ui.Grid; +import com.vaadin.ui.Grid.Column; + +import elemental.json.Json; +import elemental.json.JsonArray; +import elemental.json.JsonObject; + +/** + * 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.4 + * @author Vaadin Ltd + */ +public class RpcDataProviderExtension extends AbstractExtension { + + /** + * Class for keeping track of current items and ValueChangeListeners. + * + * @since 7.6 + */ + private class ActiveItemHandler implements Serializable, DataGenerator { + + private final Map activeItemMap = new HashMap(); + private final KeyMapper keyMapper = new KeyMapper(); + private final Set droppedItems = new HashSet(); + + /** + * Registers ValueChangeListeners for given item ids. + *

+ * Note: This method will clean up any unneeded listeners and key + * mappings + * + * @param itemIds + * collection of new active item ids + */ + public void addActiveItems(Collection itemIds) { + for (Object itemId : itemIds) { + if (!activeItemMap.containsKey(itemId)) { + activeItemMap.put(itemId, new GridValueChangeListener( + itemId, container.getItem(itemId))); + } + } + + // Remove still active rows that were "dropped" + droppedItems.removeAll(itemIds); + internalDropItems(droppedItems); + droppedItems.clear(); + } + + /** + * Marks given item id as dropped. Dropped items are cleared when adding + * new active items. + * + * @param itemId + * dropped item id + */ + public void dropActiveItem(Object itemId) { + if (activeItemMap.containsKey(itemId)) { + droppedItems.add(itemId); + } + } + + /** + * Gets a collection copy of currently active item ids. + * + * @return collection of item ids + */ + public Collection getActiveItemIds() { + return new HashSet(activeItemMap.keySet()); + } + + /** + * Gets a collection copy of currently active ValueChangeListeners. + * + * @return collection of value change listeners + */ + public Collection getValueChangeListeners() { + return new HashSet(activeItemMap.values()); + } + + @Override + public void generateData(Object itemId, Item item, JsonObject rowData) { + rowData.put(GridState.JSONKEY_ROWKEY, keyMapper.key(itemId)); + } + + @Override + public void destroyData(Object itemId) { + keyMapper.remove(itemId); + GridValueChangeListener removed = activeItemMap.remove(itemId); + + if (removed != null) { + removed.removeListener(); + } + } + } + + /** + * A class to listen to changes in property values in the Container added + * with {@link Grid#setContainerDatasource(Container.Indexed)}, and notifies + * the data source to update the client-side representation of the modified + * item. + *

+ * One instance of this class can (and should) be reused for all the + * properties in an item, since this class will inform that the entire row + * needs to be re-evaluated (in contrast to a property-based change + * management) + *

+ * Since there's no Container-wide possibility to listen to any kind of + * value changes, an instance of this class needs to be attached to each and + * every Item's Property in the container. + * + * @see Grid#addValueChangeListener(Container, Object, Object) + * @see Grid#valueChangeListeners + */ + private class GridValueChangeListener implements ValueChangeListener { + private final Object itemId; + private final Item item; + + public GridValueChangeListener(Object itemId, Item item) { + /* + * Using an assert instead of an exception throw, just to optimize + * prematurely + */ + assert itemId != null : "null itemId not accepted"; + this.itemId = itemId; + this.item = item; + + internalAddColumns(getGrid().getColumns()); + } + + @Override + public void valueChange(ValueChangeEvent event) { + updateRowData(itemId); + } + + public void removeListener() { + removeColumns(getGrid().getColumns()); + } + + public void addColumns(Collection addedColumns) { + internalAddColumns(addedColumns); + updateRowData(itemId); + } + + private void internalAddColumns(Collection addedColumns) { + for (final Column column : addedColumns) { + final Property property = item.getItemProperty(column + .getPropertyId()); + if (property instanceof ValueChangeNotifier) { + ((ValueChangeNotifier) property) + .addValueChangeListener(this); + } + } + } + + public void removeColumns(Collection removedColumns) { + for (final Column column : removedColumns) { + final Property property = item.getItemProperty(column + .getPropertyId()); + if (property instanceof ValueChangeNotifier) { + ((ValueChangeNotifier) property) + .removeValueChangeListener(this); + } + } + } + } + + private final Indexed container; + + private DataProviderRpc rpc; + + private final ItemSetChangeListener itemListener = new ItemSetChangeListener() { + @Override + public void containerItemSetChange(ItemSetChangeEvent event) { + + if (event instanceof ItemAddEvent) { + ItemAddEvent addEvent = (ItemAddEvent) event; + int firstIndex = addEvent.getFirstIndex(); + int count = addEvent.getAddedItemsCount(); + insertRowData(firstIndex, count); + } + + else if (event instanceof ItemRemoveEvent) { + ItemRemoveEvent removeEvent = (ItemRemoveEvent) event; + int firstIndex = removeEvent.getFirstIndex(); + int count = removeEvent.getRemovedItemsCount(); + removeRowData(firstIndex, count); + } + + else { + /* Mark as dirty to push changes in beforeClientResponse */ + bareItemSetTriggeredSizeChange = true; + markAsDirty(); + } + } + }; + + /** RpcDataProvider should send the current cache again. */ + private boolean refreshCache = false; + + /** Set of updated item ids */ + private Set updatedItemIds = new LinkedHashSet(); + + /** + * Queued RPC calls for adding and removing rows. Queue will be handled in + * {@link beforeClientResponse} + */ + private List rowChanges = new ArrayList(); + + /** Size possibly changed with a bare ItemSetChangeEvent */ + private boolean bareItemSetTriggeredSizeChange = false; + + private final Set dataGenerators = new LinkedHashSet(); + + private final ActiveItemHandler activeItemHandler = new ActiveItemHandler(); + + /** + * Creates a new data provider using the given container. + * + * @param container + * the container to make available + */ + public RpcDataProviderExtension(Indexed container) { + this.container = container; + rpc = getRpcProxy(DataProviderRpc.class); + + registerRpc(new DataRequestRpc() { + @Override + public void requestRows(int firstRow, int numberOfRows, + int firstCachedRowIndex, int cacheSize) { + pushRowData(firstRow, numberOfRows, firstCachedRowIndex, + cacheSize); + } + + @Override + public void dropRows(JsonArray rowKeys) { + for (int i = 0; i < rowKeys.length(); ++i) { + activeItemHandler.dropActiveItem(getKeyMapper().get( + rowKeys.getString(i))); + } + } + }); + + if (container instanceof ItemSetChangeNotifier) { + ((ItemSetChangeNotifier) container) + .addItemSetChangeListener(itemListener); + } + + addDataGenerator(activeItemHandler); + } + + /** + * {@inheritDoc} + *

+ * RpcDataProviderExtension makes all actual RPC calls from this function + * based on changes in the container. + */ + @Override + public void beforeClientResponse(boolean initial) { + if (initial || bareItemSetTriggeredSizeChange) { + /* + * Push initial set of rows, assuming Grid will initially be + * rendered scrolled to the top and with a decent amount of rows + * visible. If this guess is right, initial data can be shown + * without a round-trip and if it's wrong, the data will simply be + * discarded. + */ + int size = container.size(); + rpc.resetDataAndSize(size); + + int numberOfRows = Math.min(40, size); + pushRowData(0, numberOfRows, 0, 0); + } else { + // Only do row changes if not initial response. + for (Runnable r : rowChanges) { + r.run(); + } + + // Send current rows again if needed. + if (refreshCache) { + updatedItemIds.addAll(activeItemHandler.getActiveItemIds()); + } + } + + internalUpdateRows(updatedItemIds); + + // Clear all changes. + rowChanges.clear(); + refreshCache = false; + updatedItemIds.clear(); + bareItemSetTriggeredSizeChange = false; + + super.beforeClientResponse(initial); + } + + private void pushRowData(int firstRowToPush, int numberOfRows, + int firstCachedRowIndex, int cacheSize) { + Range newRange = Range.withLength(firstRowToPush, numberOfRows); + Range cached = Range.withLength(firstCachedRowIndex, cacheSize); + Range fullRange = newRange; + if (!cached.isEmpty()) { + fullRange = newRange.combineWith(cached); + } + + List itemIds = container.getItemIds(fullRange.getStart(), + fullRange.length()); + + JsonArray rows = Json.createArray(); + + // Offset the index to match the wanted range. + int diff = 0; + if (!cached.isEmpty() && newRange.getStart() > cached.getStart()) { + diff = cached.length(); + } + + for (int i = 0; i < newRange.length() && i + diff < itemIds.size(); ++i) { + Object itemId = itemIds.get(i + diff); + + rows.set(i, getRowData(getGrid().getColumns(), itemId)); + } + rpc.setRowData(firstRowToPush, rows); + + activeItemHandler.addActiveItems(itemIds); + } + + private JsonObject getRowData(Collection columns, Object itemId) { + Item item = container.getItem(itemId); + + final JsonObject rowObject = Json.createObject(); + for (DataGenerator dg : dataGenerators) { + dg.generateData(itemId, item, rowObject); + } + + return rowObject; + } + + /** + * Makes the data source available to the given {@link Grid} component. + * + * @param component + * the remote data grid component to extend + * @param columnKeys + * the key mapper for columns + */ + public void extend(Grid component) { + super.extend(component); + } + + /** + * Adds a {@link DataGenerator} for this {@code RpcDataProviderExtension}. + * DataGenerators are called when sending row data to client. If given + * DataGenerator is already added, this method does nothing. + * + * @since 7.6 + * @param generator + * generator to add + */ + public void addDataGenerator(DataGenerator generator) { + dataGenerators.add(generator); + } + + /** + * Removes a {@link DataGenerator} from this + * {@code RpcDataProviderExtension}. If given DataGenerator is not added to + * this data provider, this method does nothing. + * + * @since 7.6 + * @param generator + * generator to remove + */ + public void removeDataGenerator(DataGenerator generator) { + dataGenerators.remove(generator); + } + + /** + * Informs the client side that new rows have been inserted into the data + * source. + * + * @param index + * the index at which new rows have been inserted + * @param count + * the number of rows inserted at index + */ + private void insertRowData(final int index, final int count) { + if (rowChanges.isEmpty()) { + markAsDirty(); + } + + /* + * Since all changes should be processed in a consistent order, we don't + * send the RPC call immediately. beforeClientResponse will decide + * whether to send these or not. Valid situation to not send these is + * initial response or bare ItemSetChange event. + */ + rowChanges.add(new Runnable() { + @Override + public void run() { + rpc.insertRowData(index, count); + } + }); + } + + /** + * Informs the client side that rows have been removed from the data source. + * + * @param index + * the index of the first row removed + * @param count + * the number of rows removed + * @param firstItemId + * the item id of the first removed item + */ + private void removeRowData(final int index, final int count) { + if (rowChanges.isEmpty()) { + markAsDirty(); + } + + /* See comment in insertRowData */ + rowChanges.add(new Runnable() { + @Override + public void run() { + rpc.removeRowData(index, count); + } + }); + } + + /** + * Informs the client side that data of a row has been modified in the data + * source. + * + * @param itemId + * the item Id the row that was updated + */ + public void updateRowData(Object itemId) { + if (updatedItemIds.isEmpty()) { + // At least one new item will be updated. Mark as dirty to actually + // update before response to client. + markAsDirty(); + } + + updatedItemIds.add(itemId); + } + + private void internalUpdateRows(Set itemIds) { + if (itemIds.isEmpty()) { + return; + } + + JsonArray rowData = Json.createArray(); + int i = 0; + for (Object itemId : itemIds) { + if (activeItemHandler.getActiveItemIds().contains(itemId)) { + JsonObject row = getRowData(getGrid().getColumns(), itemId); + rowData.set(i++, row); + } + } + rpc.updateRowData(rowData); + } + + /** + * Pushes a new version of all the rows in the active cache range. + */ + public void refreshCache() { + if (!refreshCache) { + refreshCache = true; + markAsDirty(); + } + } + + @Override + public void setParent(ClientConnector parent) { + if (parent == null) { + // We're being detached, release various listeners + internalDropItems(activeItemHandler.getActiveItemIds()); + + if (container instanceof ItemSetChangeNotifier) { + ((ItemSetChangeNotifier) container) + .removeItemSetChangeListener(itemListener); + } + + } else if (!(parent instanceof Grid)) { + throw new IllegalStateException( + "Grid is the only accepted parent type"); + } + super.setParent(parent); + } + + /** + * Informs all DataGenerators than an item id has been dropped. + * + * @param droppedItemIds + * collection of dropped item ids + */ + private void internalDropItems(Collection droppedItemIds) { + for (Object itemId : droppedItemIds) { + for (DataGenerator generator : dataGenerators) { + generator.destroyData(itemId); + } + } + } + + /** + * Informs this data provider that given columns have been removed from + * grid. + * + * @param removedColumns + * a list of removed columns + */ + public void columnsRemoved(List removedColumns) { + for (GridValueChangeListener l : activeItemHandler + .getValueChangeListeners()) { + l.removeColumns(removedColumns); + } + + // No need to resend unchanged data. Client will remember the old + // columns until next set of rows is sent. + } + + /** + * Informs this data provider that given columns have been added to grid. + * + * @param addedColumns + * a list of added columns + */ + public void columnsAdded(List addedColumns) { + for (GridValueChangeListener l : activeItemHandler + .getValueChangeListeners()) { + l.addColumns(addedColumns); + } + + // Resend all rows to contain new data. + refreshCache(); + } + + public KeyMapper getKeyMapper() { + return activeItemHandler.keyMapper; + } + + protected Grid getGrid() { + return (Grid) getParent(); + } +} diff --git a/server/src/com/vaadin/ui/Grid.java b/server/src/com/vaadin/ui/Grid.java index 7179ddf0a2..d7efdf5814 100644 --- a/server/src/com/vaadin/ui/Grid.java +++ b/server/src/com/vaadin/ui/Grid.java @@ -51,10 +51,8 @@ import com.vaadin.data.Container.PropertySetChangeEvent; import com.vaadin.data.Container.PropertySetChangeListener; import com.vaadin.data.Container.PropertySetChangeNotifier; import com.vaadin.data.Container.Sortable; -import com.vaadin.data.DataGenerator; import com.vaadin.data.Item; import com.vaadin.data.Property; -import com.vaadin.data.RpcDataProviderExtension; import com.vaadin.data.Validator.InvalidValueException; import com.vaadin.data.fieldgroup.DefaultFieldGroupFieldFactory; import com.vaadin.data.fieldgroup.FieldGroup; @@ -83,6 +81,8 @@ import com.vaadin.server.Extension; import com.vaadin.server.JsonCodec; import com.vaadin.server.KeyMapper; import com.vaadin.server.VaadinSession; +import com.vaadin.server.communication.data.DataGenerator; +import com.vaadin.server.communication.data.RpcDataProviderExtension; import com.vaadin.shared.MouseEventDetails; import com.vaadin.shared.data.sort.SortDirection; import com.vaadin.shared.ui.grid.EditorClientRpc; diff --git a/server/tests/src/com/vaadin/tests/server/component/grid/TestGrid.java b/server/tests/src/com/vaadin/tests/server/component/grid/TestGrid.java index 63fa569ab4..97fb2d8309 100644 --- a/server/tests/src/com/vaadin/tests/server/component/grid/TestGrid.java +++ b/server/tests/src/com/vaadin/tests/server/component/grid/TestGrid.java @@ -19,8 +19,8 @@ import java.lang.reflect.Field; import org.easymock.EasyMock; -import com.vaadin.data.RpcDataProviderExtension; import com.vaadin.data.util.IndexedContainer; +import com.vaadin.server.communication.data.RpcDataProviderExtension; import com.vaadin.ui.ConnectorTracker; import com.vaadin.ui.Grid; import com.vaadin.ui.UI; -- cgit v1.2.3