Skip to content

Commit 1d12de6

Browse files
authored
feat: Introduce flattened hierarchy format (#22040)
The PR introduces a new getHierarchyFormat() method to HierarchicalDataProvider. It can be configured to return either HierarchyFormat#NESTED (default) or HierarchyFormat#FLATTENED (new). The selected format defines in what way the data provider returns hierarchical data and how HierarchicalDataCommunicator should fetch and render it.
1 parent 7a0e62f commit 1d12de6

File tree

6 files changed

+653
-58
lines changed

6 files changed

+653
-58
lines changed

flow-data/src/main/java/com/vaadin/flow/data/provider/DataProvider.java

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import com.vaadin.flow.data.binder.HasFilterableDataProvider;
2828
import com.vaadin.flow.data.provider.CallbackDataProvider.CountCallback;
2929
import com.vaadin.flow.data.provider.CallbackDataProvider.FetchCallback;
30+
import com.vaadin.flow.data.provider.hierarchy.HierarchicalDataProvider.HierarchyFormat;
3031
import com.vaadin.flow.function.SerializableBiFunction;
3132
import com.vaadin.flow.function.SerializableFunction;
3233
import com.vaadin.flow.shared.Registration;
@@ -109,14 +110,17 @@ public interface DataProvider<T, F> extends Serializable {
109110
void refreshItem(T item);
110111

111112
/**
112-
* Refreshes the given item and its children if refreshChildren is true and
113-
* a hierarchical data provider is used. Otherwise, it behaves like regular
114-
* {@link #refreshItem(Object)}.
113+
* Refreshes the given item and its children when {@code refreshChildren} is
114+
* true.
115115
* <p>
116-
* It's important to note that this method resets the item's hierarchy which
117-
* can cause a content shift if the item contains expanded children: their
118-
* descendants aren't guaranteed to be re-fetched eagerly, which may affect
119-
* the overall size of the rendered hierarchy, leading to content shifts.
116+
* This method will reset the item's cached hierarchy which can cause a
117+
* content shift if the item also contains <i>expanded</i> children: their
118+
* descendants aren't guaranteed to be re-fetched eagerly if they aren't
119+
* visible, which may affect the overall size of the rendered hierarchy,
120+
* leading to content shifts.
121+
* <p>
122+
* This method is only supported for hierarchical data providers that use
123+
* {@link HierarchyFormat#NESTED}.
120124
*
121125
* @param item
122126
* the item to refresh

flow-data/src/main/java/com/vaadin/flow/data/provider/hierarchy/AbstractHierarchicalDataProvider.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,23 @@
3434
public abstract class AbstractHierarchicalDataProvider<T, F> extends
3535
AbstractDataProvider<T, F> implements HierarchicalDataProvider<T, F> {
3636

37+
/**
38+
* @throws UnsupportedOperationException
39+
* if the hierarchy format is not {@link HierarchyFormat#NESTED}
40+
*/
41+
@Override
42+
public void refreshItem(T item, boolean refreshChildren) {
43+
if (!getHierarchyFormat().equals(HierarchyFormat.NESTED)) {
44+
throw new UnsupportedOperationException(
45+
"""
46+
Refreshing children of an item is only supported when the data provider \
47+
uses HierarchyFormat#NESTED. For other formats, use refreshAll() instead.
48+
""");
49+
}
50+
51+
super.refreshItem(item, refreshChildren);
52+
}
53+
3754
@Override
3855
public <Q, C> HierarchicalConfigurableFilterDataProvider<T, Q, C> withConfigurableFilter(
3956
SerializableBiFunction<Q, C, F> filterCombiner) {

flow-data/src/main/java/com/vaadin/flow/data/provider/hierarchy/HierarchicalDataCommunicator.java

Lines changed: 96 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import java.util.Arrays;
1919
import java.util.Collection;
20+
import java.util.Collections;
2021
import java.util.HashSet;
2122
import java.util.LinkedList;
2223
import java.util.List;
@@ -32,6 +33,7 @@
3233
import com.vaadin.flow.data.provider.DataGenerator;
3334
import com.vaadin.flow.data.provider.DataProvider;
3435
import com.vaadin.flow.data.provider.KeyMapper;
36+
import com.vaadin.flow.data.provider.hierarchy.HierarchicalDataProvider.HierarchyFormat;
3537
import com.vaadin.flow.function.SerializableConsumer;
3638
import com.vaadin.flow.function.SerializableSupplier;
3739
import com.vaadin.flow.function.ValueProvider;
@@ -45,13 +47,25 @@
4547
import elemental.json.JsonValue;
4648

4749
/**
50+
* WARNING: Direct use of this class in application code is not recommended and
51+
* may result in unexpected behavior. Use the API provided by the component
52+
* instead.
53+
* <p>
4854
* {@link HierarchicalDataCommunicator} is a middleware layer between
4955
* {@link HierarchicalDataProvider} and the client-side. It handles the loading
50-
* and caching of hierarchical data from the data provider, tracks expanded and
51-
* collapsed items, and delivers data to the client based on the
52-
* {@link #setViewportRange(int, int) requested viewport range}.
56+
* and caching of hierarchical data from the data provider based on its
57+
* hierarchy format, tracks expanded and collapsed items, and delivers data to
58+
* the client based on the {@link #setViewportRange(int, int) requested viewport
59+
* range}.
60+
* <p>
61+
* The communicator supports data providers that implement in one of the
62+
* following formats: {@link HierarchyFormat#NESTED} or
63+
* {@link HierarchyFormat#FLATTENED}.
5364
* <p>
54-
* Internally, it stores data in a hierarchical cache structure where each level
65+
* <strong>Nested Hierarchy Format</strong>
66+
* <p>
67+
* When using data providers with {@link HierarchyFormat#NESTED}, the
68+
* communicator stores data in a hierarchical cache structure where each level
5569
* is represented by a {@link Cache} object, and the root by {@link RootCache}.
5670
* <p>
5771
* Before sending data to the client, the visible range is flattened into a
@@ -60,14 +74,21 @@
6074
* method should be used by the component to get an item's depth and apply
6175
* indentation or other visual styling based on hierarchy level.
6276
* <p>
77+
* <strong>Flattened Hierarchy Format</strong>
78+
* <p>
79+
* When using data providers whose format is {@link HierarchyFormat#FLATTENED},
80+
* the communicator maintains all items in a single flat list, managed through
81+
* {@link RootCache}, which is directly suitable for client-side rendering
82+
* without any additional processing. The {@link #getDepth(Object)} method uses
83+
* the data provider's implementation to determine the depth of an item in the
84+
* hierarchy.
85+
* <p>
86+
* <strong>KeyMapper</strong>
87+
* <p>
6388
* For each item in the visible range, the communicator generates a client-side
6489
* key using {@link KeyMapper}. This key is used to identify the item on the
6590
* server when the client sends updates or interaction events for that item such
6691
* as selection, expansion, etc.
67-
* <p>
68-
* WARNING: It's not recommended to rely on this class directly in application
69-
* code. Instead, the API provided by the component should be used. Direct use
70-
* may lead to unexpected behavior and isn't guaranteed to be stable.
7192
*
7293
* @param <T>
7394
* the bean type
@@ -137,15 +158,15 @@ private void requestFlush() {
137158

138159
/**
139160
* Clears all cached data and recursively re-fetches items from hierarchy
140-
* levels that happen to be within the current viewport range, starting from
161+
* levels that are still within the current viewport range, starting from
141162
* the root level.
142163
* <p>
143-
* WARNING: This method performs a full hierarchy reset which discards
144-
* information about previously visited expanded items and their positions
145-
* in the hierarchy. As a result, the viewport's start index may become
146-
* pointing to a different item if there were visited expanded items before
147-
* the start index, which can cause a shift in the currently displayed
148-
* items.
164+
* WARNING: For data providers that use {@link HierarchyFormat#NESTED}, this
165+
* method will clear all cached hierarchy state, discarding any potential
166+
* information about the positions of expanded items in the hierarchy. As a
167+
* result, the viewport's start index may become pointing to a different
168+
* item if there were cached expanded items before the start index, causing
169+
* a shift in the currently displayed items.
149170
*/
150171
@Override
151172
public void reset() {
@@ -175,26 +196,38 @@ public void refresh(T item) {
175196

176197
/**
177198
* Replaces the cached item with a new instance and schedules a client
178-
* update to re-render this item. If {@code refreshChildren} is true, the
179-
* item's children are cleared from the cache and forced to be re-fetched
180-
* from the data provider when visible.
199+
* update to re-render this item. When {@code refreshChildren} is true, the
200+
* item's sub-hierarchy is cleared from the cache and scheduled to be
201+
* re-fetched from the data provider once visible.
181202
* <p>
182-
* WARNING: When {@code refreshChildren} is true, the method resets the
183-
* item's hierarchy, which may in turn cause visible range shifts if the
184-
* refreshed item contains expanded children. In such cases, their
185-
* descendants might not be re-fetched immediately, which can affect the
186-
* flattened hierarchy size and result in the viewport range pointing to a
187-
* different set of items than before the refresh.
203+
* WARNING: This method is only supported with data providers that use
204+
* {@link HierarchyFormat#NESTED} and may cause visible range shift if the
205+
* refreshed item contains <i>expanded</i> descendants. In such cases, they
206+
* might not be re-fetched immediately if they are not visible. This can
207+
* affect the flattened hierarchy size and result in the viewport range
208+
* pointing to a different set of items than before the refresh.
188209
*
189210
* @since 25.0
190211
* @param item
191212
* the item to refresh
192213
* @param refreshChildren
193214
* whether or not to refresh child items
215+
* @throws UnsupportedOperationException
216+
* if {@code refreshChildren} is true and the data provider's
217+
* hierarchy format is not {@link HierarchyFormat#NESTED}
194218
*/
195219
public void refresh(T item, boolean refreshChildren) {
196220
Objects.requireNonNull(item, "Item cannot be null");
197221

222+
if (!getHierarchyFormat().equals(HierarchyFormat.NESTED)
223+
&& refreshChildren) {
224+
throw new UnsupportedOperationException(
225+
"""
226+
Refreshing children of an item is only supported when the data provider \
227+
uses HierarchyFormat#NESTED. For other formats, use reset() instead.
228+
""");
229+
}
230+
198231
getKeyMapper().refresh(item);
199232
dataGenerator.refreshData(item);
200233

@@ -311,6 +344,11 @@ public Collection<T> collapse(Collection<T> items) {
311344
if (rootCache != null) {
312345
rootCache.removeDescendantCacheIf(
313346
(cache) -> !isExpanded(cache.getParentItem()));
347+
}
348+
349+
if (getHierarchyFormat().equals(HierarchyFormat.FLATTENED)) {
350+
reset();
351+
} else {
314352
requestFlush();
315353
}
316354

@@ -345,7 +383,11 @@ public Collection<T> expand(Collection<T> items) {
345383
return expandedItemIds.add(getDataProvider().getId(item));
346384
}).toList();
347385

348-
requestFlush();
386+
if (getHierarchyFormat().equals(HierarchyFormat.FLATTENED)) {
387+
reset();
388+
} else {
389+
requestFlush();
390+
}
349391

350392
return expandedItems;
351393
}
@@ -384,6 +426,10 @@ public boolean isExpanded(T item) {
384426
public int getDepth(T item) {
385427
Objects.requireNonNull(item, "Item cannot be null");
386428

429+
if (getHierarchyFormat().equals(HierarchyFormat.FLATTENED)) {
430+
return getDataProvider().getDepth(item);
431+
}
432+
387433
if (rootCache == null) {
388434
return -1;
389435
}
@@ -447,8 +493,14 @@ private void resolveIndexPath(Cache<T> cache, int... path) {
447493
preloadRange(cache, index, 1);
448494
}
449495

496+
if (restPath.length == 0) {
497+
// If there is no rest path, we are at the target item
498+
return;
499+
}
500+
450501
var item = cache.getItem(index);
451-
if (restPath.length > 0 && isExpanded(item)) {
502+
if (getHierarchyFormat().equals(HierarchyFormat.NESTED)
503+
&& isExpanded(item)) {
452504
var subCache = cache.ensureSubCache(index,
453505
() -> getDataProviderChildCount(item));
454506
resolveIndexPath(subCache, restPath);
@@ -507,7 +559,8 @@ protected List<T> preloadFlatRangeBackward(int start, int length) {
507559
// Checking result.size() > 0 ensures that the start item
508560
// won't be expanded and its descendants won't be included
509561
// in the result.
510-
if (isExpanded(item) && !cache.hasSubCache(index)
562+
if (getHierarchyFormat().equals(HierarchyFormat.NESTED)
563+
&& isExpanded(item) && !cache.hasSubCache(index)
511564
&& result.size() > 0) {
512565
var subCache = cache.ensureSubCache(index,
513566
() -> getDataProviderChildCount(item));
@@ -556,7 +609,8 @@ protected List<T> preloadFlatRangeForward(int start, int length) {
556609
}
557610

558611
var item = cache.getItem(index);
559-
if (isExpanded(item)) {
612+
if (getHierarchyFormat().equals(HierarchyFormat.NESTED)
613+
&& isExpanded(item)) {
560614
cache.ensureSubCache(index,
561615
() -> getDataProviderChildCount(item));
562616
}
@@ -604,10 +658,21 @@ private void flush(ExecutionContext context) {
604658
update.commit(nextUpdateId++);
605659
}
606660

661+
private HierarchyFormat getHierarchyFormat() {
662+
return getDataProvider().getHierarchyFormat();
663+
}
664+
665+
private Set<Object> getExpandedItemIds() {
666+
return getHierarchyFormat().equals(HierarchyFormat.FLATTENED)
667+
? Collections.unmodifiableSet(this.expandedItemIds)
668+
: Collections.emptySet();
669+
}
670+
607671
@SuppressWarnings("unchecked")
608672
private Stream<T> fetchDataProviderChildren(T parent, Range range) {
609673
var query = new HierarchicalQuery<>(range.getStart(), range.length(),
610-
getBackEndSorting(), getInMemorySorting(), getFilter(), parent);
674+
getBackEndSorting(), getInMemorySorting(), getFilter(),
675+
getExpandedItemIds(), parent);
611676

612677
return ((HierarchicalDataProvider<T, Object>) getDataProvider())
613678
.fetchChildren(query).peek((item) -> {
@@ -620,7 +685,8 @@ private Stream<T> fetchDataProviderChildren(T parent, Range range) {
620685

621686
@SuppressWarnings("unchecked")
622687
private int getDataProviderChildCount(T parent) {
623-
var query = new HierarchicalQuery<>(getFilter(), parent);
688+
var query = new HierarchicalQuery<>(getFilter(), getExpandedItemIds(),
689+
parent);
624690

625691
var count = ((HierarchicalDataProvider<T, Object>) getDataProvider())
626692
.getChildCount(query);

0 commit comments

Comments
 (0)