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.

HierarchicalDataCommunicator.java 20KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555
  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.data.provider;
  17. import java.util.ArrayList;
  18. import java.util.Arrays;
  19. import java.util.HashSet;
  20. import java.util.List;
  21. import java.util.Objects;
  22. import java.util.Optional;
  23. import java.util.Set;
  24. import java.util.function.BiConsumer;
  25. import java.util.logging.Logger;
  26. import java.util.stream.Collectors;
  27. import java.util.stream.Stream;
  28. import com.vaadin.data.TreeData;
  29. import com.vaadin.data.provider.HierarchyMapper.TreeLevelQuery;
  30. import com.vaadin.data.provider.HierarchyMapper.TreeNode;
  31. import com.vaadin.server.SerializableConsumer;
  32. import com.vaadin.shared.Range;
  33. import com.vaadin.shared.data.HierarchicalDataCommunicatorConstants;
  34. import com.vaadin.shared.extension.datacommunicator.HierarchicalDataCommunicatorState;
  35. import com.vaadin.ui.ItemCollapseAllowedProvider;
  36. import elemental.json.Json;
  37. import elemental.json.JsonArray;
  38. import elemental.json.JsonObject;
  39. /**
  40. * Data communicator that handles requesting hierarchical data from
  41. * {@link HierarchicalDataProvider} and sending it to client side.
  42. *
  43. * @param <T>
  44. * the bean type
  45. * @author Vaadin Ltd
  46. * @since 8.1
  47. */
  48. public class HierarchicalDataCommunicator<T> extends DataCommunicator<T> {
  49. private static final Logger LOGGER = Logger
  50. .getLogger(HierarchicalDataCommunicator.class.getName());
  51. /**
  52. * The amount of root level nodes to fetch and push to the client.
  53. */
  54. private static final int INITIAL_FETCH_SIZE = 100;
  55. private HierarchyMapper mapper = new HierarchyMapper();
  56. private Set<String> rowKeysPendingExpand = new HashSet<>();
  57. /**
  58. * Collapse allowed provider used to allow/disallow collapsing nodes.
  59. */
  60. private ItemCollapseAllowedProvider<T> itemCollapseAllowedProvider = t -> true;
  61. /**
  62. * The captured client side cache size.
  63. */
  64. private int latestCacheSize = INITIAL_FETCH_SIZE;
  65. /**
  66. * Construct a new hierarchical data communicator backed by a
  67. * {@link TreeDataProvider}.
  68. */
  69. public HierarchicalDataCommunicator() {
  70. super();
  71. dataProvider = new TreeDataProvider<>(new TreeData<>());
  72. }
  73. @Override
  74. protected HierarchicalDataCommunicatorState getState() {
  75. return (HierarchicalDataCommunicatorState) super.getState();
  76. }
  77. @Override
  78. protected HierarchicalDataCommunicatorState getState(boolean markAsDirty) {
  79. return (HierarchicalDataCommunicatorState) super.getState(markAsDirty);
  80. }
  81. @Override
  82. protected void sendDataToClient(boolean initial) {
  83. // on purpose do not call super
  84. if (getDataProvider() == null) {
  85. return;
  86. }
  87. if (initial || reset) {
  88. loadInitialData();
  89. } else {
  90. loadRequestedRows();
  91. }
  92. if (!getUpdatedData().isEmpty()) {
  93. JsonArray dataArray = Json.createArray();
  94. int i = 0;
  95. for (T data : getUpdatedData()) {
  96. dataArray.set(i++, createDataObject(data, -1));
  97. }
  98. getClientRpc().updateData(dataArray);
  99. getUpdatedData().clear();
  100. }
  101. }
  102. private void loadInitialData() {
  103. int rootSize = doSizeQuery(null);
  104. mapper.reset(rootSize);
  105. if (rootSize != 0) {
  106. Range initialRange = getInitialRowsToPush(rootSize);
  107. assert !initialRange
  108. .isEmpty() : "Initial range should never be empty.";
  109. Stream<T> rootItems = doFetchQuery(initialRange.getStart(),
  110. initialRange.length(), null);
  111. // for now just fetching data for the root level as everything is
  112. // collapsed by default
  113. List<T> items = rootItems.collect(Collectors.toList());
  114. List<JsonObject> dataObjects = items.stream()
  115. .map(item -> createDataObject(item, 0))
  116. .collect(Collectors.toList());
  117. getClientRpc().reset(rootSize);
  118. sendData(0, dataObjects);
  119. getActiveDataHandler().addActiveData(items.stream());
  120. getActiveDataHandler().cleanUp(items.stream());
  121. } else {
  122. getClientRpc().reset(0);
  123. }
  124. setPushRows(Range.withLength(0, 0));
  125. // any updated data is ignored at this point
  126. getUpdatedData().clear();
  127. reset = false;
  128. }
  129. private void loadRequestedRows() {
  130. final Range requestedRows = getPushRows();
  131. if (!requestedRows.isEmpty()) {
  132. doPushRows(requestedRows, 0);
  133. }
  134. setPushRows(Range.withLength(0, 0));
  135. }
  136. /**
  137. * Attempts to push the requested range of rows to the client. Will trigger
  138. * a reset if the data provider is unable to provide the requested number of
  139. * items.
  140. *
  141. * @param requestedRows
  142. * the range of rows to push
  143. * @param insertRowsCount
  144. * number of rows to insert, beginning at the start index of
  145. * {@code requestedRows}, 0 to not insert any rows
  146. * @return {@code true} if the range was successfully pushed to the client,
  147. * {@code false} if the push was unsuccessful and a reset was
  148. * triggered
  149. */
  150. private boolean doPushRows(final Range requestedRows, int insertRowsCount) {
  151. Stream<TreeLevelQuery> levelQueries = mapper.splitRangeToLevelQueries(
  152. requestedRows.getStart(), requestedRows.getEnd() - 1);
  153. JsonObject[] dataObjects = new JsonObject[requestedRows.length()];
  154. BiConsumer<JsonObject, Integer> rowDataMapper = (object,
  155. index) -> dataObjects[index
  156. - requestedRows.getStart()] = object;
  157. List<T> fetchedItems = new ArrayList<>(dataObjects.length);
  158. levelQueries.forEach(query -> {
  159. List<T> results = doFetchQuery(query.startIndex, query.size,
  160. getKeyMapper().get(query.node.getParentKey()))
  161. .collect(Collectors.toList());
  162. fetchedItems.addAll(results);
  163. List<JsonObject> rowData = results.stream()
  164. .map(item -> createDataObject(item, query.depth))
  165. .collect(Collectors.toList());
  166. mapper.reorderLevelQueryResultsToFlatOrdering(rowDataMapper, query,
  167. rowData);
  168. });
  169. if (hasNullItems(dataObjects, requestedRows)) {
  170. reset();
  171. return false;
  172. }
  173. if (insertRowsCount > 0) {
  174. getClientRpc().insertRows(requestedRows.getStart(),
  175. insertRowsCount);
  176. }
  177. sendData(requestedRows.getStart(), Arrays.asList(dataObjects));
  178. getActiveDataHandler().addActiveData(fetchedItems.stream());
  179. getActiveDataHandler().cleanUp(fetchedItems.stream());
  180. return true;
  181. }
  182. private boolean hasNullItems(JsonObject[] dataObjects,
  183. Range requestedRange) {
  184. for (JsonObject object : dataObjects) {
  185. if (object == null) {
  186. return true;
  187. }
  188. }
  189. return false;
  190. }
  191. private JsonObject createDataObject(T item, int depth) {
  192. JsonObject dataObject = getDataObject(item);
  193. JsonObject hierarchyData = Json.createObject();
  194. if (depth != -1) {
  195. hierarchyData.put(HierarchicalDataCommunicatorConstants.ROW_DEPTH,
  196. depth);
  197. }
  198. boolean isLeaf = !getDataProvider().hasChildren(item);
  199. if (isLeaf) {
  200. hierarchyData.put(HierarchicalDataCommunicatorConstants.ROW_LEAF,
  201. true);
  202. } else {
  203. String key = getKeyMapper().key(item);
  204. hierarchyData.put(
  205. HierarchicalDataCommunicatorConstants.ROW_COLLAPSED,
  206. mapper.isCollapsed(key));
  207. hierarchyData.put(HierarchicalDataCommunicatorConstants.ROW_LEAF,
  208. false);
  209. hierarchyData.put(
  210. HierarchicalDataCommunicatorConstants.ROW_COLLAPSE_ALLOWED,
  211. itemCollapseAllowedProvider.test(item));
  212. }
  213. // add hierarchy information to row as metadata
  214. dataObject.put(
  215. HierarchicalDataCommunicatorConstants.ROW_HIERARCHY_DESCRIPTION,
  216. hierarchyData);
  217. return dataObject;
  218. }
  219. private void sendData(int startIndex, List<JsonObject> dataObjects) {
  220. JsonArray dataArray = Json.createArray();
  221. int i = 0;
  222. for (JsonObject dataObject : dataObjects) {
  223. dataArray.set(i++, dataObject);
  224. }
  225. getClientRpc().setData(startIndex, dataArray);
  226. }
  227. /**
  228. * Returns the range of rows to push on initial response.
  229. *
  230. * @param rootLevelSize
  231. * the amount of rows on the root level
  232. * @return the range of rows to push initially
  233. */
  234. private Range getInitialRowsToPush(int rootLevelSize) {
  235. // TODO optimize initial level to avoid unnecessary requests
  236. return Range.between(0, Math.min(rootLevelSize, latestCacheSize));
  237. }
  238. @SuppressWarnings({ "rawtypes", "unchecked" })
  239. private Stream<T> doFetchQuery(int start, int length, T parentItem) {
  240. return getDataProvider()
  241. .fetch(new HierarchicalQuery(start, length, getBackEndSorting(),
  242. getInMemorySorting(), getFilter(), parentItem));
  243. }
  244. @SuppressWarnings({ "unchecked", "rawtypes" })
  245. private int doSizeQuery(T parentItem) {
  246. return getDataProvider()
  247. .getChildCount(new HierarchicalQuery(getFilter(), parentItem));
  248. }
  249. @Override
  250. protected void onRequestRows(int firstRowIndex, int numberOfRows,
  251. int firstCachedRowIndex, int cacheSize) {
  252. super.onRequestRows(firstRowIndex, numberOfRows, firstCachedRowIndex,
  253. cacheSize);
  254. }
  255. @Override
  256. protected void onDropRows(JsonArray keys) {
  257. for (int i = 0; i < keys.length(); i++) {
  258. // cannot drop keys of expanded rows, parents of expanded rows or
  259. // rows that are pending expand
  260. String itemKey = keys.getString(i);
  261. if (!mapper.isKeyStored(itemKey)
  262. && !rowKeysPendingExpand.contains(itemKey)) {
  263. getActiveDataHandler().dropActiveData(itemKey);
  264. }
  265. }
  266. }
  267. @Override
  268. protected void dropAllData() {
  269. super.dropAllData();
  270. rowKeysPendingExpand.clear();
  271. }
  272. @Override
  273. public HierarchicalDataProvider<T, ?> getDataProvider() {
  274. return (HierarchicalDataProvider<T, ?>) super.getDataProvider();
  275. }
  276. /**
  277. * Set the current hierarchical data provider for this communicator.
  278. *
  279. * @param dataProvider
  280. * the data provider to set, not <code>null</code>
  281. * @param initialFilter
  282. * the initial filter value to use, or <code>null</code> to not
  283. * use any initial filter value
  284. *
  285. * @param <F>
  286. * the filter type
  287. *
  288. * @return a consumer that accepts a new filter value to use
  289. */
  290. public <F> SerializableConsumer<F> setDataProvider(
  291. HierarchicalDataProvider<T, F> dataProvider, F initialFilter) {
  292. return super.setDataProvider(dataProvider, initialFilter);
  293. }
  294. /**
  295. * Set the current hierarchical data provider for this communicator.
  296. *
  297. * @param dataProvider
  298. * the data provider to set, must extend
  299. * {@link HierarchicalDataProvider}, not <code>null</code>
  300. * @param initialFilter
  301. * the initial filter value to use, or <code>null</code> to not
  302. * use any initial filter value
  303. *
  304. * @param <F>
  305. * the filter type
  306. *
  307. * @return a consumer that accepts a new filter value to use
  308. */
  309. @Override
  310. public <F> SerializableConsumer<F> setDataProvider(
  311. DataProvider<T, F> dataProvider, F initialFilter) {
  312. if (dataProvider instanceof HierarchicalDataProvider) {
  313. return super.setDataProvider(dataProvider, initialFilter);
  314. }
  315. throw new IllegalArgumentException(
  316. "Only " + HierarchicalDataProvider.class.getName()
  317. + " and subtypes supported.");
  318. }
  319. /**
  320. * Collapses given row, removing all its subtrees. Calling this method will
  321. * have no effect if the row is already collapsed.
  322. *
  323. * @param collapsedRowKey
  324. * the key of the row, not {@code null}
  325. * @param collapsedRowIndex
  326. * the index of row to collapse
  327. * @return {@code true} if the row was collapsed, {@code false} otherwise
  328. */
  329. public boolean doCollapse(String collapsedRowKey, int collapsedRowIndex) {
  330. if (collapsedRowIndex < 0 | collapsedRowIndex >= mapper.getTreeSize()) {
  331. throw new IllegalArgumentException("Invalid row index "
  332. + collapsedRowIndex + " when tree grid size of "
  333. + mapper.getTreeSize());
  334. }
  335. Objects.requireNonNull(collapsedRowKey, "Row key cannot be null");
  336. T collapsedItem = getKeyMapper().get(collapsedRowKey);
  337. Objects.requireNonNull(collapsedItem,
  338. "Cannot find item for given key " + collapsedItem);
  339. if (mapper.isCollapsed(collapsedRowKey)) {
  340. return false;
  341. }
  342. int collapsedSubTreeSize = mapper.collapse(collapsedRowKey,
  343. collapsedRowIndex);
  344. getClientRpc().removeRows(collapsedRowIndex + 1, collapsedSubTreeSize);
  345. // FIXME seems like a slight overkill to do this just for refreshing
  346. // expanded status
  347. refresh(collapsedItem);
  348. return true;
  349. }
  350. /**
  351. * Expands the given row. Calling this method will have no effect if the row
  352. * is already expanded.
  353. *
  354. * @param expandedRowKey
  355. * the key of the row, not {@code null}
  356. * @param expandedRowIndex
  357. * the index of the row to expand
  358. * @param userOriginated
  359. * whether this expand was originated from the server or client
  360. * @return {@code true} if the row was expanded, {@code false} otherwise
  361. */
  362. public boolean doExpand(String expandedRowKey, final int expandedRowIndex,
  363. boolean userOriginated) {
  364. if (!userOriginated && !rowKeysPendingExpand.contains(expandedRowKey)) {
  365. return false;
  366. }
  367. if (expandedRowIndex < 0 | expandedRowIndex >= mapper.getTreeSize()) {
  368. throw new IllegalArgumentException("Invalid row index "
  369. + expandedRowIndex + " when tree grid size of "
  370. + mapper.getTreeSize());
  371. }
  372. Objects.requireNonNull(expandedRowKey, "Row key cannot be null");
  373. final T expandedItem = getKeyMapper().get(expandedRowKey);
  374. Objects.requireNonNull(expandedItem,
  375. "Cannot find item for given key " + expandedRowKey);
  376. int expandedNodeSize = doSizeQuery(expandedItem);
  377. if (expandedNodeSize == 0) {
  378. reset();
  379. return false;
  380. }
  381. if (!mapper.isCollapsed(expandedRowKey)) {
  382. return false;
  383. }
  384. expandedNodeSize = mapper.expand(expandedRowKey, expandedRowIndex,
  385. expandedNodeSize);
  386. rowKeysPendingExpand.remove(expandedRowKey);
  387. boolean success = doPushRows(
  388. Range.withLength(expandedRowIndex + 1,
  389. Math.min(expandedNodeSize, latestCacheSize)),
  390. expandedNodeSize);
  391. if (success) {
  392. // FIXME seems like a slight overkill to do this just for refreshing
  393. // expanded status
  394. refresh(expandedItem);
  395. return true;
  396. }
  397. return false;
  398. }
  399. /**
  400. * Set an item as pending expansion.
  401. * <p>
  402. * Calling this method reserves a communication key for the item that is
  403. * guaranteed to not be invalidated until the item is expanded. Has no
  404. * effect and returns an empty optional if the given item is already
  405. * expanded or has no children.
  406. *
  407. * @param item
  408. * the item to set as pending expansion
  409. * @return an optional of the communication key used for the item, empty if
  410. * the item cannot be expanded
  411. */
  412. public Optional<String> setPendingExpand(T item) {
  413. Objects.requireNonNull(item, "Item cannot be null");
  414. if (getKeyMapper().has(item)
  415. && !mapper.isCollapsed(getKeyMapper().key(item))) {
  416. // item is already expanded
  417. return Optional.empty();
  418. }
  419. if (!getDataProvider().hasChildren(item)) {
  420. // ignore item with no children
  421. return Optional.empty();
  422. }
  423. String key = getKeyMapper().key(item);
  424. rowKeysPendingExpand.add(key);
  425. return Optional.of(key);
  426. }
  427. /**
  428. * Collapse an item.
  429. * <p>
  430. * This method will either collapse an item directly, or remove its pending
  431. * expand status. If the item is not expanded or pending expansion, calling
  432. * this method has no effect.
  433. *
  434. * @param item
  435. * the item to collapse
  436. * @return an optional of the communication key used for the item, empty if
  437. * the item cannot be collapsed
  438. */
  439. public Optional<String> collapseItem(T item) {
  440. Objects.requireNonNull(item, "Item cannot be null");
  441. if (!getKeyMapper().has(item)) {
  442. // keymapper should always have items that are expanded or pending
  443. // expand
  444. return Optional.empty();
  445. }
  446. String nodeKey = getKeyMapper().key(item);
  447. Optional<TreeNode> node = mapper.getNodeForKey(nodeKey);
  448. if (node.isPresent()) {
  449. rowKeysPendingExpand.remove(nodeKey);
  450. doCollapse(nodeKey, node.get().getStartIndex() - 1);
  451. return Optional.of(nodeKey);
  452. }
  453. if (rowKeysPendingExpand.contains(nodeKey)) {
  454. rowKeysPendingExpand.remove(nodeKey);
  455. return Optional.of(nodeKey);
  456. }
  457. return Optional.empty();
  458. }
  459. /**
  460. * Sets the item collapse allowed provider for this
  461. * HierarchicalDataCommunicator. The provider should return {@code true} for
  462. * any item that the user can collapse.
  463. * <p>
  464. * <strong>Note:</strong> This callback will be accessed often when sending
  465. * data to the client. The callback should not do any costly operations.
  466. *
  467. * @param provider
  468. * the item collapse allowed provider, not {@code null}
  469. */
  470. public void setItemCollapseAllowedProvider(
  471. ItemCollapseAllowedProvider<T> provider) {
  472. Objects.requireNonNull(provider, "Provider can't be null");
  473. itemCollapseAllowedProvider = provider;
  474. getActiveDataHandler().getActiveData().values().forEach(this::refresh);
  475. }
  476. /**
  477. * Returns parent index for the row or {@code null}
  478. *
  479. * @param rowIndex
  480. * the row index
  481. * @return the parent index or {@code null} for top-level items
  482. */
  483. public Integer getParentIndex(int rowIndex) {
  484. return mapper.getParentIndex(rowIndex);
  485. }
  486. /**
  487. * Gets the item collapse allowed provider.
  488. *
  489. * @return the item collapse allowed provider
  490. */
  491. public ItemCollapseAllowedProvider<T> getItemCollapseAllowedProvider() {
  492. return itemCollapseAllowedProvider;
  493. }
  494. }