diff --git a/flow-data/src/main/java/com/vaadin/flow/data/provider/hierarchy/HierarchicalData.java b/flow-data/src/main/java/com/vaadin/flow/data/provider/hierarchy/HierarchicalData.java new file mode 100644 index 00000000000..f8b21cbc2bc --- /dev/null +++ b/flow-data/src/main/java/com/vaadin/flow/data/provider/hierarchy/HierarchicalData.java @@ -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. + *

+ * Typically used as a backing data source for + * {@link InMemoryHierarchicalDataProvider}. + * + * @author Vaadin Ltd + * @since 25.2 + * + * @param + * data type + */ +public interface HierarchicalData 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 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); +} diff --git a/flow-data/src/main/java/com/vaadin/flow/data/provider/hierarchy/InMemoryHierarchicalDataProvider.java b/flow-data/src/main/java/com/vaadin/flow/data/provider/hierarchy/InMemoryHierarchicalDataProvider.java new file mode 100644 index 00000000000..a9d71b51640 --- /dev/null +++ b/flow-data/src/main/java/com/vaadin/flow/data/provider/hierarchy/InMemoryHierarchicalDataProvider.java @@ -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 + * data type + * @param + * concrete type of the {@link HierarchicalData} used in this + * provider. + */ +public class InMemoryHierarchicalDataProvider> + extends AbstractHierarchicalDataProvider> + implements InMemoryDataProvider { + + private final U hierarchicalData; + + private SerializablePredicate filter = null; + + private SerializableComparator sortOrder = null; + + private HierarchyFormat hierarchyFormat = HierarchyFormat.NESTED; + + /** + * Constructs a new InMemoryHierarchicalDataProvider. + *

+ * 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}. + *

+ * 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> query) { + Optional> 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 fetchChildren( + HierarchicalQuery> 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> combinedFilter = getCombinedFilter( + query.getFilter()); + + Optional> 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 getFilter() { + return filter; + } + + @Override + public void setFilter(SerializablePredicate filter) { + this.filter = filter; + refreshAll(); + } + + @Override + public SerializableComparator getSortComparator() { + return sortOrder; + } + + @Override + public void setSortComparator(SerializableComparator comparator) { + sortOrder = comparator; + refreshAll(); + } + + private Optional> getCombinedFilter( + Optional> queryFilter) { + return filter != null + ? Optional.of(queryFilter.map(filter::and).orElse(filter)) + : queryFilter; + } + + private List flatten(T parent, Set expandedItemIds, + Optional> combinedFilter, + Optional> comparator) { + List result = new ArrayList<>(); + List 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 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; + } +} diff --git a/flow-data/src/main/java/com/vaadin/flow/data/provider/hierarchy/TreeData.java b/flow-data/src/main/java/com/vaadin/flow/data/provider/hierarchy/TreeData.java index 10701920a17..c8758c76f88 100644 --- a/flow-data/src/main/java/com/vaadin/flow/data/provider/hierarchy/TreeData.java +++ b/flow-data/src/main/java/com/vaadin/flow/data/provider/hierarchy/TreeData.java @@ -40,7 +40,7 @@ * @param * data type */ -public class TreeData implements Serializable { +public class TreeData implements HierarchicalData { private static class HierarchyWrapper implements Serializable { private T parent; @@ -333,17 +333,7 @@ public List 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 getChildren(T item) { if (!contains(item)) { throw new IllegalArgumentException( @@ -353,16 +343,7 @@ public List 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( @@ -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); } diff --git a/flow-data/src/main/java/com/vaadin/flow/data/provider/hierarchy/TreeDataProvider.java b/flow-data/src/main/java/com/vaadin/flow/data/provider/hierarchy/TreeDataProvider.java index 5344918491e..54489e1b82b 100644 --- a/flow-data/src/main/java/com/vaadin/flow/data/provider/hierarchy/TreeDataProvider.java +++ b/flow-data/src/main/java/com/vaadin/flow/data/provider/hierarchy/TreeDataProvider.java @@ -15,19 +15,6 @@ */ 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 TreeData} as its source of data. @@ -39,16 +26,7 @@ * data type */ public class TreeDataProvider - extends AbstractHierarchicalDataProvider> - implements InMemoryDataProvider { - - private final TreeData treeData; - - private SerializablePredicate filter = null; - - private SerializableComparator sortOrder = null; - - private HierarchyFormat hierarchyFormat = HierarchyFormat.NESTED; + extends InMemoryHierarchicalDataProvider> { /** * Constructs a new TreeDataProvider. @@ -61,8 +39,7 @@ public class TreeDataProvider * {@code null} */ public TreeDataProvider(TreeData treeData) { - this.treeData = Objects.requireNonNull(treeData, - "treeData cannot be null"); + super(treeData); } /** @@ -81,147 +58,17 @@ public TreeDataProvider(TreeData treeData) { */ public TreeDataProvider(TreeData treeData, HierarchyFormat hierarchyFormat) { - this(treeData); - this.hierarchyFormat = Objects.requireNonNull(hierarchyFormat, - "hierarchyFormat cannot be null"); - } - - @Override - public HierarchyFormat getHierarchyFormat() { - return hierarchyFormat; + super(treeData, hierarchyFormat); } /** - * Return the underlying hierarchical data of this provider. + * Return the underlying {@link TreeData} of this provider. * * @return the underlying data of this provider + * @deprecated use {@link #getHierarchicalData()} instead. */ + @Deprecated(forRemoval = true, since = "25.2") public TreeData getTreeData() { - return treeData; - } - - @Override - public boolean hasChildren(T item) { - if (!treeData.contains(item)) { - // The item might be dropped from the tree already - return false; - } - return !treeData.getChildren(item).isEmpty(); - } - - @Override - public T getParent(T item) { - Objects.requireNonNull(item, "Item cannot be null."); - try { - return getTreeData().getParent(item); - } catch (IllegalArgumentException e) { - return null; - } - } - - @Override - public int getDepth(T item) { - int depth = 0; - while ((item = treeData.getParent(item)) != null) { - depth++; - } - return depth; - } - - @Override - public int getChildCount( - HierarchicalQuery> query) { - Optional> 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 fetchChildren( - HierarchicalQuery> query) { - if (!treeData.contains(query.getParent())) { - throw new IllegalArgumentException("The queried item " - + query.getParent() - + " could not be found in the backing TreeData. " - + "Did you forget to refresh this data provider after item removal?"); - } - - Optional> combinedFilter = getCombinedFilter( - query.getFilter()); - - Optional> comparator = Stream - .of(query.getInMemorySorting(), sortOrder) - .filter(Objects::nonNull) - .reduce((c1, c2) -> c1.thenComparing(c2)); - - return flatten(query.getParent(), query.getExpandedItemIds(), - combinedFilter, comparator).stream().skip(query.getOffset()) - .limit(query.getLimit()); - } - - @Override - public SerializablePredicate getFilter() { - return filter; - } - - @Override - public void setFilter(SerializablePredicate filter) { - this.filter = filter; - refreshAll(); - } - - @Override - public SerializableComparator getSortComparator() { - return sortOrder; - } - - @Override - public void setSortComparator(SerializableComparator comparator) { - sortOrder = comparator; - refreshAll(); - } - - private Optional> getCombinedFilter( - Optional> queryFilter) { - return filter != null - ? Optional.of(queryFilter.map(filter::and).orElse(filter)) - : queryFilter; - } - - private List flatten(T parent, Set expandedItemIds, - Optional> combinedFilter, - Optional> comparator) { - List result = new ArrayList<>(); - List children = getTreeData().getChildren(parent); - - if (comparator.isPresent()) { - children = children.stream().sorted(comparator.get()).toList(); - } - - for (T child : children) { - boolean isExpanded = expandedItemIds.contains(getId(child)); - List 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; + return getHierarchicalData(); } }