Skip to content

Commit 7a0e62f

Browse files
authored
refactor!: Centralize hierarchy management server-side (#22019)
The PR refactors HierarchicalDataCommunicator so that hierarchy management (expanded items, hierarchical cache) is centralized on the server side instead of being shared between client and server. This makes the client a simple consumer of flattened data lists, which significantly reduces the client-side complexity and opens the door for introducing flat data providers to support fetching flattened data directly from the data source. Key changes HierarchyMapper and HierarchicalCommunicationController have been replaced with the new concept, Cache. This new class provides a system for storing data in a hierarchical structure while enabling access in a flattened format for client-side consumption. setRequestedRange and setParentRequestedRange have been replaced with a single setViewportRange which spans all hierarchy levels. Caveats setViewportRange does not currently remove items from Cache and KeyMapper when they move out of the viewport. However, the previous version also only removed items from KeyMapper while keeping them in HierarchyMapper, so item instances actually continued to remain in memory. I feel this could be optimized separately if needed. setViewportRange currently always sends the full viewport to the client even when some items are already present on the client-side. I plan to address this separately, see #21989 refresh(T item) currently also sends the full viewport on every item refresh. I plan to address this separately, see #21989 Test coverage The PR aims at providing test coverage for all core functionality and public APIs. Individual tests for internal APIs such as Cache will be added separately. The refactoring successfully passes all TreeGrid ITs, see #7676 Depends on refactor: remove deprecated HierarchicalDataCommunicator APIs #21965 Fixes #21876, #21877
1 parent eda19b9 commit 7a0e62f

13 files changed

+2413
-982
lines changed
Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
/*
2+
* Copyright 2000-2025 Vaadin Ltd.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
5+
* use this file except in compliance with the License. You may obtain a copy of
6+
* the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations under
14+
* the License.
15+
*/
16+
package com.vaadin.flow.data.provider.hierarchy;
17+
18+
import java.io.Serializable;
19+
import java.util.HashMap;
20+
import java.util.List;
21+
import java.util.Map;
22+
import java.util.Map.Entry;
23+
import java.util.Set;
24+
import java.util.SortedMap;
25+
import java.util.TreeMap;
26+
27+
import com.vaadin.flow.function.SerializablePredicate;
28+
import com.vaadin.flow.function.SerializableSupplier;
29+
30+
/**
31+
* A cache for hierarchical data. Each instance of {@link Cache} represents a
32+
* level in the hierarchy. It is used to store the size of the level, the items,
33+
* and references to their child caches if they are expanded.
34+
* <p>
35+
* WARNING: This class is intended for internal use only and may change at any
36+
* time without notice. It is not part of the public API and should not be used
37+
* directly in your applications.
38+
*
39+
* @since 25.0
40+
*
41+
* @param <T>
42+
* the type of items in the cache
43+
*/
44+
class Cache<T> implements Serializable {
45+
private final RootCache<T> rootCache;
46+
private final Cache<T> parentCache;
47+
private final int parentIndex;
48+
private int size;
49+
50+
private final Map<Object, T> itemIdToItem = new HashMap<>();
51+
private final SortedMap<Integer, Object> indexToItemId = new TreeMap<>();
52+
private final SortedMap<Integer, Cache<T>> indexToCache = new TreeMap<>();
53+
54+
/**
55+
* Creates a new cache instance with the specified parent cache, parent
56+
* index, and size.
57+
*
58+
* @param parentCache
59+
* the parent cache, or {@code null} if this is the root cache
60+
* @param parentIndex
61+
* the index of this cache in the parent cache
62+
* @param size
63+
* the size of this cache
64+
*/
65+
protected Cache(Cache<T> parentCache, int parentIndex, int size) {
66+
this.rootCache = parentCache != null ? parentCache.rootCache
67+
: (RootCache<T>) this;
68+
this.parentCache = parentCache;
69+
this.parentIndex = parentIndex;
70+
this.size = size;
71+
}
72+
73+
/**
74+
* Gets the item in the parent cache that this cache is associated with.
75+
*
76+
* @return the parent item or {@code null} if there is no parent cache
77+
*/
78+
public T getParentItem() {
79+
return parentCache != null ? parentCache.getItem(parentIndex) : null;
80+
}
81+
82+
/**
83+
* Gets the depth of this cache level in the hierarchy.
84+
*
85+
* @return the depth of this cache level
86+
*/
87+
public int getDepth() {
88+
return parentCache != null ? parentCache.getDepth() + 1 : 0;
89+
}
90+
91+
/**
92+
* Sets the size of this individual cache instance.
93+
*/
94+
public void setSize(int size) {
95+
this.size = size;
96+
}
97+
98+
/**
99+
* Gets the size of this individual cache instance.
100+
*
101+
* @return the number of items stored directly in this cache level,
102+
* excluding all descendant caches
103+
*/
104+
public int getSize() {
105+
return size;
106+
}
107+
108+
/**
109+
* Checks if this cache contains an item at the specified index.
110+
*
111+
* @param index
112+
* the index to check
113+
* @return {@code true} if an item is found, {@code false} otherwise
114+
*/
115+
public boolean hasItem(int index) {
116+
return indexToItemId.containsKey(index);
117+
}
118+
119+
/**
120+
* Gets the item at the specified index in this cache.
121+
*
122+
* @param index
123+
* the index of the item to retrieve
124+
* @return the item at the specified index, or {@code null} if not found
125+
*/
126+
public T getItem(int index) {
127+
var itemId = indexToItemId.get(index);
128+
return itemIdToItem.get(itemId);
129+
}
130+
131+
/**
132+
* Replaces a cached item with a new instance. Items are matched by their
133+
* IDs in the data provider.
134+
*
135+
* @param item
136+
* the new item instance with identical ID to cached item
137+
*/
138+
public void refreshItem(T item) {
139+
var itemId = rootCache.getItemId(item);
140+
itemIdToItem.replace(itemId, item);
141+
}
142+
143+
/**
144+
* Sets the items in this cache starting from the specified index.
145+
*
146+
* @param startIndex
147+
* the index to start setting items
148+
* @param items
149+
* the list of items to set
150+
*/
151+
public void setItems(int startIndex, List<T> items) {
152+
var index = startIndex;
153+
for (T item : items) {
154+
var itemId = rootCache.getItemId(item);
155+
156+
indexToItemId.put(index, itemId);
157+
itemIdToItem.put(itemId, item);
158+
rootCache.addItemContext(item, this, index);
159+
160+
index++;
161+
}
162+
}
163+
164+
/**
165+
* Removes all items and sub-caches from this cache.
166+
*/
167+
public void clear() {
168+
indexToCache.values().forEach(Cache::clear);
169+
indexToCache.clear();
170+
171+
indexToItemId.values().forEach(itemId -> {
172+
rootCache.removeItemContext(itemIdToItem.get(itemId));
173+
});
174+
indexToItemId.clear();
175+
176+
itemIdToItem.clear();
177+
}
178+
179+
/**
180+
* Checks if this cache has a sub-cache at the specified index.
181+
*
182+
* @param index
183+
* the index to check
184+
* @return {@code true} if a sub-cache is found, {@code false} otherwise
185+
*/
186+
public boolean hasSubCache(int index) {
187+
return indexToCache.containsKey(index);
188+
}
189+
190+
/**
191+
* Gets the sub-cache at the specified index.
192+
*
193+
* @param index
194+
* the index to check
195+
* @return the sub-cache at the specified index, or {@code null} if not
196+
* found
197+
*/
198+
public Cache<T> getSubCache(int index) {
199+
return indexToCache.get(index);
200+
}
201+
202+
/**
203+
* Gets all sub-caches of this cache ordered by their indexes.
204+
*
205+
* @return a set of entries where the key is the index of the sub-cache and
206+
* the value is the sub-cache itself
207+
*/
208+
public Set<Entry<Integer, Cache<T>>> getSubCaches() {
209+
return indexToCache.entrySet();
210+
}
211+
212+
/**
213+
* Returns a sub-cache at the specified index or creates a new one if it
214+
* does not exist. The new sub-cache is initialized with the size provided
215+
* by the given supplier.
216+
*
217+
* @param index
218+
* the index of the new sub-cache
219+
* @param sizeSupplier
220+
* a supplier that provides the size of the new sub-cache
221+
* @return the sub-cache instance
222+
*/
223+
public Cache<T> ensureSubCache(int index,
224+
SerializableSupplier<Integer> sizeSupplier) {
225+
return indexToCache.computeIfAbsent(index,
226+
(_key) -> new Cache<>(this, index, sizeSupplier.get()));
227+
}
228+
229+
/**
230+
* Removes all descendant caches that match the given predicate.
231+
*
232+
* @param predicate
233+
* the predicate to match caches against
234+
*/
235+
public void removeDescendantCacheIf(
236+
SerializablePredicate<Cache<T>> predicate) {
237+
indexToCache.values().removeIf(cache -> {
238+
if (predicate.test(cache)) {
239+
cache.clear();
240+
return true;
241+
}
242+
cache.removeDescendantCacheIf(predicate);
243+
return false;
244+
});
245+
}
246+
}

0 commit comments

Comments
 (0)