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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952
  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.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.List;
  23. import java.util.Locale;
  24. import java.util.Map;
  25. import java.util.Set;
  26. import com.google.gwt.thirdparty.guava.common.collect.BiMap;
  27. import com.google.gwt.thirdparty.guava.common.collect.HashBiMap;
  28. import com.vaadin.data.Container.Indexed;
  29. import com.vaadin.data.Container.Indexed.ItemAddEvent;
  30. import com.vaadin.data.Container.Indexed.ItemRemoveEvent;
  31. import com.vaadin.data.Container.ItemSetChangeEvent;
  32. import com.vaadin.data.Container.ItemSetChangeListener;
  33. import com.vaadin.data.Container.ItemSetChangeNotifier;
  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.data.util.converter.Converter;
  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.DataProviderState;
  43. import com.vaadin.shared.data.DataRequestRpc;
  44. import com.vaadin.shared.ui.grid.GridState;
  45. import com.vaadin.shared.ui.grid.Range;
  46. import com.vaadin.ui.components.grid.Grid;
  47. import com.vaadin.ui.components.grid.GridColumn;
  48. import com.vaadin.ui.components.grid.Renderer;
  49. import com.vaadin.ui.components.grid.selection.SelectionChangeEvent;
  50. import com.vaadin.ui.components.grid.selection.SelectionChangeListener;
  51. import elemental.json.Json;
  52. import elemental.json.JsonArray;
  53. import elemental.json.JsonObject;
  54. import elemental.json.JsonValue;
  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
  63. * @author Vaadin Ltd
  64. */
  65. public class RpcDataProviderExtension extends AbstractExtension {
  66. /**
  67. * ItemId to Key to ItemId mapper.
  68. * <p>
  69. * This class is used when transmitting information about items in container
  70. * related to Grid. It introduces a consistent way of mapping ItemIds and
  71. * its container to a String that can be mapped back to ItemId.
  72. * <p>
  73. * <em>Technical note:</em> This class also keeps tabs on which indices are
  74. * being shown/selected, and is able to clean up after itself once the
  75. * itemId &lrarr; key mapping is not needed anymore. In other words, this
  76. * doesn't leak memory.
  77. */
  78. public class DataProviderKeyMapper implements Serializable {
  79. private final BiMap<Integer, Object> indexToItemId = HashBiMap.create();
  80. private final BiMap<Object, String> itemIdToKey = HashBiMap.create();
  81. private Set<Object> pinnedItemIds = new HashSet<Object>();
  82. private Range activeRange = Range.withLength(0, 0);
  83. private long rollingIndex = 0;
  84. private DataProviderKeyMapper() {
  85. // private implementation
  86. }
  87. void preActiveRowsChange(Range newActiveRange, int firstNewIndex,
  88. List<?> itemIds) {
  89. final Range[] removed = activeRange.partitionWith(newActiveRange);
  90. final Range[] added = newActiveRange.partitionWith(activeRange);
  91. removeActiveRows(removed[0]);
  92. removeActiveRows(removed[2]);
  93. addActiveRows(added[0], firstNewIndex, itemIds);
  94. addActiveRows(added[2], firstNewIndex, itemIds);
  95. activeRange = newActiveRange;
  96. }
  97. private void removeActiveRows(final Range deprecated) {
  98. for (int i = deprecated.getStart(); i < deprecated.getEnd(); i++) {
  99. final Integer ii = Integer.valueOf(i);
  100. final Object itemId = indexToItemId.get(ii);
  101. if (!isPinned(itemId)) {
  102. itemIdToKey.remove(itemId);
  103. indexToItemId.remove(ii);
  104. }
  105. }
  106. }
  107. private void addActiveRows(final Range added, int firstNewIndex,
  108. List<?> newItemIds) {
  109. for (int i = added.getStart(); i < added.getEnd(); i++) {
  110. /*
  111. * We might be in a situation we have an index <-> itemId entry
  112. * already. This happens when something was selected, scrolled
  113. * out of view and now we're scrolling it back into view. It's
  114. * unnecessary to overwrite it in that case.
  115. *
  116. * Fun thought: considering branch prediction, it _might_ even
  117. * be a bit faster to simply always run the code beyond this
  118. * if-state. But it sounds too stupid (and most often too
  119. * insignificant) to try out.
  120. */
  121. final Integer ii = Integer.valueOf(i);
  122. if (indexToItemId.containsKey(ii)) {
  123. continue;
  124. }
  125. /*
  126. * We might be in a situation where we have an itemId <-> key
  127. * entry already, but no index for it. This happens when
  128. * something that is out of view is selected programmatically.
  129. * In that case, we only want to add an index for that entry,
  130. * and not overwrite the key.
  131. */
  132. final Object itemId = newItemIds.get(i - firstNewIndex);
  133. if (!itemIdToKey.containsKey(itemId)) {
  134. itemIdToKey.put(itemId, nextKey());
  135. }
  136. indexToItemId.forcePut(ii, itemId);
  137. }
  138. }
  139. private String nextKey() {
  140. return String.valueOf(rollingIndex++);
  141. }
  142. String getKey(Object itemId) {
  143. String key = itemIdToKey.get(itemId);
  144. if (key == null) {
  145. key = nextKey();
  146. itemIdToKey.put(itemId, key);
  147. }
  148. return key;
  149. }
  150. /**
  151. * Gets keys for a collection of item ids.
  152. * <p>
  153. * If the itemIds are currently cached, the existing keys will be used.
  154. * Otherwise new ones will be created.
  155. *
  156. * @param itemIds
  157. * the item ids for which to get keys
  158. * @return keys for the {@code itemIds}
  159. */
  160. public List<String> getKeys(Collection<Object> itemIds) {
  161. if (itemIds == null) {
  162. throw new IllegalArgumentException("itemIds can't be null");
  163. }
  164. ArrayList<String> keys = new ArrayList<String>(itemIds.size());
  165. for (Object itemId : itemIds) {
  166. keys.add(getKey(itemId));
  167. }
  168. return keys;
  169. }
  170. /**
  171. * Gets the registered item id based on its key.
  172. * <p>
  173. * A key is used to identify a particular row on both a server and a
  174. * client. This method can be used to get the item id for the row key
  175. * that the client has sent.
  176. *
  177. * @param key
  178. * the row key for which to retrieve an item id
  179. * @return the item id corresponding to {@code key}
  180. * @throws IllegalStateException
  181. * if the key mapper does not have a record of {@code key} .
  182. */
  183. public Object getItemId(String key) throws IllegalStateException {
  184. Object itemId = itemIdToKey.inverse().get(key);
  185. if (itemId != null) {
  186. return itemId;
  187. } else {
  188. throw new IllegalStateException("No item id for key " + key
  189. + " found.");
  190. }
  191. }
  192. /**
  193. * Gets corresponding item ids for each of the keys in a collection.
  194. *
  195. * @param keys
  196. * the keys for which to retrieve item ids
  197. * @return a collection of item ids for the {@code keys}
  198. * @throws IllegalStateException
  199. * if one or more of keys don't have a corresponding item id
  200. * in the cache
  201. */
  202. public Collection<Object> getItemIds(Collection<String> keys)
  203. throws IllegalStateException {
  204. if (keys == null) {
  205. throw new IllegalArgumentException("keys may not be null");
  206. }
  207. ArrayList<Object> itemIds = new ArrayList<Object>(keys.size());
  208. for (String key : keys) {
  209. itemIds.add(getItemId(key));
  210. }
  211. return itemIds;
  212. }
  213. /**
  214. * Pin an item id to be cached indefinitely.
  215. * <p>
  216. * Normally when an itemId is not an active row, it is discarded from
  217. * the cache. Pinning an item id will make sure that it is kept in the
  218. * cache.
  219. * <p>
  220. * In effect, while an item id is pinned, it always has the same key.
  221. *
  222. * @param itemId
  223. * the item id to pin
  224. * @throws IllegalStateException
  225. * if {@code itemId} was already pinned
  226. * @see #unpin(Object)
  227. * @see #isPinned(Object)
  228. * @see #getItemIds(Collection)
  229. */
  230. public void pin(Object itemId) throws IllegalStateException {
  231. if (isPinned(itemId)) {
  232. throw new IllegalStateException("Item id " + itemId
  233. + " was pinned already");
  234. }
  235. pinnedItemIds.add(itemId);
  236. }
  237. /**
  238. * Unpin an item id.
  239. * <p>
  240. * This cancels the effect of pinning an item id. If the item id is
  241. * currently inactive, it will be immediately removed from the cache.
  242. *
  243. * @param itemId
  244. * the item id to unpin
  245. * @throws IllegalStateException
  246. * if {@code itemId} was not pinned
  247. * @see #pin(Object)
  248. * @see #isPinned(Object)
  249. * @see #getItemIds(Collection)
  250. */
  251. public void unpin(Object itemId) throws IllegalStateException {
  252. if (!isPinned(itemId)) {
  253. throw new IllegalStateException("Item id " + itemId
  254. + " was not pinned");
  255. }
  256. pinnedItemIds.remove(itemId);
  257. final Integer index = indexToItemId.inverse().get(itemId);
  258. if (index == null || !activeRange.contains(index.intValue())) {
  259. itemIdToKey.remove(itemId);
  260. indexToItemId.remove(index);
  261. }
  262. }
  263. /**
  264. * Checks whether an item id is pinned or not.
  265. *
  266. * @param itemId
  267. * the item id to check for pin status
  268. * @return {@code true} iff the item id is currently pinned
  269. */
  270. public boolean isPinned(Object itemId) {
  271. return pinnedItemIds.contains(itemId);
  272. }
  273. Object itemIdAtIndex(int index) {
  274. return indexToItemId.inverse().get(Integer.valueOf(index));
  275. }
  276. }
  277. /**
  278. * A helper class that handles the client-side Escalator logic relating to
  279. * making sure that whatever is currently visible to the user, is properly
  280. * initialized and otherwise handled on the server side (as far as
  281. * required).
  282. * <p>
  283. * This bookeeping includes, but is not limited to:
  284. * <ul>
  285. * <li>listening to the currently visible {@link com.vaadin.data.Property
  286. * Properties'} value changes on the server side and sending those back to
  287. * the client; and
  288. * <li>attaching and detaching {@link com.vaadin.ui.Component Components}
  289. * from the Vaadin Component hierarchy.
  290. * </ul>
  291. */
  292. private class ActiveRowHandler implements Serializable {
  293. /**
  294. * A map from itemId to the value change listener used for all of its
  295. * properties
  296. */
  297. private final Map<Object, GridValueChangeListener> valueChangeListeners = new HashMap<Object, GridValueChangeListener>();
  298. /**
  299. * The currently active range. Practically, it's the range of row
  300. * indices being cached currently.
  301. */
  302. private Range activeRange = Range.withLength(0, 0);
  303. /**
  304. * A hook for making sure that appropriate data is "active". All other
  305. * rows should be "inactive".
  306. * <p>
  307. * "Active" can mean different things in different contexts. For
  308. * example, only the Properties in the active range need
  309. * ValueChangeListeners. Also, whenever a row with a Component becomes
  310. * active, it needs to be attached (and conversely, when inactive, it
  311. * needs to be detached).
  312. *
  313. * @param firstActiveRow
  314. * the first active row
  315. * @param activeRowCount
  316. * the number of active rows
  317. */
  318. public void setActiveRows(int firstActiveRow, int activeRowCount) {
  319. final Range newActiveRange = Range.withLength(firstActiveRow,
  320. activeRowCount);
  321. // TODO [[Components]] attach and detach components
  322. /*-
  323. * Example
  324. *
  325. * New Range: [3, 4, 5, 6, 7]
  326. * Old Range: [1, 2, 3, 4, 5]
  327. * Result: [1, 2][3, 4, 5] []
  328. */
  329. final Range[] depractionPartition = activeRange
  330. .partitionWith(newActiveRange);
  331. removeValueChangeListeners(depractionPartition[0]);
  332. removeValueChangeListeners(depractionPartition[2]);
  333. /*-
  334. * Example
  335. *
  336. * Old Range: [1, 2, 3, 4, 5]
  337. * New Range: [3, 4, 5, 6, 7]
  338. * Result: [] [3, 4, 5][6, 7]
  339. */
  340. final Range[] activationPartition = newActiveRange
  341. .partitionWith(activeRange);
  342. addValueChangeListeners(activationPartition[0]);
  343. addValueChangeListeners(activationPartition[2]);
  344. activeRange = newActiveRange;
  345. }
  346. private void addValueChangeListeners(Range range) {
  347. for (int i = range.getStart(); i < range.getEnd(); i++) {
  348. final Object itemId = container.getIdByIndex(i);
  349. final Item item = container.getItem(itemId);
  350. if (valueChangeListeners.containsKey(itemId)) {
  351. /*
  352. * This might occur when items are removed from above the
  353. * viewport, the escalator scrolls up to compensate, but the
  354. * same items remain in the view: It looks as if one row was
  355. * scrolled, when in fact the whole viewport was shifted up.
  356. */
  357. continue;
  358. }
  359. GridValueChangeListener listener = new GridValueChangeListener(
  360. itemId);
  361. valueChangeListeners.put(itemId, listener);
  362. for (final Object propertyId : item.getItemPropertyIds()) {
  363. final Property<?> property = item
  364. .getItemProperty(propertyId);
  365. if (property instanceof ValueChangeNotifier) {
  366. ((ValueChangeNotifier) property)
  367. .addValueChangeListener(listener);
  368. }
  369. }
  370. }
  371. }
  372. private void removeValueChangeListeners(Range range) {
  373. for (int i = range.getStart(); i < range.getEnd(); i++) {
  374. final Object itemId = container.getIdByIndex(i);
  375. final Item item = container.getItem(itemId);
  376. final GridValueChangeListener listener = valueChangeListeners
  377. .remove(itemId);
  378. if (listener != null) {
  379. for (final Object propertyId : item.getItemPropertyIds()) {
  380. final Property<?> property = item
  381. .getItemProperty(propertyId);
  382. if (property instanceof ValueChangeNotifier) {
  383. ((ValueChangeNotifier) property)
  384. .removeValueChangeListener(listener);
  385. }
  386. }
  387. }
  388. }
  389. }
  390. /**
  391. * Manages removed properties in active rows.
  392. *
  393. * @param removedPropertyIds
  394. * the property ids that have been removed from the container
  395. */
  396. public void propertiesRemoved(@SuppressWarnings("unused")
  397. Collection<Object> removedPropertyIds) {
  398. /*
  399. * no-op, for now.
  400. *
  401. * The Container should be responsible for cleaning out any
  402. * ValueChangeListeners from removed Properties. Components will
  403. * benefit from this, however.
  404. */
  405. }
  406. /**
  407. * Manages added properties in active rows.
  408. *
  409. * @param addedPropertyIds
  410. * the property ids that have been added to the container
  411. */
  412. public void propertiesAdded(Collection<Object> addedPropertyIds) {
  413. for (int i = activeRange.getStart(); i < activeRange.getEnd(); i++) {
  414. final Object itemId = container.getIdByIndex(i);
  415. final Item item = container.getItem(itemId);
  416. final GridValueChangeListener listener = valueChangeListeners
  417. .get(itemId);
  418. assert (listener != null) : "a listener should've been pre-made by addValueChangeListeners";
  419. for (final Object propertyId : addedPropertyIds) {
  420. final Property<?> property = item
  421. .getItemProperty(propertyId);
  422. if (property instanceof ValueChangeNotifier) {
  423. ((ValueChangeNotifier) property)
  424. .addValueChangeListener(listener);
  425. }
  426. }
  427. }
  428. }
  429. /**
  430. * Handles the insertion of rows.
  431. * <p>
  432. * This method's responsibilities are to:
  433. * <ul>
  434. * <li>shift the internal bookkeeping by <code>count</code> if the
  435. * insertion happens above currently active range
  436. * <li>ignore rows inserted below the currently active range
  437. * <li>shift (and deactivate) rows pushed out of view
  438. * <li>activate rows that are inserted in the current viewport
  439. * </ul>
  440. *
  441. * @param firstIndex
  442. * the index of the first inserted rows
  443. * @param count
  444. * the number of rows inserted at <code>firstIndex</code>
  445. */
  446. public void insertRows(int firstIndex, int count) {
  447. if (firstIndex < activeRange.getStart()) {
  448. activeRange = activeRange.offsetBy(count);
  449. } else if (firstIndex < activeRange.getEnd()) {
  450. final Range deprecatedRange = Range.withLength(
  451. activeRange.getEnd(), count);
  452. removeValueChangeListeners(deprecatedRange);
  453. final Range freshRange = Range.between(firstIndex, count);
  454. addValueChangeListeners(freshRange);
  455. } else {
  456. // out of view, noop
  457. }
  458. }
  459. /**
  460. * Removes a single item by its id.
  461. *
  462. * @param itemId
  463. * the id of the removed id. <em>Note:</em> this item does
  464. * not exist anymore in the datasource
  465. */
  466. public void removeItemId(Object itemId) {
  467. final GridValueChangeListener removedListener = valueChangeListeners
  468. .remove(itemId);
  469. if (removedListener != null) {
  470. /*
  471. * We removed an item from somewhere in the visible range, so we
  472. * make the active range shorter. The empty hole will be filled
  473. * by the client-side code when it asks for more information.
  474. */
  475. activeRange = Range.withLength(activeRange.getStart(),
  476. activeRange.length() - 1);
  477. }
  478. }
  479. }
  480. /**
  481. * A class to listen to changes in property values in the Container added
  482. * with {@link Grid#setContainerDatasource(Container.Indexed)}, and notifies
  483. * the data source to update the client-side representation of the modified
  484. * item.
  485. * <p>
  486. * One instance of this class can (and should) be reused for all the
  487. * properties in an item, since this class will inform that the entire row
  488. * needs to be re-evaluated (in contrast to a property-based change
  489. * management)
  490. * <p>
  491. * Since there's no Container-wide possibility to listen to any kind of
  492. * value changes, an instance of this class needs to be attached to each and
  493. * every Item's Property in the container.
  494. *
  495. * @see Grid#addValueChangeListener(Container, Object, Object)
  496. * @see Grid#valueChangeListeners
  497. */
  498. private class GridValueChangeListener implements ValueChangeListener {
  499. private final Object itemId;
  500. public GridValueChangeListener(Object itemId) {
  501. /*
  502. * Using an assert instead of an exception throw, just to optimize
  503. * prematurely
  504. */
  505. assert itemId != null : "null itemId not accepted";
  506. this.itemId = itemId;
  507. }
  508. @Override
  509. public void valueChange(ValueChangeEvent event) {
  510. updateRowData(container.indexOfId(itemId));
  511. }
  512. }
  513. private final Indexed container;
  514. private final ActiveRowHandler activeRowHandler = new ActiveRowHandler();
  515. private DataProviderRpc rpc;
  516. private final ItemSetChangeListener itemListener = new ItemSetChangeListener() {
  517. @Override
  518. public void containerItemSetChange(ItemSetChangeEvent event) {
  519. if (event instanceof ItemAddEvent) {
  520. ItemAddEvent addEvent = (ItemAddEvent) event;
  521. int firstIndex = addEvent.getFirstIndex();
  522. int count = addEvent.getAddedItemsCount();
  523. insertRowData(firstIndex, count);
  524. }
  525. else if (event instanceof ItemRemoveEvent) {
  526. ItemRemoveEvent removeEvent = (ItemRemoveEvent) event;
  527. int firstIndex = removeEvent.getFirstIndex();
  528. int count = removeEvent.getRemovedItemsCount();
  529. removeRowData(firstIndex, count);
  530. }
  531. else {
  532. /*
  533. * Clear everything we have in view, and let the client
  534. * re-request for whatever it needs.
  535. *
  536. * Why this shortcut? Well, since anything could've happened, we
  537. * don't know what has happened. There are a lot of use-cases we
  538. * can cover at once with this carte blanche operation:
  539. *
  540. * 1) Grid is scrolled somewhere in the middle and all the
  541. * rows-inview are removed. We need a new pageful.
  542. *
  543. * 2) Grid is scrolled somewhere in the middle and none of the
  544. * visible rows are removed. We need no new rows.
  545. *
  546. * 3) Grid is scrolled all the way to the bottom, and the last
  547. * rows are being removed. Grid needs to scroll up and request
  548. * for more rows at the top.
  549. *
  550. * 4) Grid is scrolled pretty much to the bottom, and the last
  551. * rows are being removed. Grid needs to be aware that some
  552. * scrolling is needed, but not to compensate for all the
  553. * removed rows. And it also needs to request for some more rows
  554. * to the top.
  555. *
  556. * 5) Some ranges of rows are removed from view. We need to
  557. * collapse the gaps with existing rows and load the missing
  558. * rows.
  559. *
  560. * 6) The ultimate use case! Grid has 1.5 pages of rows and
  561. * scrolled a bit down. One page of rows is removed. We need to
  562. * make sure that new rows are loaded, but not all old slots are
  563. * occupied, since the page can't be filled with new row data.
  564. * It also needs to be scrolled to the top.
  565. *
  566. * So, it's easier (and safer) to do the simple thing instead of
  567. * taking all the corner cases into account.
  568. */
  569. activeRowHandler.activeRange = Range.withLength(0, 0);
  570. activeRowHandler.valueChangeListeners.clear();
  571. rpc.resetDataAndSize(event.getContainer().size());
  572. getState().containerSize = event.getContainer().size();
  573. }
  574. }
  575. };
  576. private final DataProviderKeyMapper keyMapper = new DataProviderKeyMapper();
  577. private KeyMapper<Object> columnKeys;
  578. /**
  579. * Creates a new data provider using the given container.
  580. *
  581. * @param container
  582. * the container to make available
  583. */
  584. public RpcDataProviderExtension(Indexed container) {
  585. this.container = container;
  586. rpc = getRpcProxy(DataProviderRpc.class);
  587. registerRpc(new DataRequestRpc() {
  588. private Collection<String> allTemporarilyPinnedKeys = new ArrayList<String>();
  589. @Override
  590. public void requestRows(int firstRow, int numberOfRows,
  591. int firstCachedRowIndex, int cacheSize,
  592. List<String> temporarilyPinnedKeys) {
  593. for (String key : temporarilyPinnedKeys) {
  594. Object itemId = keyMapper.getItemId(key);
  595. if (!keyMapper.isPinned(itemId)) {
  596. keyMapper.pin(itemId);
  597. }
  598. }
  599. allTemporarilyPinnedKeys.addAll(temporarilyPinnedKeys);
  600. Range active = Range.withLength(firstRow, numberOfRows);
  601. if (cacheSize != 0) {
  602. Range cached = Range.withLength(firstCachedRowIndex,
  603. cacheSize);
  604. active = active.combineWith(cached);
  605. }
  606. List<?> itemIds = RpcDataProviderExtension.this.container
  607. .getItemIds(firstRow, numberOfRows);
  608. keyMapper.preActiveRowsChange(active, firstRow, itemIds);
  609. pushRows(firstRow, itemIds);
  610. activeRowHandler.setActiveRows(active.getStart(),
  611. active.length());
  612. }
  613. @Override
  614. public void releaseTemporarilyPinnedKeys() {
  615. /*
  616. * This needs to be done deferredly since the selection event
  617. * comes after this RPC call.
  618. */
  619. final SelectionChangeListener listener = new SelectionChangeListener() {
  620. @Override
  621. public void selectionChange(SelectionChangeEvent event) {
  622. for (String tempPinnedKey : allTemporarilyPinnedKeys) {
  623. /*
  624. * TODO: this could be moved into a field instead of
  625. * inline to reduce indentations.
  626. */
  627. /*
  628. * This works around the fact that when deselecting
  629. * and leaping through the cache, the client tries
  630. * to send a deselect event even though a row never
  631. * was selected. So, it tries to unpin something
  632. * that never was temporarily pinned.
  633. *
  634. * If the same thing would happen while selecting
  635. * (instead of deselecting), the row would be
  636. * pinned, not because of the temporary pinning, but
  637. * because it's selected.
  638. */
  639. if (!keyMapper.isPinned(tempPinnedKey)) {
  640. continue;
  641. }
  642. Object itemId = keyMapper.getItemId(tempPinnedKey);
  643. Integer index = keyMapper.indexToItemId.inverse()
  644. .get(itemId);
  645. if (!getGrid().isSelected(itemId)
  646. && !activeRowHandler.activeRange
  647. .contains(index.intValue())) {
  648. keyMapper.unpin(itemId);
  649. }
  650. }
  651. allTemporarilyPinnedKeys = new ArrayList<String>();
  652. getGrid().removeSelectionChangeListener(this);
  653. }
  654. };
  655. getGrid().addSelectionChangeListener(listener);
  656. }
  657. });
  658. getState().containerSize = container.size();
  659. if (container instanceof ItemSetChangeNotifier) {
  660. ((ItemSetChangeNotifier) container)
  661. .addItemSetChangeListener(itemListener);
  662. }
  663. }
  664. private void pushRows(int firstRow, List<?> itemIds) {
  665. Collection<?> propertyIds = container.getContainerPropertyIds();
  666. JsonArray rows = Json.createArray();
  667. for (int i = 0; i < itemIds.size(); ++i) {
  668. rows.set(i, getRowData(propertyIds, itemIds.get(i)));
  669. }
  670. rpc.setRowData(firstRow, rows.toJson());
  671. }
  672. private JsonValue getRowData(Collection<?> propertyIds, Object itemId) {
  673. Item item = container.getItem(itemId);
  674. JsonObject rowData = Json.createObject();
  675. Grid grid = getGrid();
  676. int i = 0;
  677. for (Object propertyId : propertyIds) {
  678. GridColumn column = grid.getColumn(propertyId);
  679. Object propertyValue = item.getItemProperty(propertyId).getValue();
  680. JsonValue encodedValue = encodeValue(propertyValue,
  681. column.getRenderer(), column.getConverter(),
  682. grid.getLocale());
  683. rowData.put(columnKeys.key(propertyId), encodedValue);
  684. }
  685. final JsonObject rowObject = Json.createObject();
  686. rowObject.put(GridState.JSONKEY_DATA, rowData);
  687. rowObject.put(GridState.JSONKEY_ROWKEY, keyMapper.getKey(itemId));
  688. return rowObject;
  689. }
  690. @Override
  691. protected DataProviderState getState() {
  692. return (DataProviderState) super.getState();
  693. }
  694. /**
  695. * Makes the data source available to the given {@link Grid} component.
  696. *
  697. * @param component
  698. * the remote data grid component to extend
  699. */
  700. public void extend(Grid component, KeyMapper<Object> columnKeys) {
  701. this.columnKeys = columnKeys;
  702. super.extend(component);
  703. }
  704. /**
  705. * Informs the client side that new rows have been inserted into the data
  706. * source.
  707. *
  708. * @param index
  709. * the index at which new rows have been inserted
  710. * @param count
  711. * the number of rows inserted at <code>index</code>
  712. */
  713. private void insertRowData(int index, int count) {
  714. getState().containerSize += count;
  715. rpc.insertRowData(index, count);
  716. activeRowHandler.insertRows(index, count);
  717. }
  718. /**
  719. * Informs the client side that rows have been removed from the data source.
  720. *
  721. * @param firstIndex
  722. * the index of the first row removed
  723. * @param count
  724. * the number of rows removed
  725. * @param firstItemId
  726. * the item id of the first removed item
  727. */
  728. private void removeRowData(int firstIndex, int count) {
  729. getState().containerSize -= count;
  730. rpc.removeRowData(firstIndex, count);
  731. for (int i = 0; i < count; i++) {
  732. Object itemId = keyMapper.itemIdAtIndex(firstIndex + i);
  733. if (itemId != null) {
  734. activeRowHandler.removeItemId(itemId);
  735. }
  736. }
  737. }
  738. /**
  739. * Informs the client side that data of a row has been modified in the data
  740. * source.
  741. *
  742. * @param index
  743. * the index of the row that was updated
  744. */
  745. public void updateRowData(int index) {
  746. /*
  747. * TODO: ignore duplicate requests for the same index during the same
  748. * roundtrip.
  749. */
  750. Object itemId = container.getIdByIndex(index);
  751. JsonValue row = getRowData(container.getContainerPropertyIds(), itemId);
  752. JsonArray rowArray = Json.createArray();
  753. rowArray.set(0, row);
  754. rpc.setRowData(index, rowArray.toJson());
  755. }
  756. @Override
  757. public void setParent(ClientConnector parent) {
  758. super.setParent(parent);
  759. if (parent == null) {
  760. // We're detached, release various listeners
  761. activeRowHandler
  762. .removeValueChangeListeners(activeRowHandler.activeRange);
  763. if (container instanceof ItemSetChangeNotifier) {
  764. ((ItemSetChangeNotifier) container)
  765. .removeItemSetChangeListener(itemListener);
  766. }
  767. }
  768. }
  769. /**
  770. * Informs this data provider that some of the properties have been removed
  771. * from the container.
  772. * <p>
  773. * Please note that we could add our own
  774. * {@link com.vaadin.data.Container.PropertySetChangeListener
  775. * PropertySetChangeListener} to the container, but then we'd need to
  776. * implement the same bookeeping for finding what's added and removed that
  777. * Grid already does in its own listener.
  778. *
  779. * @param removedColumns
  780. * a list of property ids for the removed columns
  781. */
  782. public void propertiesRemoved(List<Object> removedColumns) {
  783. activeRowHandler.propertiesRemoved(removedColumns);
  784. }
  785. /**
  786. * Informs this data provider that some of the properties have been added to
  787. * the container.
  788. * <p>
  789. * Please note that we could add our own
  790. * {@link com.vaadin.data.Container.PropertySetChangeListener
  791. * PropertySetChangeListener} to the container, but then we'd need to
  792. * implement the same bookeeping for finding what's added and removed that
  793. * Grid already does in its own listener.
  794. *
  795. * @param addedPropertyIds
  796. * a list of property ids for the added columns
  797. */
  798. public void propertiesAdded(HashSet<Object> addedPropertyIds) {
  799. activeRowHandler.propertiesAdded(addedPropertyIds);
  800. }
  801. public DataProviderKeyMapper getKeyMapper() {
  802. return keyMapper;
  803. }
  804. protected Grid getGrid() {
  805. return (Grid) getParent();
  806. }
  807. /**
  808. * Converts and encodes the given data model property value using the given
  809. * converter and renderer. This method is public only for testing purposes.
  810. *
  811. * @param renderer
  812. * the renderer to use
  813. * @param converter
  814. * the converter to use
  815. * @param modelValue
  816. * the value to convert and encode
  817. * @param locale
  818. * the locale to use in conversion
  819. * @return an encoded value ready to be sent to the client
  820. */
  821. public static <T> JsonValue encodeValue(Object modelValue,
  822. Renderer<T> renderer, Converter<?, ?> converter, Locale locale) {
  823. Class<T> presentationType = renderer.getPresentationType();
  824. T presentationValue;
  825. if (converter == null) {
  826. try {
  827. presentationValue = presentationType.cast(modelValue);
  828. } catch (ClassCastException e) {
  829. throw new Converter.ConversionException(
  830. "Unable to convert value of type "
  831. + modelValue.getClass().getName()
  832. + " to presentation type "
  833. + presentationType.getName()
  834. + ". No converter is set and the types are not compatible.");
  835. }
  836. } else {
  837. assert presentationType.isAssignableFrom(converter
  838. .getPresentationType());
  839. @SuppressWarnings("unchecked")
  840. Converter<T, Object> safeConverter = (Converter<T, Object>) converter;
  841. presentationValue = safeConverter.convertToPresentation(modelValue,
  842. safeConverter.getPresentationType(), locale);
  843. }
  844. JsonValue encodedValue = renderer.encode(presentationValue);
  845. return encodedValue;
  846. }
  847. }