]> source.dussan.org Git - vaadin-framework.git/blob
5e017a523c5da980e07f287b919b65099e462afb
[vaadin-framework.git] /
1 /*
2  * Copyright 2000-2018 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.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;
25 import java.util.Map;
26 import java.util.Set;
27
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;
48
49 import elemental.json.Json;
50 import elemental.json.JsonArray;
51 import elemental.json.JsonObject;
52
53 /**
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
58  * been implemented.
59  *
60  * @since 7.4
61  * @author Vaadin Ltd
62  *
63  * @deprecated As of 8.0, no replacement available.
64  */
65 @Deprecated
66 public class RpcDataProviderExtension extends AbstractExtension {
67
68     /**
69      * Class for keeping track of current items and ValueChangeListeners.
70      *
71      * @since 7.6
72      */
73     private class ActiveItemHandler implements DataGenerator {
74
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>();
78
79         /**
80          * Registers ValueChangeListeners for given item ids.
81          * <p>
82          * Note: This method will clean up any unneeded listeners and key
83          * mappings
84          *
85          * @param itemIds
86          *            collection of new active item ids
87          */
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)));
93                 }
94             }
95
96             // Remove still active rows that were "dropped"
97             droppedItems.removeAll(itemIds);
98             internalDropItems(droppedItems);
99             droppedItems.clear();
100         }
101
102         /**
103          * Marks given item id as dropped. Dropped items are cleared when adding
104          * new active items.
105          *
106          * @param itemId
107          *            dropped item id
108          */
109         public void dropActiveItem(Object itemId) {
110             if (activeItemMap.containsKey(itemId)) {
111                 droppedItems.add(itemId);
112             }
113         }
114
115         /**
116          * Gets a collection copy of currently active item ids.
117          *
118          * @return collection of item ids
119          */
120         public Collection<Object> getActiveItemIds() {
121             return new HashSet<Object>(activeItemMap.keySet());
122         }
123
124         /**
125          * Gets a collection copy of currently active ValueChangeListeners.
126          *
127          * @return collection of value change listeners
128          */
129         public Collection<GridValueChangeListener> getValueChangeListeners() {
130             return new HashSet<GridValueChangeListener>(activeItemMap.values());
131         }
132
133         @Override
134         public void generateData(Object itemId, Item item, JsonObject rowData) {
135             rowData.put(GridState.JSONKEY_ROWKEY, keyMapper.key(itemId));
136         }
137
138         @Override
139         public void destroyData(Object itemId) {
140             keyMapper.remove(itemId);
141             removeListener(itemId);
142         }
143
144         private void removeListener(Object itemId) {
145             GridValueChangeListener removed = activeItemMap.remove(itemId);
146
147             if (removed != null) {
148                 removed.removeListener();
149             }
150         }
151     }
152
153     /**
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.
159      * <p>
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
163      * management)
164      * <p>
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.
168      *
169      * @see Grid#addValueChangeListener(com.vaadin.v7.data.Container, Object, Object)
170      *      Grid#addValueChangeListener(Container, Object, Object)
171      * @see Grid#valueChangeListeners
172      */
173     private class GridValueChangeListener implements ValueChangeListener {
174         private final Object itemId;
175         private final Item item;
176
177         public GridValueChangeListener(Object itemId, Item item) {
178             /*
179              * Using an assert instead of an exception throw, just to optimize
180              * prematurely
181              */
182             assert itemId != null : "null itemId not accepted";
183             this.itemId = itemId;
184             this.item = item;
185
186             internalAddColumns(getGrid().getColumns());
187         }
188
189         @Override
190         public void valueChange(ValueChangeEvent event) {
191             updateRowData(itemId);
192         }
193
194         public void removeListener() {
195             removeColumns(getGrid().getColumns());
196         }
197
198         public void addColumns(Collection<Column> addedColumns) {
199             internalAddColumns(addedColumns);
200             updateRowData(itemId);
201         }
202
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);
210                 }
211             }
212         }
213
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);
221                 }
222             }
223         }
224     }
225
226     private final Indexed container;
227
228     private DataProviderRpc rpc;
229
230     private final ItemSetChangeListener itemListener = new ItemSetChangeListener() {
231         @Override
232         public void containerItemSetChange(ItemSetChangeEvent event) {
233
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);
244             } else {
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);
250                 }
251
252                 /* Mark as dirty to push changes in beforeClientResponse */
253                 bareItemSetTriggeredSizeChange = true;
254                 markAsDirty();
255             }
256         }
257     };
258
259     /** RpcDataProvider should send the current cache again. */
260     private boolean refreshCache = false;
261
262     /** Set of updated item ids */
263     private transient Set<Object> updatedItemIds;
264
265     /**
266      * Queued RPC calls for adding and removing rows. Queue will be handled in
267      * {@link beforeClientResponse}
268      */
269     private transient List<Runnable> rowChanges;
270
271     /** Size possibly changed with a bare ItemSetChangeEvent */
272     private boolean bareItemSetTriggeredSizeChange = false;
273
274     private final Set<DataGenerator> dataGenerators = new LinkedHashSet<DataGenerator>();
275
276     private final ActiveItemHandler activeItemHandler = new ActiveItemHandler();
277
278     /**
279      * Creates a new data provider using the given container.
280      *
281      * @param container
282      *            the container to make available
283      */
284     @Deprecated
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      */
416     public void extend(Grid component) {
417         super.extend(component);
418     }
419
420     /**
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.
424      *
425      * @since 7.6
426      * @param generator
427      *            generator to add
428      */
429     public void addDataGenerator(DataGenerator generator) {
430         dataGenerators.add(generator);
431     }
432
433     /**
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.
437      *
438      * @since 7.6
439      * @param generator
440      *            generator to remove
441      */
442     public void removeDataGenerator(DataGenerator generator) {
443         dataGenerators.remove(generator);
444     }
445
446     /**
447      * Informs the client side that new rows have been inserted into the data
448      * source.
449      *
450      * @param index
451      *            the index at which new rows have been inserted
452      * @param count
453      *            the number of rows inserted at <code>index</code>
454      */
455     private void insertRowData(final int index, final int count) {
456         if (rowChanges == null) {
457             rowChanges = new ArrayList<Runnable>();
458         }
459
460         if (rowChanges.isEmpty()) {
461             markAsDirty();
462         }
463
464         /*
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.
469          */
470         rowChanges.add(new Runnable() {
471             @Override
472             public void run() {
473                 rpc.insertRowData(index, count);
474             }
475         });
476     }
477
478     /**
479      * Informs the client side that rows have been removed from the data source.
480      *
481      * @param index
482      *            the index of the first row removed
483      * @param count
484      *            the number of rows removed
485      * @param firstItemId
486      *            the item id of the first removed item
487      */
488     private void removeRowData(final int index, final int count) {
489         if (rowChanges == null) {
490             rowChanges = new ArrayList<Runnable>();
491         }
492
493         if (rowChanges.isEmpty()) {
494             markAsDirty();
495         }
496
497         /* See comment in insertRowData */
498         rowChanges.add(new Runnable() {
499             @Override
500             public void run() {
501                 rpc.removeRowData(index, count);
502             }
503         });
504     }
505
506     /**
507      * Informs the client side that data of a row has been modified in the data
508      * source.
509      *
510      * @param itemId
511      *            the item Id the row that was updated
512      */
513     public void updateRowData(Object itemId) {
514         if (updatedItemIds == null) {
515             updatedItemIds = new LinkedHashSet<Object>();
516         }
517
518         if (updatedItemIds.isEmpty()) {
519             // At least one new item will be updated. Mark as dirty to actually
520             // update before response to client.
521             markAsDirty();
522         }
523
524         updatedItemIds.add(itemId);
525     }
526
527     private void internalUpdateRows(Set<Object> itemIds) {
528         if (itemIds == null || itemIds.isEmpty()) {
529             return;
530         }
531
532         Collection<Object> activeItemIds = activeItemHandler.getActiveItemIds();
533         List<Column> columns = getGrid().getColumns();
534         JsonArray rowData = Json.createArray();
535         int i = 0;
536         for (Object itemId : itemIds) {
537             if (activeItemIds.contains(itemId)) {
538                 Item item = container.getItem(itemId);
539                 if (item != null) {
540                     JsonObject row = getRowData(columns, itemId, item);
541                     rowData.set(i++, row);
542                 }
543             }
544         }
545         rpc.updateRowData(rowData);
546     }
547
548     /**
549      * Pushes a new version of all the rows in the active cache range.
550      */
551     public void refreshCache() {
552         if (!refreshCache) {
553             refreshCache = true;
554             markAsDirty();
555         }
556     }
557
558     @Override
559     public void setParent(ClientConnector parent) {
560         if (parent == null) {
561             // We're being detached, release various listeners
562             internalDropItems(activeItemHandler.getActiveItemIds());
563
564             if (container instanceof ItemSetChangeNotifier) {
565                 ((ItemSetChangeNotifier) container)
566                         .removeItemSetChangeListener(itemListener);
567             }
568
569         } else if (!(parent instanceof Grid)) {
570             throw new IllegalStateException(
571                     "Grid is the only accepted parent type");
572         }
573         super.setParent(parent);
574     }
575
576     /**
577      * Informs all DataGenerators than an item id has been dropped.
578      *
579      * @param droppedItemIds
580      *            collection of dropped item ids
581      */
582     private void internalDropItems(Collection<Object> droppedItemIds) {
583         for (Object itemId : droppedItemIds) {
584             for (DataGenerator generator : dataGenerators) {
585                 generator.destroyData(itemId);
586             }
587         }
588     }
589
590     /**
591      * Informs this data provider that given columns have been removed from
592      * grid.
593      *
594      * @param removedColumns
595      *            a list of removed columns
596      */
597     public void columnsRemoved(List<Column> removedColumns) {
598         for (GridValueChangeListener l : activeItemHandler
599                 .getValueChangeListeners()) {
600             l.removeColumns(removedColumns);
601         }
602
603         // No need to resend unchanged data. Client will remember the old
604         // columns until next set of rows is sent.
605     }
606
607     /**
608      * Informs this data provider that given columns have been added to grid.
609      *
610      * @param addedColumns
611      *            a list of added columns
612      */
613     public void columnsAdded(List<Column> addedColumns) {
614         for (GridValueChangeListener l : activeItemHandler
615                 .getValueChangeListeners()) {
616             l.addColumns(addedColumns);
617         }
618
619         // Resend all rows to contain new data.
620         refreshCache();
621     }
622
623     public KeyMapper<Object> getKeyMapper() {
624         return activeItemHandler.keyMapper;
625     }
626
627     protected Grid getGrid() {
628         return (Grid) getParent();
629     }
630 }