All Downloads are FREE. Search and download functionalities are using the official Maven repository.

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; } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy