Java tutorial
/* * Copyright 2000-2018 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.ui; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.stream.Stream; import org.jsoup.nodes.Attributes; import org.jsoup.nodes.Element; import org.jsoup.select.Elements; import com.vaadin.data.HasHierarchicalDataProvider; import com.vaadin.data.HasValue; import com.vaadin.data.PropertyDefinition; import com.vaadin.data.PropertySet; import com.vaadin.data.TreeData; import com.vaadin.data.ValueProvider; import com.vaadin.data.provider.DataProvider; import com.vaadin.data.provider.HierarchicalDataCommunicator; import com.vaadin.data.provider.HierarchicalDataProvider; import com.vaadin.data.provider.HierarchicalQuery; import com.vaadin.data.provider.TreeDataProvider; import com.vaadin.event.CollapseEvent; import com.vaadin.event.CollapseEvent.CollapseListener; import com.vaadin.event.ExpandEvent; import com.vaadin.event.ExpandEvent.ExpandListener; import com.vaadin.shared.Registration; import com.vaadin.shared.ui.grid.ScrollDestination; import com.vaadin.shared.ui.treegrid.FocusParentRpc; import com.vaadin.shared.ui.treegrid.FocusRpc; import com.vaadin.shared.ui.treegrid.NodeCollapseRpc; import com.vaadin.shared.ui.treegrid.TreeGridClientRpc; import com.vaadin.shared.ui.treegrid.TreeGridState; import com.vaadin.ui.declarative.DesignAttributeHandler; import com.vaadin.ui.declarative.DesignContext; import com.vaadin.ui.declarative.DesignFormatter; /** * A grid component for displaying hierarchical tabular data. * * Visual hierarchy depth positioning of rows is done via styles, see * <code>_treegrid.scss</code> from Valo theme. * * @author Vaadin Ltd * @since 8.1 * * @param <T> * the grid bean type */ public class TreeGrid<T> extends Grid<T> implements HasHierarchicalDataProvider<T> { /** * Creates a new {@code TreeGrid} without support for creating columns based * on property names. Use an alternative constructor, such as * {@link TreeGrid#TreeGrid(Class)}, to create a {@code TreeGrid} that * automatically sets up columns based on the type of presented data. */ public TreeGrid() { this(new HierarchicalDataCommunicator<>()); } /** * Creates a new {@code TreeGrid} that uses reflection based on the provided * bean type to automatically set up an initial set of columns. All columns * will be configured using the same {@link Object#toString()} renderer that * is used by {@link #addColumn(ValueProvider)}. * * @param beanType * the bean type to use, not {@code null} */ public TreeGrid(Class<T> beanType) { super(beanType, new HierarchicalDataCommunicator<>()); registerTreeGridRpc(); } /** * Creates a new {@code TreeGrid} using the given * {@code HierarchicalDataProvider}, without support for creating columns * based on property names. Use an alternative constructor, such as * {@link TreeGrid#TreeGrid(Class)}, to create a {@code TreeGrid} that * automatically sets up columns based on the type of presented data. * * @param dataProvider * the data provider, not {@code null} */ public TreeGrid(HierarchicalDataProvider<T, ?> dataProvider) { this(); setDataProvider(dataProvider); } /** * Creates a {@code TreeGrid} using a custom {@link PropertySet} * implementation and custom data communicator. * <p> * Property set is used for configuring the initial columns and resolving * property names for {@link #addColumn(String)} and * {@link Column#setEditorComponent(HasValue)}. * * @param propertySet * the property set implementation to use, not {@code null} * @param dataCommunicator * the data communicator to use, not {@code null} */ protected TreeGrid(PropertySet<T> propertySet, HierarchicalDataCommunicator<T> dataCommunicator) { super(propertySet, dataCommunicator); registerTreeGridRpc(); } /** * Creates a new TreeGrid with the given data communicator and without * support for creating columns based on property names. * * @param dataCommunicator * the custom data communicator to set */ protected TreeGrid(HierarchicalDataCommunicator<T> dataCommunicator) { this(new PropertySet<T>() { @Override public Stream<PropertyDefinition<T, ?>> getProperties() { // No columns configured by default return Stream.empty(); } @Override public Optional<PropertyDefinition<T, ?>> getProperty(String name) { throw new IllegalStateException( "A TreeGrid created without a bean type class literal or a custom property set" + " doesn't support finding properties by name."); } }, dataCommunicator); } /** * Creates a {@code TreeGrid} using a custom {@link PropertySet} * implementation for creating a default set of columns and for resolving * property names with {@link #addColumn(String)} and * {@link Column#setEditorComponent(HasValue)}. * <p> * This functionality is provided as static method instead of as a public * constructor in order to make it possible to use a custom property set * without creating a subclass while still leaving the public constructors * focused on the common use cases. * * @see TreeGrid#TreeGrid() * @see TreeGrid#TreeGrid(Class) * * @param propertySet * the property set implementation to use, not {@code null} * @return a new tree grid using the provided property set, not {@code null} */ public static <BEAN> TreeGrid<BEAN> withPropertySet(PropertySet<BEAN> propertySet) { return new TreeGrid<BEAN>(propertySet, new HierarchicalDataCommunicator<>()); } private void registerTreeGridRpc() { registerRpc((NodeCollapseRpc) (rowKey, rowIndex, collapse, userOriginated) -> { T item = getDataCommunicator().getKeyMapper().get(rowKey); if (collapse && getDataCommunicator().isExpanded(item)) { getDataCommunicator().collapse(item, rowIndex); fireCollapseEvent(getDataCommunicator().getKeyMapper().get(rowKey), userOriginated); } else if (!collapse && !getDataCommunicator().isExpanded(item)) { getDataCommunicator().expand(item, rowIndex); fireExpandEvent(getDataCommunicator().getKeyMapper().get(rowKey), userOriginated); } }); registerRpc((FocusParentRpc) (rowKey, cellIndex) -> { Integer parentIndex = getDataCommunicator() .getParentIndex(getDataCommunicator().getKeyMapper().get(rowKey)); if (parentIndex != null) { getRpcProxy(FocusRpc.class).focusCell(parentIndex, cellIndex); } }); } /** * This method is inherited from Grid but should never be called directly * with a TreeGrid */ @Override @Deprecated public void scrollTo(int row) throws IllegalArgumentException { super.scrollTo(row); } /** * This method is inherited from Grid but should never be called directly * with a TreeGrid */ @Deprecated @Override public void scrollTo(int row, ScrollDestination destination) { super.scrollTo(row, destination); } /** * Adds an ExpandListener to this TreeGrid. * * @see ExpandEvent * * @param listener * the listener to add * @return a registration for the listener */ public Registration addExpandListener(ExpandListener<T> listener) { return addListener(ExpandEvent.class, listener, ExpandListener.EXPAND_METHOD); } /** * Adds a CollapseListener to this TreeGrid. * * @see CollapseEvent * * @param listener * the listener to add * @return a registration for the listener */ public Registration addCollapseListener(CollapseListener<T> listener) { return addListener(CollapseEvent.class, listener, CollapseListener.COLLAPSE_METHOD); } @Override public void setDataProvider(DataProvider<T, ?> dataProvider) { if (!(dataProvider instanceof HierarchicalDataProvider)) { throw new IllegalArgumentException("TreeGrid only accepts hierarchical data providers"); } getRpcProxy(TreeGridClientRpc.class).clearPendingExpands(); super.setDataProvider(dataProvider); } /** * Get the currently set hierarchy column. * * @return the currently set hierarchy column, or {@code null} if no column * has been explicitly set */ public Column<T, ?> getHierarchyColumn() { return getColumnByInternalId(getState(false).hierarchyColumnId); } /** * Set the column that displays the hierarchy of this grid's data. By * default the hierarchy will be displayed in the first column. * <p> * Setting a hierarchy column by calling this method also sets the column to * be visible and not hidable. * <p> * <strong>Note:</strong> Changing the Renderer of the hierarchy column is * not supported. * * @param column * the column to use for displaying hierarchy */ public void setHierarchyColumn(Column<T, ?> column) { Objects.requireNonNull(column, "column may not be null"); if (!getColumns().contains(column)) { throw new IllegalArgumentException("Given column is not a column of this TreeGrid"); } column.setHidden(false); column.setHidable(false); getState().hierarchyColumnId = getInternalIdForColumn(column); } /** * Set the column that displays the hierarchy of this grid's data. By * default the hierarchy will be displayed in the first column. * <p> * Setting a hierarchy column by calling this method also sets the column to * be visible and not hidable. * <p> * <strong>Note:</strong> Changing the Renderer of the hierarchy column is * not supported. * * @see Column#setId(String) * * @param id * id of the column to use for displaying hierarchy */ public void setHierarchyColumn(String id) { Objects.requireNonNull(id, "id may not be null"); if (getColumn(id) == null) { throw new IllegalArgumentException("No column found for given id"); } setHierarchyColumn(getColumn(id)); } /** * Sets the item collapse allowed provider for this TreeGrid. The provider * should return {@code true} for any item that the user can collapse. * <p> * <strong>Note:</strong> This callback will be accessed often when sending * data to the client. The callback should not do any costly operations. * <p> * This method is a shortcut to method with the same name in * {@link HierarchicalDataCommunicator}. * * @param provider * the item collapse allowed provider, not {@code null} * * @see HierarchicalDataCommunicator#setItemCollapseAllowedProvider(ItemCollapseAllowedProvider) */ public void setItemCollapseAllowedProvider(ItemCollapseAllowedProvider<T> provider) { getDataCommunicator().setItemCollapseAllowedProvider(provider); } /** * Expands the given items. * <p> * If an item is currently expanded, does nothing. If an item does not have * any children, does nothing. * * @param items * the items to expand */ public void expand(T... items) { expand(Arrays.asList(items)); } /** * Expands the given items. * <p> * If an item is currently expanded, does nothing. If an item does not have * any children, does nothing. * * @param items * the items to expand */ public void expand(Collection<T> items) { HierarchicalDataCommunicator<T> communicator = getDataCommunicator(); items.forEach(item -> { if (!communicator.isExpanded(item) && communicator.hasChildren(item)) { communicator.expand(item); fireExpandEvent(item, false); } }); } /** * Expands the given items and their children recursively until the given * depth. * <p> * {@code depth} describes the maximum distance between a given item and its * descendant, meaning that {@code expandRecursively(items, 0)} expands only * the given items while {@code expandRecursively(items, 2)} expands the * given items as well as their children and grandchildren. * <p> * This method will <i>not</i> fire events for expanded nodes. * * @param items * the items to expand recursively * @param depth * the maximum depth of recursion * @since 8.4 */ public void expandRecursively(Collection<T> items, int depth) { expandRecursively(items.stream(), depth); } /** * Expands the given items and their children recursively until the given * depth. * <p> * {@code depth} describes the maximum distance between a given item and its * descendant, meaning that {@code expandRecursively(items, 0)} expands only * the given items while {@code expandRecursively(items, 2)} expands the * given items as well as their children and grandchildren. * <p> * This method will <i>not</i> fire events for expanded nodes. * * @param items * the items to expand recursively * @param depth * the maximum depth of recursion * @since 8.4 */ public void expandRecursively(Stream<T> items, int depth) { if (depth < 0) { return; } HierarchicalDataCommunicator<T> communicator = getDataCommunicator(); items.forEach(item -> { if (communicator.hasChildren(item)) { communicator.expand(item, false); expandRecursively(getDataProvider().fetchChildren(new HierarchicalQuery<>(null, item)), depth - 1); } }); getDataProvider().refreshAll(); } /** * Collapse the given items. * <p> * For items that are already collapsed, does nothing. * * @param items * the collection of items to collapse */ public void collapse(T... items) { collapse(Arrays.asList(items)); } /** * Collapse the given items. * <p> * For items that are already collapsed, does nothing. * * @param items * the collection of items to collapse */ public void collapse(Collection<T> items) { HierarchicalDataCommunicator<T> communicator = getDataCommunicator(); items.forEach(item -> { if (communicator.isExpanded(item)) { communicator.collapse(item); fireCollapseEvent(item, false); } }); } /** * Collapse the given items and their children recursively until the given * depth. * <p> * {@code depth} describes the maximum distance between a given item and its * descendant, meaning that {@code collapseRecursively(items, 0)} collapses * only the given items while {@code collapseRecursively(items, 2)} * collapses the given items as well as their children and grandchildren. * <p> * This method will <i>not</i> fire events for collapsed nodes. * * @param items * the items to collapse recursively * @param depth * the maximum depth of recursion * @since 8.4 */ public void collapseRecursively(Collection<T> items, int depth) { collapseRecursively(items.stream(), depth); } /** * Collapse the given items and their children recursively until the given * depth. * <p> * {@code depth} describes the maximum distance between a given item and its * descendant, meaning that {@code collapseRecursively(items, 0)} collapses * only the given items while {@code collapseRecursively(items, 2)} * collapses the given items as well as their children and grandchildren. * <p> * This method will <i>not</i> fire events for collapsed nodes. * * @param items * the items to collapse recursively * @param depth * the maximum depth of recursion * @since 8.4 */ public void collapseRecursively(Stream<T> items, int depth) { if (depth < 0) { return; } HierarchicalDataCommunicator<T> communicator = getDataCommunicator(); items.forEach(item -> { if (communicator.hasChildren(item)) { collapseRecursively(getDataProvider().fetchChildren(new HierarchicalQuery<>(null, item)), depth - 1); communicator.collapse(item, false); } }); getDataProvider().refreshAll(); } /** * Returns whether a given item is expanded or collapsed. * * @param item * the item to check * @return true if the item is expanded, false if collapsed */ public boolean isExpanded(T item) { return getDataCommunicator().isExpanded(item); } @Override protected TreeGridState getState() { return (TreeGridState) super.getState(); } @Override protected TreeGridState getState(boolean markAsDirty) { return (TreeGridState) super.getState(markAsDirty); } @Override public HierarchicalDataCommunicator<T> getDataCommunicator() { return (HierarchicalDataCommunicator<T>) super.getDataCommunicator(); } @Override public HierarchicalDataProvider<T, ?> getDataProvider() { if (!(super.getDataProvider() instanceof HierarchicalDataProvider)) { return null; } return (HierarchicalDataProvider<T, ?>) super.getDataProvider(); } @Override protected void doReadDesign(Element design, DesignContext context) { super.doReadDesign(design, context); Attributes attrs = design.attributes(); if (attrs.hasKey("hierarchy-column")) { setHierarchyColumn(DesignAttributeHandler.readAttribute("hierarchy-column", attrs, String.class)); } } @Override protected void readData(Element body, List<DeclarativeValueProvider<T>> providers) { getSelectionModel().deselectAll(); List<T> selectedItems = new ArrayList<>(); TreeData<T> data = new TreeData<T>(); for (Element row : body.children()) { T item = deserializeDeclarativeRepresentation(row.attr("item")); T parent = null; if (row.hasAttr("parent")) { parent = deserializeDeclarativeRepresentation(row.attr("parent")); } data.addItem(parent, item); if (row.hasAttr("selected")) { selectedItems.add(item); } Elements cells = row.children(); int i = 0; for (Element cell : cells) { providers.get(i).addValue(item, cell.html()); i++; } } setDataProvider(new TreeDataProvider<>(data)); selectedItems.forEach(getSelectionModel()::select); } @Override protected void doWriteDesign(Element design, DesignContext designContext) { super.doWriteDesign(design, designContext); if (getColumnByInternalId(getState(false).hierarchyColumnId) != null) { String hierarchyColumn = getColumnByInternalId(getState(false).hierarchyColumnId).getId(); DesignAttributeHandler.writeAttribute("hierarchy-column", design.attributes(), hierarchyColumn, null, String.class, designContext); } } @Override protected void writeData(Element body, DesignContext designContext) { getDataProvider().fetch(new HierarchicalQuery<>(null, null)) .forEach(item -> writeRow(body, item, null, designContext)); } private void writeRow(Element container, T item, T parent, DesignContext context) { Element tableRow = container.appendElement("tr"); tableRow.attr("item", serializeDeclarativeRepresentation(item)); if (parent != null) { tableRow.attr("parent", serializeDeclarativeRepresentation(parent)); } if (getSelectionModel().isSelected(item)) { tableRow.attr("selected", true); } for (Column<T, ?> column : getColumns()) { Object value = column.getValueProvider().apply(item); tableRow.appendElement("td").append(Optional.ofNullable(value).map(Object::toString) .map(DesignFormatter::encodeForTextNode).orElse("")); } getDataProvider().fetch(new HierarchicalQuery<>(null, item)) .forEach(childItem -> writeRow(container, childItem, item, context)); } /** * Emit an expand event. * * @param item * the item that was expanded * @param userOriginated * whether the expand was triggered by a user interaction or the * server */ private void fireExpandEvent(T item, boolean userOriginated) { fireEvent(new ExpandEvent<>(this, item, userOriginated)); } /** * Emit a collapse event. * * @param item * the item that was collapsed * @param userOriginated * whether the collapse was triggered by a user interaction or * the server */ private void fireCollapseEvent(T item, boolean userOriginated) { fireEvent(new CollapseEvent<>(this, item, userOriginated)); } /** * Gets the item collapse allowed provider. * * @return the item collapse allowed provider */ public ItemCollapseAllowedProvider<T> getItemCollapseAllowedProvider() { return getDataCommunicator().getItemCollapseAllowedProvider(); } }