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 18KB

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