You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

RpcDataProviderExtension.java 21KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632
  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. package com.vaadin.server.communication.data;
  17. import java.io.Serializable;
  18. import java.util.ArrayList;
  19. import java.util.Collection;
  20. import java.util.HashMap;
  21. import java.util.HashSet;
  22. import java.util.LinkedHashSet;
  23. import java.util.List;
  24. import java.util.Map;
  25. import java.util.Set;
  26. import com.vaadin.data.Container;
  27. import com.vaadin.data.Container.Indexed;
  28. import com.vaadin.data.Container.Indexed.ItemAddEvent;
  29. import com.vaadin.data.Container.Indexed.ItemRemoveEvent;
  30. import com.vaadin.data.Container.ItemSetChangeEvent;
  31. import com.vaadin.data.Container.ItemSetChangeListener;
  32. import com.vaadin.data.Container.ItemSetChangeNotifier;
  33. import com.vaadin.data.Item;
  34. import com.vaadin.data.Property;
  35. import com.vaadin.data.Property.ValueChangeEvent;
  36. import com.vaadin.data.Property.ValueChangeListener;
  37. import com.vaadin.data.Property.ValueChangeNotifier;
  38. import com.vaadin.server.AbstractExtension;
  39. import com.vaadin.server.ClientConnector;
  40. import com.vaadin.server.KeyMapper;
  41. import com.vaadin.shared.data.DataProviderRpc;
  42. import com.vaadin.shared.data.DataRequestRpc;
  43. import com.vaadin.shared.ui.grid.GridState;
  44. import com.vaadin.shared.ui.grid.Range;
  45. import com.vaadin.ui.LegacyGrid;
  46. import com.vaadin.ui.LegacyGrid.Column;
  47. import elemental.json.Json;
  48. import elemental.json.JsonArray;
  49. import elemental.json.JsonObject;
  50. /**
  51. * Provides Vaadin server-side container data source to a
  52. * {@link com.vaadin.client.ui.grid.GridConnector}. This is currently
  53. * implemented as an Extension hardcoded to support a specific connector type.
  54. * This will be changed once framework support for something more flexible has
  55. * been implemented.
  56. *
  57. * @since 7.4
  58. * @author Vaadin Ltd
  59. */
  60. public class RpcDataProviderExtension extends AbstractExtension {
  61. /**
  62. * Class for keeping track of current items and ValueChangeListeners.
  63. *
  64. * @since 7.6
  65. */
  66. private class ActiveItemHandler implements Serializable, DataGenerator {
  67. private final Map<Object, GridValueChangeListener> activeItemMap = new HashMap<Object, GridValueChangeListener>();
  68. private final KeyMapper<Object> keyMapper = new KeyMapper<Object>();
  69. private final Set<Object> droppedItems = new HashSet<Object>();
  70. /**
  71. * Registers ValueChangeListeners for given item ids.
  72. * <p>
  73. * Note: This method will clean up any unneeded listeners and key
  74. * mappings
  75. *
  76. * @param itemIds
  77. * collection of new active item ids
  78. */
  79. public void addActiveItems(Collection<?> itemIds) {
  80. for (Object itemId : itemIds) {
  81. if (!activeItemMap.containsKey(itemId)) {
  82. activeItemMap.put(itemId, new GridValueChangeListener(
  83. itemId, container.getItem(itemId)));
  84. }
  85. }
  86. // Remove still active rows that were "dropped"
  87. droppedItems.removeAll(itemIds);
  88. internalDropItems(droppedItems);
  89. droppedItems.clear();
  90. }
  91. /**
  92. * Marks given item id as dropped. Dropped items are cleared when adding
  93. * new active items.
  94. *
  95. * @param itemId
  96. * dropped item id
  97. */
  98. public void dropActiveItem(Object itemId) {
  99. if (activeItemMap.containsKey(itemId)) {
  100. droppedItems.add(itemId);
  101. }
  102. }
  103. /**
  104. * Gets a collection copy of currently active item ids.
  105. *
  106. * @return collection of item ids
  107. */
  108. public Collection<Object> getActiveItemIds() {
  109. return new HashSet<Object>(activeItemMap.keySet());
  110. }
  111. /**
  112. * Gets a collection copy of currently active ValueChangeListeners.
  113. *
  114. * @return collection of value change listeners
  115. */
  116. public Collection<GridValueChangeListener> getValueChangeListeners() {
  117. return new HashSet<GridValueChangeListener>(activeItemMap.values());
  118. }
  119. @Override
  120. public void generateData(Object itemId, Item item, JsonObject rowData) {
  121. rowData.put(GridState.JSONKEY_ROWKEY, keyMapper.key(itemId));
  122. }
  123. @Override
  124. public void destroyData(Object itemId) {
  125. keyMapper.remove(itemId);
  126. removeListener(itemId);
  127. }
  128. private void removeListener(Object itemId) {
  129. GridValueChangeListener removed = activeItemMap.remove(itemId);
  130. if (removed != null) {
  131. removed.removeListener();
  132. }
  133. }
  134. }
  135. /**
  136. * A class to listen to changes in property values in the Container added
  137. * with {@link LegacyGrid#setContainerDatasource(Container.Indexed)}, and notifies
  138. * the data source to update the client-side representation of the modified
  139. * item.
  140. * <p>
  141. * One instance of this class can (and should) be reused for all the
  142. * properties in an item, since this class will inform that the entire row
  143. * needs to be re-evaluated (in contrast to a property-based change
  144. * management)
  145. * <p>
  146. * Since there's no Container-wide possibility to listen to any kind of
  147. * value changes, an instance of this class needs to be attached to each and
  148. * every Item's Property in the container.
  149. *
  150. * @see LegacyGrid#addValueChangeListener(Container, Object, Object)
  151. * @see LegacyGrid#valueChangeListeners
  152. */
  153. private class GridValueChangeListener implements ValueChangeListener {
  154. private final Object itemId;
  155. private final Item item;
  156. public GridValueChangeListener(Object itemId, Item item) {
  157. /*
  158. * Using an assert instead of an exception throw, just to optimize
  159. * prematurely
  160. */
  161. assert itemId != null : "null itemId not accepted";
  162. this.itemId = itemId;
  163. this.item = item;
  164. internalAddColumns(getGrid().getColumns());
  165. }
  166. @Override
  167. public void valueChange(ValueChangeEvent event) {
  168. updateRowData(itemId);
  169. }
  170. public void removeListener() {
  171. removeColumns(getGrid().getColumns());
  172. }
  173. public void addColumns(Collection<Column> addedColumns) {
  174. internalAddColumns(addedColumns);
  175. updateRowData(itemId);
  176. }
  177. private void internalAddColumns(Collection<Column> addedColumns) {
  178. for (final Column column : addedColumns) {
  179. final Property<?> property = item
  180. .getItemProperty(column.getPropertyId());
  181. if (property instanceof ValueChangeNotifier) {
  182. ((ValueChangeNotifier) property)
  183. .addValueChangeListener(this);
  184. }
  185. }
  186. }
  187. public void removeColumns(Collection<Column> removedColumns) {
  188. for (final Column column : removedColumns) {
  189. final Property<?> property = item
  190. .getItemProperty(column.getPropertyId());
  191. if (property instanceof ValueChangeNotifier) {
  192. ((ValueChangeNotifier) property)
  193. .removeValueChangeListener(this);
  194. }
  195. }
  196. }
  197. }
  198. private final Indexed container;
  199. private DataProviderRpc rpc;
  200. private final ItemSetChangeListener itemListener = new ItemSetChangeListener() {
  201. @Override
  202. public void containerItemSetChange(ItemSetChangeEvent event) {
  203. if (event instanceof ItemAddEvent) {
  204. ItemAddEvent addEvent = (ItemAddEvent) event;
  205. int firstIndex = addEvent.getFirstIndex();
  206. int count = addEvent.getAddedItemsCount();
  207. insertRowData(firstIndex, count);
  208. }
  209. else if (event instanceof ItemRemoveEvent) {
  210. ItemRemoveEvent removeEvent = (ItemRemoveEvent) event;
  211. int firstIndex = removeEvent.getFirstIndex();
  212. int count = removeEvent.getRemovedItemsCount();
  213. removeRowData(firstIndex, count);
  214. }
  215. else {
  216. // Remove obsolete value change listeners.
  217. Set<Object> keySet = new HashSet<Object>(
  218. activeItemHandler.activeItemMap.keySet());
  219. for (Object itemId : keySet) {
  220. activeItemHandler.removeListener(itemId);
  221. }
  222. /* Mark as dirty to push changes in beforeClientResponse */
  223. bareItemSetTriggeredSizeChange = true;
  224. markAsDirty();
  225. }
  226. }
  227. };
  228. /** RpcDataProvider should send the current cache again. */
  229. private boolean refreshCache = false;
  230. /** Set of updated item ids */
  231. private transient Set<Object> updatedItemIds;
  232. /**
  233. * Queued RPC calls for adding and removing rows. Queue will be handled in
  234. * {@link beforeClientResponse}
  235. */
  236. private transient List<Runnable> rowChanges;
  237. /** Size possibly changed with a bare ItemSetChangeEvent */
  238. private boolean bareItemSetTriggeredSizeChange = false;
  239. private final Set<DataGenerator> dataGenerators = new LinkedHashSet<DataGenerator>();
  240. private final ActiveItemHandler activeItemHandler = new ActiveItemHandler();
  241. /**
  242. * Creates a new data provider using the given container.
  243. *
  244. * @param container
  245. * the container to make available
  246. */
  247. public RpcDataProviderExtension(Indexed container) {
  248. this.container = container;
  249. rpc = getRpcProxy(DataProviderRpc.class);
  250. registerRpc(new DataRequestRpc() {
  251. @Override
  252. public void requestRows(int firstRow, int numberOfRows,
  253. int firstCachedRowIndex, int cacheSize) {
  254. pushRowData(firstRow, numberOfRows, firstCachedRowIndex,
  255. cacheSize);
  256. }
  257. @Override
  258. public void dropRows(JsonArray rowKeys) {
  259. for (int i = 0; i < rowKeys.length(); ++i) {
  260. activeItemHandler.dropActiveItem(
  261. getKeyMapper().get(rowKeys.getString(i)));
  262. }
  263. }
  264. });
  265. if (container instanceof ItemSetChangeNotifier) {
  266. ((ItemSetChangeNotifier) container)
  267. .addItemSetChangeListener(itemListener);
  268. }
  269. addDataGenerator(activeItemHandler);
  270. }
  271. /**
  272. * {@inheritDoc}
  273. * <p>
  274. * RpcDataProviderExtension makes all actual RPC calls from this function
  275. * based on changes in the container.
  276. */
  277. @Override
  278. public void beforeClientResponse(boolean initial) {
  279. if (initial || bareItemSetTriggeredSizeChange) {
  280. /*
  281. * Push initial set of rows, assuming Grid will initially be
  282. * rendered scrolled to the top and with a decent amount of rows
  283. * visible. If this guess is right, initial data can be shown
  284. * without a round-trip and if it's wrong, the data will simply be
  285. * discarded.
  286. */
  287. int size = container.size();
  288. rpc.resetDataAndSize(size);
  289. int numberOfRows = Math.min(40, size);
  290. pushRowData(0, numberOfRows, 0, 0);
  291. } else {
  292. // Only do row changes if not initial response.
  293. if (rowChanges != null) {
  294. for (Runnable r : rowChanges) {
  295. r.run();
  296. }
  297. }
  298. // Send current rows again if needed.
  299. if (refreshCache) {
  300. for (Object itemId : activeItemHandler.getActiveItemIds()) {
  301. updateRowData(itemId);
  302. }
  303. }
  304. }
  305. internalUpdateRows(updatedItemIds);
  306. // Clear all changes.
  307. if (rowChanges != null) {
  308. rowChanges.clear();
  309. }
  310. if (updatedItemIds != null) {
  311. updatedItemIds.clear();
  312. }
  313. refreshCache = false;
  314. bareItemSetTriggeredSizeChange = false;
  315. super.beforeClientResponse(initial);
  316. }
  317. private void pushRowData(int firstRowToPush, int numberOfRows,
  318. int firstCachedRowIndex, int cacheSize) {
  319. Range newRange = Range.withLength(firstRowToPush, numberOfRows);
  320. Range cached = Range.withLength(firstCachedRowIndex, cacheSize);
  321. Range fullRange = newRange;
  322. if (!cached.isEmpty()) {
  323. fullRange = newRange.combineWith(cached);
  324. }
  325. List<?> itemIds = container.getItemIds(fullRange.getStart(),
  326. fullRange.length());
  327. JsonArray rows = Json.createArray();
  328. // Offset the index to match the wanted range.
  329. int diff = 0;
  330. if (!cached.isEmpty() && newRange.getStart() > cached.getStart()) {
  331. diff = cached.length();
  332. }
  333. for (int i = 0; i < newRange.length()
  334. && i + diff < itemIds.size(); ++i) {
  335. Object itemId = itemIds.get(i + diff);
  336. Item item = container.getItem(itemId);
  337. rows.set(i, getRowData(getGrid().getColumns(), itemId, item));
  338. }
  339. rpc.setRowData(firstRowToPush, rows);
  340. activeItemHandler.addActiveItems(itemIds);
  341. }
  342. private JsonObject getRowData(Collection<Column> columns, Object itemId,
  343. Item item) {
  344. final JsonObject rowObject = Json.createObject();
  345. for (DataGenerator dg : dataGenerators) {
  346. dg.generateData(itemId, item, rowObject);
  347. }
  348. return rowObject;
  349. }
  350. /**
  351. * Makes the data source available to the given {@link LegacyGrid} component.
  352. *
  353. * @param component
  354. * the remote data grid component to extend
  355. * @param columnKeys
  356. * the key mapper for columns
  357. */
  358. public void extend(LegacyGrid component) {
  359. super.extend(component);
  360. }
  361. /**
  362. * Adds a {@link DataGenerator} for this {@code RpcDataProviderExtension}.
  363. * DataGenerators are called when sending row data to client. If given
  364. * DataGenerator is already added, this method does nothing.
  365. *
  366. * @since 7.6
  367. * @param generator
  368. * generator to add
  369. */
  370. public void addDataGenerator(DataGenerator generator) {
  371. dataGenerators.add(generator);
  372. }
  373. /**
  374. * Removes a {@link DataGenerator} from this
  375. * {@code RpcDataProviderExtension}. If given DataGenerator is not added to
  376. * this data provider, this method does nothing.
  377. *
  378. * @since 7.6
  379. * @param generator
  380. * generator to remove
  381. */
  382. public void removeDataGenerator(DataGenerator generator) {
  383. dataGenerators.remove(generator);
  384. }
  385. /**
  386. * Informs the client side that new rows have been inserted into the data
  387. * source.
  388. *
  389. * @param index
  390. * the index at which new rows have been inserted
  391. * @param count
  392. * the number of rows inserted at <code>index</code>
  393. */
  394. private void insertRowData(final int index, final int count) {
  395. if (rowChanges == null) {
  396. rowChanges = new ArrayList<Runnable>();
  397. }
  398. if (rowChanges.isEmpty()) {
  399. markAsDirty();
  400. }
  401. /*
  402. * Since all changes should be processed in a consistent order, we don't
  403. * send the RPC call immediately. beforeClientResponse will decide
  404. * whether to send these or not. Valid situation to not send these is
  405. * initial response or bare ItemSetChange event.
  406. */
  407. rowChanges.add(new Runnable() {
  408. @Override
  409. public void run() {
  410. rpc.insertRowData(index, count);
  411. }
  412. });
  413. }
  414. /**
  415. * Informs the client side that rows have been removed from the data source.
  416. *
  417. * @param index
  418. * the index of the first row removed
  419. * @param count
  420. * the number of rows removed
  421. * @param firstItemId
  422. * the item id of the first removed item
  423. */
  424. private void removeRowData(final int index, final int count) {
  425. if (rowChanges == null) {
  426. rowChanges = new ArrayList<Runnable>();
  427. }
  428. if (rowChanges.isEmpty()) {
  429. markAsDirty();
  430. }
  431. /* See comment in insertRowData */
  432. rowChanges.add(new Runnable() {
  433. @Override
  434. public void run() {
  435. rpc.removeRowData(index, count);
  436. }
  437. });
  438. }
  439. /**
  440. * Informs the client side that data of a row has been modified in the data
  441. * source.
  442. *
  443. * @param itemId
  444. * the item Id the row that was updated
  445. */
  446. public void updateRowData(Object itemId) {
  447. if (updatedItemIds == null) {
  448. updatedItemIds = new LinkedHashSet<Object>();
  449. }
  450. if (updatedItemIds.isEmpty()) {
  451. // At least one new item will be updated. Mark as dirty to actually
  452. // update before response to client.
  453. markAsDirty();
  454. }
  455. updatedItemIds.add(itemId);
  456. }
  457. private void internalUpdateRows(Set<Object> itemIds) {
  458. if (itemIds == null || itemIds.isEmpty()) {
  459. return;
  460. }
  461. Collection<Object> activeItemIds = activeItemHandler.getActiveItemIds();
  462. List<Column> columns = getGrid().getColumns();
  463. JsonArray rowData = Json.createArray();
  464. int i = 0;
  465. for (Object itemId : itemIds) {
  466. if (activeItemIds.contains(itemId)) {
  467. Item item = container.getItem(itemId);
  468. if (item != null) {
  469. JsonObject row = getRowData(columns, itemId, item);
  470. rowData.set(i++, row);
  471. }
  472. }
  473. }
  474. rpc.updateRowData(rowData);
  475. }
  476. /**
  477. * Pushes a new version of all the rows in the active cache range.
  478. */
  479. public void refreshCache() {
  480. if (!refreshCache) {
  481. refreshCache = true;
  482. markAsDirty();
  483. }
  484. }
  485. @Override
  486. public void setParent(ClientConnector parent) {
  487. if (parent == null) {
  488. // We're being detached, release various listeners
  489. internalDropItems(activeItemHandler.getActiveItemIds());
  490. if (container instanceof ItemSetChangeNotifier) {
  491. ((ItemSetChangeNotifier) container)
  492. .removeItemSetChangeListener(itemListener);
  493. }
  494. } else if (!(parent instanceof LegacyGrid)) {
  495. throw new IllegalStateException(
  496. "Grid is the only accepted parent type");
  497. }
  498. super.setParent(parent);
  499. }
  500. /**
  501. * Informs all DataGenerators than an item id has been dropped.
  502. *
  503. * @param droppedItemIds
  504. * collection of dropped item ids
  505. */
  506. private void internalDropItems(Collection<Object> droppedItemIds) {
  507. for (Object itemId : droppedItemIds) {
  508. for (DataGenerator generator : dataGenerators) {
  509. generator.destroyData(itemId);
  510. }
  511. }
  512. }
  513. /**
  514. * Informs this data provider that given columns have been removed from
  515. * grid.
  516. *
  517. * @param removedColumns
  518. * a list of removed columns
  519. */
  520. public void columnsRemoved(List<Column> removedColumns) {
  521. for (GridValueChangeListener l : activeItemHandler
  522. .getValueChangeListeners()) {
  523. l.removeColumns(removedColumns);
  524. }
  525. // No need to resend unchanged data. Client will remember the old
  526. // columns until next set of rows is sent.
  527. }
  528. /**
  529. * Informs this data provider that given columns have been added to grid.
  530. *
  531. * @param addedColumns
  532. * a list of added columns
  533. */
  534. public void columnsAdded(List<Column> addedColumns) {
  535. for (GridValueChangeListener l : activeItemHandler
  536. .getValueChangeListeners()) {
  537. l.addColumns(addedColumns);
  538. }
  539. // Resend all rows to contain new data.
  540. refreshCache();
  541. }
  542. public KeyMapper<Object> getKeyMapper() {
  543. return activeItemHandler.keyMapper;
  544. }
  545. protected LegacyGrid getGrid() {
  546. return (LegacyGrid) getParent();
  547. }
  548. }