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.

TreeData.java 16KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494
  1. /*
  2. * Copyright 2000-2018 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.Arrays;
  20. import java.util.Collection;
  21. import java.util.Collections;
  22. import java.util.LinkedHashMap;
  23. import java.util.List;
  24. import java.util.Map;
  25. import java.util.Objects;
  26. import java.util.stream.Collectors;
  27. import java.util.stream.Stream;
  28. import com.vaadin.data.provider.TreeDataProvider;
  29. /**
  30. * Class for representing hierarchical data.
  31. * <p>
  32. * Typically used as a backing data source for {@link TreeDataProvider}.
  33. *
  34. * @author Vaadin Ltd
  35. * @since 8.1
  36. *
  37. * @param <T>
  38. * data type
  39. */
  40. public class TreeData<T> implements Serializable {
  41. private static class HierarchyWrapper<T> implements Serializable {
  42. private T parent;
  43. private List<T> children;
  44. public HierarchyWrapper(T parent) {
  45. this.parent = parent;
  46. children = new ArrayList<>();
  47. }
  48. public T getParent() {
  49. return parent;
  50. }
  51. public void setParent(T parent) {
  52. this.parent = parent;
  53. }
  54. public List<T> getChildren() {
  55. return children;
  56. }
  57. public void addChild(T child) {
  58. children.add(child);
  59. }
  60. public void removeChild(T child) {
  61. children.remove(child);
  62. }
  63. }
  64. private final Map<T, HierarchyWrapper<T>> itemToWrapperMap;
  65. /**
  66. * Creates an initially empty hierarchical data representation to which
  67. * items can be added or removed.
  68. */
  69. public TreeData() {
  70. itemToWrapperMap = new LinkedHashMap<>();
  71. itemToWrapperMap.put(null, new HierarchyWrapper<>(null));
  72. }
  73. /**
  74. * Adds the items as root items to this structure.
  75. *
  76. * @param items
  77. * the items to add
  78. * @return this
  79. *
  80. * @throws IllegalArgumentException
  81. * if any of the given items have already been added to this
  82. * structure
  83. * @throws NullPointerException
  84. * if any of the items are {code null}
  85. */
  86. public TreeData<T> addRootItems(T... items) {
  87. addItems(null, items);
  88. return this;
  89. }
  90. /**
  91. * Adds the items of the given collection as root items to this structure.
  92. *
  93. * @param items
  94. * the collection of items to add
  95. * @return this
  96. *
  97. * @throws IllegalArgumentException
  98. * if any of the given items have already been added to this
  99. * structure
  100. * @throws NullPointerException
  101. * if any of the items are {code null}
  102. */
  103. public TreeData<T> addRootItems(Collection<T> items) {
  104. addItems(null, items);
  105. return this;
  106. }
  107. /**
  108. * Adds the items of the given stream as root items to this structure.
  109. *
  110. * @param items
  111. * the stream of root items to add
  112. * @return this
  113. *
  114. * @throws IllegalArgumentException
  115. * if any of the given items have already been added to this
  116. * structure
  117. * @throws NullPointerException
  118. * if any of the items are {code null}
  119. */
  120. public TreeData<T> addRootItems(Stream<T> items) {
  121. addItems(null, items);
  122. return this;
  123. }
  124. /**
  125. * Adds a data item as a child of {@code parent}. Call with {@code null} as
  126. * parent to add a root level item. The given parent item must already exist
  127. * in this structure, and an item can only be added to this structure once.
  128. *
  129. * @param parent
  130. * the parent item for which the items are added as children
  131. * @param item
  132. * the item to add
  133. * @return this
  134. *
  135. * @throws IllegalArgumentException
  136. * if parent is not null and not already added to this structure
  137. * @throws IllegalArgumentException
  138. * if the item has already been added to this structure
  139. * @throws NullPointerException
  140. * if item is null
  141. */
  142. public TreeData<T> addItem(T parent, T item) {
  143. Objects.requireNonNull(item, "Item cannot be null");
  144. if (parent != null && !contains(parent)) {
  145. throw new IllegalArgumentException(
  146. "Parent needs to be added before children. "
  147. + "To add root items, call with parent as null");
  148. }
  149. if (contains(item)) {
  150. throw new IllegalArgumentException(
  151. "Cannot add the same item multiple times: " + item);
  152. }
  153. putItem(item, parent);
  154. return this;
  155. }
  156. /**
  157. * Adds a list of data items as children of {@code parent}. Call with
  158. * {@code null} as parent to add root level items. The given parent item
  159. * must already exist in this structure, and an item can only be added to
  160. * this structure once.
  161. *
  162. * @param parent
  163. * the parent item for which the items are added as children
  164. * @param items
  165. * the list of items to add
  166. * @return this
  167. *
  168. * @throws IllegalArgumentException
  169. * if parent is not null and not already added to this structure
  170. * @throws IllegalArgumentException
  171. * if any of the given items have already been added to this
  172. * structure
  173. * @throws NullPointerException
  174. * if any of the items are null
  175. */
  176. public TreeData<T> addItems(T parent,
  177. @SuppressWarnings("unchecked") T... items) {
  178. Arrays.asList(items).stream().forEach(item -> addItem(parent, item));
  179. return this;
  180. }
  181. /**
  182. * Adds a list of data items as children of {@code parent}. Call with
  183. * {@code null} as parent to add root level items. The given parent item
  184. * must already exist in this structure, and an item can only be added to
  185. * this structure once.
  186. *
  187. * @param parent
  188. * the parent item for which the items are added as children
  189. * @param items
  190. * the collection of items to add
  191. * @return this
  192. *
  193. * @throws IllegalArgumentException
  194. * if parent is not null and not already added to this structure
  195. * @throws IllegalArgumentException
  196. * if any of the given items have already been added to this
  197. * structure
  198. * @throws NullPointerException
  199. * if any of the items are null
  200. */
  201. public TreeData<T> addItems(T parent, Collection<T> items) {
  202. items.stream().forEach(item -> addItem(parent, item));
  203. return this;
  204. }
  205. /**
  206. * Adds data items contained in a stream as children of {@code parent}. Call
  207. * with {@code null} as parent to add root level items. The given parent
  208. * item must already exist in this structure, and an item can only be added
  209. * to this structure once.
  210. *
  211. * @param parent
  212. * the parent item for which the items are added as children
  213. * @param items
  214. * stream of items to add
  215. * @return this
  216. *
  217. * @throws IllegalArgumentException
  218. * if parent is not null and not already added to this structure
  219. * @throws IllegalArgumentException
  220. * if any of the given items have already been added to this
  221. * structure
  222. * @throws NullPointerException
  223. * if any of the items are null
  224. */
  225. public TreeData<T> addItems(T parent, Stream<T> items) {
  226. items.forEach(item -> addItem(parent, item));
  227. return this;
  228. }
  229. /**
  230. * Adds the given items as root items and uses the given value provider to
  231. * recursively populate children of the root items.
  232. *
  233. * @param rootItems
  234. * the root items to add
  235. * @param childItemProvider
  236. * the value provider used to recursively populate this TreeData
  237. * from the given root items
  238. * @return this
  239. */
  240. public TreeData<T> addItems(Collection<T> rootItems,
  241. ValueProvider<T, Collection<T>> childItemProvider) {
  242. rootItems.forEach(item -> {
  243. addItem(null, item);
  244. Collection<T> childItems = childItemProvider.apply(item);
  245. addItems(item, childItems);
  246. addItemsRecursively(childItems, childItemProvider);
  247. });
  248. return this;
  249. }
  250. /**
  251. * Adds the given items as root items and uses the given value provider to
  252. * recursively populate children of the root items.
  253. *
  254. * @param rootItems
  255. * the root items to add
  256. * @param childItemProvider
  257. * the value provider used to recursively populate this TreeData
  258. * from the given root items
  259. * @return this
  260. */
  261. public TreeData<T> addItems(Stream<T> rootItems,
  262. ValueProvider<T, Stream<T>> childItemProvider) {
  263. // Must collect to lists since the algorithm iterates multiple times
  264. return addItems(rootItems.collect(Collectors.toList()),
  265. item -> childItemProvider.apply(item)
  266. .collect(Collectors.toList()));
  267. }
  268. /**
  269. * Remove a given item from this structure. Additionally, this will
  270. * recursively remove any descendants of the item.
  271. *
  272. * @param item
  273. * the item to remove, or null to clear all data
  274. * @return this
  275. *
  276. * @throws IllegalArgumentException
  277. * if the item does not exist in this structure
  278. */
  279. public TreeData<T> removeItem(T item) {
  280. if (!contains(item)) {
  281. throw new IllegalArgumentException(
  282. "Item '" + item + "' not in the hierarchy");
  283. }
  284. new ArrayList<>(getChildren(item)).forEach(child -> removeItem(child));
  285. itemToWrapperMap.get(itemToWrapperMap.get(item).getParent())
  286. .removeChild(item);
  287. if (item != null) {
  288. // remove non root item from backing map
  289. itemToWrapperMap.remove(item);
  290. }
  291. return this;
  292. }
  293. /**
  294. * Clear all items from this structure. Shorthand for calling
  295. * {@link #removeItem(Object)} with null.
  296. *
  297. * @return this
  298. */
  299. public TreeData<T> clear() {
  300. removeItem(null);
  301. return this;
  302. }
  303. /**
  304. * Gets the root items of this structure.
  305. *
  306. * @return an unmodifiable list of root items of this structure
  307. */
  308. public List<T> getRootItems() {
  309. return getChildren(null);
  310. }
  311. /**
  312. * Get the immediate child items for the given item.
  313. *
  314. * @param item
  315. * the item for which to retrieve child items for, null to
  316. * retrieve all root items
  317. * @return an unmodifiable list of child items for the given item
  318. *
  319. * @throws IllegalArgumentException
  320. * if the item does not exist in this structure
  321. */
  322. public List<T> getChildren(T item) {
  323. if (!contains(item)) {
  324. throw new IllegalArgumentException(
  325. "Item '" + item + "' not in the hierarchy");
  326. }
  327. return Collections
  328. .unmodifiableList(itemToWrapperMap.get(item).getChildren());
  329. }
  330. /**
  331. * Get the parent item for the given item.
  332. *
  333. * @param item
  334. * the item for which to retrieve the parent item for
  335. * @return parent item for the given item or {@code null} if the item is a
  336. * root item.
  337. * @throws IllegalArgumentException
  338. * if the item does not exist in this structure
  339. * @since 8.1.1
  340. */
  341. public T getParent(T item) {
  342. if (!contains(item)) {
  343. throw new IllegalArgumentException(
  344. "Item '" + item + "' not in hierarchy");
  345. }
  346. return itemToWrapperMap.get(item).getParent();
  347. }
  348. /**
  349. * Moves an item to become a child of the given parent item. The new parent
  350. * item must exist in the hierarchy. Setting the parent to {@code null}
  351. * makes the item a root item. After making changes to the tree data,
  352. * {@link TreeDataProvider#refreshAll()} should be called.
  353. *
  354. * @param item
  355. * the item to be set as the child of {@code parent}
  356. * @param parent
  357. * the item to be set as parent or {@code null} to set the item
  358. * as root
  359. * @since 8.1
  360. */
  361. public void setParent(T item, T parent) {
  362. if (!contains(item)) {
  363. throw new IllegalArgumentException(
  364. "Item '" + item + "' not in the hierarchy");
  365. }
  366. if (parent != null && !contains(parent)) {
  367. throw new IllegalArgumentException(
  368. "Parent needs to be added before children. "
  369. + "To set as root item, call with parent as null");
  370. }
  371. if (item.equals(parent)) {
  372. throw new IllegalArgumentException(
  373. "Item cannot be the parent of itself");
  374. }
  375. T oldParent = itemToWrapperMap.get(item).getParent();
  376. if (!Objects.equals(oldParent, parent)) {
  377. // Remove item from old parent's children
  378. itemToWrapperMap.get(oldParent).removeChild(item);
  379. // Add item to parent's children
  380. itemToWrapperMap.get(parent).addChild(item);
  381. // Set item's new parent
  382. itemToWrapperMap.get(item).setParent(parent);
  383. }
  384. }
  385. /**
  386. * Moves an item to the position immediately after a sibling item. The two
  387. * items must have the same parent. After making changes to the tree data,
  388. * {@link TreeDataProvider#refreshAll()} should be called.
  389. *
  390. * @param item
  391. * the item to be moved
  392. * @param sibling
  393. * the item after which the moved item will be located, or {@code
  394. * null} to move item to first position
  395. * @since 8.1
  396. */
  397. public void moveAfterSibling(T item, T sibling) {
  398. if (!contains(item)) {
  399. throw new IllegalArgumentException(
  400. "Item '" + item + "' not in the hierarchy");
  401. }
  402. if (sibling == null) {
  403. List<T> children = itemToWrapperMap.get(getParent(item))
  404. .getChildren();
  405. // Move item to first position
  406. children.remove(item);
  407. children.add(0, item);
  408. } else {
  409. if (!contains(sibling)) {
  410. throw new IllegalArgumentException(
  411. "Item '" + sibling + "' not in the hierarchy");
  412. }
  413. T parent = itemToWrapperMap.get(item).getParent();
  414. if (!Objects.equals(parent,
  415. itemToWrapperMap.get(sibling).getParent())) {
  416. throw new IllegalArgumentException("Items '" + item + "' and '"
  417. + sibling + "' don't have the same parent");
  418. }
  419. List<T> children = itemToWrapperMap.get(parent).getChildren();
  420. // Move item to the position after the sibling
  421. children.remove(item);
  422. children.add(children.indexOf(sibling) + 1, item);
  423. }
  424. }
  425. /**
  426. * Check whether the given item is in this hierarchy.
  427. *
  428. * @param item
  429. * the item to check
  430. * @return {@code true} if the item is in this hierarchy, {@code false} if
  431. * not
  432. */
  433. public boolean contains(T item) {
  434. return itemToWrapperMap.containsKey(item);
  435. }
  436. private void putItem(T item, T parent) {
  437. HierarchyWrapper<T> wrappedItem = new HierarchyWrapper<>(parent);
  438. if (itemToWrapperMap.containsKey(parent)) {
  439. itemToWrapperMap.get(parent).addChild(item);
  440. }
  441. itemToWrapperMap.put(item, wrappedItem);
  442. }
  443. private void addItemsRecursively(Collection<T> items,
  444. ValueProvider<T, Collection<T>> childItemProvider) {
  445. items.forEach(item -> {
  446. Collection<T> childItems = childItemProvider.apply(item);
  447. addItems(item, childItems);
  448. addItemsRecursively(childItems, childItemProvider);
  449. });
  450. }
  451. }