2 * Copyright 2000-2018 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.util.ArrayList;
20 import java.util.Collection;
21 import java.util.HashMap;
22 import java.util.HashSet;
23 import java.util.LinkedHashSet;
24 import java.util.List;
28 import com.vaadin.server.AbstractExtension;
29 import com.vaadin.server.ClientConnector;
30 import com.vaadin.server.KeyMapper;
31 import com.vaadin.shared.Range;
32 import com.vaadin.shared.data.DataProviderRpc;
33 import com.vaadin.shared.data.DataRequestRpc;
34 import com.vaadin.v7.data.Container.Indexed;
35 import com.vaadin.v7.data.Container.Indexed.ItemAddEvent;
36 import com.vaadin.v7.data.Container.Indexed.ItemRemoveEvent;
37 import com.vaadin.v7.data.Container.ItemSetChangeEvent;
38 import com.vaadin.v7.data.Container.ItemSetChangeListener;
39 import com.vaadin.v7.data.Container.ItemSetChangeNotifier;
40 import com.vaadin.v7.data.Item;
41 import com.vaadin.v7.data.Property;
42 import com.vaadin.v7.data.Property.ValueChangeEvent;
43 import com.vaadin.v7.data.Property.ValueChangeListener;
44 import com.vaadin.v7.data.Property.ValueChangeNotifier;
45 import com.vaadin.v7.shared.ui.grid.GridState;
46 import com.vaadin.v7.ui.Grid;
47 import com.vaadin.v7.ui.Grid.Column;
49 import elemental.json.Json;
50 import elemental.json.JsonArray;
51 import elemental.json.JsonObject;
54 * Provides Vaadin server-side container data source to a
55 * {@link com.vaadin.v7.client.connectors.GridConnector GridConnector}. This is currently
56 * implemented as an Extension hardcoded to support a specific connector type.
57 * This will be changed once framework support for something more flexible has
63 * @deprecated As of 8.0, no replacement available.
66 public class RpcDataProviderExtension extends AbstractExtension {
69 * Class for keeping track of current items and ValueChangeListeners.
73 private class ActiveItemHandler implements DataGenerator {
75 private final Map<Object, GridValueChangeListener> activeItemMap = new HashMap<Object, GridValueChangeListener>();
76 private final KeyMapper<Object> keyMapper = new KeyMapper<Object>();
77 private final Set<Object> droppedItems = new HashSet<Object>();
80 * Registers ValueChangeListeners for given item ids.
82 * Note: This method will clean up any unneeded listeners and key
86 * collection of new active item ids
88 public void addActiveItems(Collection<?> itemIds) {
89 for (Object itemId : itemIds) {
90 if (!activeItemMap.containsKey(itemId)) {
91 activeItemMap.put(itemId, new GridValueChangeListener(
92 itemId, container.getItem(itemId)));
96 // Remove still active rows that were "dropped"
97 droppedItems.removeAll(itemIds);
98 internalDropItems(droppedItems);
103 * Marks given item id as dropped. Dropped items are cleared when adding
109 public void dropActiveItem(Object itemId) {
110 if (activeItemMap.containsKey(itemId)) {
111 droppedItems.add(itemId);
116 * Gets a collection copy of currently active item ids.
118 * @return collection of item ids
120 public Collection<Object> getActiveItemIds() {
121 return new HashSet<Object>(activeItemMap.keySet());
125 * Gets a collection copy of currently active ValueChangeListeners.
127 * @return collection of value change listeners
129 public Collection<GridValueChangeListener> getValueChangeListeners() {
130 return new HashSet<GridValueChangeListener>(activeItemMap.values());
134 public void generateData(Object itemId, Item item, JsonObject rowData) {
135 rowData.put(GridState.JSONKEY_ROWKEY, keyMapper.key(itemId));
139 public void destroyData(Object itemId) {
140 keyMapper.remove(itemId);
141 removeListener(itemId);
144 private void removeListener(Object itemId) {
145 GridValueChangeListener removed = activeItemMap.remove(itemId);
147 if (removed != null) {
148 removed.removeListener();
154 * A class to listen to changes in property values in the Container added
155 * with {@link Grid#setContainerDatasource(com.vaadin.v7.data.Container.Indexed)
156 * Grid#setContainerDatasource(Container.Indexed)},
157 * and notifies the data source to update the client-side representation
158 * of the modified item.
160 * One instance of this class can (and should) be reused for all the
161 * properties in an item, since this class will inform that the entire row
162 * needs to be re-evaluated (in contrast to a property-based change
165 * Since there's no Container-wide possibility to listen to any kind of
166 * value changes, an instance of this class needs to be attached to each and
167 * every Item's Property in the container.
169 * @see Grid#addValueChangeListener(com.vaadin.v7.data.Container, Object, Object)
170 * Grid#addValueChangeListener(Container, Object, Object)
171 * @see Grid#valueChangeListeners
173 private class GridValueChangeListener implements ValueChangeListener {
174 private final Object itemId;
175 private final Item item;
177 public GridValueChangeListener(Object itemId, Item item) {
179 * Using an assert instead of an exception throw, just to optimize
182 assert itemId != null : "null itemId not accepted";
183 this.itemId = itemId;
186 internalAddColumns(getGrid().getColumns());
190 public void valueChange(ValueChangeEvent event) {
191 updateRowData(itemId);
194 public void removeListener() {
195 removeColumns(getGrid().getColumns());
198 public void addColumns(Collection<Column> addedColumns) {
199 internalAddColumns(addedColumns);
200 updateRowData(itemId);
203 private void internalAddColumns(Collection<Column> addedColumns) {
204 for (final Column column : addedColumns) {
205 final Property<?> property = item
206 .getItemProperty(column.getPropertyId());
207 if (property instanceof ValueChangeNotifier) {
208 ((ValueChangeNotifier) property)
209 .addValueChangeListener(this);
214 public void removeColumns(Collection<Column> removedColumns) {
215 for (final Column column : removedColumns) {
216 final Property<?> property = item
217 .getItemProperty(column.getPropertyId());
218 if (property instanceof ValueChangeNotifier) {
219 ((ValueChangeNotifier) property)
220 .removeValueChangeListener(this);
226 private final Indexed container;
228 private DataProviderRpc rpc;
230 private final ItemSetChangeListener itemListener = new ItemSetChangeListener() {
232 public void containerItemSetChange(ItemSetChangeEvent event) {
234 if (event instanceof ItemAddEvent) {
235 ItemAddEvent addEvent = (ItemAddEvent) event;
236 int firstIndex = addEvent.getFirstIndex();
237 int count = addEvent.getAddedItemsCount();
238 insertRowData(firstIndex, count);
239 } else if (event instanceof ItemRemoveEvent) {
240 ItemRemoveEvent removeEvent = (ItemRemoveEvent) event;
241 int firstIndex = removeEvent.getFirstIndex();
242 int count = removeEvent.getRemovedItemsCount();
243 removeRowData(firstIndex, count);
245 // Remove obsolete value change listeners.
246 Set<Object> keySet = new HashSet<Object>(
247 activeItemHandler.activeItemMap.keySet());
248 for (Object itemId : keySet) {
249 activeItemHandler.removeListener(itemId);
252 /* Mark as dirty to push changes in beforeClientResponse */
253 bareItemSetTriggeredSizeChange = true;
259 /** RpcDataProvider should send the current cache again. */
260 private boolean refreshCache = false;
262 /** Set of updated item ids */
263 private transient Set<Object> updatedItemIds;
266 * Queued RPC calls for adding and removing rows. Queue will be handled in
267 * {@link beforeClientResponse}
269 private transient List<Runnable> rowChanges;
271 /** Size possibly changed with a bare ItemSetChangeEvent */
272 private boolean bareItemSetTriggeredSizeChange = false;
274 private final Set<DataGenerator> dataGenerators = new LinkedHashSet<DataGenerator>();
276 private final ActiveItemHandler activeItemHandler = new ActiveItemHandler();
279 * Creates a new data provider using the given container.
282 * 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 public void extend(Grid component) {
417 super.extend(component);
421 * Adds a {@link DataGenerator} for this {@code RpcDataProviderExtension}.
422 * DataGenerators are called when sending row data to client. If given
423 * DataGenerator is already added, this method does nothing.
429 public void addDataGenerator(DataGenerator generator) {
430 dataGenerators.add(generator);
434 * Removes a {@link DataGenerator} from this
435 * {@code RpcDataProviderExtension}. If given DataGenerator is not added to
436 * this data provider, this method does nothing.
440 * generator to remove
442 public void removeDataGenerator(DataGenerator generator) {
443 dataGenerators.remove(generator);
447 * Informs the client side that new rows have been inserted into the data
451 * the index at which new rows have been inserted
453 * the number of rows inserted at <code>index</code>
455 private void insertRowData(final int index, final int count) {
456 if (rowChanges == null) {
457 rowChanges = new ArrayList<Runnable>();
460 if (rowChanges.isEmpty()) {
465 * Since all changes should be processed in a consistent order, we don't
466 * send the RPC call immediately. beforeClientResponse will decide
467 * whether to send these or not. Valid situation to not send these is
468 * initial response or bare ItemSetChange event.
470 rowChanges.add(new Runnable() {
473 rpc.insertRowData(index, count);
479 * Informs the client side that rows have been removed from the data source.
482 * the index of the first row removed
484 * the number of rows removed
486 * the item id of the first removed item
488 private void removeRowData(final int index, final int count) {
489 if (rowChanges == null) {
490 rowChanges = new ArrayList<Runnable>();
493 if (rowChanges.isEmpty()) {
497 /* See comment in insertRowData */
498 rowChanges.add(new Runnable() {
501 rpc.removeRowData(index, count);
507 * Informs the client side that data of a row has been modified in the data
511 * the item Id the row that was updated
513 public void updateRowData(Object itemId) {
514 if (updatedItemIds == null) {
515 updatedItemIds = new LinkedHashSet<Object>();
518 if (updatedItemIds.isEmpty()) {
519 // At least one new item will be updated. Mark as dirty to actually
520 // update before response to client.
524 updatedItemIds.add(itemId);
527 private void internalUpdateRows(Set<Object> itemIds) {
528 if (itemIds == null || itemIds.isEmpty()) {
532 Collection<Object> activeItemIds = activeItemHandler.getActiveItemIds();
533 List<Column> columns = getGrid().getColumns();
534 JsonArray rowData = Json.createArray();
536 for (Object itemId : itemIds) {
537 if (activeItemIds.contains(itemId)) {
538 Item item = container.getItem(itemId);
540 JsonObject row = getRowData(columns, itemId, item);
541 rowData.set(i++, row);
545 rpc.updateRowData(rowData);
549 * Pushes a new version of all the rows in the active cache range.
551 public void refreshCache() {
559 public void setParent(ClientConnector parent) {
560 if (parent == null) {
561 // We're being detached, release various listeners
562 internalDropItems(activeItemHandler.getActiveItemIds());
564 if (container instanceof ItemSetChangeNotifier) {
565 ((ItemSetChangeNotifier) container)
566 .removeItemSetChangeListener(itemListener);
569 } else if (!(parent instanceof Grid)) {
570 throw new IllegalStateException(
571 "Grid is the only accepted parent type");
573 super.setParent(parent);
577 * Informs all DataGenerators than an item id has been dropped.
579 * @param droppedItemIds
580 * collection of dropped item ids
582 private void internalDropItems(Collection<Object> droppedItemIds) {
583 for (Object itemId : droppedItemIds) {
584 for (DataGenerator generator : dataGenerators) {
585 generator.destroyData(itemId);
591 * Informs this data provider that given columns have been removed from
594 * @param removedColumns
595 * a list of removed columns
597 public void columnsRemoved(List<Column> removedColumns) {
598 for (GridValueChangeListener l : activeItemHandler
599 .getValueChangeListeners()) {
600 l.removeColumns(removedColumns);
603 // No need to resend unchanged data. Client will remember the old
604 // columns until next set of rows is sent.
608 * Informs this data provider that given columns have been added to grid.
610 * @param addedColumns
611 * a list of added columns
613 public void columnsAdded(List<Column> addedColumns) {
614 for (GridValueChangeListener l : activeItemHandler
615 .getValueChangeListeners()) {
616 l.addColumns(addedColumns);
619 // Resend all rows to contain new data.
623 public KeyMapper<Object> getKeyMapper() {
624 return activeItemHandler.keyMapper;
627 protected Grid getGrid() {
628 return (Grid) getParent();