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.

HierarchyMapper.java 17KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445
  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.io.Serializable;
  18. import java.util.ArrayList;
  19. import java.util.List;
  20. import java.util.Objects;
  21. import java.util.Optional;
  22. import java.util.TreeSet;
  23. import java.util.concurrent.atomic.AtomicInteger;
  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. /**
  29. * Mapper for hierarchical data.
  30. * <p>
  31. * Keeps track of the expanded nodes, and size of of the subtrees for each
  32. * expanded node.
  33. * <p>
  34. * This class is framework internal implementation details, and can be changed /
  35. * moved at any point. This means that you should not directly use this for
  36. * anything.
  37. *
  38. * @author Vaadin Ltd
  39. * @since
  40. */
  41. class HierarchyMapper implements Serializable {
  42. private static final Logger LOGGER = Logger
  43. .getLogger(HierarchyMapper.class.getName());
  44. /**
  45. * A POJO that represents a query data for a certain tree level.
  46. */
  47. static class TreeLevelQuery { // not serializable since not stored
  48. /**
  49. * The tree node that the query is for. Only used for fetching parent
  50. * key.
  51. */
  52. final TreeNode node;
  53. /** The start index of the query, from 0 to level's size - 1. */
  54. final int startIndex;
  55. /** The number of rows to fetch. s */
  56. final int size;
  57. /** The depth of this node. */
  58. final int depth;
  59. /** The first row index in grid, including all the nodes. */
  60. final int firstRowIndex;
  61. /** The direct subtrees for the node that effect the indexing. */
  62. final List<TreeNode> subTrees;
  63. TreeLevelQuery(TreeNode node, int startIndex, int size, int depth,
  64. int firstRowIndex, List<TreeNode> subTrees) {
  65. this.node = node;
  66. this.startIndex = startIndex;
  67. this.size = size;
  68. this.depth = depth;
  69. this.firstRowIndex = firstRowIndex;
  70. this.subTrees = subTrees;
  71. }
  72. }
  73. /**
  74. * A level in the tree, either the root level or an expanded subtree level.
  75. * <p>
  76. * Comparable based on the {@link #startIndex}, which is flat from 0 to data
  77. * size - 1.
  78. */
  79. static class TreeNode implements Serializable, Comparable<TreeNode> {
  80. /** The key for the expanded item that this is a subtree of. */
  81. private final String parentKey;
  82. /** The first index on this level. */
  83. private int startIndex;
  84. /** The last index on this level, INCLUDING subtrees. */
  85. private int endIndex;
  86. TreeNode(String parentKey, int startIndex, int size) {
  87. this.parentKey = parentKey;
  88. this.startIndex = startIndex;
  89. endIndex = startIndex + size - 1;
  90. }
  91. TreeNode(int startIndex) {
  92. parentKey = "INVALID";
  93. this.startIndex = startIndex;
  94. }
  95. int getStartIndex() {
  96. return startIndex;
  97. }
  98. int getEndIndex() {
  99. return endIndex;
  100. }
  101. String getParentKey() {
  102. return parentKey;
  103. }
  104. private void push(int offset) {
  105. startIndex += offset;
  106. endIndex += offset;
  107. }
  108. private void pushEnd(int offset) {
  109. endIndex += offset;
  110. }
  111. @Override
  112. public int compareTo(TreeNode other) {
  113. return Integer.valueOf(startIndex).compareTo(other.startIndex);
  114. }
  115. @Override
  116. public String toString() {
  117. return "TreeNode [parent=" + parentKey + ", start=" + startIndex
  118. + ", end=" + getEndIndex() + "]";
  119. }
  120. }
  121. /** The expanded nodes in the tree. */
  122. private final TreeSet<TreeNode> nodes = new TreeSet<>();
  123. /**
  124. * Resets the tree, sets given the root level size.
  125. *
  126. * @param rootLevelSize
  127. * the number of items in the root level
  128. */
  129. public void reset(int rootLevelSize) {
  130. nodes.clear();
  131. nodes.add(new TreeNode(null, 0, rootLevelSize));
  132. }
  133. /**
  134. * Returns the complete size of the tree, including all expanded subtrees.
  135. *
  136. * @return the size of the tree
  137. */
  138. public int getTreeSize() {
  139. TreeNode rootNode = getNodeForKey(null)
  140. .orElse(new TreeNode(null, 0, 0));
  141. return rootNode.endIndex + 1;
  142. }
  143. /**
  144. * Returns whether the node with the given is collapsed or not.
  145. *
  146. * @param itemKey
  147. * the key of node to check
  148. * @return {@code true} if collapsed, {@code false} if expanded
  149. */
  150. public boolean isCollapsed(String itemKey) {
  151. return !getNodeForKey(itemKey).isPresent();
  152. }
  153. /**
  154. * Return the depth of expanded node's subtree.
  155. * <p>
  156. * The root node depth is 0.
  157. *
  158. * @param expandedNodeKey
  159. * the item key of the expanded node
  160. * @return the depth of the expanded node
  161. * @throws IllegalArgumentException
  162. * if the node was not expanded
  163. */
  164. protected int getDepth(String expandedNodeKey) {
  165. Optional<TreeNode> node = getNodeForKey(expandedNodeKey);
  166. if (!node.isPresent()) {
  167. throw new IllegalArgumentException("No node with given key "
  168. + expandedNodeKey + " was expanded.");
  169. }
  170. TreeNode treeNode = node.get();
  171. AtomicInteger start = new AtomicInteger(treeNode.startIndex);
  172. AtomicInteger end = new AtomicInteger(treeNode.getEndIndex());
  173. AtomicInteger depth = new AtomicInteger();
  174. nodes.headSet(treeNode, false).descendingSet().forEach(higherNode -> {
  175. if (higherNode.startIndex < start.get()
  176. && higherNode.getEndIndex() >= end.get()) {
  177. start.set(higherNode.startIndex);
  178. depth.incrementAndGet();
  179. }
  180. });
  181. return depth.get();
  182. }
  183. /**
  184. * Returns the tree node for the given expanded item key, or an empty
  185. * optional if the item was not expanded.
  186. *
  187. * @param expandedNodeKey
  188. * the key of the item
  189. * @return the tree node for the expanded item, or an empty optional if not
  190. * expanded
  191. */
  192. protected Optional<TreeNode> getNodeForKey(String expandedNodeKey) {
  193. return nodes.stream()
  194. .filter(node -> Objects.equals(node.parentKey, expandedNodeKey))
  195. .findAny();
  196. }
  197. /**
  198. * Expands the node in the given index and with the given key.
  199. *
  200. * @param expanedRowKey
  201. * the key of the expanded item
  202. * @param expandedRowIndex
  203. * the index of the expanded item
  204. * @param expandedNodeSize
  205. * the size of the subtree of the expanded node
  206. * @throws IllegalStateException
  207. * if the node was expanded already
  208. */
  209. protected void expand(String expanedRowKey, int expandedRowIndex,
  210. int expandedNodeSize) {
  211. if (expandedNodeSize < 1) {
  212. throw new IllegalArgumentException(
  213. "The expanded node's size cannot be less than 1, was "
  214. + expandedNodeSize);
  215. }
  216. TreeNode newNode = new TreeNode(expanedRowKey, expandedRowIndex + 1,
  217. expandedNodeSize);
  218. boolean added = nodes.add(newNode);
  219. if (!added) {
  220. throw new IllegalStateException("Node in index " + expandedRowIndex
  221. + " was expanded already.");
  222. }
  223. // push end indexes for parent nodes
  224. List<TreeNode> updated = nodes.headSet(newNode, false).stream()
  225. .filter(node -> node.getEndIndex() >= expandedRowIndex)
  226. .collect(Collectors.toList());
  227. nodes.removeAll(updated);
  228. updated.stream().forEach(node -> node.pushEnd(expandedNodeSize));
  229. nodes.addAll(updated);
  230. // push start and end indexes for later nodes
  231. updated = nodes.tailSet(newNode, false).stream()
  232. .collect(Collectors.toList());
  233. nodes.removeAll(updated);
  234. updated.stream().forEach(node -> node.push(expandedNodeSize));
  235. nodes.addAll(updated);
  236. }
  237. /**
  238. * Collapses the node in the given index.
  239. *
  240. * @param key
  241. * the key of the collapsed item
  242. * @param collapsedRowIndex
  243. * the index of the collapsed item
  244. * @return the size of the complete subtree that was collapsed
  245. * @throws IllegalStateException
  246. * if the node was not collapsed, or if the given key is not the
  247. * same as it was when the node has been expanded
  248. */
  249. protected int collapse(String key, int collapsedRowIndex) {
  250. Objects.requireNonNull(key,
  251. "The key for the item to collapse cannot be null.");
  252. TreeNode collapsedNode = nodes
  253. .ceiling(new TreeNode(collapsedRowIndex + 1));
  254. if (collapsedNode == null
  255. || collapsedNode.startIndex != collapsedRowIndex + 1) {
  256. throw new IllegalStateException(
  257. "Could not find expanded node for index "
  258. + collapsedRowIndex + ", node was not collapsed");
  259. }
  260. if (!Objects.equals(key, collapsedNode.parentKey)) {
  261. throw new IllegalStateException("The expected parent key " + key
  262. + " is different for the collapsed node " + collapsedNode);
  263. }
  264. // remove complete subtree
  265. AtomicInteger removedSubTreeSize = new AtomicInteger(
  266. collapsedNode.getEndIndex() - collapsedNode.startIndex + 1);
  267. nodes.tailSet(collapsedNode, false).removeIf(
  268. node -> node.startIndex <= collapsedNode.getEndIndex());
  269. final int offset = -1 * removedSubTreeSize.get();
  270. // adjust parent end indexes
  271. List<TreeNode> updated = nodes.headSet(collapsedNode, false).stream()
  272. .filter(node -> node.getEndIndex() >= collapsedRowIndex)
  273. .collect(Collectors.toList());
  274. nodes.removeAll(updated);
  275. updated.stream().forEach(node -> node.pushEnd(offset));
  276. nodes.addAll(updated);
  277. // adjust start and end indexes for latter nodes
  278. updated = nodes.tailSet(collapsedNode, false).stream()
  279. .collect(Collectors.toList());
  280. nodes.removeAll(updated);
  281. updated.stream().forEach(node -> node.push(offset));
  282. nodes.addAll(updated);
  283. nodes.remove(collapsedNode);
  284. return removedSubTreeSize.get();
  285. }
  286. /**
  287. * Splits the given range into queries per tree level.
  288. *
  289. * @param firstRow
  290. * the first row to fetch
  291. * @param lastRow
  292. * the last row to fetch
  293. * @return a stream of query data per level
  294. * @see #reorderLevelQueryResultsToFlatOrdering(BiConsumer, TreeLevelQuery,
  295. * List)
  296. */
  297. protected Stream<TreeLevelQuery> splitRangeToLevelQueries(
  298. final int firstRow, final int lastRow) {
  299. return nodes.stream()
  300. // filter to parts intersecting with the range
  301. .filter(node -> node.startIndex <= lastRow
  302. && firstRow <= node.getEndIndex())
  303. // split into queries per level with level based indexing
  304. .map(node -> {
  305. // calculate how subtrees effect indexing and size
  306. int depth = getDepth(node.parentKey);
  307. List<TreeNode> directSubTrees = nodes.tailSet(node, false)
  308. .stream()
  309. // find subtrees
  310. .filter(subTree -> node.startIndex < subTree
  311. .getEndIndex()
  312. && subTree.startIndex < node.getEndIndex())
  313. // filter to direct subtrees
  314. .filter(subTree -> getDepth(
  315. subTree.parentKey) == (depth + 1))
  316. .collect(Collectors.toList());
  317. // first intersecting index in flat order
  318. AtomicInteger firstIntersectingRowIndex = new AtomicInteger(
  319. Math.max(node.startIndex, firstRow));
  320. // last intersecting index in flat order
  321. final int lastIntersectingRowIndex = Math
  322. .min(node.getEndIndex(), lastRow);
  323. // start index for this level
  324. AtomicInteger start = new AtomicInteger(
  325. firstIntersectingRowIndex.get() - node.startIndex);
  326. // how many nodes should be fetched for this level
  327. AtomicInteger size = new AtomicInteger(
  328. lastIntersectingRowIndex
  329. - firstIntersectingRowIndex.get() + 1);
  330. // reduce subtrees before requested index
  331. directSubTrees.stream().filter(subtree -> subtree
  332. .getEndIndex() < firstIntersectingRowIndex.get())
  333. .forEachOrdered(subtree -> {
  334. start.addAndGet(-1 * (subtree.getEndIndex()
  335. - subtree.startIndex + 1));
  336. });
  337. // if requested start index is in the middle of a
  338. // subtree, start is after that
  339. List<TreeNode> intersectingSubTrees = new ArrayList<>();
  340. directSubTrees.stream()
  341. .filter(subtree -> subtree.startIndex <= firstIntersectingRowIndex
  342. .get() && firstIntersectingRowIndex
  343. .get() <= subtree.getEndIndex())
  344. .findFirst().ifPresent(subtree -> {
  345. int previous = firstIntersectingRowIndex
  346. .getAndSet(subtree.getEndIndex() + 1);
  347. int delta = previous
  348. - firstIntersectingRowIndex.get();
  349. start.addAndGet(subtree.startIndex - previous);
  350. size.addAndGet(delta);
  351. intersectingSubTrees.add(subtree);
  352. });
  353. // reduce size of subtrees after first row that intersect
  354. // with requested range
  355. directSubTrees.stream()
  356. .filter(subtree -> firstIntersectingRowIndex
  357. .get() < subtree.startIndex
  358. && subtree.endIndex <= lastIntersectingRowIndex)
  359. .forEachOrdered(subtree -> {
  360. // reduce subtree size that is part of the
  361. // requested range from query size
  362. size.addAndGet(
  363. -1 * (Math.min(subtree.getEndIndex(),
  364. lastIntersectingRowIndex)
  365. - subtree.startIndex + 1));
  366. intersectingSubTrees.add(subtree);
  367. });
  368. return new TreeLevelQuery(node, start.get(), size.get(),
  369. depth, firstIntersectingRowIndex.get(),
  370. intersectingSubTrees);
  371. }).filter(query -> query.size > 0);
  372. }
  373. /**
  374. * Merges the tree level query results into flat grid ordering.
  375. *
  376. * @param rangePositionCallback
  377. * the callback to place the results into
  378. * @param query
  379. * the query data for the results
  380. * @param results
  381. * the results to reorder
  382. * @param <T>
  383. * the type of the results
  384. */
  385. protected <T> void reorderLevelQueryResultsToFlatOrdering(
  386. BiConsumer<T, Integer> rangePositionCallback, TreeLevelQuery query,
  387. List<T> results) {
  388. AtomicInteger nextPossibleIndex = new AtomicInteger(
  389. query.firstRowIndex);
  390. for (T item : results) {
  391. // search for any intersecting subtrees and push index if necessary
  392. query.subTrees.stream().filter(
  393. subTree -> subTree.startIndex <= nextPossibleIndex.get()
  394. && nextPossibleIndex.get() <= subTree.getEndIndex())
  395. .findAny().ifPresent(intersecting -> {
  396. nextPossibleIndex.addAndGet(intersecting.getEndIndex()
  397. - intersecting.startIndex + 1);
  398. query.subTrees.remove(intersecting);
  399. });
  400. rangePositionCallback.accept(item,
  401. nextPossibleIndex.getAndIncrement());
  402. }
  403. }
  404. }