2 * Copyright 2000-2016 Vaadin Ltd.
4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
5 * use this file except in compliance with the License. You may obtain a copy of
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13 * License for the specific language governing permissions and limitations under
17 package com.vaadin.v7.server.communication.data;
19 import java.io.Serializable;
20 import java.util.ArrayList;
21 import java.util.Collection;
22 import java.util.HashMap;
23 import java.util.HashSet;
24 import java.util.LinkedHashSet;
25 import java.util.List;
29 import com.vaadin.server.AbstractExtension;
30 import com.vaadin.server.ClientConnector;
31 import com.vaadin.server.KeyMapper;
32 import com.vaadin.shared.data.DataProviderRpc;
33 import com.vaadin.shared.data.DataRequestRpc;
34 import com.vaadin.shared.ui.grid.Range;
35 import com.vaadin.v7.data.Container;
36 import com.vaadin.v7.data.Container.Indexed;
37 import com.vaadin.v7.data.Container.Indexed.ItemAddEvent;
38 import com.vaadin.v7.data.Container.Indexed.ItemRemoveEvent;
39 import com.vaadin.v7.data.Container.ItemSetChangeEvent;
40 import com.vaadin.v7.data.Container.ItemSetChangeListener;
41 import com.vaadin.v7.data.Container.ItemSetChangeNotifier;
42 import com.vaadin.v7.data.Item;
43 import com.vaadin.v7.data.Property;
44 import com.vaadin.v7.data.Property.ValueChangeEvent;
45 import com.vaadin.v7.data.Property.ValueChangeListener;
46 import com.vaadin.v7.data.Property.ValueChangeNotifier;
47 import com.vaadin.v7.shared.ui.grid.GridState;
48 import com.vaadin.v7.ui.Grid;
49 import com.vaadin.v7.ui.Grid.Column;
51 import elemental.json.Json;
52 import elemental.json.JsonArray;
53 import elemental.json.JsonObject;
56 * Provides Vaadin server-side container data source to a
57 * {@link com.vaadin.client.ui.grid.GridConnector}. This is currently
58 * implemented as an Extension hardcoded to support a specific connector type.
59 * This will be changed once framework support for something more flexible has
65 public class RpcDataProviderExtension extends AbstractExtension {
68 * Class for keeping track of current items and ValueChangeListeners.
72 private class ActiveItemHandler implements Serializable, DataGenerator {
74 private final Map<Object, GridValueChangeListener> activeItemMap = new HashMap<Object, GridValueChangeListener>();
75 private final KeyMapper<Object> keyMapper = new KeyMapper<Object>();
76 private final Set<Object> droppedItems = new HashSet<Object>();
79 * Registers ValueChangeListeners for given item ids.
81 * Note: This method will clean up any unneeded listeners and key
85 * collection of new active item ids
87 public void addActiveItems(Collection<?> itemIds) {
88 for (Object itemId : itemIds) {
89 if (!activeItemMap.containsKey(itemId)) {
90 activeItemMap.put(itemId, new GridValueChangeListener(
91 itemId, container.getItem(itemId)));
95 // Remove still active rows that were "dropped"
96 droppedItems.removeAll(itemIds);
97 internalDropItems(droppedItems);
102 * Marks given item id as dropped. Dropped items are cleared when adding
108 public void dropActiveItem(Object itemId) {
109 if (activeItemMap.containsKey(itemId)) {
110 droppedItems.add(itemId);
115 * Gets a collection copy of currently active item ids.
117 * @return collection of item ids
119 public Collection<Object> getActiveItemIds() {
120 return new HashSet<Object>(activeItemMap.keySet());
124 * Gets a collection copy of currently active ValueChangeListeners.
126 * @return collection of value change listeners
128 public Collection<GridValueChangeListener> getValueChangeListeners() {
129 return new HashSet<GridValueChangeListener>(activeItemMap.values());
133 public void generateData(Object itemId, Item item, JsonObject rowData) {
134 rowData.put(GridState.JSONKEY_ROWKEY, keyMapper.key(itemId));
138 public void destroyData(Object itemId) {
139 keyMapper.remove(itemId);
140 removeListener(itemId);
143 private void removeListener(Object itemId) {
144 GridValueChangeListener removed = activeItemMap.remove(itemId);
146 if (removed != null) {
147 removed.removeListener();
153 * A class to listen to changes in property values in the Container added
154 * with {@link Grid#setContainerDatasource(Container.Indexed)}, and notifies
155 * the data source to update the client-side representation of the modified
158 * One instance of this class can (and should) be reused for all the
159 * properties in an item, since this class will inform that the entire row
160 * needs to be re-evaluated (in contrast to a property-based change
163 * Since there's no Container-wide possibility to listen to any kind of
164 * value changes, an instance of this class needs to be attached to each and
165 * every Item's Property in the container.
167 * @see Grid#addValueChangeListener(Container, Object, Object)
168 * @see Grid#valueChangeListeners
170 private class GridValueChangeListener implements ValueChangeListener {
171 private final Object itemId;
172 private final Item item;
174 public GridValueChangeListener(Object itemId, Item item) {
176 * Using an assert instead of an exception throw, just to optimize
179 assert itemId != null : "null itemId not accepted";
180 this.itemId = itemId;
183 internalAddColumns(getGrid().getColumns());
187 public void valueChange(ValueChangeEvent event) {
188 updateRowData(itemId);
191 public void removeListener() {
192 removeColumns(getGrid().getColumns());
195 public void addColumns(Collection<Column> addedColumns) {
196 internalAddColumns(addedColumns);
197 updateRowData(itemId);
200 private void internalAddColumns(Collection<Column> addedColumns) {
201 for (final Column column : addedColumns) {
202 final Property<?> property = item
203 .getItemProperty(column.getPropertyId());
204 if (property instanceof ValueChangeNotifier) {
205 ((ValueChangeNotifier) property)
206 .addValueChangeListener(this);
211 public void removeColumns(Collection<Column> removedColumns) {
212 for (final Column column : removedColumns) {
213 final Property<?> property = item
214 .getItemProperty(column.getPropertyId());
215 if (property instanceof ValueChangeNotifier) {
216 ((ValueChangeNotifier) property)
217 .removeValueChangeListener(this);
223 private final Indexed container;
225 private DataProviderRpc rpc;
227 private final ItemSetChangeListener itemListener = new ItemSetChangeListener() {
229 public void containerItemSetChange(ItemSetChangeEvent event) {
231 if (event instanceof ItemAddEvent) {
232 ItemAddEvent addEvent = (ItemAddEvent) event;
233 int firstIndex = addEvent.getFirstIndex();
234 int count = addEvent.getAddedItemsCount();
235 insertRowData(firstIndex, count);
238 else if (event instanceof ItemRemoveEvent) {
239 ItemRemoveEvent removeEvent = (ItemRemoveEvent) event;
240 int firstIndex = removeEvent.getFirstIndex();
241 int count = removeEvent.getRemovedItemsCount();
242 removeRowData(firstIndex, count);
246 // Remove obsolete value change listeners.
247 Set<Object> keySet = new HashSet<Object>(
248 activeItemHandler.activeItemMap.keySet());
249 for (Object itemId : keySet) {
250 activeItemHandler.removeListener(itemId);
253 /* Mark as dirty to push changes in beforeClientResponse */
254 bareItemSetTriggeredSizeChange = true;
260 /** RpcDataProvider should send the current cache again. */
261 private boolean refreshCache = false;
263 /** Set of updated item ids */
264 private transient Set<Object> updatedItemIds;
267 * Queued RPC calls for adding and removing rows. Queue will be handled in
268 * {@link beforeClientResponse}
270 private transient List<Runnable> rowChanges;
272 /** Size possibly changed with a bare ItemSetChangeEvent */
273 private boolean bareItemSetTriggeredSizeChange = false;
275 private final Set<DataGenerator> dataGenerators = new LinkedHashSet<DataGenerator>();
277 private final ActiveItemHandler activeItemHandler = new ActiveItemHandler();
280 * Creates a new data provider using the given container.
283 * the container to make available
285 public RpcDataProviderExtension(Indexed container) {
286 this.container = container;
287 rpc = getRpcProxy(DataProviderRpc.class);
289 registerRpc(new DataRequestRpc() {
291 public void requestRows(int firstRow, int numberOfRows,
292 int firstCachedRowIndex, int cacheSize) {
293 pushRowData(firstRow, numberOfRows, firstCachedRowIndex,
298 public void dropRows(JsonArray rowKeys) {
299 for (int i = 0; i < rowKeys.length(); ++i) {
300 activeItemHandler.dropActiveItem(
301 getKeyMapper().get(rowKeys.getString(i)));
306 if (container instanceof ItemSetChangeNotifier) {
307 ((ItemSetChangeNotifier) container)
308 .addItemSetChangeListener(itemListener);
311 addDataGenerator(activeItemHandler);
317 * RpcDataProviderExtension makes all actual RPC calls from this function
318 * based on changes in the container.
321 public void beforeClientResponse(boolean initial) {
322 if (initial || bareItemSetTriggeredSizeChange) {
324 * Push initial set of rows, assuming Grid will initially be
325 * rendered scrolled to the top and with a decent amount of rows
326 * visible. If this guess is right, initial data can be shown
327 * without a round-trip and if it's wrong, the data will simply be
330 int size = container.size();
331 rpc.resetDataAndSize(size);
333 int numberOfRows = Math.min(40, size);
334 pushRowData(0, numberOfRows, 0, 0);
336 // Only do row changes if not initial response.
337 if (rowChanges != null) {
338 for (Runnable r : rowChanges) {
343 // Send current rows again if needed.
345 for (Object itemId : activeItemHandler.getActiveItemIds()) {
346 updateRowData(itemId);
351 internalUpdateRows(updatedItemIds);
353 // Clear all changes.
354 if (rowChanges != null) {
357 if (updatedItemIds != null) {
358 updatedItemIds.clear();
360 refreshCache = false;
361 bareItemSetTriggeredSizeChange = false;
363 super.beforeClientResponse(initial);
366 private void pushRowData(int firstRowToPush, int numberOfRows,
367 int firstCachedRowIndex, int cacheSize) {
368 Range newRange = Range.withLength(firstRowToPush, numberOfRows);
369 Range cached = Range.withLength(firstCachedRowIndex, cacheSize);
370 Range fullRange = newRange;
371 if (!cached.isEmpty()) {
372 fullRange = newRange.combineWith(cached);
375 List<?> itemIds = container.getItemIds(fullRange.getStart(),
378 JsonArray rows = Json.createArray();
380 // Offset the index to match the wanted range.
382 if (!cached.isEmpty() && newRange.getStart() > cached.getStart()) {
383 diff = cached.length();
386 for (int i = 0; i < newRange.length()
387 && i + diff < itemIds.size(); ++i) {
388 Object itemId = itemIds.get(i + diff);
390 Item item = container.getItem(itemId);
392 rows.set(i, getRowData(getGrid().getColumns(), itemId, item));
394 rpc.setRowData(firstRowToPush, rows);
396 activeItemHandler.addActiveItems(itemIds);
399 private JsonObject getRowData(Collection<Column> columns, Object itemId,
402 final JsonObject rowObject = Json.createObject();
403 for (DataGenerator dg : dataGenerators) {
404 dg.generateData(itemId, item, rowObject);
411 * Makes the data source available to the given {@link Grid} component.
414 * the remote data grid component to extend
416 * the key mapper for columns
418 public void extend(Grid component) {
419 super.extend(component);
423 * Adds a {@link DataGenerator} for this {@code RpcDataProviderExtension}.
424 * DataGenerators are called when sending row data to client. If given
425 * DataGenerator is already added, this method does nothing.
431 public void addDataGenerator(DataGenerator generator) {
432 dataGenerators.add(generator);
436 * Removes a {@link DataGenerator} from this
437 * {@code RpcDataProviderExtension}. If given DataGenerator is not added to
438 * this data provider, this method does nothing.
442 * generator to remove
444 public void removeDataGenerator(DataGenerator generator) {
445 dataGenerators.remove(generator);
449 * Informs the client side that new rows have been inserted into the data
453 * the index at which new rows have been inserted
455 * the number of rows inserted at <code>index</code>
457 private void insertRowData(final int index, final int count) {
458 if (rowChanges == null) {
459 rowChanges = new ArrayList<Runnable>();
462 if (rowChanges.isEmpty()) {
467 * Since all changes should be processed in a consistent order, we don't
468 * send the RPC call immediately. beforeClientResponse will decide
469 * whether to send these or not. Valid situation to not send these is
470 * initial response or bare ItemSetChange event.
472 rowChanges.add(new Runnable() {
475 rpc.insertRowData(index, count);
481 * Informs the client side that rows have been removed from the data source.
484 * the index of the first row removed
486 * the number of rows removed
488 * the item id of the first removed item
490 private void removeRowData(final int index, final int count) {
491 if (rowChanges == null) {
492 rowChanges = new ArrayList<Runnable>();
495 if (rowChanges.isEmpty()) {
499 /* See comment in insertRowData */
500 rowChanges.add(new Runnable() {
503 rpc.removeRowData(index, count);
509 * Informs the client side that data of a row has been modified in the data
513 * the item Id the row that was updated
515 public void updateRowData(Object itemId) {
516 if (updatedItemIds == null) {
517 updatedItemIds = new LinkedHashSet<Object>();
520 if (updatedItemIds.isEmpty()) {
521 // At least one new item will be updated. Mark as dirty to actually
522 // update before response to client.
526 updatedItemIds.add(itemId);
529 private void internalUpdateRows(Set<Object> itemIds) {
530 if (itemIds == null || itemIds.isEmpty()) {
534 Collection<Object> activeItemIds = activeItemHandler.getActiveItemIds();
535 List<Column> columns = getGrid().getColumns();
536 JsonArray rowData = Json.createArray();
538 for (Object itemId : itemIds) {
539 if (activeItemIds.contains(itemId)) {
540 Item item = container.getItem(itemId);
542 JsonObject row = getRowData(columns, itemId, item);
543 rowData.set(i++, row);
547 rpc.updateRowData(rowData);
551 * Pushes a new version of all the rows in the active cache range.
553 public void refreshCache() {
561 public void setParent(ClientConnector parent) {
562 if (parent == null) {
563 // We're being detached, release various listeners
564 internalDropItems(activeItemHandler.getActiveItemIds());
566 if (container instanceof ItemSetChangeNotifier) {
567 ((ItemSetChangeNotifier) container)
568 .removeItemSetChangeListener(itemListener);
571 } else if (!(parent instanceof Grid)) {
572 throw new IllegalStateException(
573 "Grid is the only accepted parent type");
575 super.setParent(parent);
579 * Informs all DataGenerators than an item id has been dropped.
581 * @param droppedItemIds
582 * collection of dropped item ids
584 private void internalDropItems(Collection<Object> droppedItemIds) {
585 for (Object itemId : droppedItemIds) {
586 for (DataGenerator generator : dataGenerators) {
587 generator.destroyData(itemId);
593 * Informs this data provider that given columns have been removed from
596 * @param removedColumns
597 * a list of removed columns
599 public void columnsRemoved(List<Column> removedColumns) {
600 for (GridValueChangeListener l : activeItemHandler
601 .getValueChangeListeners()) {
602 l.removeColumns(removedColumns);
605 // No need to resend unchanged data. Client will remember the old
606 // columns until next set of rows is sent.
610 * Informs this data provider that given columns have been added to grid.
612 * @param addedColumns
613 * a list of added columns
615 public void columnsAdded(List<Column> addedColumns) {
616 for (GridValueChangeListener l : activeItemHandler
617 .getValueChangeListeners()) {
618 l.addColumns(addedColumns);
621 // Resend all rows to contain new data.
625 public KeyMapper<Object> getKeyMapper() {
626 return activeItemHandler.keyMapper;
629 protected Grid getGrid() {
630 return (Grid) getParent();