/*
* Copyright 2000-2022 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.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
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.stream.Collectors;
import java.util.stream.Stream;
import com.vaadin.shared.Range;
import com.vaadin.shared.data.HierarchicalDataCommunicatorConstants;
import com.vaadin.ui.ItemCollapseAllowedProvider;
import elemental.json.Json;
import elemental.json.JsonObject;
/**
* Mapper for hierarchical data.
*
* Keeps track of the expanded nodes, and size of of the subtrees for each
* expanded node.
*
* 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
*
* @param
* the data type
* @param
* the filter type
*/
public class HierarchyMapper implements DataGenerator {
// childMap is only used for finding parents of items and clean up on
// removing children of expanded nodes.
private Map> childMap = new HashMap<>();
private Map parentIdMap = new HashMap<>();
private final HierarchicalDataProvider provider;
private F filter;
private List backEndSorting;
private Comparator inMemorySorting;
private ItemCollapseAllowedProvider itemCollapseAllowedProvider = t -> true;
private Set expandedItemIds = new HashSet<>();
/**
* Constructs a new HierarchyMapper.
*
* @param provider
* the hierarchical data provider for this mapper
*/
public HierarchyMapper(HierarchicalDataProvider provider) {
this.provider = provider;
}
/**
* Returns the size of the currently expanded hierarchy.
*
* @return the amount of available data
*/
public int getTreeSize() {
return (int) getHierarchy(null).count();
}
/**
* Finds the index of the parent of the item in given target index.
*
* @param item
* the item to get the parent of
* @return the parent index or a negative value if the parent is not found
*
*/
public Integer getParentIndex(T item) {
// TODO: This can be optimized.
List flatHierarchy = getHierarchy(null).collect(Collectors.toList());
return flatHierarchy.indexOf(getParentOfItem(item));
}
/**
* Returns whether the given item is expanded.
*
* @param item
* the item to test
* @return {@code true} if item is expanded; {@code false} if not
*/
public boolean isExpanded(T item) {
if (item == null) {
// Root nodes are always visible.
return true;
}
return expandedItemIds.contains(getDataProvider().getId(item));
}
/**
* Expands the given item.
*
* @param item
* the item to expand
* @param position
* the index of the item
* @return range of rows added by expanding the item
*/
public Range expand(T item, Integer position) {
if (doExpand(item) && position != null) {
return Range.withLength(position + 1,
(int) getHierarchy(item, false).count());
}
return Range.emptyRange();
}
/**
* Expands the given item.
*
* @param item
* the item to expand
* @param position
* the index of item
* @return range of rows added by expanding the item
* @deprecated Use {@link #expand(Object, Integer)} instead.
*/
@Deprecated
public Range doExpand(T item, Optional position) {
return expand(item, position.orElse(null));
}
/**
* Expands the given item if it is collapsed and has children, and returns
* whether this method expanded the item.
*
* @param item
* the item to expand
* @return {@code true} if this method expanded the item, {@code false}
* otherwise
*/
private boolean doExpand(T item) {
boolean expanded = false;
if (!isExpanded(item) && hasChildren(item)) {
expandedItemIds.add(getDataProvider().getId(item));
expanded = true;
}
return expanded;
}
/**
* Collapses the given item.
*
* @param item
* the item to collapse
* @param position
* the index of the item
*
* @return range of rows removed by collapsing the item
*/
public Range collapse(T item, Integer position) {
Range removedRows = Range.emptyRange();
if (isExpanded(item)) {
if (position != null) {
removedRows = Range.withLength(position + 1,
(int) getHierarchy(item, false).count());
}
expandedItemIds.remove(getDataProvider().getId(item));
}
return removedRows;
}
/**
* Collapses the given item.
*
* @param item
* the item to collapse
* @param position
* the index of item
*
* @return range of rows removed by collapsing the item
* @deprecated Use {@link #collapse(Object, Integer)} instead.
*/
@Deprecated
public Range doCollapse(T item, Optional position) {
return collapse(item, position.orElse(null));
}
@Override
public void generateData(T item, JsonObject jsonObject) {
JsonObject hierarchyData = Json.createObject();
int depth = getDepth(item);
if (depth >= 0) {
hierarchyData.put(HierarchicalDataCommunicatorConstants.ROW_DEPTH,
depth);
}
boolean isLeaf = !getDataProvider().hasChildren(item);
if (isLeaf) {
hierarchyData.put(HierarchicalDataCommunicatorConstants.ROW_LEAF,
true);
} else {
hierarchyData.put(
HierarchicalDataCommunicatorConstants.ROW_COLLAPSED,
!isExpanded(item));
hierarchyData.put(HierarchicalDataCommunicatorConstants.ROW_LEAF,
false);
hierarchyData.put(
HierarchicalDataCommunicatorConstants.ROW_COLLAPSE_ALLOWED,
getItemCollapseAllowedProvider().test(item));
}
// add hierarchy information to row as metadata
jsonObject.put(
HierarchicalDataCommunicatorConstants.ROW_HIERARCHY_DESCRIPTION,
hierarchyData);
}
/**
* Gets the current item collapse allowed provider.
*
* @return the item collapse allowed provider
*/
public ItemCollapseAllowedProvider getItemCollapseAllowedProvider() {
return itemCollapseAllowedProvider;
}
/**
* Sets the current item collapse allowed provider.
*
* @param itemCollapseAllowedProvider
* the item collapse allowed provider
*/
public void setItemCollapseAllowedProvider(
ItemCollapseAllowedProvider itemCollapseAllowedProvider) {
this.itemCollapseAllowedProvider = itemCollapseAllowedProvider;
}
/**
* Gets the current in-memory sorting.
*
* @return the in-memory sorting
*/
public Comparator getInMemorySorting() {
return inMemorySorting;
}
/**
* Sets the current in-memory sorting. This will cause the hierarchy to be
* constructed again.
*
* @param inMemorySorting
* the in-memory sorting
*/
public void setInMemorySorting(Comparator inMemorySorting) {
this.inMemorySorting = inMemorySorting;
}
/**
* Gets the current back-end sorting.
*
* @return the back-end sorting
*/
public List getBackEndSorting() {
return backEndSorting;
}
/**
* Sets the current back-end sorting. This will cause the hierarchy to be
* constructed again.
*
* @param backEndSorting
* the back-end sorting
*/
public void setBackEndSorting(List backEndSorting) {
this.backEndSorting = backEndSorting;
}
/**
* Gets the current filter.
*
* @return the filter
*/
public F getFilter() {
return filter;
}
/**
* Sets the current filter. This will cause the hierarchy to be constructed
* again.
*
* @param filter
* the filter
*/
public void setFilter(Object filter) {
this.filter = (F) filter;
}
/**
* Gets the {@code HierarchicalDataProvider} for this
* {@code HierarchyMapper}.
*
* @return the hierarchical data provider
*/
public HierarchicalDataProvider getDataProvider() {
return provider;
}
/**
* Returns whether given item has children.
*
* @param item
* the node to test
* @return {@code true} if node has children; {@code false} if not
*/
public boolean hasChildren(T item) {
return getDataProvider().hasChildren(item);
}
/* Fetch methods. These are used to calculate what to request. */
/**
* Gets a stream of items in the form of a flattened hierarchy from the
* back-end and filter the wanted results from the list.
*
* @param range
* the requested item range
* @return the stream of items
*/
public Stream fetchItems(Range range) {
return getHierarchy(null).skip(range.getStart()).limit(range.length());
}
/**
* Gets a stream of children for the given item in the form of a flattened
* hierarchy from the back-end and filter the wanted results from the list.
*
* @param parent
* the parent item for the fetch
* @param range
* the requested item range
* @return the stream of items
*/
public Stream fetchItems(T parent, Range range) {
return getHierarchy(parent, false).skip(range.getStart())
.limit(range.length());
}
/* Methods for providing information on the hierarchy. */
/**
* Generic method for finding direct children of a given parent, limited by
* given range.
*
* @param parent
* the parent
* @param range
* the range of direct children to return
* @return the requested children of the given parent
*/
@SuppressWarnings({ "rawtypes", "unchecked" })
private Stream doFetchDirectChildren(T parent, Range range) {
return getDataProvider().fetchChildren(new HierarchicalQuery(
range.getStart(), range.length(), getBackEndSorting(),
getInMemorySorting(), getFilter(), parent));
}
private int getDepth(T item) {
int depth = -1;
while (item != null) {
item = getParentOfItem(item);
++depth;
}
return depth;
}
/**
* Find parent for the given item among open folders.
*
* @param item
* the item
* @return parent item or {@code null} for root items or if the parent is
* closed
*/
protected T getParentOfItem(T item) {
Objects.requireNonNull(item, "Can not find the parent of null");
return parentIdMap.get(getDataProvider().getId(item));
}
/**
* Removes all children of an item identified by a given id. Items removed
* by this method as well as the original item are all marked to be
* collapsed. May be overridden in subclasses for removing obsolete data to
* avoid memory leaks.
*
* @param id
* the item id
*/
protected void removeChildren(Object id) {
// Clean up removed nodes from child map
Iterator>> iterator = childMap.entrySet().iterator();
Set invalidatedChildren = new HashSet<>();
while (iterator.hasNext()) {
Entry> entry = iterator.next();
T key = entry.getKey();
if (key != null && getDataProvider().getId(key).equals(id)) {
invalidatedChildren.addAll(entry.getValue());
iterator.remove();
}
}
expandedItemIds.remove(id);
invalidatedChildren.stream().map(getDataProvider()::getId)
.forEach(x -> {
removeChildren(x);
parentIdMap.remove(x);
});
}
/**
* Finds the current index of given object. This is based on a search in
* flattened version of the hierarchy.
*
* @param target
* the target object to find
* @return optional index of given object
*/
public Optional getIndexOf(T target) {
if (target == null) {
return Optional.empty();
}
final List collect = getHierarchy(null).map(provider::getId)
.collect(Collectors.toList());
int index = collect.indexOf(getDataProvider().getId(target));
return Optional.ofNullable(index < 0 ? null : index);
}
/**
* Gets the full hierarchy tree starting from given node.
*
* @param parent
* the parent node to start from
* @return the flattened hierarchy as a stream
*/
private Stream getHierarchy(T parent) {
return getHierarchy(parent, true);
}
/**
* Getst hte full hierarchy tree starting from given node. The starting node
* can be omitted.
*
* @param parent
* the parent node to start from
* @param includeParent
* {@code true} to include the parent; {@code false} if not
* @return the flattened hierarchy as a stream
*/
private Stream getHierarchy(T parent, boolean includeParent) {
return Stream.of(parent)
.flatMap(node -> getChildrenStream(node, includeParent));
}
/**
* Gets the stream of direct children for given node.
*
* @param parent
* the parent node
* @return the stream of direct children
*/
private Stream getDirectChildren(T parent) {
return doFetchDirectChildren(parent, Range.between(0, getDataProvider()
.getChildCount(new HierarchicalQuery<>(filter, parent))));
}
/**
* The method to recursively fetch the children of given parent. Used with
* {@link Stream#flatMap} to expand a stream of parent nodes into a
* flattened hierarchy.
*
* @param parent
* the parent node
* @return the stream of all children under the parent, includes the parent
*/
private Stream getChildrenStream(T parent) {
return getChildrenStream(parent, true);
}
/**
* The method to recursively fetch the children of given parent. Used with
* {@link Stream#flatMap} to expand a stream of parent nodes into a
* flattened hierarchy.
*
* @param parent
* the parent node
* @param includeParent
* {@code true} to include the parent in the stream;
* {@code false} if not
* @return the stream of all children under the parent
*/
private Stream getChildrenStream(T parent, boolean includeParent) {
List childList = Collections.emptyList();
if (isExpanded(parent)) {
childList = getDirectChildren(parent).collect(Collectors.toList());
if (childList.isEmpty()) {
removeChildren(parent == null ? null
: getDataProvider().getId(parent));
} else {
registerChildren(parent, childList);
}
}
return combineParentAndChildStreams(parent,
childList.stream().flatMap(this::getChildrenStream),
includeParent);
}
/**
* Register parent and children items into inner structures. May be
* overridden in subclasses.
*
* @param parent
* the parent item
* @param childList
* list of parents children to be registered.
*/
protected void registerChildren(T parent, List childList) {
childMap.put(parent, new HashSet<>(childList));
childList.forEach(
x -> parentIdMap.put(getDataProvider().getId(x), parent));
}
/**
* Helper method for combining parent and a stream of children into one
* stream. {@code null} item is never included, and parent can be skipped by
* providing the correct value for {@code includeParent}.
*
* @param parent
* the parent node
* @param children
* the stream of children
* @param includeParent
* {@code true} to include the parent in the stream;
* {@code false} if not
* @return the combined stream of parent and its children
*/
private Stream combineParentAndChildStreams(T parent, Stream children,
boolean includeParent) {
boolean parentIncluded = includeParent && parent != null;
Stream parentStream = parentIncluded ? Stream.of(parent)
: Stream.empty();
return Stream.concat(parentStream, children);
}
@Override
public void destroyAllData() {
childMap.clear();
parentIdMap.clear();
}
}