/*
* Copyright 2000-2016 Vaadin Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.vaadin.data.provider;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BiConsumer;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* Mapper for hierarchical data.
* <p>
* Keeps track of the expanded nodes, and size of of the subtrees for each
* expanded node.
* <p>
* This class is framework internal implementation details, and can be changed /
* moved at any point. This means that you should not directly use this for
* anything.
*
* @author Vaadin Ltd
* @since 8.1
*/
class HierarchyMapper implements Serializable {
private static final Logger LOGGER = Logger
.getLogger(HierarchyMapper.class.getName());
/**
* A POJO that represents a query data for a certain tree level.
*/
static class TreeLevelQuery { // not serializable since not stored
/**
* The tree node that the query is for. Only used for fetching parent
* key.
*/
final TreeNode node;
/** The start index of the query, from 0 to level's size - 1. */
final int startIndex;
/** The number of rows to fetch. s */
final int size;
/** The depth of this node. */
final int depth;
/** The first row index in grid, including all the nodes. */
final int firstRowIndex;
/** The direct subtrees for the node that effect the indexing. */
final List<TreeNode> subTrees;
TreeLevelQuery(TreeNode node, int startIndex, int size, int depth,
int firstRowIndex, List<TreeNode> subTrees) {
this.node = node;
this.startIndex = startIndex;
this.size = size;
this.depth = depth;
this.firstRowIndex = firstRowIndex;
this.subTrees = subTrees;
}
}
/**
* A level in the tree, either the root level or an expanded subtree level.
* <p>
* Comparable based on the {@link #startIndex}, which is flat from 0 to data
* size - 1.
*/
static class TreeNode implements Serializable, Comparable<TreeNode> {
/** The key for the expanded item that this is a subtree of. */
private final String parentKey;
/** The first index on this level. */
private int startIndex;
/** The last index on this level, INCLUDING subtrees. */
private int endIndex;
TreeNode(String parentKey, int startIndex, int size) {
this.parentKey = parentKey;
this.startIndex = startIndex;
endIndex = startIndex + size - 1;
}
TreeNode(int startIndex) {
parentKey = "INVALID";
this.startIndex = startIndex;
}
int getStartIndex() {
return startIndex;
}
int getEndIndex() {
return endIndex;
}
String getParentKey() {
return parentKey;
}
private void push(int offset) {
startIndex += offset;
endIndex += offset;
}
private void pushEnd(int offset) {
endIndex += offset;
}
@Override
public int compareTo(TreeNode other) {
return Integer.valueOf(startIndex).compareTo(other.startIndex);
}
@Override
public String toString() {
return "TreeNode [parent=" + parentKey + ", start=" + startIndex
+ ", end=" + getEndIndex() + "]";
}
}
/** The expanded nodes in the tree. */
private final TreeSet<TreeNode> nodes = new TreeSet<>();
/**
* Map of collapsed subtrees. The keys of this map are the collapsed
* subtrees parent keys and values are the {@code TreeSet}s of the subtree's
* expanded {@code TreeNode}s.
*/
private final Map<String, TreeSet<TreeNode>> collapsedNodes = new HashMap<>();
/**
* Resets the tree, sets given the root level size.
*
* @param rootLevelSize
* the number of items in the root level
*/
public void reset(int rootLevelSize) {
collapsedNodes.clear();
nodes.clear();
nodes.add(new TreeNode(null, 0, rootLevelSize));
}
/**
* Returns the complete size of the tree, including all expanded subtrees.
*
* @return the size of the tree
*/
public int getTreeSize() {
TreeNode rootNode = getNodeForKey(null)
.orElse(new TreeNode(null, 0, 0));
return rootNode.endIndex + 1;
}
/**
* Returns whether the node with the given is collapsed or not.
*
* @param itemKey
* the key of node to check
* @return {@code true} if collapsed, {@code false} if expanded
*/
public boolean isCollapsed(String itemKey) {
return !getNodeForKey(itemKey).isPresent();
}
/**
* Return whether the given item key is still being used in this mapper.
*
* @param itemKey
* the item key to look for
* @return {@code true} if the item key is still used, {@code false}
* otherwise
*/
public boolean isKeyStored(String itemKey) {
if (getNodeForKey(itemKey).isPresent()) {
return true;
}
// Is the key used in a collapsed subtree?
for (Entry<String, TreeSet<TreeNode>> entry : collapsedNodes.entrySet()) {
if (entry.getKey() != null && entry.getKey().equals(itemKey)) {
return true;
}
for (TreeNode subTreeNode : entry.getValue()) {
if (subTreeNode.getParentKey() != null
&& subTreeNode.getParentKey().equals(itemKey)) {
return true;
}
}
}
return false;
}
/**
* Return the depth of expanded node's subtree.
* <p>
* The root node depth is 0.
*
* @param expandedNodeKey
* the item key of the expanded node
* @return the depth of the expanded node
* @throws IllegalArgumentException
* if the node was not expanded
*/
protected int getDepth(String expandedNodeKey) {
Optional<TreeNode> node = getNodeForKey(expandedNodeKey);
if (!node.isPresent()) {
throw new IllegalArgumentException("No node with given key "
+ expandedNodeKey + " was expanded.");
}
TreeNode treeNode = node.get();
AtomicInteger start = new AtomicInteger(treeNode.startIndex);
AtomicInteger end = new AtomicInteger(treeNode.getEndIndex());
AtomicInteger depth = new AtomicInteger();
nodes.headSet(treeNode, false).descendingSet().forEach(higherNode -> {
if (higherNode.startIndex < start.get()
&& higherNode.getEndIndex() >= end.get()) {
start.set(higherNode.startIndex);
depth.incrementAndGet();
}
});
return depth.get();
}
/**
* Returns the tree node for the given expanded item key, or an empty
* optional if the item was not expanded.
*
* @param expandedNodeKey
* the key of the item
* @return the tree node for the expanded item, or an empty optional if not
* expanded
*/
protected Optional<TreeNode> getNodeForKey(String expandedNodeKey) {
return nodes.stream()
.filter(node -> Objects.equals(node.parentKey, expandedNodeKey))
.findAny();
}
/**
* Expands the node in the given index and with the given key.
*
* @param expandedRowKey
* the key of the expanded item
* @param expandedRowIndex
* the index of the expanded item
* @param expandedNodeSize
* the size of the subtree of the expanded node, used if
* previously unknown
* @throws IllegalStateException
* if the node was expanded already
* @return the actual size of the expand
*/
protected int expand(String expandedRowKey, int expandedRowIndex,
int expandedNodeSize) {
if (expandedNodeSize < 1) {
throw new IllegalArgumentException(
"The expanded node's size cannot be less than 1, was "
+ expandedNodeSize);
}
TreeNode newNode;
TreeSet<TreeNode> subTree = null;
if (collapsedNodes.containsKey(expandedRowKey)) {
subTree = collapsedNodes.remove(expandedRowKey);
newNode = subTree.first();
int offset = expandedRowIndex - newNode.getStartIndex() + 1;
subTree.forEach(node -> node.push(offset));
expandedNodeSize = newNode.getEndIndex() - newNode.getStartIndex()
+ 1;
} else {
newNode = new TreeNode(expandedRowKey, expandedRowIndex + 1,
expandedNodeSize);
}
boolean added = nodes.add(newNode);
if (!added) {
throw new IllegalStateException("Node in index " + expandedRowIndex
+ " was expanded already.");
}
// push end indexes for parent nodes
final int expandSize = expandedNodeSize;
List<TreeNode> updated = nodes.headSet(newNode, false).stream()
.filter(node -> node.getEndIndex() >= expandedRowIndex)
.collect(Collectors.toList());
nodes.removeAll(updated);
updated.stream().forEach(node -> node.pushEnd(expandSize));
nodes.addAll(updated);
// push start and end indexes for later nodes
updated = nodes.tailSet(newNode, false).stream()
.collect(Collectors.toList());
nodes.removeAll(updated);
updated.stream().forEach(node -> node.push(expandSize));
nodes.addAll(updated);
if (subTree != null) {
nodes.addAll(subTree);
}
return expandSize;
}
/**
* Collapses the node in the given index.
*
* @param key
* the key of the collapsed item
* @param collapsedRowIndex
* the index of the collapsed item
* @return the size of the complete subtree that was collapsed
* @throws IllegalStateException
* if the node was not collapsed, or if the given key is not the
* same as it was when the node has been expanded
*/
protected int collapse(String key, int collapsedRowIndex) {
Objects.requireNonNull(key,
"The key for the item to collapse cannot be null.");
TreeNode collapsedNode = nodes
.ceiling(new TreeNode(collapsedRowIndex + 1));
if (collapsedNode == null
|| collapsedNode.startIndex != collapsedRowIndex + 1) {
throw new IllegalStateException(
"Could not find expanded node for index "
+ collapsedRowIndex + ", node was not collapsed");
}
if (!Objects.equals(key, collapsedNode.parentKey)) {
throw new IllegalStateException("The expected parent key " + key
+ " is different for the collapsed node " + collapsedNode);
}
Set<TreeNode> subTreeNodes = nodes
.tailSet(collapsedNode).stream().filter(node -> collapsedNode
.getEndIndex() >= node.getEndIndex())
.collect(Collectors.toSet());
collapsedNodes.put(collapsedNode.getParentKey(), new TreeSet<>(subTreeNodes));
// remove complete subtree
AtomicInteger removedSubTreeSize = new AtomicInteger(
collapsedNode.getEndIndex() - collapsedNode.startIndex + 1);
nodes.tailSet(collapsedNode, false).removeIf(
node -> node.startIndex <= collapsedNode.getEndIndex());
final int offset = -1 * removedSubTreeSize.get();
// adjust parent end indexes
List<TreeNode> updated = nodes.headSet(collapsedNode, false).stream()
.filter(node -> node.getEndIndex() >= collapsedRowIndex)
.collect(Collectors.toList());
nodes.removeAll(updated);
updated.stream().forEach(node -> node.pushEnd(offset));
nodes.addAll(updated);
// adjust start and end indexes for latter nodes
updated = nodes.tailSet(collapsedNode, false).stream()
.collect(Collectors.toList());
nodes.removeAll(updated);
updated.stream().forEach(node -> node.push(offset));
nodes.addAll(updated);
nodes.remove(collapsedNode);
return removedSubTreeSize.get();
}
/**
* Splits the given range into queries per tree level.
*
* @param firstRow
* the first row to fetch
* @param lastRow
* the last row to fetch
* @return a stream of query data per level
* @see #reorderLevelQueryResultsToFlatOrdering(BiConsumer, TreeLevelQuery,
* List)
*/
protected Stream<TreeLevelQuery> splitRangeToLevelQueries(
final int firstRow, final int lastRow) {
return nodes.stream()
// filter to parts intersecting with the range
.filter(node -> node.startIndex <= lastRow
&& firstRow <= node.getEndIndex())
// split into queries per level with level based indexing
.map(node -> {
// calculate how subtrees effect indexing and size
int depth = getDepth(node.parentKey);
List<TreeNode> directSubTrees = nodes.tailSet(node, false)
.stream()
// find subtrees
.filter(subTree -> node.startIndex < subTree
.getEndIndex()
&& subTree.startIndex < node.getEndIndex())
// filter to direct subtrees
.filter(subTree -> getDepth(
subTree.parentKey) == (depth + 1))
.collect(Collectors.toList());
// first intersecting index in flat order
AtomicInteger firstIntersectingRowIndex = new AtomicInteger(
Math.max(node.startIndex, firstRow));
// last intersecting index in flat order
final int lastIntersectingRowIndex = Math
.min(node.getEndIndex(), lastRow);
// start index for this level
AtomicInteger start = new AtomicInteger(
firstIntersectingRowIndex.get() - node.startIndex);
// how many nodes should be fetched for this level
AtomicInteger size = new AtomicInteger(
lastIntersectingRowIndex
- firstIntersectingRowIndex.get() + 1);
// reduce subtrees before requested index
directSubTrees.stream().filter(subtree -> subtree
.getEndIndex() < firstIntersectingRowIndex.get())
.forEachOrdered(subtree -> {
start.addAndGet(-1 * (subtree.getEndIndex()
- subtree.startIndex + 1));
});
// if requested start index is in the middle of a
// subtree, start is after that
List<TreeNode> intersectingSubTrees = new ArrayList<>();
directSubTrees.stream()
.filter(subtree -> subtree.startIndex <= firstIntersectingRowIndex
.get() && firstIntersectingRowIndex
.get() <= subtree.getEndIndex())
.findFirst().ifPresent(subtree -> {
int previous = firstIntersectingRowIndex
.getAndSet(subtree.getEndIndex() + 1);
int delta = previous
- firstIntersectingRowIndex.get();
start.addAndGet(subtree.startIndex - previous);
size.addAndGet(delta);
intersectingSubTrees.add(subtree);
});
// reduce size of subtrees after first row that intersect
// with requested range
directSubTrees.stream()
.filter(subtree -> firstIntersectingRowIndex
.get() < subtree.startIndex
&& subtree.endIndex <= lastIntersectingRowIndex)
.forEachOrdered(subtree -> {
// reduce subtree size that is part of the
// requested range from query size
size.addAndGet(
-1 * (Math.min(subtree.getEndIndex(),
lastIntersectingRowIndex)
- subtree.startIndex + 1));
intersectingSubTrees.add(subtree);
});
return new TreeLevelQuery(node, start.get(), size.get(),
depth, firstIntersectingRowIndex.get(),
intersectingSubTrees);
}).filter(query -> query.size > 0);
}
/**
* Merges the tree level query results into flat grid ordering.
*
* @param rangePositionCallback
* the callback to place the results into
* @param query
* the query data for the results
* @param results
* the results to reorder
* @param <T>
* the type of the results
*/
protected <T> void reorderLevelQueryResultsToFlatOrdering(
BiConsumer<T, Integer> rangePositionCallback, TreeLevelQuery query,
List<T> results) {
AtomicInteger nextPossibleIndex = new AtomicInteger(
query.firstRowIndex);
for (T item : results) {
// search for any intersecting subtrees and push index if necessary
query.subTrees.stream().filter(
subTree -> subTree.startIndex <= nextPossibleIndex.get()
&& nextPossibleIndex.get() <= subTree.getEndIndex())
.findAny().ifPresent(intersecting -> {
nextPossibleIndex.addAndGet(intersecting.getEndIndex()
- intersecting.startIndex + 1);
query.subTrees.remove(intersecting);
});
rangePositionCallback.accept(item,
nextPossibleIndex.getAndIncrement());
}
}
/**
* Returns parent index for the row or {@code null}
*
* @param rowIndex the row index
* @return the parent index or {@code null} for top-level items
*/
public Integer getParentIndex(int rowIndex) {
return nodes.stream()
.filter(treeNode -> treeNode.getParentKey() != null
&& treeNode.getStartIndex() <= rowIndex
&& treeNode.getEndIndex() >= rowIndex)
.min((a, b) -> Math.min(a.getEndIndex() - a.getStartIndex(),
b.getEndIndex() - b.getStartIndex()))
.map(treeNode -> treeNode.getStartIndex() - 1)
.orElse(null);
}
}