-
Notifications
You must be signed in to change notification settings - Fork 193
feat: extract interface HierarchicalTreeData from TreeData #23950
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
sclassen
wants to merge
5
commits into
vaadin:main
Choose a base branch
from
sclassen:hierarchicalTreeDataInterface
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+321
−190
Open
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
510379c
feat: extract interface HierarchicalTreeData from TreeData
sclassen 1224857
feature: introduce second generic param in HierarchicalTreeDataProvider
sclassen ccaecb1
Merge branch 'main' into hierarchicalTreeDataInterface
mcollovati 10d4ab6
refactor: renaming classes and parameters
sclassen d7ff3d7
Merge branch 'vaadin:main' into hierarchicalTreeDataInterface
sclassen File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
69 changes: 69 additions & 0 deletions
69
flow-data/src/main/java/com/vaadin/flow/data/provider/hierarchy/HierarchicalData.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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); | ||
| } | ||
240 changes: 240 additions & 0 deletions
240
...c/main/java/com/vaadin/flow/data/provider/hierarchy/InMemoryHierarchicalDataProvider.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | ||
| */ | ||
sclassen marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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; | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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 byTreeDataProvider.There was a problem hiding this comment.
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.