]> source.dussan.org Git - vaadin-framework.git/blob
ce960fa5fd06beae6bc729963c736eb019948727
[vaadin-framework.git] /
1 /*
2  * Copyright 2000-2016 Vaadin Ltd.
3  *
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
6  * the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
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
14  * the License.
15  */
16
17 package com.vaadin.v7.server.communication.data;
18
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;
26 import java.util.Map;
27 import java.util.Set;
28
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;
50
51 import elemental.json.Json;
52 import elemental.json.JsonArray;
53 import elemental.json.JsonObject;
54
55 /**
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
60  * been implemented.
61  *
62  * @since 7.4
63  * @author Vaadin Ltd
64  */
65 public class RpcDataProviderExtension extends AbstractExtension {
66
67     /**
68      * Class for keeping track of current items and ValueChangeListeners.
69      *
70      * @since 7.6
71      */
72     private class ActiveItemHandler implements Serializable, DataGenerator {
73
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>();
77
78         /**
79          * Registers ValueChangeListeners for given item ids.
80          * <p>
81          * Note: This method will clean up any unneeded listeners and key
82          * mappings
83          *
84          * @param itemIds
85          *            collection of new active item ids
86          */
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)));
92                 }
93             }
94
95             // Remove still active rows that were "dropped"
96             droppedItems.removeAll(itemIds);
97             internalDropItems(droppedItems);
98             droppedItems.clear();
99         }
100
101         /**
102          * Marks given item id as dropped. Dropped items are cleared when adding
103          * new active items.
104          *
105          * @param itemId
106          *            dropped item id
107          */
108         public void dropActiveItem(Object itemId) {
109             if (activeItemMap.containsKey(itemId)) {
110                 droppedItems.add(itemId);
111             }
112         }
113
114         /**
115          * Gets a collection copy of currently active item ids.
116          *
117          * @return collection of item ids
118          */
119         public Collection<Object> getActiveItemIds() {
120             return new HashSet<Object>(activeItemMap.keySet());
121         }
122
123         /**
124          * Gets a collection copy of currently active ValueChangeListeners.
125          *
126          * @return collection of value change listeners
127          */
128         public Collection<GridValueChangeListener> getValueChangeListeners() {
129             return new HashSet<GridValueChangeListener>(activeItemMap.values());
130         }
131
132         @Override
133         public void generateData(Object itemId, Item item, JsonObject rowData) {
134             rowData.put(GridState.JSONKEY_ROWKEY, keyMapper.key(itemId));
135         }
136
137         @Override
138         public void destroyData(Object itemId) {
139             keyMapper.remove(itemId);
140             removeListener(itemId);
141         }
142
143         private void removeListener(Object itemId) {
144             GridValueChangeListener removed = activeItemMap.remove(itemId);
145
146             if (removed != null) {
147                 removed.removeListener();
148             }
149         }
150     }
151
152     /**
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
156      * item.
157      * <p>
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
161      * management)
162      * <p>
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.
166      *
167      * @see Grid#addValueChangeListener(Container, Object, Object)
168      * @see Grid#valueChangeListeners
169      */
170     private class GridValueChangeListener implements ValueChangeListener {
171         private final Object itemId;
172         private final Item item;
173
174         public GridValueChangeListener(Object itemId, Item item) {
175             /*
176              * Using an assert instead of an exception throw, just to optimize
177              * prematurely
178              */
179             assert itemId != null : "null itemId not accepted";
180             this.itemId = itemId;
181             this.item = item;
182
183             internalAddColumns(getGrid().getColumns());
184         }
185
186         @Override
187         public void valueChange(ValueChangeEvent event) {
188             updateRowData(itemId);
189         }
190
191         public void removeListener() {
192             removeColumns(getGrid().getColumns());
193         }
194
195         public void addColumns(Collection<Column> addedColumns) {
196             internalAddColumns(addedColumns);
197             updateRowData(itemId);
198         }
199
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);
207                 }
208             }
209         }
210
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);
218                 }
219             }
220         }
221     }
222
223     private final Indexed container;
224
225     private DataProviderRpc rpc;
226
227     private final ItemSetChangeListener itemListener = new ItemSetChangeListener() {
228         @Override
229         public void containerItemSetChange(ItemSetChangeEvent event) {
230
231             if (event instanceof ItemAddEvent) {
232                 ItemAddEvent addEvent = (ItemAddEvent) event;
233                 int firstIndex = addEvent.getFirstIndex();
234                 int count = addEvent.getAddedItemsCount();
235                 insertRowData(firstIndex, count);
236             }
237
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);
243             }
244
245             else {
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);
251                 }
252
253                 /* Mark as dirty to push changes in beforeClientResponse */
254                 bareItemSetTriggeredSizeChange = true;
255                 markAsDirty();
256             }
257         }
258     };
259
260     /** RpcDataProvider should send the current cache again. */
261     private boolean refreshCache = false;
262
263     /** Set of updated item ids */
264     private transient Set<Object> updatedItemIds;
265
266     /**
267      * Queued RPC calls for adding and removing rows. Queue will be handled in
268      * {@link beforeClientResponse}
269      */
270     private transient List<Runnable> rowChanges;
271
272     /** Size possibly changed with a bare ItemSetChangeEvent */
273     private boolean bareItemSetTriggeredSizeChange = false;
274
275     private final Set<DataGenerator> dataGenerators = new LinkedHashSet<DataGenerator>();
276
277     private final ActiveItemHandler activeItemHandler = new ActiveItemHandler();
278
279     /**
280      * Creates a new data provider using the given container.
281      *
282      * @param container
283      *            the container to make available
284      */
285     public RpcDataProviderExtension(Indexed container) {
286         this.container = container;
287         rpc = getRpcProxy(DataProviderRpc.class);
288
289         registerRpc(new DataRequestRpc() {
290             @Override
291             public void requestRows(int firstRow, int numberOfRows,
292                     int firstCachedRowIndex, int cacheSize) {
293                 pushRowData(firstRow, numberOfRows, firstCachedRowIndex,
294                         cacheSize);
295             }
296
297             @Override
298             public void dropRows(JsonArray rowKeys) {
299                 for (int i = 0; i < rowKeys.length(); ++i) {
300                     activeItemHandler.dropActiveItem(
301                             getKeyMapper().get(rowKeys.getString(i)));
302                 }
303             }
304         });
305
306         if (container instanceof ItemSetChangeNotifier) {
307             ((ItemSetChangeNotifier) container)
308                     .addItemSetChangeListener(itemListener);
309         }
310
311         addDataGenerator(activeItemHandler);
312     }
313
314     /**
315      * {@inheritDoc}
316      * <p>
317      * RpcDataProviderExtension makes all actual RPC calls from this function
318      * based on changes in the container.
319      */
320     @Override
321     public void beforeClientResponse(boolean initial) {
322         if (initial || bareItemSetTriggeredSizeChange) {
323             /*
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
328              * discarded.
329              */
330             int size = container.size();
331             rpc.resetDataAndSize(size);
332
333             int numberOfRows = Math.min(40, size);
334             pushRowData(0, numberOfRows, 0, 0);
335         } else {
336             // Only do row changes if not initial response.
337             if (rowChanges != null) {
338                 for (Runnable r : rowChanges) {
339                     r.run();
340                 }
341             }
342
343             // Send current rows again if needed.
344             if (refreshCache) {
345                 for (Object itemId : activeItemHandler.getActiveItemIds()) {
346                     updateRowData(itemId);
347                 }
348             }
349         }
350
351         internalUpdateRows(updatedItemIds);
352
353         // Clear all changes.
354         if (rowChanges != null) {
355             rowChanges.clear();
356         }
357         if (updatedItemIds != null) {
358             updatedItemIds.clear();
359         }
360         refreshCache = false;
361         bareItemSetTriggeredSizeChange = false;
362
363         super.beforeClientResponse(initial);
364     }
365
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);
373         }
374
375         List<?> itemIds = container.getItemIds(fullRange.getStart(),
376                 fullRange.length());
377
378         JsonArray rows = Json.createArray();
379
380         // Offset the index to match the wanted range.
381         int diff = 0;
382         if (!cached.isEmpty() && newRange.getStart() > cached.getStart()) {
383             diff = cached.length();
384         }
385
386         for (int i = 0; i < newRange.length()
387                 && i + diff < itemIds.size(); ++i) {
388             Object itemId = itemIds.get(i + diff);
389
390             Item item = container.getItem(itemId);
391
392             rows.set(i, getRowData(getGrid().getColumns(), itemId, item));
393         }
394         rpc.setRowData(firstRowToPush, rows);
395
396         activeItemHandler.addActiveItems(itemIds);
397     }
398
399     private JsonObject getRowData(Collection<Column> columns, Object itemId,
400             Item item) {
401
402         final JsonObject rowObject = Json.createObject();
403         for (DataGenerator dg : dataGenerators) {
404             dg.generateData(itemId, item, rowObject);
405         }
406
407         return rowObject;
408     }
409
410     /**
411      * Makes the data source available to the given {@link Grid} component.
412      *
413      * @param component
414      *            the remote data grid component to extend
415      * @param columnKeys
416      *            the key mapper for columns
417      */
418     public void extend(Grid component) {
419         super.extend(component);
420     }
421
422     /**
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.
426      *
427      * @since 7.6
428      * @param generator
429      *            generator to add
430      */
431     public void addDataGenerator(DataGenerator generator) {
432         dataGenerators.add(generator);
433     }
434
435     /**
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.
439      *
440      * @since 7.6
441      * @param generator
442      *            generator to remove
443      */
444     public void removeDataGenerator(DataGenerator generator) {
445         dataGenerators.remove(generator);
446     }
447
448     /**
449      * Informs the client side that new rows have been inserted into the data
450      * source.
451      *
452      * @param index
453      *            the index at which new rows have been inserted
454      * @param count
455      *            the number of rows inserted at <code>index</code>
456      */
457     private void insertRowData(final int index, final int count) {
458         if (rowChanges == null) {
459             rowChanges = new ArrayList<Runnable>();
460         }
461
462         if (rowChanges.isEmpty()) {
463             markAsDirty();
464         }
465
466         /*
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.
471          */
472         rowChanges.add(new Runnable() {
473             @Override
474             public void run() {
475                 rpc.insertRowData(index, count);
476             }
477         });
478     }
479
480     /**
481      * Informs the client side that rows have been removed from the data source.
482      *
483      * @param index
484      *            the index of the first row removed
485      * @param count
486      *            the number of rows removed
487      * @param firstItemId
488      *            the item id of the first removed item
489      */
490     private void removeRowData(final int index, final int count) {
491         if (rowChanges == null) {
492             rowChanges = new ArrayList<Runnable>();
493         }
494
495         if (rowChanges.isEmpty()) {
496             markAsDirty();
497         }
498
499         /* See comment in insertRowData */
500         rowChanges.add(new Runnable() {
501             @Override
502             public void run() {
503                 rpc.removeRowData(index, count);
504             }
505         });
506     }
507
508     /**
509      * Informs the client side that data of a row has been modified in the data
510      * source.
511      *
512      * @param itemId
513      *            the item Id the row that was updated
514      */
515     public void updateRowData(Object itemId) {
516         if (updatedItemIds == null) {
517             updatedItemIds = new LinkedHashSet<Object>();
518         }
519
520         if (updatedItemIds.isEmpty()) {
521             // At least one new item will be updated. Mark as dirty to actually
522             // update before response to client.
523             markAsDirty();
524         }
525
526         updatedItemIds.add(itemId);
527     }
528
529     private void internalUpdateRows(Set<Object> itemIds) {
530         if (itemIds == null || itemIds.isEmpty()) {
531             return;
532         }
533
534         Collection<Object> activeItemIds = activeItemHandler.getActiveItemIds();
535         List<Column> columns = getGrid().getColumns();
536         JsonArray rowData = Json.createArray();
537         int i = 0;
538         for (Object itemId : itemIds) {
539             if (activeItemIds.contains(itemId)) {
540                 Item item = container.getItem(itemId);
541                 if (item != null) {
542                     JsonObject row = getRowData(columns, itemId, item);
543                     rowData.set(i++, row);
544                 }
545             }
546         }
547         rpc.updateRowData(rowData);
548     }
549
550     /**
551      * Pushes a new version of all the rows in the active cache range.
552      */
553     public void refreshCache() {
554         if (!refreshCache) {
555             refreshCache = true;
556             markAsDirty();
557         }
558     }
559
560     @Override
561     public void setParent(ClientConnector parent) {
562         if (parent == null) {
563             // We're being detached, release various listeners
564             internalDropItems(activeItemHandler.getActiveItemIds());
565
566             if (container instanceof ItemSetChangeNotifier) {
567                 ((ItemSetChangeNotifier) container)
568                         .removeItemSetChangeListener(itemListener);
569             }
570
571         } else if (!(parent instanceof Grid)) {
572             throw new IllegalStateException(
573                     "Grid is the only accepted parent type");
574         }
575         super.setParent(parent);
576     }
577
578     /**
579      * Informs all DataGenerators than an item id has been dropped.
580      *
581      * @param droppedItemIds
582      *            collection of dropped item ids
583      */
584     private void internalDropItems(Collection<Object> droppedItemIds) {
585         for (Object itemId : droppedItemIds) {
586             for (DataGenerator generator : dataGenerators) {
587                 generator.destroyData(itemId);
588             }
589         }
590     }
591
592     /**
593      * Informs this data provider that given columns have been removed from
594      * grid.
595      *
596      * @param removedColumns
597      *            a list of removed columns
598      */
599     public void columnsRemoved(List<Column> removedColumns) {
600         for (GridValueChangeListener l : activeItemHandler
601                 .getValueChangeListeners()) {
602             l.removeColumns(removedColumns);
603         }
604
605         // No need to resend unchanged data. Client will remember the old
606         // columns until next set of rows is sent.
607     }
608
609     /**
610      * Informs this data provider that given columns have been added to grid.
611      *
612      * @param addedColumns
613      *            a list of added columns
614      */
615     public void columnsAdded(List<Column> addedColumns) {
616         for (GridValueChangeListener l : activeItemHandler
617                 .getValueChangeListeners()) {
618             l.addColumns(addedColumns);
619         }
620
621         // Resend all rows to contain new data.
622         refreshCache();
623     }
624
625     public KeyMapper<Object> getKeyMapper() {
626         return activeItemHandler.keyMapper;
627     }
628
629     protected Grid getGrid() {
630         return (Grid) getParent();
631     }
632 }