/*
* Copyright 2000-2016 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.HierarchyData;
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.InMemoryHierarchicalDataProvider;
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.server.SerializablePredicate;
import com.vaadin.shared.Registration;
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;
import com.vaadin.ui.renderers.AbstractRenderer;
import com.vaadin.ui.renderers.Renderer;
/**
* A grid component for displaying hierarchical tabular data.
*
* @author Vaadin Ltd
* @since 8.1
*
* @param <T>
* the grid bean type
*/
public class TreeGrid<T> extends Grid<T> {
public TreeGrid() {
super(new HierarchicalDataCommunicator<>());
registerRpc(new NodeCollapseRpc() {
@Override
public void setNodeCollapsed(String rowKey, int rowIndex,
boolean collapse, boolean userOriginated) {
if (collapse) {
if (getDataCommunicator().doCollapse(rowKey, rowIndex)
&& userOriginated) {
fireCollapseEvent(getDataCommunicator().getKeyMapper()
.get(rowKey), true);
}
} else {
if (getDataCommunicator().doExpand(rowKey, rowIndex,
userOriginated) && userOriginated) {
fireExpandEvent(getDataCommunicator().getKeyMapper()
.get(rowKey), true);
}
}
}
});
registerRpc(new FocusParentRpc() {
@Override
public void focusParent(int rowIndex, int cellIndex) {
Integer parentIndex = getDataCommunicator()
.getParentIndex(rowIndex);
if (parentIndex != null) {
getRpcProxy(FocusRpc.class).focusCell(parentIndex,
cellIndex);
}
}
});
}
/**
* 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);
}
/**
* Sets the data items of this component provided as a collection.
* <p>
* The provided items are wrapped into a
* {@link InMemoryHierarchicalDataProvider} backed by a flat
* {@link HierarchyData} structure. The data provider instance is used as a
* parameter for the {@link #setDataProvider(DataProvider)} method. It means
* that the items collection can be accessed later on via
* {@link InMemoryHierarchicalDataProvider#getData()}:
*
* <pre>
* <code>
* TreeGrid<String> treeGrid = new TreeGrid<>();
* treeGrid.setItems(Arrays.asList("a","b"));
* ...
*
* HierarchyData<String> data = ((InMemoryHierarchicalDataProvider<String>)treeGrid.getDataProvider()).getData();
* </code>
* </pre>
* <p>
* The returned HierarchyData instance may be used as-is to add, remove or
* modify items in the hierarchy. These modifications to the object are not
* automatically reflected back to the TreeGrid. Items modified should be
* refreshed with {@link HierarchicalDataProvider#refreshItem(Object)} and
* when adding or removing items
* {@link HierarchicalDataProvider#refreshAll()} should be called.
*
* @param items
* the data items to display, not null
*/
@Override
public void setItems(Collection<T> items) {
Objects.requireNonNull(items, "Given collection may not be null");
setDataProvider(new InMemoryHierarchicalDataProvider<>(
new HierarchyData<T>().addItems(null, items)));
}
/**
* Sets the data items of this component provided as a stream.
* <p>
* The provided items are wrapped into a
* {@link InMemoryHierarchicalDataProvider} backed by a flat
* {@link HierarchyData} structure. The data provider instance is used as a
* parameter for the {@link #setDataProvider(DataProvider)} method. It means
* that the items collection can be accessed later on via
* {@link InMemoryHierarchicalDataProvider#getData()}:
*
* <pre>
* <code>
* TreeGrid<String> treeGrid = new TreeGrid<>();
* treeGrid.setItems(Stream.of("a","b"));
* ...
*
* HierarchyData<String> data = ((InMemoryHierarchicalDataProvider<String>)treeGrid.getDataProvider()).getData();
* </code>
* </pre>
* <p>
* The returned HierarchyData instance may be used as-is to add, remove or
* modify items in the hierarchy. These modifications to the object are not
* automatically reflected back to the TreeGrid. Items modified should be
* refreshed with {@link HierarchicalDataProvider#refreshItem(Object)} and
* when adding or removing items
* {@link HierarchicalDataProvider#refreshAll()} should be called.
*
* @param items
* the data items to display, not null
*/
@Override
public void setItems(Stream<T> items) {
Objects.requireNonNull(items, "Given stream may not be null");
setDataProvider(new InMemoryHierarchicalDataProvider<>(
new HierarchyData<T>().addItems(null, items)));
}
/**
* Sets the data items of this listing.
* <p>
* The provided items are wrapped into a
* {@link InMemoryHierarchicalDataProvider} backed by a flat
* {@link HierarchyData} structure. The data provider instance is used as a
* parameter for the {@link #setDataProvider(DataProvider)} method. It means
* that the items collection can be accessed later on via
* {@link InMemoryHierarchicalDataProvider#getData()}:
*
* <pre>
* <code>
* TreeGrid<String> treeGrid = new TreeGrid<>();
* treeGrid.setItems("a","b");
* ...
*
* HierarchyData<String> data = ((InMemoryHierarchicalDataProvider<String>)treeGrid.getDataProvider()).getData();
* </code>
* </pre>
* <p>
* The returned HierarchyData instance may be used as-is to add, remove or
* modify items in the hierarchy. These modifications to the object are not
* automatically reflected back to the TreeGrid. Items modified should be
* refreshed with {@link HierarchicalDataProvider#refreshItem(Object)} and
* when adding or removing items
* {@link HierarchicalDataProvider#refreshAll()} should be called.
*
* @param items
* the data items to display, not null
*/
@Override
public void setItems(@SuppressWarnings("unchecked") T... items) {
Objects.requireNonNull(items, "Given items may not be null");
setDataProvider(new InMemoryHierarchicalDataProvider<>(
new HierarchyData<T>().addItems(null, items)));
}
@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);
}
/**
* 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");
}
getColumn(id).setHidden(false);
getColumn(id).setHidable(false);
getState().hierarchyColumnId = getInternalIdForColumn(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(SerializablePredicate)
*/
public void setItemCollapseAllowedProvider(
SerializablePredicate<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) {
List<String> expandedKeys = new ArrayList<>();
List<T> expandedItems = new ArrayList<>();
items.forEach(item -> getDataCommunicator().setPendingExpand(item)
.ifPresent(key -> {
expandedKeys.add(key);
expandedItems.add(item);
}));
getRpcProxy(TreeGridClientRpc.class).setExpanded(expandedKeys);
expandedItems.forEach(item -> fireExpandEvent(item, false));
}
/**
* 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) {
List<String> collapsedKeys = new ArrayList<>();
List<T> collapsedItems = new ArrayList<>();
items.forEach(item -> getDataCommunicator().collapseItem(item)
.ifPresent(key -> {
collapsedKeys.add(key);
collapsedItems.add(item);
}));
getRpcProxy(TreeGridClientRpc.class).setCollapsed(collapsedKeys);
collapsedItems.forEach(item -> fireCollapseEvent(item, false));
}
@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<>();
HierarchyData<T> data = new HierarchyData<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 InMemoryHierarchicalDataProvider<>(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", "");
}
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));
}
@Override
protected <V> Column<T, V> createColumn(ValueProvider<T, V> valueProvider,
AbstractRenderer<? super T, ? super V> renderer) {
return new Column<T, V>(valueProvider, renderer) {
@Override
public com.vaadin.ui.Grid.Column<T, V> setRenderer(
Renderer<? super V> renderer) {
// Disallow changing renderer for the hierarchy column
if (getInternalIdForColumn(this).equals(
TreeGrid.this.getState(false).hierarchyColumnId)) {
throw new IllegalStateException(
"Changing the renderer of the hierarchy column is not allowed.");
}
return super.setRenderer(renderer);
}
};
}
/**
* 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));
}
}