Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense to add List<T> getRootItems(); as a convenient shorthand even though it's not currently used by TreeDataProvider.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not agree.
This would require that every implementation can produce the list of all roots from any given item. This is demanding if the underlying implementation relies on references instead of a central map.
For me this is a high barrier to pass especially for a functionality which is not used by flow at all.

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
/*
* 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;
}

/**
* Return the underlying {@link HierarchicalData} of this provider.
*
* @return the underlying data of this provider
* @deprecated use {@link #getHierarchicalData()} instead.
*/
@Deprecated
public U getTreeData() {
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