Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
* Copyright 2000-2026 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.flow.data.provider.hierarchy;

import java.io.Serializable;
import java.util.List;

/**
* Represents hierarchical data.
* <p>
* Typically used as a backing data source for
* {@link InMemoryHierarchicalDataProvider}.
*
* @author Vaadin Ltd
* @since 25.2
*
* @param <T>
* data type
*/
public interface HierarchicalData<T> extends Serializable {

/**
* Get the immediate child items for the given item.
*
* @param item
* the item for which to retrieve child items for, null to
* retrieve all root items
* @return an unmodifiable list of child items for the given item
*
* @throws IllegalArgumentException
* if the item does not exist in this structure
*/
List<T> getChildren(T item);

/**
* Get the parent item for the given item.
*
* @param item
* the item for which to retrieve the parent item for
* @return parent item for the given item or {@code null} if the item is a
* root item.
* @throws IllegalArgumentException
* if the item does not exist in this structure
*/
T getParent(T item);

/**
* Check whether the given item is in this hierarchy.
*
* @param item
* the item to check
* @return {@code true} if the item is in this hierarchy, {@code false} if
* not
*/
boolean contains(T item);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
/*
* Copyright 2000-2026 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.flow.data.provider.hierarchy;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Stream;

import com.vaadin.flow.data.provider.InMemoryDataProvider;
import com.vaadin.flow.function.SerializableComparator;
import com.vaadin.flow.function.SerializablePredicate;

/**
* An in-memory data provider for listing components that display hierarchical
* data. Uses an instance of {@link HierarchicalData} as its source of data.
*
* @author Vaadin Ltd
* @since 25.2
*
* @param <T>
* data type
* @param <U>
* concrete type of the {@link HierarchicalData} used in this
* provider.
*/
public class InMemoryHierarchicalDataProvider<T, U extends HierarchicalData<T>>
extends AbstractHierarchicalDataProvider<T, SerializablePredicate<T>>
implements InMemoryDataProvider<T> {

private final U hierarchicalData;

private SerializablePredicate<T> filter = null;

private SerializableComparator<T> sortOrder = null;

private HierarchyFormat hierarchyFormat = HierarchyFormat.NESTED;

/**
* Constructs a new InMemoryHierarchicalDataProvider.
* <p>
* The data provider should be refreshed after making changes to the
* underlying {@link HierarchicalData} instance.
*
* @param hierarchicalData
* the backing {@link HierarchicalData} for this provider, not
* {@code null}
*/
public InMemoryHierarchicalDataProvider(U hierarchicalData) {
this.hierarchicalData = Objects.requireNonNull(hierarchicalData,
"hierarchicalData cannot be null");
}

/**
* Creates a new InMemoryHierarchicalDataProvider and configures it to
* return the hierarchical data in the specified format:
* {@link HierarchyFormat#NESTED} or {@link HierarchyFormat#FLATTENED}.
* <p>
* The data provider should be refreshed after making changes to the
* underlying {@link HierarchicalData} instance.
*
* @param hierarchicalData
* the backing {@link HierarchicalData} for this provider, not
* {@code null}
* @param hierarchyFormat
* the hierarchy format to return data in, not {@code null}
*/
public InMemoryHierarchicalDataProvider(U hierarchicalData,
HierarchyFormat hierarchyFormat) {
this(hierarchicalData);
this.hierarchyFormat = Objects.requireNonNull(hierarchyFormat,
"hierarchyFormat cannot be null");
}

@Override
public HierarchyFormat getHierarchyFormat() {
return hierarchyFormat;
}

/**
* Return the underlying {@link HierarchicalData} of this provider.
*
* @return the underlying data of this provider
*/
public U getHierarchicalData() {
return hierarchicalData;
}

@Override
public boolean hasChildren(T item) {
if (!hierarchicalData.contains(item)) {
// The item might be dropped from the tree already
return false;
}
return !hierarchicalData.getChildren(item).isEmpty();
}

@Override
public T getParent(T item) {
Objects.requireNonNull(item, "Item cannot be null.");
try {
return hierarchicalData.getParent(item);
} catch (IllegalArgumentException e) {
return null;
}
}

@Override
public int getDepth(T item) {
int depth = 0;
while ((item = hierarchicalData.getParent(item)) != null) {
depth++;
}
return depth;
}

@Override
public int getChildCount(
HierarchicalQuery<T, SerializablePredicate<T>> query) {
Optional<SerializablePredicate<T>> combinedFilter = getCombinedFilter(
query.getFilter());

return (int) flatten(query.getParent(), query.getExpandedItemIds(),
combinedFilter, Optional.empty()).stream()
.skip(query.getOffset()).limit(query.getLimit()).count();
}

@Override
public Stream<T> fetchChildren(
HierarchicalQuery<T, SerializablePredicate<T>> query) {
if (!hierarchicalData.contains(query.getParent())) {
throw new IllegalArgumentException("The queried item "
+ query.getParent()
+ " could not be found in the backing HierarchicalData. "
+ "Did you forget to refresh this data provider after item removal?");
}

Optional<SerializablePredicate<T>> combinedFilter = getCombinedFilter(
query.getFilter());

Optional<Comparator<T>> comparator = Stream
.of(query.getInMemorySorting(), sortOrder)
.filter(Objects::nonNull).reduce(Comparator::thenComparing);

return flatten(query.getParent(), query.getExpandedItemIds(),
combinedFilter, comparator).stream().skip(query.getOffset())
.limit(query.getLimit());
}

@Override
public SerializablePredicate<T> getFilter() {
return filter;
}

@Override
public void setFilter(SerializablePredicate<T> filter) {
this.filter = filter;
refreshAll();
}

@Override
public SerializableComparator<T> getSortComparator() {
return sortOrder;
}

@Override
public void setSortComparator(SerializableComparator<T> comparator) {
sortOrder = comparator;
refreshAll();
}

private Optional<SerializablePredicate<T>> getCombinedFilter(
Optional<SerializablePredicate<T>> queryFilter) {
return filter != null
? Optional.of(queryFilter.map(filter::and).orElse(filter))
: queryFilter;
}

private List<T> flatten(T parent, Set<Object> expandedItemIds,
Optional<SerializablePredicate<T>> combinedFilter,
Optional<Comparator<T>> comparator) {
List<T> result = new ArrayList<>();
List<T> children = hierarchicalData.getChildren(parent);

if (comparator.isPresent()) {
children = children.stream().sorted(comparator.get()).toList();
}

for (T child : children) {
boolean isExpanded = expandedItemIds.contains(getId(child));
List<T> descendants = Collections.emptyList();
if (getHierarchyFormat().equals(HierarchyFormat.NESTED)
|| isExpanded || combinedFilter.isPresent()) {
descendants = flatten(child, expandedItemIds, combinedFilter,
comparator);
}

boolean matchesFilter = combinedFilter.map(f -> f.test(child))
.orElse(true) || !descendants.isEmpty();
if (matchesFilter) {
result.add(child);
}
if (matchesFilter
&& getHierarchyFormat().equals(HierarchyFormat.FLATTENED)
&& isExpanded) {
result.addAll(descendants);
}
}

return result;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
* @param <T>
* data type
*/
public class TreeData<T> implements Serializable {
public class TreeData<T> implements HierarchicalData<T> {

private static class HierarchyWrapper<T> implements Serializable {
private T parent;
Expand Down Expand Up @@ -333,17 +333,7 @@ public List<T> getRootItems() {
return getChildren(null);
}

/**
* Get the immediate child items for the given item.
*
* @param item
* the item for which to retrieve child items for, null to
* retrieve all root items
* @return an unmodifiable list of child items for the given item
*
* @throws IllegalArgumentException
* if the item does not exist in this structure
*/
@Override
public List<T> getChildren(T item) {
if (!contains(item)) {
throw new IllegalArgumentException(
Expand All @@ -353,16 +343,7 @@ public List<T> getChildren(T item) {
.unmodifiableList(itemToWrapperMap.get(item).getChildren());
}

/**
* Get the parent item for the given item.
*
* @param item
* the item for which to retrieve the parent item for
* @return parent item for the given item or {@code null} if the item is a
* root item.
* @throws IllegalArgumentException
* if the item does not exist in this structure
*/
@Override
public T getParent(T item) {
if (!contains(item)) {
throw new IllegalArgumentException(
Expand Down Expand Up @@ -468,14 +449,7 @@ public void moveAfterSibling(T item, T sibling) {
}
}

/**
* Check whether the given item is in this hierarchy.
*
* @param item
* the item to check
* @return {@code true} if the item is in this hierarchy, {@code false} if
* not
*/
@Override
public boolean contains(T item) {
return itemToWrapperMap.containsKey(item);
}
Expand Down
Loading
Loading