
com.vaadin.data.provider.HierarchicalDataCommunicator Maven / Gradle / Ivy
/*
* Copyright (C) 2000-2024 Vaadin Ltd
*
* This program is available under Vaadin Commercial License and Service Terms.
*
* See for the full
* license.
*/
package com.vaadin.data.provider;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import com.vaadin.data.TreeData;
import com.vaadin.server.SerializableConsumer;
import com.vaadin.shared.Range;
import com.vaadin.shared.data.HierarchicalDataCommunicatorClientRpc;
import com.vaadin.shared.extension.datacommunicator.HierarchicalDataCommunicatorState;
import com.vaadin.ui.ItemCollapseAllowedProvider;
import elemental.json.JsonArray;
/**
* Data communicator that handles requesting hierarchical data from
* {@link HierarchicalDataProvider} and sending it to client side.
*
* @param
* the bean type
* @author Vaadin Ltd
* @since 8.1
*/
public class HierarchicalDataCommunicator extends DataCommunicator {
private HierarchyMapper mapper;
private boolean pendingExpandCollapse = false;
private boolean resetSize = false;
private final HierarchicalDataCommunicatorClientRpc rpc;
/**
* Collapse allowed provider used to allow/disallow collapsing nodes.
*/
private ItemCollapseAllowedProvider itemCollapseAllowedProvider = t -> true;
/**
* Construct a new hierarchical data communicator backed by a
* {@link TreeDataProvider}.
*/
public HierarchicalDataCommunicator() {
super();
rpc = getRpcProxy(HierarchicalDataCommunicatorClientRpc.class);
setDataProvider(new TreeDataProvider<>(new TreeData<>()), null);
}
@Override
protected HierarchicalDataCommunicatorState getState() {
return (HierarchicalDataCommunicatorState) super.getState();
}
@Override
protected HierarchicalDataCommunicatorState getState(boolean markAsDirty) {
return (HierarchicalDataCommunicatorState) super.getState(markAsDirty);
}
@Override
public List fetchItemsWithRange(int offset, int limit) {
// Instead of adding logic to this class, delegate request to the
// separate object handling hierarchies.
// Ensure speedy closing in case the stream is connected to IO channels.
try (Stream stream = mapper
.fetchItems(Range.withLength(offset, limit))) {
return stream.collect(Collectors.toList());
}
}
@Override
public HierarchicalDataProvider getDataProvider() {
return (HierarchicalDataProvider) super.getDataProvider();
}
/**
* Set the current hierarchical data provider for this communicator.
*
* @param dataProvider
* the data provider to set, not null
* @param initialFilter
* the initial filter value to use, or null
to not
* use any initial filter value
*
* @param
* the filter type
*
* @return a consumer that accepts a new filter value to use
*/
public SerializableConsumer setDataProvider(
HierarchicalDataProvider dataProvider, F initialFilter) {
SerializableConsumer consumer = super.setDataProvider(dataProvider,
initialFilter);
// Remove old mapper
if (mapper != null) {
removeDataGenerator(mapper);
}
mapper = createHierarchyMapper(dataProvider);
// Set up mapper for requests
mapper.setBackEndSorting(getBackEndSorting());
mapper.setInMemorySorting(getInMemorySorting());
mapper.setFilter(getFilter());
mapper.setItemCollapseAllowedProvider(getItemCollapseAllowedProvider());
// Provide hierarchy data to json
addDataGenerator(mapper);
return consumer;
}
/**
* Create new {@code HierarchyMapper} for the given data provider. May be
* overridden in subclasses.
*
* @param dataProvider
* the data provider
* @param
* Query type
* @return new {@link HierarchyMapper}
*/
protected HierarchyMapper createHierarchyMapper(
HierarchicalDataProvider dataProvider) {
return new HierarchyMapper<>(dataProvider);
}
/**
* Set the current hierarchical data provider for this communicator.
*
* @param dataProvider
* the data provider to set, must extend
* {@link HierarchicalDataProvider}, not null
* @param initialFilter
* the initial filter value to use, or null
to not
* use any initial filter value
*
* @param
* the filter type
*
* @return a consumer that accepts a new filter value to use
*/
@Override
public SerializableConsumer setDataProvider(
DataProvider dataProvider, F initialFilter) {
if (dataProvider instanceof HierarchicalDataProvider) {
return setDataProvider(
(HierarchicalDataProvider) dataProvider,
initialFilter);
}
throw new IllegalArgumentException(
"Only " + HierarchicalDataProvider.class.getName()
+ " and subtypes supported.");
}
@Override
protected void sendDataToClient(boolean initial) {
super.sendDataToClient(initial);
if (pendingExpandCollapse) {
if (resetSize) {
getClientRpc().reset(mapper.getTreeSize());
}
rpc.setExpandCollapsePending(false);
pendingExpandCollapse = false;
resetSize = false;
}
}
/**
* Collapses the given item and removes its sub-hierarchy. Calling this
* method will have no effect if the row is already collapsed.
*
* @param item
* the item to collapse
*/
public void collapse(T item) {
collapse(item, true);
}
/**
* Collapses the given item and removes its sub-hierarchy. Calling this
* method will have no effect if the row is already collapsed.
* {@code syncAndRefresh} indicates whether the changes should be
* synchronised to the client and the data provider be notified.
*
* @param item
* the item to collapse
* @param syncAndRefresh
* {@code true} if the changes should be synchronised to the
* client and the data provider should be notified of the
* changes, {@code false} otherwise.
*/
public void collapse(T item, boolean syncAndRefresh) {
Integer index = syncAndRefresh ? mapper.getIndexOf(item).orElse(null)
: null;
doCollapse(item, index, syncAndRefresh);
}
/**
* Collapses the given item and removes its sub-hierarchy. Calling this
* method will have no effect if the row is already collapsed.
*
* @param item
* the item to collapse
* @param index
* the index of the item
*/
public void collapse(T item, Integer index) {
doCollapse(item, index, true);
}
/**
* Collapses given item and removes its sub-hierarchy. Calling this method
* will have no effect if the row is already collapsed. The index is
* provided by the client-side or calculated from a full data request.
*
*
* @param item
* the item to collapse
* @param index
* the index of the item
* @deprecated Use {@link #collapse(Object, Integer)} instead.
*/
@Deprecated
public void doCollapse(T item, Optional index) {
doCollapse(item, index.orElse(null), true);
}
/**
* Collapses the given item and removes its sub-hierarchy. Calling this
* method will have no effect if the row is already collapsed. The index is
* provided by the client-side or calculated from a full data request.
* {@code syncAndRefresh} indicates whether the changes should be
* synchronised to the client and the data provider be notified.
*
* @param item
* the item to collapse
* @param index
* the index of the item
* @param syncAndRefresh
* {@code true} if the changes should be synchronised to the
* client and the data provider should be notified of the
* changes, {@code false} otherwise.
*/
private void doCollapse(T item, Integer index, boolean syncAndRefresh) {
Range removedRows = mapper.collapse(item, index);
if (syncAndRefresh) {
if (!reset) {
if (!pendingExpandCollapse) {
rpc.setExpandCollapsePending(true);
pendingExpandCollapse = true;
}
if (!removedRows.isEmpty()) {
getClientRpc().removeRows(removedRows.getStart(),
removedRows.length());
} else if (!resetSize && mapper.hasChildren(item)) {
resetSize = true;
}
}
// only refresh if still within cache
if (mapper.isActive(getDataProvider().getId(item))) {
refresh(item);
}
}
}
@Override
protected void onDropRows(JsonArray keys) {
for (int i = 0; i < keys.length(); ++i) {
String key = keys.getString(i);
T data = getKeyMapper().get(key);
// Only instruct ActiveDataHandler to drop the data if
// HierarchyMapper deems it fit for removal (it hasn't
// been re-added in the meantime).
if (data != null
&& mapper.prepareForDrop(getDataProvider().getId(data))) {
getActiveDataHandler().dropActiveData(key);
}
}
if (!getActiveDataHandler().getDroppedData().isEmpty()) {
getActiveDataHandler().cleanUp(Stream.empty());
}
}
/**
* Expands the given item. Calling this method will have no effect if the
* item is already expanded or if it has no children.
*
* @param item
* the item to expand
*/
public void expand(T item) {
expand(item, true);
}
/**
* Expands the given item. Calling this method will have no effect if the
* item is already expanded or if it has no children. {@code syncAndRefresh}
* indicates whether the changes should be synchronised to the client and
* the data provider be notified.
*
* @param item
* the item to expand
* @param syncAndRefresh
* {@code true} if the changes should be synchronised to the
* client and the data provider should be notified of the
* changes, {@code
* false} otherwise.
*/
public void expand(T item, boolean syncAndRefresh) {
Integer index = syncAndRefresh ? mapper.getIndexOf(item).orElse(null)
: null;
doExpand(item, index, syncAndRefresh);
}
/**
* Expands the given item at the given index. Calling this method will have
* no effect if the item is already expanded.
*
* @param item
* the item to expand
* @param index
* the index of the item
*/
public void expand(T item, Integer index) {
doExpand(item, index, true);
}
/**
* Expands the given item. Calling this method will have no effect if the
* item is already expanded or if it has no children. The index is provided
* by the client-side or calculated from a full data request.
* {@code syncAndRefresh} indicates whether the changes should be
* synchronised to the client and the data provider be notified.
*
* @param item
* the item to expand
* @param index
* the index of the item
* @param syncAndRefresh
* {@code true} if the changes should be synchronised to the
* client and the data provider should be notified of the
* changes, {@code false} otherwise.
*/
private void doExpand(T item, Integer index, boolean syncAndRefresh) {
// The range can be empty for many reasons, including that the mapper
// hasn't been initialised yet or the data provider has been reset and
// no new data has been sent to the client yet.
Range addedRows = mapper.expand(item, index);
if (syncAndRefresh) {
if (!reset) {
if (!pendingExpandCollapse) {
rpc.setExpandCollapsePending(true);
pendingExpandCollapse = true;
}
if (!addedRows.isEmpty()) {
getClientRpc().insertRows(addedRows.getStart(),
addedRows.length());
// Only push data if the expanded row is within the cache.
if (getActiveDataHandler().getActiveData()
.containsValue(item)) {
// Ensure speedy closing in case the stream is connected
// to IO channels.
try (Stream children = mapper.fetchItems(item,
Range.withLength(0, addedRows.length()))) {
pushData(addedRows.getStart(),
children.collect(Collectors.toList()));
}
}
} else if (!resetSize && mapper.hasChildren(item)) {
resetSize = true;
}
}
refresh(item);
}
}
/**
* Expands the given item at given index. Calling this method will have no
* effect if the row is already expanded. The index is provided by the
* client-side or calculated from a full data request.
*
* @param item
* the item to expand
* @param index
* the index of the item
* @see #expand(Object)
* @deprecated use {@link #expand(Object, Integer)} instead
*/
@Deprecated
public void doExpand(T item, Optional index) {
expand(item, index.orElse(null));
}
/**
* Returns whether given item has children.
*
* @param item
* the item to test
* @return {@code true} if item has children; {@code false} if not
*/
public boolean hasChildren(T item) {
return mapper.hasChildren(item);
}
/**
* Returns whether given item is expanded.
*
* @param item
* the item to test
* @return {@code true} if item is expanded; {@code false} if not
*/
public boolean isExpanded(T item) {
return mapper.isExpanded(item);
}
/**
* Sets the item collapse allowed provider for this
* HierarchicalDataCommunicator. The provider should return {@code true} for
* any item that the user can collapse.
*
* Note: This callback will be accessed often when sending
* data to the client. The callback should not do any costly operations.
*
* @param provider
* the item collapse allowed provider, not {@code null}
*/
public void setItemCollapseAllowedProvider(
ItemCollapseAllowedProvider provider) {
Objects.requireNonNull(provider, "Provider can't be null");
itemCollapseAllowedProvider = provider;
// Update hierarchy mapper
mapper.setItemCollapseAllowedProvider(provider);
getActiveDataHandler().getActiveData().values().forEach(this::refresh);
}
/**
* Returns parent index for the row or a negative value.
*
* @param item
* the item to find the parent of
* @return the parent index or a negative value for top-level items
*/
public Integer getParentIndex(T item) {
return mapper.getParentIndex(item);
}
/**
* Gets the item collapse allowed provider.
*
* @return the item collapse allowed provider
*/
public ItemCollapseAllowedProvider getItemCollapseAllowedProvider() {
return itemCollapseAllowedProvider;
}
@Override
public int getDataProviderSize() {
return mapper.getTreeSize();
}
@Override
public void setBackEndSorting(List sortOrder,
boolean immediateReset) {
if (mapper != null) {
mapper.setBackEndSorting(sortOrder);
}
super.setBackEndSorting(sortOrder, immediateReset);
}
@Override
public void setBackEndSorting(List sortOrder) {
setBackEndSorting(sortOrder, true);
}
@Override
public void setInMemorySorting(Comparator comparator,
boolean immediateReset) {
if (mapper != null) {
mapper.setInMemorySorting(comparator);
}
super.setInMemorySorting(comparator, immediateReset);
}
@Override
public void setInMemorySorting(Comparator comparator) {
setInMemorySorting(comparator, true);
}
@Override
protected void setFilter(F filter) {
if (mapper != null) {
mapper.setFilter(filter);
}
super.setFilter(filter);
}
/**
* Returns the {@code HierarchyMapper} used by this data communicator.
*
* @return the hierarchy mapper used by this data communicator
*/
protected HierarchyMapper getHierarchyMapper() {
return mapper;
}
}