/*
* 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.io.Serializable;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.function.BinaryOperator;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.jsoup.nodes.Attributes;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import com.vaadin.data.BeanPropertySet;
import com.vaadin.data.Binder;
import com.vaadin.data.Binder.Binding;
import com.vaadin.data.HasDataProvider;
import com.vaadin.data.HasValue;
import com.vaadin.data.PropertyDefinition;
import com.vaadin.data.PropertySet;
import com.vaadin.data.ValueProvider;
import com.vaadin.data.provider.CallbackDataProvider;
import com.vaadin.data.provider.DataCommunicator;
import com.vaadin.data.provider.DataProvider;
import com.vaadin.data.provider.GridSortOrder;
import com.vaadin.data.provider.GridSortOrderBuilder;
import com.vaadin.data.provider.Query;
import com.vaadin.data.provider.QuerySortOrder;
import com.vaadin.event.ConnectorEvent;
import com.vaadin.event.ContextClickEvent;
import com.vaadin.event.SortEvent;
import com.vaadin.event.SortEvent.SortListener;
import com.vaadin.event.SortEvent.SortNotifier;
import com.vaadin.event.selection.MultiSelectionListener;
import com.vaadin.event.selection.SelectionListener;
import com.vaadin.event.selection.SingleSelectionListener;
import com.vaadin.server.EncodeResult;
import com.vaadin.server.Extension;
import com.vaadin.server.JsonCodec;
import com.vaadin.server.SerializableComparator;
import com.vaadin.server.SerializableSupplier;
import com.vaadin.server.Setter;
import com.vaadin.server.VaadinServiceClassLoaderUtil;
import com.vaadin.shared.Connector;
import com.vaadin.shared.MouseEventDetails;
import com.vaadin.shared.Registration;
import com.vaadin.shared.data.DataCommunicatorConstants;
import com.vaadin.shared.data.sort.SortDirection;
import com.vaadin.shared.ui.grid.AbstractGridExtensionState;
import com.vaadin.shared.ui.grid.ColumnResizeMode;
import com.vaadin.shared.ui.grid.ColumnState;
import com.vaadin.shared.ui.grid.DetailsManagerState;
import com.vaadin.shared.ui.grid.GridClientRpc;
import com.vaadin.shared.ui.grid.GridConstants;
import com.vaadin.shared.ui.grid.GridConstants.Section;
import com.vaadin.shared.ui.grid.GridServerRpc;
import com.vaadin.shared.ui.grid.GridState;
import com.vaadin.shared.ui.grid.GridStaticCellType;
import com.vaadin.shared.ui.grid.HeightMode;
import com.vaadin.shared.ui.grid.ScrollDestination;
import com.vaadin.shared.ui.grid.SectionState;
import com.vaadin.ui.components.grid.ColumnReorderListener;
import com.vaadin.ui.components.grid.ColumnResizeListener;
import com.vaadin.ui.components.grid.ColumnVisibilityChangeListener;
import com.vaadin.ui.components.grid.DescriptionGenerator;
import com.vaadin.ui.components.grid.DetailsGenerator;
import com.vaadin.ui.components.grid.Editor;
import com.vaadin.ui.components.grid.EditorImpl;
import com.vaadin.ui.components.grid.Footer;
import com.vaadin.ui.components.grid.FooterRow;
import com.vaadin.ui.components.grid.GridSelectionModel;
import com.vaadin.ui.components.grid.Header;
import com.vaadin.ui.components.grid.Header.Row;
import com.vaadin.ui.components.grid.HeaderCell;
import com.vaadin.ui.components.grid.HeaderRow;
import com.vaadin.ui.components.grid.ItemClickListener;
import com.vaadin.ui.components.grid.MultiSelectionModel;
import com.vaadin.ui.components.grid.MultiSelectionModelImpl;
import com.vaadin.ui.components.grid.NoSelectionModel;
import com.vaadin.ui.components.grid.SingleSelectionModel;
import com.vaadin.ui.components.grid.SingleSelectionModelImpl;
import com.vaadin.ui.components.grid.SortOrderProvider;
import com.vaadin.ui.declarative.DesignAttributeHandler;
import com.vaadin.ui.declarative.DesignContext;
import com.vaadin.ui.declarative.DesignException;
import com.vaadin.ui.declarative.DesignFormatter;
import com.vaadin.ui.renderers.AbstractRenderer;
import com.vaadin.ui.renderers.ComponentRenderer;
import com.vaadin.ui.renderers.HtmlRenderer;
import com.vaadin.ui.renderers.Renderer;
import com.vaadin.ui.renderers.TextRenderer;
import com.vaadin.util.ReflectTools;
import elemental.json.Json;
import elemental.json.JsonObject;
import elemental.json.JsonValue;
/**
* A grid component for displaying tabular data.
*
* @author Vaadin Ltd
* @since 8.0
*
* @param <T>
* the grid bean type
*/
public class Grid<T> extends AbstractListing<T> implements HasComponents,
HasDataProvider<T>, SortNotifier<GridSortOrder<T>> {
private static final String DECLARATIVE_DATA_ITEM_TYPE = "data-item-type";
/**
* A callback method for fetching items. The callback is provided with a
* list of sort orders, offset index and limit.
*
* @param <T>
* the grid bean type
*/
@FunctionalInterface
public interface FetchItemsCallback<T> extends Serializable {
/**
* Returns a stream of items ordered by given sort orders, limiting the
* results with given offset and limit.
* <p>
* This method is called after the size of the data set is asked from a
* related size callback. The offset and limit are promised to be within
* the size of the data set.
*
* @param sortOrder
* a list of sort orders
* @param offset
* the first index to fetch
* @param limit
* the fetched item count
* @return stream of items
*/
public Stream<T> fetchItems(List<QuerySortOrder> sortOrder, int offset,
int limit);
}
@Deprecated
private static final Method COLUMN_REORDER_METHOD = ReflectTools.findMethod(
ColumnReorderListener.class, "columnReorder",
ColumnReorderEvent.class);
private static final Method SORT_ORDER_CHANGE_METHOD = ReflectTools
.findMethod(SortListener.class, "sort", SortEvent.class);
@Deprecated
private static final Method COLUMN_RESIZE_METHOD = ReflectTools.findMethod(
ColumnResizeListener.class, "columnResize",
ColumnResizeEvent.class);
@Deprecated
private static final Method ITEM_CLICK_METHOD = ReflectTools
.findMethod(ItemClickListener.class, "itemClick", ItemClick.class);
@Deprecated
private static final Method COLUMN_VISIBILITY_METHOD = ReflectTools
.findMethod(ColumnVisibilityChangeListener.class,
"columnVisibilityChanged",
ColumnVisibilityChangeEvent.class);
/**
* Selection mode representing the built-in selection models in grid.
* <p>
* These enums can be used in {@link Grid#setSelectionMode(SelectionMode)}
* to easily switch between the build-in selection models.
*
* @see Grid#setSelectionMode(SelectionMode)
* @see Grid#setSelectionModel(GridSelectionModel)
*/
public enum SelectionMode {
/**
* Single selection mode that maps to build-in
* {@link SingleSelectionModel}.
*
* @see SingleSelectionModelImpl
*/
SINGLE {
@Override
protected <T> GridSelectionModel<T> createModel() {
return new SingleSelectionModelImpl<>();
}
},
/**
* Multiselection mode that maps to build-in {@link MultiSelectionModel}
* .
*
* @see MultiSelectionModelImpl
*/
MULTI {
@Override
protected <T> GridSelectionModel<T> createModel() {
return new MultiSelectionModelImpl<>();
}
},
/**
* Selection model that doesn't allow selection.
*
* @see NoSelectionModel
*/
NONE {
@Override
protected <T> GridSelectionModel<T> createModel() {
return new NoSelectionModel<>();
}
};
/**
* Creates the selection model to use with this enum.
*
* @param <T>
* the type of items in the grid
* @return the selection model
*/
protected abstract <T> GridSelectionModel<T> createModel();
}
/**
* An event that is fired when the columns are reordered.
*/
public static class ColumnReorderEvent extends Component.Event {
private final boolean userOriginated;
/**
*
* @param source
* the grid where the event originated from
* @param userOriginated
* <code>true</code> if event is a result of user
* interaction, <code>false</code> if from API call
*/
public ColumnReorderEvent(Grid source, boolean userOriginated) {
super(source);
this.userOriginated = userOriginated;
}
/**
* Returns <code>true</code> if the column reorder was done by the user,
* <code>false</code> if not and it was triggered by server side code.
*
* @return <code>true</code> if event is a result of user interaction
*/
public boolean isUserOriginated() {
return userOriginated;
}
}
/**
* An event that is fired when a column is resized, either programmatically
* or by the user.
*/
public static class ColumnResizeEvent extends Component.Event {
private final Column<?, ?> column;
private final boolean userOriginated;
/**
*
* @param source
* the grid where the event originated from
* @param userOriginated
* <code>true</code> if event is a result of user
* interaction, <code>false</code> if from API call
*/
public ColumnResizeEvent(Grid<?> source, Column<?, ?> column,
boolean userOriginated) {
super(source);
this.column = column;
this.userOriginated = userOriginated;
}
/**
* Returns the column that was resized.
*
* @return the resized column.
*/
public Column<?, ?> getColumn() {
return column;
}
/**
* Returns <code>true</code> if the column resize was done by the user,
* <code>false</code> if not and it was triggered by server side code.
*
* @return <code>true</code> if event is a result of user interaction
*/
public boolean isUserOriginated() {
return userOriginated;
}
}
/**
* An event fired when an item in the Grid has been clicked.
*
* @param <T>
* the grid bean type
*/
public static class ItemClick<T> extends ConnectorEvent {
private final T item;
private final Column<T, ?> column;
private final MouseEventDetails mouseEventDetails;
/**
* Creates a new {@code ItemClick} event containing the given item and
* Column originating from the given Grid.
*
*/
public ItemClick(Grid<T> source, Column<T, ?> column, T item,
MouseEventDetails mouseEventDetails) {
super(source);
this.column = column;
this.item = item;
this.mouseEventDetails = mouseEventDetails;
}
/**
* Returns the clicked item.
*
* @return the clicked item
*/
public T getItem() {
return item;
}
/**
* Returns the clicked column.
*
* @return the clicked column
*/
public Column<T, ?> getColumn() {
return column;
}
/**
* Returns the source Grid.
*
* @return the grid
*/
@Override
public Grid<T> getSource() {
return (Grid<T>) super.getSource();
}
/**
* Returns the mouse event details.
*
* @return the mouse event details
*/
public MouseEventDetails getMouseEventDetails() {
return mouseEventDetails;
}
}
/**
* ContextClickEvent for the Grid Component.
*
* @param <T>
* the grid bean type
*/
public static class GridContextClickEvent<T> extends ContextClickEvent {
private final T item;
private final int rowIndex;
private final Column<T, ?> column;
private final Section section;
/**
* Creates a new context click event.
*
* @param source
* the grid where the context click occurred
* @param mouseEventDetails
* details about mouse position
* @param section
* the section of the grid which was clicked
* @param rowIndex
* the index of the row which was clicked
* @param item
* the item which was clicked
* @param column
* the column which was clicked
*/
public GridContextClickEvent(Grid<T> source,
MouseEventDetails mouseEventDetails, Section section,
int rowIndex, T item, Column<T, ?> column) {
super(source, mouseEventDetails);
this.item = item;
this.section = section;
this.column = column;
this.rowIndex = rowIndex;
}
/**
* Returns the item of context clicked row.
*
* @return item of clicked row; <code>null</code> if header or footer
*/
public T getItem() {
return item;
}
/**
* Returns the clicked column.
*
* @return the clicked column
*/
public Column<T, ?> getColumn() {
return column;
}
/**
* Return the clicked section of Grid.
*
* @return section of grid
*/
public Section getSection() {
return section;
}
/**
* Returns the clicked row index.
* <p>
* Header and Footer rows for index can be fetched with
* {@link Grid#getHeaderRow(int)} and {@link Grid#getFooterRow(int)}.
*
* @return row index in section
*/
public int getRowIndex() {
return rowIndex;
}
@Override
public Grid<T> getComponent() {
return (Grid<T>) super.getComponent();
}
}
/**
* An event that is fired when a column's visibility changes.
*
* @since 7.5.0
*/
public static class ColumnVisibilityChangeEvent extends Component.Event {
private final Column<?, ?> column;
private final boolean userOriginated;
private final boolean hidden;
/**
* Constructor for a column visibility change event.
*
* @param source
* the grid from which this event originates
* @param column
* the column that changed its visibility
* @param hidden
* <code>true</code> if the column was hidden,
* <code>false</code> if it became visible
* @param isUserOriginated
* <code>true</code> iff the event was triggered by an UI
* interaction
*/
public ColumnVisibilityChangeEvent(Grid<?> source, Column<?, ?> column,
boolean hidden, boolean isUserOriginated) {
super(source);
this.column = column;
this.hidden = hidden;
userOriginated = isUserOriginated;
}
/**
* Gets the column that became hidden or visible.
*
* @return the column that became hidden or visible.
* @see Column#isHidden()
*/
public Column<?, ?> getColumn() {
return column;
}
/**
* Was the column set hidden or visible.
*
* @return <code>true</code> if the column was hidden <code>false</code>
* if it was set visible
*/
public boolean isHidden() {
return hidden;
}
/**
* Returns <code>true</code> if the column reorder was done by the user,
* <code>false</code> if not and it was triggered by server side code.
*
* @return <code>true</code> if event is a result of user interaction
*/
public boolean isUserOriginated() {
return userOriginated;
}
}
/**
* A helper base class for creating extensions for the Grid component.
*
* @param <T>
*/
public abstract static class AbstractGridExtension<T>
extends AbstractListingExtension<T> {
@Override
public void extend(AbstractListing<T> grid) {
if (!(grid instanceof Grid)) {
throw new IllegalArgumentException(
getClass().getSimpleName() + " can only extend Grid");
}
super.extend(grid);
}
/**
* Adds given component to the connector hierarchy of Grid.
*
* @param c
* the component to add
*/
protected void addComponentToGrid(Component c) {
getParent().addExtensionComponent(c);
}
/**
* Removes given component from the connector hierarchy of Grid.
*
* @param c
* the component to remove
*/
protected void removeComponentFromGrid(Component c) {
getParent().removeExtensionComponent(c);
}
@Override
public Grid<T> getParent() {
return (Grid<T>) super.getParent();
}
@Override
protected AbstractGridExtensionState getState() {
return (AbstractGridExtensionState) super.getState();
}
@Override
protected AbstractGridExtensionState getState(boolean markAsDirty) {
return (AbstractGridExtensionState) super.getState(markAsDirty);
}
protected String getInternalIdForColumn(Column<T, ?> column) {
return getParent().getInternalIdForColumn(column);
}
}
private final class GridServerRpcImpl implements GridServerRpc {
@Override
public void sort(String[] columnInternalIds, SortDirection[] directions,
boolean isUserOriginated) {
assert columnInternalIds.length == directions.length : "Column and sort direction counts don't match.";
List<GridSortOrder<T>> list = new ArrayList<>(directions.length);
for (int i = 0; i < columnInternalIds.length; ++i) {
Column<T, ?> column = columnKeys.get(columnInternalIds[i]);
list.add(new GridSortOrder<>(column, directions[i]));
}
setSortOrder(list, isUserOriginated);
}
@Override
public void itemClick(String rowKey, String columnInternalId,
MouseEventDetails details) {
Column<T, ?> column = getColumnByInternalId(columnInternalId);
T item = getDataCommunicator().getKeyMapper().get(rowKey);
fireEvent(new ItemClick<>(Grid.this, column, item, details));
}
@Override
public void contextClick(int rowIndex, String rowKey,
String columnInternalId, Section section,
MouseEventDetails details) {
T item = null;
if (rowKey != null) {
item = getDataCommunicator().getKeyMapper().get(rowKey);
}
fireEvent(new GridContextClickEvent<>(Grid.this, details, section,
rowIndex, item, getColumnByInternalId(columnInternalId)));
}
@Override
public void columnsReordered(List<String> newColumnOrder,
List<String> oldColumnOrder) {
final String diffStateKey = "columnOrder";
ConnectorTracker connectorTracker = getUI().getConnectorTracker();
JsonObject diffState = connectorTracker.getDiffState(Grid.this);
// discard the change if the columns have been reordered from
// the server side, as the server side is always right
if (getState(false).columnOrder.equals(oldColumnOrder)) {
// Don't mark as dirty since client has the state already
getState(false).columnOrder = newColumnOrder;
// write changes to diffState so that possible reverting the
// column order is sent to client
assert diffState
.hasKey(diffStateKey) : "Field name has changed";
Type type = null;
try {
type = getState(false).getClass().getField(diffStateKey)
.getGenericType();
} catch (NoSuchFieldException | SecurityException e) {
e.printStackTrace();
}
EncodeResult encodeResult = JsonCodec.encode(
getState(false).columnOrder, diffState, type,
connectorTracker);
diffState.put(diffStateKey, encodeResult.getEncodedValue());
fireColumnReorderEvent(true);
} else {
// make sure the client is reverted to the order that the
// server thinks it is
diffState.remove(diffStateKey);
markAsDirty();
}
}
@Override
public void columnVisibilityChanged(String internalId, boolean hidden) {
Column<T, ?> column = getColumnByInternalId(internalId);
ColumnState columnState = column.getState(false);
if (columnState.hidden != hidden) {
columnState.hidden = hidden;
fireColumnVisibilityChangeEvent(column, hidden, true);
}
}
@Override
public void columnResized(String internalId, double pixels) {
final Column<T, ?> column = getColumnByInternalId(internalId);
if (column != null && column.isResizable()) {
column.getState().width = pixels;
fireColumnResizeEvent(column, true);
}
}
}
/**
* Class for managing visible details rows.
*
* @param <T>
* the grid bean type
*/
public static class DetailsManager<T> extends AbstractGridExtension<T> {
private final Set<T> visibleDetails = new HashSet<>();
private final Map<T, Component> components = new HashMap<>();
private DetailsGenerator<T> generator;
/**
* Sets the details component generator.
*
* @param generator
* the generator for details components
*/
public void setDetailsGenerator(DetailsGenerator<T> generator) {
if (this.generator != generator) {
removeAllComponents();
}
this.generator = generator;
visibleDetails.forEach(this::refresh);
}
@Override
public void remove() {
removeAllComponents();
super.remove();
}
private void removeAllComponents() {
// Clean up old components
components.values().forEach(this::removeComponentFromGrid);
components.clear();
}
@Override
public void generateData(T item, JsonObject jsonObject) {
if (generator == null || !visibleDetails.contains(item)) {
return;
}
if (!components.containsKey(item)) {
Component detailsComponent = generator.apply(item);
Objects.requireNonNull(detailsComponent,
"Details generator can't create null components");
if (detailsComponent.getParent() != null) {
throw new IllegalStateException(
"Details component was already attached");
}
addComponentToGrid(detailsComponent);
components.put(item, detailsComponent);
}
jsonObject.put(GridState.JSONKEY_DETAILS_VISIBLE,
components.get(item).getConnectorId());
}
@Override
public void destroyData(T item) {
// No clean up needed. Components are removed when hiding details
// and/or changing details generator
}
/**
* Sets the visibility of details component for given item.
*
* @param item
* the item to show details for
* @param visible
* {@code true} if details component should be visible;
* {@code false} if it should be hidden
*/
public void setDetailsVisible(T item, boolean visible) {
boolean refresh = false;
if (!visible) {
refresh = visibleDetails.remove(item);
if (components.containsKey(item)) {
removeComponentFromGrid(components.remove(item));
}
} else {
refresh = visibleDetails.add(item);
}
if (refresh) {
refresh(item);
}
}
/**
* Returns the visibility of details component for given item.
*
* @param item
* the item to show details for
*
* @return {@code true} if details component should be visible;
* {@code false} if it should be hidden
*/
public boolean isDetailsVisible(T item) {
return visibleDetails.contains(item);
}
@Override
public Grid<T> getParent() {
return super.getParent();
}
@Override
protected DetailsManagerState getState() {
return (DetailsManagerState) super.getState();
}
@Override
protected DetailsManagerState getState(boolean markAsDirty) {
return (DetailsManagerState) super.getState(markAsDirty);
}
}
/**
* This extension manages the configuration and data communication for a
* Column inside of a Grid component.
*
* @param <T>
* the grid bean type
* @param <V>
* the column value type
*/
public static class Column<T, V> extends AbstractGridExtension<T> {
private final ValueProvider<T, V> valueProvider;
private SortOrderProvider sortOrderProvider = direction -> {
String id = getId();
if (id == null) {
return Stream.empty();
} else {
return Stream.of(new QuerySortOrder(id, direction));
}
};
private SerializableComparator<T> comparator;
private StyleGenerator<T> styleGenerator = item -> null;
private DescriptionGenerator<T> descriptionGenerator;
private Binding<T, ?> editorBinding;
private Map<T, Component> activeComponents = new HashMap<>();
private String userId;
/**
* Constructs a new Column configuration with given renderer and value
* provider.
*
* @param valueProvider
* the function to get values from items, not
* <code>null</code>
* @param renderer
* the type of value, not <code>null</code>
*/
protected Column(ValueProvider<T, V> valueProvider,
Renderer<? super V> renderer) {
Objects.requireNonNull(valueProvider,
"Value provider can't be null");
Objects.requireNonNull(renderer, "Renderer can't be null");
ColumnState state = getState();
this.valueProvider = valueProvider;
state.renderer = renderer;
state.caption = "";
// Add the renderer as a child extension of this extension, thus
// ensuring the renderer will be unregistered when this column is
// removed
addExtension(renderer);
Class<? super V> valueType = renderer.getPresentationType();
if (Comparable.class.isAssignableFrom(valueType)) {
comparator = (a, b) -> compareComparables(
valueProvider.apply(a), valueProvider.apply(b));
} else if (Number.class.isAssignableFrom(valueType)) {
/*
* Value type will be Number whenever using NumberRenderer.
* Provide explicit comparison support in this case even though
* Number itself isn't Comparable.
*/
comparator = (a, b) -> compareNumbers(
(Number) valueProvider.apply(a),
(Number) valueProvider.apply(b));
} else {
comparator = (a, b) -> compareMaybeComparables(
valueProvider.apply(a), valueProvider.apply(b));
}
}
private static int compareMaybeComparables(Object a, Object b) {
if (hasCommonComparableBaseType(a, b)) {
return compareComparables(a, b);
} else {
return compareComparables(Objects.toString(a, ""),
Objects.toString(b, ""));
}
}
private static boolean hasCommonComparableBaseType(Object a, Object b) {
if (a instanceof Comparable<?> && b instanceof Comparable<?>) {
Class<?> aClass = a.getClass();
Class<?> bClass = b.getClass();
if (aClass == bClass) {
return true;
}
Class<?> baseType = ReflectTools.findCommonBaseType(aClass,
bClass);
if (Comparable.class.isAssignableFrom(baseType)) {
return true;
}
}
if ((a == null && b instanceof Comparable<?>)
|| (b == null && a instanceof Comparable<?>)) {
return true;
}
return false;
}
@SuppressWarnings("unchecked")
private static int compareComparables(Object a, Object b) {
return ((Comparator) Comparator
.nullsLast(Comparator.naturalOrder())).compare(a, b);
}
@SuppressWarnings("unchecked")
private static int compareNumbers(Number a, Number b) {
Number valueA = a != null ? a : Double.POSITIVE_INFINITY;
Number valueB = b != null ? b : Double.POSITIVE_INFINITY;
// Most Number implementations are Comparable
if (valueA instanceof Comparable
&& valueA.getClass().isInstance(valueB)) {
return ((Comparable<Number>) valueA).compareTo(valueB);
} else if (valueA.equals(valueB)) {
return 0;
} else {
// Fall back to comparing based on potentially truncated values
int compare = Long.compare(valueA.longValue(),
valueB.longValue());
if (compare == 0) {
// This might still produce 0 even though the values are not
// equals, but there's nothing more we can do about that
compare = Double.compare(valueA.doubleValue(),
valueB.doubleValue());
}
return compare;
}
}
@Override
public void generateData(T item, JsonObject jsonObject) {
ColumnState state = getState(false);
String communicationId = getConnectorId();
assert communicationId != null : "No communication ID set for column "
+ state.caption;
@SuppressWarnings("unchecked")
Renderer<V> renderer = (Renderer<V>) state.renderer;
JsonObject obj = getDataObject(jsonObject,
DataCommunicatorConstants.DATA);
V providerValue = valueProvider.apply(item);
// Make Grid track components.
if (renderer instanceof ComponentRenderer
&& providerValue instanceof Component) {
addComponent(item, (Component) providerValue);
}
JsonValue rendererValue = renderer.encode(providerValue);
obj.put(communicationId, rendererValue);
String style = styleGenerator.apply(item);
if (style != null && !style.isEmpty()) {
JsonObject styleObj = getDataObject(jsonObject,
GridState.JSONKEY_CELLSTYLES);
styleObj.put(communicationId, style);
}
if (descriptionGenerator != null) {
String description = descriptionGenerator.apply(item);
if (description != null && !description.isEmpty()) {
JsonObject descriptionObj = getDataObject(jsonObject,
GridState.JSONKEY_CELLDESCRIPTION);
descriptionObj.put(communicationId, description);
}
}
}
private void addComponent(T item, Component component) {
if (activeComponents.containsKey(item)) {
if (activeComponents.get(item).equals(component)) {
// Reusing old component
return;
}
removeComponent(item);
}
activeComponents.put(item, component);
addComponentToGrid(component);
}
@Override
public void destroyData(T item) {
removeComponent(item);
}
private void removeComponent(T item) {
Component component = activeComponents.remove(item);
if (component != null) {
removeComponentFromGrid(component);
}
}
@Override
public void destroyAllData() {
// Make a defensive copy of keys, as the map gets cleared when
// removing components.
new HashSet<>(activeComponents.keySet())
.forEach(this::removeComponent);
}
/**
* Gets a data object with the given key from the given JsonObject. If
* there is no object with the key, this method creates a new
* JsonObject.
*
* @param jsonObject
* the json object
* @param key
* the key where the desired data object is stored
* @return data object for the given key
*/
private JsonObject getDataObject(JsonObject jsonObject, String key) {
if (!jsonObject.hasKey(key)) {
jsonObject.put(key, Json.createObject());
}
return jsonObject.getObject(key);
}
@Override
protected ColumnState getState() {
return getState(true);
}
@Override
protected ColumnState getState(boolean markAsDirty) {
return (ColumnState) super.getState(markAsDirty);
}
/**
* This method extends the given Grid with this Column.
*
* @param grid
* the grid to extend
*/
private void extend(Grid<T> grid) {
super.extend(grid);
}
/**
* Returns the identifier used with this Column in communication.
*
* @return the identifier string
*/
private String getInternalId() {
return getState(false).internalId;
}
/**
* Sets the identifier to use with this Column in communication.
*
* @param id
* the identifier string
*/
private void setInternalId(String id) {
Objects.requireNonNull(id, "Communication ID can't be null");
getState().internalId = id;
}
/**
* Returns the user-defined identifier for this column.
*
* @return the identifier string
*/
public String getId() {
return userId;
}
/**
* Sets the user-defined identifier to map this column. The identifier
* can be used for example in {@link Grid#getColumn(String)}.
* <p>
* The id is also used as the {@link #setSortProperty(String...) backend
* sort property} for this column if no sort property or sort order
* provider has been set for this column.
*
* @see #setSortProperty(String...)
* @see #setSortOrderProvider(SortOrderProvider)
*
* @param id
* the identifier string
* @return this column
*/
public Column<T, V> setId(String id) {
Objects.requireNonNull(id, "Column identifier cannot be null");
if (this.userId != null) {
throw new IllegalStateException(
"Column identifier cannot be changed");
}
this.userId = id;
getGrid().setColumnId(id, this);
return this;
}
/**
* Gets the function used to produce the value for data in this column
* based on the row item.
*
* @return the value provider function
*
* @since 8.0.3
*/
public ValueProvider<T, V> getValueProvider() {
return valueProvider;
}
/**
* Sets whether the user can sort this column or not.
*
* @param sortable
* {@code true} if the column can be sorted by the user;
* {@code false} if not
* @return this column
*/
public Column<T, V> setSortable(boolean sortable) {
getState().sortable = sortable;
return this;
}
/**
* Gets whether the user can sort this column or not.
*
* @return {@code true} if the column can be sorted by the user;
* {@code false} if not
*/
public boolean isSortable() {
return getState(false).sortable;
}
/**
* Sets the header caption for this column.
*
* @param caption
* the header caption, not null
*
* @return this column
*/
public Column<T, V> setCaption(String caption) {
Objects.requireNonNull(caption, "Header caption can't be null");
if (caption.equals(getState(false).caption)) {
return this;
}
getState().caption = caption;
HeaderRow row = getGrid().getDefaultHeaderRow();
if (row != null) {
row.getCell(this).setText(caption);
}
return this;
}
/**
* Gets the header caption for this column.
*
* @return header caption
*/
public String getCaption() {
return getState(false).caption;
}
/**
* Sets a comparator to use with in-memory sorting with this column.
* Sorting with a back-end is done using
* {@link Column#setSortProperty(String...)}.
*
* @param comparator
* the comparator to use when sorting data in this column
* @return this column
*/
public Column<T, V> setComparator(
SerializableComparator<T> comparator) {
Objects.requireNonNull(comparator, "Comparator can't be null");
this.comparator = comparator;
return this;
}
/**
* Gets the comparator to use with in-memory sorting for this column
* when sorting in the given direction.
*
* @param sortDirection
* the direction this column is sorted by
* @return comparator for this column
*/
public SerializableComparator<T> getComparator(
SortDirection sortDirection) {
Objects.requireNonNull(comparator,
"No comparator defined for sorted column.");
boolean reverse = sortDirection != SortDirection.ASCENDING;
return reverse ? (t1, t2) -> comparator.reversed().compare(t1, t2)
: comparator;
}
/**
* Sets strings describing back end properties to be used when sorting
* this column.
* <p>
* By default, the {@link #setId(String) column id} will be used as the
* sort property.
*
* @param properties
* the array of strings describing backend properties
* @return this column
*/
public Column<T, V> setSortProperty(String... properties) {
Objects.requireNonNull(properties, "Sort properties can't be null");
sortOrderProvider = dir -> Arrays.stream(properties)
.map(s -> new QuerySortOrder(s, dir));
return this;
}
/**
* Sets the sort orders when sorting this column. The sort order
* provider is a function which provides {@link QuerySortOrder} objects
* to describe how to sort by this column.
* <p>
* By default, the {@link #setId(String) column id} will be used as the
* sort property.
*
* @param provider
* the function to use when generating sort orders with the
* given direction
* @return this column
*/
public Column<T, V> setSortOrderProvider(SortOrderProvider provider) {
Objects.requireNonNull(provider,
"Sort order provider can't be null");
sortOrderProvider = provider;
return this;
}
/**
* Gets the sort orders to use with back-end sorting for this column
* when sorting in the given direction.
*
* @see #setSortProperty(String...)
* @see #setId(String)
* @see #setSortOrderProvider(SortOrderProvider)
*
* @param direction
* the sorting direction
* @return stream of sort orders
*/
public Stream<QuerySortOrder> getSortOrder(SortDirection direction) {
return sortOrderProvider.apply(direction);
}
/**
* Sets the style generator that is used for generating class names for
* cells in this column. Returning null from the generator results in no
* custom style name being set.
*
* @param cellStyleGenerator
* the cell style generator to set, not null
* @return this column
* @throws NullPointerException
* if {@code cellStyleGenerator} is {@code null}
*/
public Column<T, V> setStyleGenerator(
StyleGenerator<T> cellStyleGenerator) {
Objects.requireNonNull(cellStyleGenerator,
"Cell style generator must not be null");
this.styleGenerator = cellStyleGenerator;
getGrid().getDataCommunicator().reset();
return this;
}
/**
* Gets the style generator that is used for generating styles for
* cells.
*
* @return the cell style generator
*/
public StyleGenerator<T> getStyleGenerator() {
return styleGenerator;
}
/**
* Sets the description generator that is used for generating
* descriptions for cells in this column.
*
* @param cellDescriptionGenerator
* the cell description generator to set, or
* <code>null</code> to remove a previously set generator
* @return this column
*/
public Column<T, V> setDescriptionGenerator(
DescriptionGenerator<T> cellDescriptionGenerator) {
this.descriptionGenerator = cellDescriptionGenerator;
getGrid().getDataCommunicator().reset();
return this;
}
/**
* Gets the description generator that is used for generating
* descriptions for cells.
*
* @return the cell description generator, or <code>null</code> if no
* generator is set
*/
public DescriptionGenerator<T> getDescriptionGenerator() {
return descriptionGenerator;
}
/**
* Sets the ratio with which the column expands.
* <p>
* By default, all columns expand equally (treated as if all of them had
* an expand ratio of 1). Once at least one column gets a defined expand
* ratio, the implicit expand ratio is removed, and only the defined
* expand ratios are taken into account.
* <p>
* If a column has a defined width ({@link #setWidth(double)}), it
* overrides this method's effects.
* <p>
* <em>Example:</em> A grid with three columns, with expand ratios 0, 1
* and 2, respectively. The column with a <strong>ratio of 0 is exactly
* as wide as its contents requires</strong>. The column with a ratio of
* 1 is as wide as it needs, <strong>plus a third of any excess
* space</strong>, because we have 3 parts total, and this column
* reserves only one of those. The column with a ratio of 2, is as wide
* as it needs to be, <strong>plus two thirds</strong> of the excess
* width.
*
* @param expandRatio
* the expand ratio of this column. {@code 0} to not have it
* expand at all. A negative number to clear the expand
* value.
* @throws IllegalStateException
* if the column is no longer attached to any grid
* @see #setWidth(double)
*/
public Column<T, V> setExpandRatio(int expandRatio)
throws IllegalStateException {
checkColumnIsAttached();
if (expandRatio != getExpandRatio()) {
getState().expandRatio = expandRatio;
getGrid().markAsDirty();
}
return this;
}
/**
* Returns the column's expand ratio.
*
* @return the column's expand ratio
* @see #setExpandRatio(int)
*/
public int getExpandRatio() {
return getState(false).expandRatio;
}
/**
* Clears the expand ratio for this column.
* <p>
* Equal to calling {@link #setExpandRatio(int) setExpandRatio(-1)}
*
* @throws IllegalStateException
* if the column is no longer attached to any grid
*/
public Column<T, V> clearExpandRatio() throws IllegalStateException {
return setExpandRatio(-1);
}
/**
* Returns the width (in pixels). By default a column is 100px wide.
*
* @return the width in pixels of the column
* @throws IllegalStateException
* if the column is no longer attached to any grid
*/
public double getWidth() throws IllegalStateException {
checkColumnIsAttached();
return getState(false).width;
}
/**
* Sets the width (in pixels).
* <p>
* This overrides any configuration set by any of
* {@link #setExpandRatio(int)}, {@link #setMinimumWidth(double)} or
* {@link #setMaximumWidth(double)}.
*
* @param pixelWidth
* the new pixel width of the column
* @return the column itself
*
* @throws IllegalStateException
* if the column is no longer attached to any grid
* @throws IllegalArgumentException
* thrown if pixel width is less than zero
*/
public Column<T, V> setWidth(double pixelWidth)
throws IllegalStateException, IllegalArgumentException {
checkColumnIsAttached();
if (pixelWidth < 0) {
throw new IllegalArgumentException(
"Pixel width should be greated than 0 (in " + toString()
+ ")");
}
if (pixelWidth != getWidth()) {
getState().width = pixelWidth;
getGrid().markAsDirty();
getGrid().fireColumnResizeEvent(this, false);
}
return this;
}
/**
* Returns whether this column has an undefined width.
*
* @since 7.6
* @return whether the width is undefined
* @throws IllegalStateException
* if the column is no longer attached to any grid
*/
public boolean isWidthUndefined() {
checkColumnIsAttached();
return getState(false).width < 0;
}
/**
* Marks the column width as undefined. An undefined width means the
* grid is free to resize the column based on the cell contents and
* available space in the grid.
*
* @return the column itself
*/
public Column<T, V> setWidthUndefined() {
checkColumnIsAttached();
if (!isWidthUndefined()) {
getState().width = -1;
getGrid().markAsDirty();
getGrid().fireColumnResizeEvent(this, false);
}
return this;
}
/**
* Sets the minimum width for this column.
* <p>
* This defines the minimum guaranteed pixel width of the column
* <em>when it is set to expand</em>.
*
* @throws IllegalStateException
* if the column is no longer attached to any grid
* @see #setExpandRatio(int)
*/
public Column<T, V> setMinimumWidth(double pixels)
throws IllegalStateException {
checkColumnIsAttached();
final double maxwidth = getMaximumWidth();
if (pixels >= 0 && pixels > maxwidth && maxwidth >= 0) {
throw new IllegalArgumentException("New minimum width ("
+ pixels + ") was greater than maximum width ("
+ maxwidth + ")");
}
getState().minWidth = pixels;
getGrid().markAsDirty();
return this;
}
/**
* Return the minimum width for this column.
*
* @return the minimum width for this column
* @see #setMinimumWidth(double)
*/
public double getMinimumWidth() {
return getState(false).minWidth;
}
/**
* Sets the maximum width for this column.
* <p>
* This defines the maximum allowed pixel width of the column <em>when
* it is set to expand</em>.
*
* @param pixels
* the maximum width
* @throws IllegalStateException
* if the column is no longer attached to any grid
* @see #setExpandRatio(int)
*/
public Column<T, V> setMaximumWidth(double pixels) {
checkColumnIsAttached();
final double minwidth = getMinimumWidth();
if (pixels >= 0 && pixels < minwidth && minwidth >= 0) {
throw new IllegalArgumentException("New maximum width ("
+ pixels + ") was less than minimum width (" + minwidth
+ ")");
}
getState().maxWidth = pixels;
getGrid().markAsDirty();
return this;
}
/**
* Returns the maximum width for this column.
*
* @return the maximum width for this column
* @see #setMaximumWidth(double)
*/
public double getMaximumWidth() {
return getState(false).maxWidth;
}
/**
* Sets whether this column can be resized by the user.
*
* @since 7.6
* @param resizable
* {@code true} if this column should be resizable,
* {@code false} otherwise
* @throws IllegalStateException
* if the column is no longer attached to any grid
*/
public Column<T, V> setResizable(boolean resizable) {
checkColumnIsAttached();
if (resizable != isResizable()) {
getState().resizable = resizable;
getGrid().markAsDirty();
}
return this;
}
/**
* Gets the caption of the hiding toggle for this column.
*
* @since 7.5.0
* @see #setHidingToggleCaption(String)
* @return the caption for the hiding toggle for this column
*/
public String getHidingToggleCaption() {
return getState(false).hidingToggleCaption;
}
/**
* Sets the caption of the hiding toggle for this column. Shown in the
* toggle for this column in the grid's sidebar when the column is
* {@link #isHidable() hidable}.
* <p>
* The default value is <code>null</code>, and in that case the column's
* {@link #getCaption() header caption} is used.
* <p>
* <em>NOTE:</em> setting this to empty string might cause the hiding
* toggle to not render correctly.
*
* @since 7.5.0
* @param hidingToggleCaption
* the text to show in the column hiding toggle
* @return the column itself
*/
public Column<T, V> setHidingToggleCaption(String hidingToggleCaption) {
if (hidingToggleCaption != getHidingToggleCaption()) {
getState().hidingToggleCaption = hidingToggleCaption;
}
return this;
}
/**
* Hides or shows the column. By default columns are visible before
* explicitly hiding them.
*
* @since 7.5.0
* @param hidden
* <code>true</code> to hide the column, <code>false</code>
* to show
* @return this column
* @throws IllegalStateException
* if the column is no longer attached to any grid
*/
public Column<T, V> setHidden(boolean hidden) {
checkColumnIsAttached();
if (hidden != isHidden()) {
getState().hidden = hidden;
getGrid().fireColumnVisibilityChangeEvent(this, hidden, false);
}
return this;
}
/**
* Returns whether this column is hidden. Default is {@code false}.
*
* @since 7.5.0
* @return <code>true</code> if the column is currently hidden,
* <code>false</code> otherwise
*/
public boolean isHidden() {
return getState(false).hidden;
}
/**
* Sets whether this column can be hidden by the user. Hidable columns
* can be hidden and shown via the sidebar menu.
*
* @since 7.5.0
* @param hidable
* <code>true</code> iff the column may be hidable by the
* user via UI interaction
* @return this column
*/
public Column<T, V> setHidable(boolean hidable) {
if (hidable != isHidable()) {
getState().hidable = hidable;
}
return this;
}
/**
* Returns whether this column can be hidden by the user. Default is
* {@code false}.
* <p>
* <em>Note:</em> the column can be programmatically hidden using
* {@link #setHidden(boolean)} regardless of the returned value.
*
* @since 7.5.0
* @return <code>true</code> if the user can hide the column,
* <code>false</code> if not
*/
public boolean isHidable() {
return getState(false).hidable;
}
/**
* Returns whether this column can be resized by the user. Default is
* {@code true}.
* <p>
* <em>Note:</em> the column can be programmatically resized using
* {@link #setWidth(double)} and {@link #setWidthUndefined()} regardless
* of the returned value.
*
* @since 7.6
* @return {@code true} if this column is resizable, {@code false}
* otherwise
*/
public boolean isResizable() {
return getState(false).resizable;
}
/**
* Sets whether this Column has a component displayed in Editor or not.
* A column can only be editable if an editor component or binding has
* been set.
*
* @param editable
* {@code true} if column is editable; {@code false} if not
* @return this column
*
* @see #setEditorComponent(HasValue, Setter)
* @see #setEditorBinding(Binding)
*/
public Column<T, V> setEditable(boolean editable) {
Objects.requireNonNull(editorBinding,
"Column has no editor binding or component defined");
getState().editable = editable;
return this;
}
/**
* Gets whether this Column has a component displayed in Editor or not.
*
* @return {@code true} if the column displays an editor component;
* {@code false} if not
*/
public boolean isEditable() {
return getState(false).editable;
}
/**
* Sets an editor binding for this column. The {@link Binding} is used
* when a row is in editor mode to define how to populate an editor
* component based on the edited row and how to update an item based on
* the value in the editor component.
* <p>
* To create a binding to use with a column, define a binding for the
* editor binder (<code>grid.getEditor().getBinder()</code>) using e.g.
* {@link Binder#forField(HasValue)}. You can also use
* {@link #setEditorComponent(HasValue, Setter)} if no validator or
* converter is needed for the binding.
* <p>
* The {@link HasValue} that the binding is defined to use must be a
* {@link Component}.
*
* @param binding
* the binding to use for this column
* @return this column
*
* @see #setEditorComponent(HasValue, Setter)
* @see Binding
* @see Grid#getEditor()
* @see Editor#getBinder()
*/
public Column<T, V> setEditorBinding(Binding<T, ?> binding) {
Objects.requireNonNull(binding, "null is not a valid editor field");
if (!(binding.getField() instanceof Component)) {
throw new IllegalArgumentException(
"Binding target must be a component.");
}
this.editorBinding = binding;
return setEditable(true);
}
/**
* Gets the binder binding that is currently used for this column.
*
* @return the used binder binding, or <code>null</code> if no binding
* is configured
*
* @see #setEditorBinding(Binding)
*/
public Binding<T, ?> getEditorBinding() {
return editorBinding;
}
/**
* Sets a component and setter to use for editing values of this column
* in the editor row. This is a shorthand for use in simple cases where
* no validator or converter is needed. Use
* {@link #setEditorBinding(Binding)} to support more complex cases.
* <p>
* <strong>Note:</strong> The same component cannot be used for multiple
* columns.
*
* @param editorComponent
* the editor component
* @param setter
* a setter that stores the component value in the row item
* @return this column
*
* @see #setEditorBinding(Binding)
* @see Grid#getEditor()
* @see Binder#bind(HasValue, ValueProvider, Setter)
*/
public <C extends HasValue<V> & Component> Column<T, V> setEditorComponent(
C editorComponent, Setter<T, V> setter) {
Objects.requireNonNull(editorComponent,
"Editor component cannot be null");
Objects.requireNonNull(setter, "Setter cannot be null");
Binding<T, V> binding = getGrid().getEditor().getBinder()
.bind(editorComponent, valueProvider::apply, setter);
return setEditorBinding(binding);
}
/**
* Sets a component to use for editing values of this columns in the
* editor row. This method can only be used if the column has an
* {@link #setId(String) id} and the {@link Grid} has been created using
* {@link Grid#Grid(Class)} or some other way that allows finding
* properties based on property names.
* <p>
* This is a shorthand for use in simple cases where no validator or
* converter is needed. Use {@link #setEditorBinding(Binding)} to
* support more complex cases.
* <p>
* <strong>Note:</strong> The same component cannot be used for multiple
* columns.
*
* @param editorComponent
* the editor component
* @return this column
*
* @see #setEditorBinding(Binding)
* @see Grid#getEditor()
* @see Binder#bind(HasValue, String)
* @see Grid#Grid(Class)
*/
public <F, C extends HasValue<F> & Component> Column<T, V> setEditorComponent(
C editorComponent) {
Objects.requireNonNull(editorComponent,
"Editor component cannot be null");
String propertyName = getId();
if (propertyName == null) {
throw new IllegalStateException(
"setEditorComponent without a setter can only be used if the column has an id. "
+ "Use another setEditorComponent(Component, Setter) or setEditorBinding(Binding) instead.");
}
Binding<T, F> binding = getGrid().getEditor().getBinder()
.bind(editorComponent, propertyName);
return setEditorBinding(binding);
}
/**
* Sets the Renderer for this Column. Setting the renderer will cause
* all currently available row data to be recreated and sent to the
* client.
*
* @param renderer
* the new renderer
* @return this column
*
* @since 8.0.3
*/
public Column<T, V> setRenderer(Renderer<? super V> renderer) {
Objects.requireNonNull(renderer, "Renderer can't be null");
// Remove old renderer
Connector oldRenderer = getState().renderer;
if (oldRenderer != null && oldRenderer instanceof Extension) {
removeExtension((Extension) oldRenderer);
}
// Set new renderer
getState().renderer = renderer;
addExtension(renderer);
// Trigger redraw
getParent().getDataCommunicator().reset();
return this;
}
/**
* Gets the grid that this column belongs to.
*
* @return the grid that this column belongs to, or <code>null</code> if
* this column has not yet been associated with any grid
*/
protected Grid<T> getGrid() {
return getParent();
}
/**
* Checks if column is attached and throws an
* {@link IllegalStateException} if it is not.
*
* @throws IllegalStateException
* if the column is no longer attached to any grid
*/
protected void checkColumnIsAttached() throws IllegalStateException {
if (getGrid() == null) {
throw new IllegalStateException(
"Column is no longer attached to a grid.");
}
}
/**
* Writes the design attributes for this column into given element.
*
* @since 7.5.0
*
* @param element
* Element to write attributes into
*
* @param designContext
* the design context
*/
protected void writeDesign(Element element,
DesignContext designContext) {
Attributes attributes = element.attributes();
ColumnState defaultState = new ColumnState();
if (getId() == null) {
setId("column" + getGrid().getColumns().indexOf(this));
}
DesignAttributeHandler.writeAttribute("column-id", attributes,
getId(), null, String.class, designContext);
// Sortable is a special attribute that depends on the data
// provider.
DesignAttributeHandler.writeAttribute("sortable", attributes,
isSortable(), null, boolean.class, designContext);
DesignAttributeHandler.writeAttribute("editable", attributes,
isEditable(), defaultState.editable, boolean.class,
designContext);
DesignAttributeHandler.writeAttribute("resizable", attributes,
isResizable(), defaultState.resizable, boolean.class,
designContext);
DesignAttributeHandler.writeAttribute("hidable", attributes,
isHidable(), defaultState.hidable, boolean.class,
designContext);
DesignAttributeHandler.writeAttribute("hidden", attributes,
isHidden(), defaultState.hidden, boolean.class,
designContext);
DesignAttributeHandler.writeAttribute("hiding-toggle-caption",
attributes, getHidingToggleCaption(),
defaultState.hidingToggleCaption, String.class,
designContext);
DesignAttributeHandler.writeAttribute("width", attributes,
getWidth(), defaultState.width, Double.class,
designContext);
DesignAttributeHandler.writeAttribute("min-width", attributes,
getMinimumWidth(), defaultState.minWidth, Double.class,
designContext);
DesignAttributeHandler.writeAttribute("max-width", attributes,
getMaximumWidth(), defaultState.maxWidth, Double.class,
designContext);
DesignAttributeHandler.writeAttribute("expand", attributes,
getExpandRatio(), defaultState.expandRatio, Integer.class,
designContext);
}
/**
* Reads the design attributes for this column from given element.
*
* @since 7.5.0
* @param design
* Element to read attributes from
* @param designContext
* the design context
*/
@SuppressWarnings("unchecked")
protected void readDesign(Element design, DesignContext designContext) {
Attributes attributes = design.attributes();
if (design.hasAttr("sortable")) {
setSortable(DesignAttributeHandler.readAttribute("sortable",
attributes, boolean.class));
} else {
setSortable(false);
}
if (design.hasAttr("editable")) {
/*
* This is a fake editor just to have something (otherwise
* "setEditable" throws an exception.
*
* Let's use TextField here because we support only Strings as
* inline data type. It will work incorrectly for other types
* but we don't support them anyway.
*/
setEditorComponent((HasValue<V> & Component) new TextField(),
(item, value) -> {
// Ignore user value since we don't know the setter
});
setEditable(DesignAttributeHandler.readAttribute("editable",
attributes, boolean.class));
}
if (design.hasAttr("resizable")) {
setResizable(DesignAttributeHandler.readAttribute("resizable",
attributes, boolean.class));
}
if (design.hasAttr("hidable")) {
setHidable(DesignAttributeHandler.readAttribute("hidable",
attributes, boolean.class));
}
if (design.hasAttr("hidden")) {
setHidden(DesignAttributeHandler.readAttribute("hidden",
attributes, boolean.class));
}
if (design.hasAttr("hiding-toggle-caption")) {
setHidingToggleCaption(DesignAttributeHandler.readAttribute(
"hiding-toggle-caption", attributes, String.class));
}
// Read size info where necessary.
if (design.hasAttr("width")) {
setWidth(DesignAttributeHandler.readAttribute("width",
attributes, Double.class));
}
if (design.hasAttr("min-width")) {
setMinimumWidth(DesignAttributeHandler
.readAttribute("min-width", attributes, Double.class));
}
if (design.hasAttr("max-width")) {
setMaximumWidth(DesignAttributeHandler
.readAttribute("max-width", attributes, Double.class));
}
if (design.hasAttr("expand")) {
if (design.attr("expand").isEmpty()) {
setExpandRatio(1);
} else {
setExpandRatio(DesignAttributeHandler.readAttribute(
"expand", attributes, Integer.class));
}
}
}
}
private class HeaderImpl extends Header {
@Override
protected Grid<T> getGrid() {
return Grid.this;
}
@Override
protected SectionState getState(boolean markAsDirty) {
return Grid.this.getState(markAsDirty).header;
}
@Override
protected Column<?, ?> getColumnByInternalId(String internalId) {
return getGrid().getColumnByInternalId(internalId);
}
@Override
@SuppressWarnings("unchecked")
protected String getInternalIdForColumn(Column<?, ?> column) {
return getGrid().getInternalIdForColumn((Column<T, ?>) column);
}
};
private class FooterImpl extends Footer {
@Override
protected Grid<T> getGrid() {
return Grid.this;
}
@Override
protected SectionState getState(boolean markAsDirty) {
return Grid.this.getState(markAsDirty).footer;
}
@Override
protected Column<?, ?> getColumnByInternalId(String internalId) {
return getGrid().getColumnByInternalId(internalId);
}
@Override
@SuppressWarnings("unchecked")
protected String getInternalIdForColumn(Column<?, ?> column) {
return getGrid().getInternalIdForColumn((Column<T, ?>) column);
}
};
private final Set<Column<T, ?>> columnSet = new LinkedHashSet<>();
private final Map<String, Column<T, ?>> columnKeys = new HashMap<>();
private final Map<String, Column<T, ?>> columnIds = new HashMap<>();
private final List<GridSortOrder<T>> sortOrder = new ArrayList<>();
private final DetailsManager<T> detailsManager;
private final Set<Component> extensionComponents = new HashSet<>();
private StyleGenerator<T> styleGenerator = item -> null;
private DescriptionGenerator<T> descriptionGenerator;
private final Header header = new HeaderImpl();
private final Footer footer = new FooterImpl();
private int counter = 0;
private GridSelectionModel<T> selectionModel;
private Editor<T> editor;
private PropertySet<T> propertySet;
private Class<T> beanType = null;
/**
* Creates a new grid without support for creating columns based on property
* names. Use an alternative constructor, such as {@link Grid#Grid(Class)},
* to create a grid that automatically sets up columns based on the type of
* presented data.
*
* @see #Grid(Class)
* @see #withPropertySet(PropertySet)
*/
public Grid() {
this(new DataCommunicator<>());
}
/**
* Creates a new grid 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</code>
* @see #Grid()
* @see #withPropertySet(PropertySet)
*/
public Grid(Class<T> beanType) {
this(BeanPropertySet.get(beanType));
this.beanType = beanType;
}
/**
* Creates a new grid with the given data communicator and without support
* for creating columns based on property names.
*
* @param dataCommunicator
* the custom data communicator to set
* @see #Grid()
* @see #Grid(PropertySet, DataCommunicator)
* @since 8.1
*/
protected Grid(DataCommunicator<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 Grid created without a bean type class literal or a custom property set"
+ " doesn't support finding properties by name.");
}
}, dataCommunicator);
}
/**
* Creates a grid using a custom {@link PropertySet} implementation for
* configuring the initial columns and resolving property names for
* {@link #addColumn(String)} and
* {@link Column#setEditorComponent(HasValue)}.
*
* @see #withPropertySet(PropertySet)
*
* @param propertySet
* the property set implementation to use, not <code>null</code>.
*/
protected Grid(PropertySet<T> propertySet) {
this(propertySet, new DataCommunicator<>());
}
/**
* Creates a grid 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)}.
*
* @see #withPropertySet(PropertySet)
*
* @param propertySet
* the property set implementation to use, not <code>null</code>.
* @param dataCommunicator
* the data communicator to use, not<code>null</code>
* @since 8.1
*/
protected Grid(PropertySet<T> propertySet,
DataCommunicator<T> dataCommunicator) {
super(dataCommunicator);
registerRpc(new GridServerRpcImpl());
setDefaultHeaderRow(appendHeaderRow());
setSelectionModel(new SingleSelectionModelImpl<>());
detailsManager = new DetailsManager<>();
addExtension(detailsManager);
addDataGenerator(detailsManager);
addDataGenerator((item, json) -> {
String styleName = styleGenerator.apply(item);
if (styleName != null && !styleName.isEmpty()) {
json.put(GridState.JSONKEY_ROWSTYLE, styleName);
}
if (descriptionGenerator != null) {
String description = descriptionGenerator.apply(item);
if (description != null && !description.isEmpty()) {
json.put(GridState.JSONKEY_ROWDESCRIPTION, description);
}
}
});
setPropertySet(propertySet);
// Automatically add columns for all available properties
propertySet.getProperties().map(PropertyDefinition::getName)
.forEach(this::addColumn);
}
/**
* Sets the property set to use for this grid. Does not create or update
* columns in any way but will delete and re-create the editor.
* <p>
* This is only meant to be called from constructors and readDesign, at a
* stage where it does not matter if you throw away the editor.
*
* @param propertySet
* the property set to use
*
* @since 8.0.3
*/
protected void setPropertySet(PropertySet<T> propertySet) {
Objects.requireNonNull(propertySet, "propertySet cannot be null");
this.propertySet = propertySet;
if (editor instanceof Extension) {
removeExtension((Extension) editor);
}
editor = createEditor();
if (editor instanceof Extension) {
addExtension((Extension) editor);
}
}
/**
* Creates a grid 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 Grid#Grid()
* @see Grid#Grid(Class)
*
* @param propertySet
* the property set implementation to use, not <code>null</code>.
* @return a new grid using the provided property set, not <code>null</code>
*/
public static <BEAN> Grid<BEAN> withPropertySet(
PropertySet<BEAN> propertySet) {
return new Grid<>(propertySet);
}
/**
* Creates a new {@code Grid} using the given caption
*
* @param caption
* the caption of the grid
*/
public Grid(String caption) {
this();
setCaption(caption);
}
/**
* Creates a new {@code Grid} using the given caption and
* {@code DataProvider}
*
* @param caption
* the caption of the grid
* @param dataProvider
* the data provider, not {@code null}
*/
public Grid(String caption, DataProvider<T, ?> dataProvider) {
this(caption);
setDataProvider(dataProvider);
}
/**
* Creates a new {@code Grid} using the given {@code DataProvider}
*
* @param dataProvider
* the data provider, not {@code null}
*/
public Grid(DataProvider<T, ?> dataProvider) {
this();
setDataProvider(dataProvider);
}
/**
* Creates a new {@code Grid} using the given caption and collection of
* items
*
* @param caption
* the caption of the grid
* @param items
* the data items to use, not {@çode null}
*/
public Grid(String caption, Collection<T> items) {
this(caption, DataProvider.ofCollection(items));
}
/**
* Gets the bean type used by this grid.
* <p>
* The bean type is used to automatically set up a column added using a
* property name.
*
* @return the used bean type or <code>null</code> if no bean type has been
* defined
*
* @since 8.0.3
*/
public Class<T> getBeanType() {
return beanType;
}
public <V> void fireColumnVisibilityChangeEvent(Column<T, V> column,
boolean hidden, boolean userOriginated) {
fireEvent(new ColumnVisibilityChangeEvent(this, column, hidden,
userOriginated));
}
/**
* Adds a new column with the given property name. The column will use a
* {@link TextRenderer}. The value is converted to a String using
* {@link Object#toString()}. The property name will be used as the
* {@link Column#getId() column id} and the {@link Column#getCaption()
* column caption} will be set based on the property definition.
* <p>
* This method can only be used for a <code>Grid</code> created using
* {@link Grid#Grid(Class)} or {@link #withPropertySet(PropertySet)}.
*
* @param propertyName
* the property name of the new column, not <code>null</code>
* @return the newly added column, not <code>null</code>
*/
public Column<T, ?> addColumn(String propertyName) {
return addColumn(propertyName, new TextRenderer());
}
/**
* Adds a new column with the given property name and renderer. The property
* name will be used as the {@link Column#getId() column id} and the
* {@link Column#getCaption() column caption} will be set based on the
* property definition.
* <p>
* This method can only be used for a <code>Grid</code> created using
* {@link Grid#Grid(Class)} or {@link #withPropertySet(PropertySet)}.
*
* @param propertyName
* the property name of the new column, not <code>null</code>
* @param renderer
* the renderer to use, not <code>null</code>
* @return the newly added column, not <code>null</code>
*/
public Column<T, ?> addColumn(String propertyName,
AbstractRenderer<? super T, ?> renderer) {
Objects.requireNonNull(propertyName, "Property name cannot be null");
Objects.requireNonNull(renderer, "Renderer cannot be null");
if (getColumn(propertyName) != null) {
throw new IllegalStateException(
"There is already a column for " + propertyName);
}
PropertyDefinition<T, ?> definition = propertySet
.getProperty(propertyName)
.orElseThrow(() -> new IllegalArgumentException(
"Could not resolve property name " + propertyName
+ " from " + propertySet));
if (!renderer.getPresentationType()
.isAssignableFrom(definition.getType())) {
throw new IllegalArgumentException(renderer.toString()
+ " cannot be used with a property of type "
+ definition.getType().getName());
}
@SuppressWarnings({ "unchecked", "rawtypes" })
Column<T, ?> column = addColumn(definition.getGetter(),
(AbstractRenderer) renderer).setId(definition.getName())
.setCaption(definition.getCaption());
return column;
}
/**
* Adds a new text column to this {@link Grid} with a value provider. The
* column will use a {@link TextRenderer}. The value is converted to a
* String using {@link Object#toString()}. In-memory sorting will use the
* natural ordering of elements if they are mutually comparable and
* otherwise fall back to comparing the string representations of the
* values.
*
* @param valueProvider
* the value provider
*
* @return the new column
*/
public <V> Column<T, V> addColumn(ValueProvider<T, V> valueProvider) {
return addColumn(valueProvider, new TextRenderer());
}
/**
* Adds a new column to this {@link Grid} with typed renderer and value
* provider.
*
* @param valueProvider
* the value provider
* @param renderer
* the column value class
* @param <V>
* the column value type
*
* @return the new column
*
* @see AbstractRenderer
*/
public <V> Column<T, V> addColumn(ValueProvider<T, V> valueProvider,
AbstractRenderer<? super T, ? super V> renderer) {
String generatedIdentifier = getGeneratedIdentifier();
Column<T, V> column = createColumn(valueProvider, renderer);
addColumn(generatedIdentifier, column);
return column;
}
/**
* Creates a column instance from a value provider and a renderer.
*
* @param valueProvider
* the value provider
* @param renderer
* the renderer
* @return a new column instance
*
* @since 8.0.3
*/
protected <V> Column<T, V> createColumn(ValueProvider<T, V> valueProvider,
AbstractRenderer<? super T, ? super V> renderer) {
return new Column<>(valueProvider, renderer);
}
private void addColumn(String identifier, Column<T, ?> column) {
if (getColumns().contains(column)) {
return;
}
column.extend(this);
columnSet.add(column);
columnKeys.put(identifier, column);
column.setInternalId(identifier);
addDataGenerator(column);
getState().columnOrder.add(identifier);
getHeader().addColumn(identifier);
getFooter().addColumn(identifier);
if (getDefaultHeaderRow() != null) {
getDefaultHeaderRow().getCell(column).setText(column.getCaption());
}
}
/**
* Removes the given column from this {@link Grid}.
*
* @param column
* the column to remove
*/
public void removeColumn(Column<T, ?> column) {
if (columnSet.remove(column)) {
String columnId = column.getInternalId();
int displayIndex = getState(false).columnOrder.indexOf(columnId);
assert displayIndex != -1 : "Tried to remove a column which is not included in columnOrder. This should not be possible as all columns should be in columnOrder.";
columnKeys.remove(columnId);
columnIds.remove(column.getId());
column.remove();
getHeader().removeColumn(columnId);
getFooter().removeColumn(columnId);
getState(true).columnOrder.remove(columnId);
if (displayIndex < getFrozenColumnCount()) {
setFrozenColumnCount(getFrozenColumnCount() - 1);
}
}
}
/**
* Removes the column with the given column id.
*
* @see #removeColumn(Column)
* @see Column#setId(String)
*
* @param columnId
* the id of the column to remove, not <code>null</code>
*/
public void removeColumn(String columnId) {
removeColumn(getColumnOrThrow(columnId));
}
/**
* Removes all columns from this Grid.
*
* @since 8.0.2
*/
public void removeAllColumns() {
for (Column<T, ?> column : getColumns()) {
removeColumn(column);
}
}
/**
* Sets the details component generator.
*
* @param generator
* the generator for details components
*/
public void setDetailsGenerator(DetailsGenerator<T> generator) {
this.detailsManager.setDetailsGenerator(generator);
}
/**
* Sets the visibility of details component for given item.
*
* @param item
* the item to show details for
* @param visible
* {@code true} if details component should be visible;
* {@code false} if it should be hidden
*/
public void setDetailsVisible(T item, boolean visible) {
detailsManager.setDetailsVisible(item, visible);
}
/**
* Returns the visibility of details component for given item.
*
* @param item
* the item to show details for
*
* @return {@code true} if details component should be visible;
* {@code false} if it should be hidden
*/
public boolean isDetailsVisible(T item) {
return detailsManager.isDetailsVisible(item);
}
/**
* Gets an unmodifiable collection of all columns currently in this
* {@link Grid}.
*
* @return unmodifiable collection of columns
*/
public List<Column<T, ?>> getColumns() {
return Collections.unmodifiableList(getState(false).columnOrder.stream()
.map(columnKeys::get).collect(Collectors.toList()));
}
/**
* Gets a {@link Column} of this grid by its identifying string.
*
* @see Column#setId(String)
*
* @param columnId
* the identifier of the column to get
* @return the column corresponding to the given column identifier, or
* <code>null</code> if there is no such column
*/
public Column<T, ?> getColumn(String columnId) {
return columnIds.get(columnId);
}
private Column<T, ?> getColumnOrThrow(String columnId) {
Objects.requireNonNull(columnId, "Column id cannot be null");
Column<T, ?> column = getColumn(columnId);
if (column == null) {
throw new IllegalStateException(
"There is no column with the id " + columnId);
}
return column;
}
/**
* {@inheritDoc}
* <p>
* Note that the order of the returned components it not specified.
*/
@Override
public Iterator<Component> iterator() {
Set<Component> componentSet = new LinkedHashSet<>(extensionComponents);
Header header = getHeader();
for (int i = 0; i < header.getRowCount(); ++i) {
HeaderRow row = header.getRow(i);
componentSet.addAll(row.getComponents());
}
Footer footer = getFooter();
for (int i = 0; i < footer.getRowCount(); ++i) {
FooterRow row = footer.getRow(i);
componentSet.addAll(row.getComponents());
}
return Collections.unmodifiableSet(componentSet).iterator();
}
/**
* Sets the number of frozen columns in this grid. Setting the count to 0
* means that no data columns will be frozen, but the built-in selection
* checkbox column will still be frozen if it's in use. Setting the count to
* -1 will also disable the selection column.
* <p>
* <em>NOTE:</em> this count includes {@link Column#isHidden() hidden
* columns} in the count.
* <p>
* The default value is 0.
*
* @param numberOfColumns
* the number of columns that should be frozen
*
* @throws IllegalArgumentException
* if the column count is less than -1 or greater than the
* number of visible columns
*/
public void setFrozenColumnCount(int numberOfColumns) {
if (numberOfColumns < -1 || numberOfColumns > columnSet.size()) {
throw new IllegalArgumentException(
"count must be between -1 and the current number of columns ("
+ columnSet.size() + "): " + numberOfColumns);
}
getState().frozenColumnCount = numberOfColumns;
}
/**
* Gets the number of frozen columns in this grid. 0 means that no data
* columns will be frozen, but the built-in selection checkbox column will
* still be frozen if it's in use. -1 means that not even the selection
* column is frozen.
* <p>
* <em>NOTE:</em> this count includes {@link Column#isHidden() hidden
* columns} in the count.
*
* @see #setFrozenColumnCount(int)
*
* @return the number of frozen columns
*/
public int getFrozenColumnCount() {
return getState(false).frozenColumnCount;
}
/**
* Sets the number of rows that should be visible in Grid's body. This
* method will set the height mode to be {@link HeightMode#ROW}.
*
* @param rows
* The height in terms of number of rows displayed in Grid's
* body. If Grid doesn't contain enough rows, white space is
* displayed instead. If <code>null</code> is given, then Grid's
* height is undefined
* @throws IllegalArgumentException
* if {@code rows} is zero or less
* @throws IllegalArgumentException
* if {@code rows} is {@link Double#isInfinite(double) infinite}
* @throws IllegalArgumentException
* if {@code rows} is {@link Double#isNaN(double) NaN}
*/
public void setHeightByRows(double rows) {
if (rows <= 0.0d) {
throw new IllegalArgumentException(
"More than zero rows must be shown.");
} else if (Double.isInfinite(rows)) {
throw new IllegalArgumentException(
"Grid doesn't support infinite heights");
} else if (Double.isNaN(rows)) {
throw new IllegalArgumentException("NaN is not a valid row count");
}
getState().heightMode = HeightMode.ROW;
getState().heightByRows = rows;
}
/**
* Gets the amount of rows in Grid's body that are shown, while
* {@link #getHeightMode()} is {@link HeightMode#ROW}.
*
* @return the amount of rows that are being shown in Grid's body
* @see #setHeightByRows(double)
*/
public double getHeightByRows() {
return getState(false).heightByRows;
}
/**
* {@inheritDoc}
* <p>
* <em>Note:</em> This method will set the height mode to be
* {@link HeightMode#CSS}.
*
* @see #setHeightMode(HeightMode)
*/
@Override
public void setHeight(float height, Unit unit) {
getState().heightMode = HeightMode.CSS;
super.setHeight(height, unit);
}
/**
* Defines the mode in which the Grid widget's height is calculated.
* <p>
* If {@link HeightMode#CSS} is given, Grid will respect the values given
* via a {@code setHeight}-method, and behave as a traditional Component.
* <p>
* If {@link HeightMode#ROW} is given, Grid will make sure that the body
* will display as many rows as {@link #getHeightByRows()} defines.
* <em>Note:</em> If headers/footers are inserted or removed, the widget
* will resize itself to still display the required amount of rows in its
* body. It also takes the horizontal scrollbar into account.
*
* @param heightMode
* the mode in to which Grid should be set
*/
public void setHeightMode(HeightMode heightMode) {
/*
* This method is a workaround for the fact that Vaadin re-applies
* widget dimensions (height/width) on each state change event. The
* original design was to have setHeight and setHeightByRow be equals,
* and whichever was called the latest was considered in effect.
*
* But, because of Vaadin always calling setHeight on the widget, this
* approach doesn't work.
*/
getState().heightMode = heightMode;
}
/**
* Returns the current {@link HeightMode} the Grid is in.
* <p>
* Defaults to {@link HeightMode#CSS}.
*
* @return the current HeightMode
*/
public HeightMode getHeightMode() {
return getState(false).heightMode;
}
/**
* Sets the height of a row. If -1 (default), the row height is calculated
* based on the theme for an empty row before the Grid is displayed.
* <p>
* Note that all header, body and footer rows get the same height if
* explicitly set. In automatic mode, each section is calculated separately
* based on an empty row of that type.
*
* @param rowHeight
* The height of a row in pixels or -1 for automatic calculation
*/
public void setRowHeight(double rowHeight) {
getState().rowHeight = rowHeight;
}
/**
* Returns the currently explicitly set row height or -1 if automatic.
*
* @return explicitly set row height in pixels or -1 if in automatic mode
*/
public double getRowHeight() {
return getState(false).rowHeight;
}
/**
* Sets the style generator that is used for generating class names for rows
* in this grid. Returning null from the generator results in no custom
* style name being set.
*
* @see StyleGenerator
*
* @param styleGenerator
* the row style generator to set, not null
* @throws NullPointerException
* if {@code styleGenerator} is {@code null}
*/
public void setStyleGenerator(StyleGenerator<T> styleGenerator) {
Objects.requireNonNull(styleGenerator,
"Style generator must not be null");
this.styleGenerator = styleGenerator;
getDataCommunicator().reset();
}
/**
* Gets the style generator that is used for generating class names for
* rows.
*
* @see StyleGenerator
*
* @return the row style generator
*/
public StyleGenerator<T> getStyleGenerator() {
return styleGenerator;
}
/**
* Sets the description generator that is used for generating descriptions
* for rows.
*
* @param descriptionGenerator
* the row description generator to set, or <code>null</code> to
* remove a previously set generator
*/
public void setDescriptionGenerator(
DescriptionGenerator<T> descriptionGenerator) {
this.descriptionGenerator = descriptionGenerator;
getDataCommunicator().reset();
}
/**
* Gets the description generator that is used for generating descriptions
* for rows.
*
* @return the row description generator, or <code>null</code> if no
* generator is set
*/
public DescriptionGenerator<T> getDescriptionGenerator() {
return descriptionGenerator;
}
//
// HEADER AND FOOTER
//
/**
* Returns the header row at the given index.
*
* @param index
* the index of the row, where the topmost row has index zero
* @return the header row at the index
* @throws IndexOutOfBoundsException
* if {@code rowIndex < 0 || rowIndex >= getHeaderRowCount()}
*/
public HeaderRow getHeaderRow(int index) {
return getHeader().getRow(index);
}
/**
* Gets the number of rows in the header section.
*
* @return the number of header rows
*/
public int getHeaderRowCount() {
return header.getRowCount();
}
/**
* Inserts a new row at the given position to the header section. Shifts the
* row currently at that position and any subsequent rows down (adds one to
* their indices). Inserting at {@link #getHeaderRowCount()} appends the row
* at the bottom of the header.
*
* @param index
* the index at which to insert the row, where the topmost row
* has index zero
* @return the inserted header row
*
* @throws IndexOutOfBoundsException
* if {@code rowIndex < 0 || rowIndex > getHeaderRowCount()}
*
* @see #appendHeaderRow()
* @see #prependHeaderRow()
* @see #removeHeaderRow(HeaderRow)
* @see #removeHeaderRow(int)
*/
public HeaderRow addHeaderRowAt(int index) {
return getHeader().addRowAt(index);
}
/**
* Adds a new row at the bottom of the header section.
*
* @return the appended header row
*
* @see #prependHeaderRow()
* @see #addHeaderRowAt(int)
* @see #removeHeaderRow(HeaderRow)
* @see #removeHeaderRow(int)
*/
public HeaderRow appendHeaderRow() {
return addHeaderRowAt(getHeaderRowCount());
}
/**
* Adds a new row at the top of the header section.
*
* @return the prepended header row
*
* @see #appendHeaderRow()
* @see #addHeaderRowAt(int)
* @see #removeHeaderRow(HeaderRow)
* @see #removeHeaderRow(int)
*/
public HeaderRow prependHeaderRow() {
return addHeaderRowAt(0);
}
/**
* Removes the given row from the header section. Removing a default row
* sets the Grid to have no default row.
*
* @param row
* the header row to be removed, not null
*
* @throws IllegalArgumentException
* if the header does not contain the row
*
* @see #removeHeaderRow(int)
* @see #addHeaderRowAt(int)
* @see #appendHeaderRow()
* @see #prependHeaderRow()
*/
public void removeHeaderRow(HeaderRow row) {
getHeader().removeRow(row);
}
/**
* Removes the row at the given position from the header section.
*
* @param index
* the index of the row to remove, where the topmost row has
* index zero
*
* @throws IndexOutOfBoundsException
* if {@code index < 0 || index >= getHeaderRowCount()}
*
* @see #removeHeaderRow(HeaderRow)
* @see #addHeaderRowAt(int)
* @see #appendHeaderRow()
* @see #prependHeaderRow()
*/
public void removeHeaderRow(int index) {
getHeader().removeRow(index);
}
/**
* Returns the current default row of the header.
*
* @return the default row or null if no default row set
*
* @see #setDefaultHeaderRow(HeaderRow)
*/
public HeaderRow getDefaultHeaderRow() {
return header.getDefaultRow();
}
/**
* Sets the default row of the header. The default row is a special header
* row that displays column captions and sort indicators. By default Grid
* has a single row which is also the default row. When a header row is set
* as the default row, any existing cell content is replaced by the column
* captions.
*
* @param row
* the new default row, or null for no default row
*
* @throws IllegalArgumentException
* if the header does not contain the row
*/
public void setDefaultHeaderRow(HeaderRow row) {
header.setDefaultRow((Row) row);
}
/**
* Returns the header section of this grid. The default header contains a
* single row, set as the {@linkplain #setDefaultHeaderRow(HeaderRow)
* default row}.
*
* @return the header section
*/
protected Header getHeader() {
return header;
}
/**
* Returns the footer row at the given index.
*
* @param index
* the index of the row, where the topmost row has index zero
* @return the footer row at the index
* @throws IndexOutOfBoundsException
* if {@code rowIndex < 0 || rowIndex >= getFooterRowCount()}
*/
public FooterRow getFooterRow(int index) {
return getFooter().getRow(index);
}
/**
* Gets the number of rows in the footer section.
*
* @return the number of footer rows
*/
public int getFooterRowCount() {
return getFooter().getRowCount();
}
/**
* Inserts a new row at the given position to the footer section. Shifts the
* row currently at that position and any subsequent rows down (adds one to
* their indices). Inserting at {@link #getFooterRowCount()} appends the row
* at the bottom of the footer.
*
* @param index
* the index at which to insert the row, where the topmost row
* has index zero
* @return the inserted footer row
*
* @throws IndexOutOfBoundsException
* if {@code rowIndex < 0 || rowIndex > getFooterRowCount()}
*
* @see #appendFooterRow()
* @see #prependFooterRow()
* @see #removeFooterRow(FooterRow)
* @see #removeFooterRow(int)
*/
public FooterRow addFooterRowAt(int index) {
return getFooter().addRowAt(index);
}
/**
* Adds a new row at the bottom of the footer section.
*
* @return the appended footer row
*
* @see #prependFooterRow()
* @see #addFooterRowAt(int)
* @see #removeFooterRow(FooterRow)
* @see #removeFooterRow(int)
*/
public FooterRow appendFooterRow() {
return addFooterRowAt(getFooterRowCount());
}
/**
* Adds a new row at the top of the footer section.
*
* @return the prepended footer row
*
* @see #appendFooterRow()
* @see #addFooterRowAt(int)
* @see #removeFooterRow(FooterRow)
* @see #removeFooterRow(int)
*/
public FooterRow prependFooterRow() {
return addFooterRowAt(0);
}
/**
* Removes the given row from the footer section. Removing a default row
* sets the Grid to have no default row.
*
* @param row
* the footer row to be removed, not null
*
* @throws IllegalArgumentException
* if the footer does not contain the row
*
* @see #removeFooterRow(int)
* @see #addFooterRowAt(int)
* @see #appendFooterRow()
* @see #prependFooterRow()
*/
public void removeFooterRow(FooterRow row) {
getFooter().removeRow(row);
}
/**
* Removes the row at the given position from the footer section.
*
* @param index
* the index of the row to remove, where the topmost row has
* index zero
*
* @throws IndexOutOfBoundsException
* if {@code index < 0 || index >= getFooterRowCount()}
*
* @see #removeFooterRow(FooterRow)
* @see #addFooterRowAt(int)
* @see #appendFooterRow()
* @see #prependFooterRow()
*/
public void removeFooterRow(int index) {
getFooter().removeRow(index);
}
/**
* Returns the footer section of this grid.
*
* @return the footer section
*/
protected Footer getFooter() {
return footer;
}
/**
* Registers a new column reorder listener.
*
* @param listener
* the listener to register, not null
* @return a registration for the listener
*/
public Registration addColumnReorderListener(
ColumnReorderListener listener) {
return addListener(ColumnReorderEvent.class, listener,
COLUMN_REORDER_METHOD);
}
/**
* Registers a new column resize listener.
*
* @param listener
* the listener to register, not null
* @return a registration for the listener
*/
public Registration addColumnResizeListener(ColumnResizeListener listener) {
return addListener(ColumnResizeEvent.class, listener,
COLUMN_RESIZE_METHOD);
}
/**
* Adds an item click listener. The listener is called when an item of this
* {@code Grid} is clicked.
*
* @param listener
* the item click listener, not null
* @return a registration for the listener
*/
public Registration addItemClickListener(
ItemClickListener<? super T> listener) {
return addListener(GridConstants.ITEM_CLICK_EVENT_ID, ItemClick.class,
listener, ITEM_CLICK_METHOD);
}
/**
* Registers a new column visibility change listener.
*
* @param listener
* the listener to register, not null
* @return a registration for the listener
*/
public Registration addColumnVisibilityChangeListener(
ColumnVisibilityChangeListener listener) {
return addListener(ColumnVisibilityChangeEvent.class, listener,
COLUMN_VISIBILITY_METHOD);
}
/**
* Returns whether column reordering is allowed. Default value is
* <code>false</code>.
*
* @return true if reordering is allowed
*/
public boolean isColumnReorderingAllowed() {
return getState(false).columnReorderingAllowed;
}
/**
* Sets whether or not column reordering is allowed. Default value is
* <code>false</code>.
*
* @param columnReorderingAllowed
* specifies whether column reordering is allowed
*/
public void setColumnReorderingAllowed(boolean columnReorderingAllowed) {
if (isColumnReorderingAllowed() != columnReorderingAllowed) {
getState().columnReorderingAllowed = columnReorderingAllowed;
}
}
/**
* Sets the columns and their order based on their column ids. Columns
* currently in this grid that are not present in the list of column ids are
* removed. This includes any column that has no id. Similarly, any new
* column in columns will be added to this grid. New columns can only be
* added for a <code>Grid</code> created using {@link Grid#Grid(Class)} or
* {@link #withPropertySet(PropertySet)}.
*
*
* @param columnIds
* the column ids to set
*
* @see Column#setId(String)
*/
public void setColumns(String... columnIds) {
// Must extract to an explicitly typed variable because otherwise javac
// cannot determine which overload of setColumnOrder to use
Column<T, ?>[] newColumnOrder = Stream.of(columnIds)
.map((Function<String, Column<T, ?>>) id -> {
Column<T, ?> column = getColumn(id);
if (column == null) {
column = addColumn(id);
}
return column;
}).toArray(Column[]::new);
setColumnOrder(newColumnOrder);
// The columns to remove are now at the end of the column list
getColumns().stream().skip(columnIds.length)
.forEach(this::removeColumn);
}
private String getIdentifier(Column<T, ?> column) {
return columnKeys.entrySet().stream()
.filter(entry -> entry.getValue().equals(column))
.map(entry -> entry.getKey()).findFirst()
.orElse(getGeneratedIdentifier());
}
private String getGeneratedIdentifier() {
String columnId = "" + counter;
counter++;
return columnId;
}
/**
* Sets a new column order for the grid. All columns which are not ordered
* here will remain in the order they were before as the last columns of
* grid.
*
* @param columns
* the columns in the order they should be
*/
public void setColumnOrder(Column<T, ?>... columns) {
setColumnOrder(Stream.of(columns));
}
private void setColumnOrder(Stream<Column<T, ?>> columns) {
List<String> columnOrder = new ArrayList<>();
columns.forEach(column -> {
if (columnSet.contains(column)) {
columnOrder.add(column.getInternalId());
} else {
throw new IllegalStateException(
"setColumnOrder should not be called "
+ "with columns that are not in the grid.");
}
});
List<String> stateColumnOrder = getState().columnOrder;
if (stateColumnOrder.size() != columnOrder.size()) {
stateColumnOrder.removeAll(columnOrder);
columnOrder.addAll(stateColumnOrder);
}
getState().columnOrder = columnOrder;
fireColumnReorderEvent(false);
}
/**
* Sets a new column order for the grid based on their column ids. All
* columns which are not ordered here will remain in the order they were
* before as the last columns of grid.
*
* @param columnIds
* the column ids in the order they should be
*
* @see Column#setId(String)
*/
public void setColumnOrder(String... columnIds) {
setColumnOrder(Stream.of(columnIds).map(this::getColumnOrThrow));
}
/**
* Returns the selection model for this grid.
*
* @return the selection model, not null
*/
public GridSelectionModel<T> getSelectionModel() {
assert selectionModel != null : "No selection model set by "
+ getClass().getName() + " constructor";
return selectionModel;
}
/**
* Use this grid as a single select in {@link Binder}.
* <p>
* Throws {@link IllegalStateException} if the grid is not using a
* {@link SingleSelectionModel}.
*
* @return the single select wrapper that can be used in binder
* @throws IllegalStateException
* if not using a single selection model
*/
public SingleSelect<T> asSingleSelect() {
GridSelectionModel<T> model = getSelectionModel();
if (!(model instanceof SingleSelectionModel)) {
throw new IllegalStateException(
"Grid is not in single select mode, it needs to be explicitly set to such with setSelectionModel(SingleSelectionModel) before being able to use single selection features.");
}
return ((SingleSelectionModel<T>) model).asSingleSelect();
}
public Editor<T> getEditor() {
return editor;
}
/**
* User this grid as a multiselect in {@link Binder}.
* <p>
* Throws {@link IllegalStateException} if the grid is not using a
* {@link MultiSelectionModel}.
*
* @return the multiselect wrapper that can be used in binder
* @throws IllegalStateException
* if not using a multiselection model
*/
public MultiSelect<T> asMultiSelect() {
GridSelectionModel<T> model = getSelectionModel();
if (!(model instanceof MultiSelectionModel)) {
throw new IllegalStateException(
"Grid is not in multiselect mode, it needs to be explicitly set to such with setSelectionModel(MultiSelectionModel) before being able to use multiselection features.");
}
return ((MultiSelectionModel<T>) model).asMultiSelect();
}
/**
* Sets the selection model for the grid.
* <p>
* This method is for setting a custom selection model, and is
* {@code protected} because {@link #setSelectionMode(SelectionMode)} should
* be used for easy switching between built-in selection models.
* <p>
* The default selection model is {@link SingleSelectionModelImpl}.
* <p>
* To use a custom selection model, you can e.g. extend the grid call this
* method with your custom selection model.
*
* @param model
* the selection model to use, not {@code null}
*
* @see #setSelectionMode(SelectionMode)
*/
@SuppressWarnings("unchecked")
protected void setSelectionModel(GridSelectionModel<T> model) {
Objects.requireNonNull(model, "selection model cannot be null");
if (selectionModel != null) { // null when called from constructor
selectionModel.remove();
}
selectionModel = model;
if (selectionModel instanceof AbstractListingExtension) {
((AbstractListingExtension<T>) selectionModel).extend(this);
} else {
addExtension(selectionModel);
}
}
/**
* Sets the grid's selection mode.
* <p>
* The built-in selection models are:
* <ul>
* <li>{@link SelectionMode#SINGLE} -> {@link SingleSelectionModelImpl},
* <b>the default model</b></li>
* <li>{@link SelectionMode#MULTI} -> {@link MultiSelectionModelImpl}, with
* checkboxes in the first column for selection</li>
* <li>{@link SelectionMode#NONE} -> {@link NoSelectionModel}, preventing
* selection</li>
* </ul>
* <p>
* To use your custom selection model, you can use
* {@link #setSelectionModel(GridSelectionModel)}, see existing selection
* model implementations for example.
*
* @param selectionMode
* the selection mode to switch to, not {@code null}
* @return the used selection model
*
* @see SelectionMode
* @see GridSelectionModel
* @see #setSelectionModel(GridSelectionModel)
*/
public GridSelectionModel<T> setSelectionMode(SelectionMode selectionMode) {
Objects.requireNonNull(selectionMode, "Selection mode cannot be null.");
GridSelectionModel<T> model = selectionMode.createModel();
setSelectionModel(model);
return model;
}
/**
* This method is a shorthand that delegates to the currently set selection
* model.
*
* @see #getSelectionModel()
* @see GridSelectionModel
*/
public Set<T> getSelectedItems() {
return getSelectionModel().getSelectedItems();
}
/**
* This method is a shorthand that delegates to the currently set selection
* model.
*
* @see #getSelectionModel()
* @see GridSelectionModel
*/
public void select(T item) {
getSelectionModel().select(item);
}
/**
* This method is a shorthand that delegates to the currently set selection
* model.
*
* @see #getSelectionModel()
* @see GridSelectionModel
*/
public void deselect(T item) {
getSelectionModel().deselect(item);
}
/**
* This method is a shorthand that delegates to the currently set selection
* model.
*
* @see #getSelectionModel()
* @see GridSelectionModel
*/
public void deselectAll() {
getSelectionModel().deselectAll();
}
/**
* Adds a selection listener to the current selection model.
* <p>
* <em>NOTE:</em> If selection mode is switched with
* {@link #setSelectionMode(SelectionMode)}, then this listener is not
* triggered anymore when selection changes!
* <p>
* This is a shorthand for
* {@code grid.getSelectionModel().addSelectionListener()}. To get more
* detailed selection events, use {@link #getSelectionModel()} and either
* {@link SingleSelectionModel#addSingleSelectionListener(SingleSelectionListener)}
* or
* {@link MultiSelectionModel#addMultiSelectionListener(MultiSelectionListener)}
* depending on the used selection mode.
*
* @param listener
* the listener to add
* @return a registration handle to remove the listener
* @throws UnsupportedOperationException
* if selection has been disabled with
* {@link SelectionMode#NONE}
*/
public Registration addSelectionListener(SelectionListener<T> listener)
throws UnsupportedOperationException {
return getSelectionModel().addSelectionListener(listener);
}
/**
* Sort this Grid in ascending order by a specified column.
*
* @param column
* a column to sort against
*
*/
public void sort(Column<T, ?> column) {
sort(column, SortDirection.ASCENDING);
}
/**
* Sort this Grid in user-specified direction by a column.
*
* @param column
* a column to sort against
* @param direction
* a sort order value (ascending/descending)
*
*/
public void sort(Column<T, ?> column, SortDirection direction) {
setSortOrder(Collections
.singletonList(new GridSortOrder<>(column, direction)));
}
/**
* Sort this Grid in ascending order by a specified column defined by id.
*
* @param columnId
* the id of the column to sort against
*
* @see Column#setId(String)
*/
public void sort(String columnId) {
sort(columnId, SortDirection.ASCENDING);
}
/**
* Sort this Grid in a user-specified direction by a column defined by id.
*
* @param columnId
* the id of the column to sort against
* @param direction
* a sort order value (ascending/descending)
*
* @see Column#setId(String)
*/
public void sort(String columnId, SortDirection direction) {
sort(getColumnOrThrow(columnId), direction);
}
/**
* Clear the current sort order, and re-sort the grid.
*/
public void clearSortOrder() {
sortOrder.clear();
sort(false);
}
/**
* Sets the sort order to use.
*
* @param order
* a sort order list.
*
* @throws IllegalArgumentException
* if order is null
*/
public void setSortOrder(List<GridSortOrder<T>> order) {
setSortOrder(order, false);
}
/**
* Sets the sort order to use, given a {@link GridSortOrderBuilder}.
* Shorthand for {@code setSortOrder(builder.build())}.
*
* @see GridSortOrderBuilder
*
* @param builder
* the sort builder to retrieve the sort order from
* @throws NullPointerException
* if builder is null
*/
public void setSortOrder(GridSortOrderBuilder<T> builder) {
Objects.requireNonNull(builder, "Sort builder cannot be null");
setSortOrder(builder.build());
}
/**
* Adds a sort order change listener that gets notified when the sort order
* changes.
*
* @param listener
* the sort order change listener to add
*/
@Override
public Registration addSortListener(
SortListener<GridSortOrder<T>> listener) {
return addListener(SortEvent.class, listener, SORT_ORDER_CHANGE_METHOD);
}
/**
* Get the current sort order list.
*
* @return a sort order list
*/
public List<GridSortOrder<T>> getSortOrder() {
return Collections.unmodifiableList(sortOrder);
}
/**
* Scrolls to a certain item, using {@link ScrollDestination#ANY}.
* <p>
* If the item has visible details, its size will also be taken into
* account.
*
* @param row
* id of item to scroll to.
* @throws IllegalArgumentException
* if the provided id is not recognized by the data source.
*/
public void scrollTo(int row) throws IllegalArgumentException {
scrollTo(row, ScrollDestination.ANY);
}
/**
* Scrolls to a certain item, using user-specified scroll destination.
* <p>
* If the row has visible details, its size will also be taken into account.
*
* @param row
* id of item to scroll to.
* @param destination
* value specifying desired position of scrolled-to row, not
* {@code null}
* @throws IllegalArgumentException
* if the provided row is outside the item range
*/
public void scrollTo(int row, ScrollDestination destination) {
Objects.requireNonNull(destination,
"ScrollDestination can not be null");
if (row > getDataProvider().size(new Query())) {
throw new IllegalArgumentException("Row outside dataProvider size");
}
getRpcProxy(GridClientRpc.class).scrollToRow(row, destination);
}
/**
* Scrolls to the beginning of the first data row.
*/
public void scrollToStart() {
getRpcProxy(GridClientRpc.class).scrollToStart();
}
/**
* Scrolls to the end of the last data row.
*/
public void scrollToEnd() {
getRpcProxy(GridClientRpc.class).scrollToEnd();
}
@Override
protected GridState getState() {
return getState(true);
}
@Override
protected GridState getState(boolean markAsDirty) {
return (GridState) super.getState(markAsDirty);
}
/**
* Sets the column resize mode to use. The default mode is
* {@link ColumnResizeMode#ANIMATED}.
*
* @param mode
* a ColumnResizeMode value
* @since 7.7.5
*/
public void setColumnResizeMode(ColumnResizeMode mode) {
getState().columnResizeMode = mode;
}
/**
* Returns the current column resize mode. The default mode is
* {@link ColumnResizeMode#ANIMATED}.
*
* @return a ColumnResizeMode value
* @since 7.7.5
*/
public ColumnResizeMode getColumnResizeMode() {
return getState(false).columnResizeMode;
}
/**
* Creates a new Editor instance. Can be overridden to create a custom
* Editor. If the Editor is a {@link AbstractGridExtension}, it will be
* automatically added to {@link DataCommunicator}.
*
* @return editor
*/
protected Editor<T> createEditor() {
return new EditorImpl<>(propertySet);
}
private void addExtensionComponent(Component c) {
if (extensionComponents.add(c)) {
c.setParent(this);
markAsDirty();
}
}
private void removeExtensionComponent(Component c) {
if (extensionComponents.remove(c)) {
c.setParent(null);
markAsDirty();
}
}
private void fireColumnReorderEvent(boolean userOriginated) {
fireEvent(new ColumnReorderEvent(this, userOriginated));
}
private void fireColumnResizeEvent(Column<?, ?> column,
boolean userOriginated) {
fireEvent(new ColumnResizeEvent(this, column, userOriginated));
}
@Override
protected void readItems(Element design, DesignContext context) {
// Grid handles reading of items in Grid#readData
}
@Override
public DataProvider<T, ?> getDataProvider() {
return internalGetDataProvider();
}
@Override
public void setDataProvider(DataProvider<T, ?> dataProvider) {
internalSetDataProvider(dataProvider);
}
/**
* Sets a CallbackDataProvider using the given fetch items callback and a
* size callback.
* <p>
* This method is a shorthand for making a {@link CallbackDataProvider} that
* handles a partial {@link Query} object.
*
* @param fetchItems
* a callback for fetching items
* @param sizeCallback
* a callback for getting the count of items
*
* @see CallbackDataProvider
* @see #setDataProvider(DataProvider)
*/
public void setDataProvider(FetchItemsCallback<T> fetchItems,
SerializableSupplier<Integer> sizeCallback) {
internalSetDataProvider(
new CallbackDataProvider<>(
q -> fetchItems.fetchItems(q.getSortOrders(),
q.getOffset(), q.getLimit()),
q -> sizeCallback.get()));
}
@Override
protected void doReadDesign(Element design, DesignContext context) {
Attributes attrs = design.attributes();
if (design.hasAttr(DECLARATIVE_DATA_ITEM_TYPE)) {
String itemType = design.attr(DECLARATIVE_DATA_ITEM_TYPE);
setBeanType(itemType);
}
if (attrs.hasKey("selection-mode")) {
setSelectionMode(DesignAttributeHandler.readAttribute(
"selection-mode", attrs, SelectionMode.class));
}
Attributes attr = design.attributes();
if (attr.hasKey("selection-allowed")) {
setReadOnly(DesignAttributeHandler
.readAttribute("selection-allowed", attr, Boolean.class));
}
if (attrs.hasKey("rows")) {
setHeightByRows(DesignAttributeHandler.readAttribute("rows", attrs,
double.class));
}
readStructure(design, context);
// Read frozen columns after columns are read.
if (attrs.hasKey("frozen-columns")) {
setFrozenColumnCount(DesignAttributeHandler
.readAttribute("frozen-columns", attrs, int.class));
}
}
/**
* Sets the bean type to use for property mapping.
* <p>
* This method is responsible also for setting or updating the property set
* so that it matches the given bean type.
* <p>
* Protected mostly for Designer needs, typically should not be overridden
* or even called.
*
* @param beanTypeClassName
* the fully qualified class name of the bean type
*
* @since 8.0.3
*/
@SuppressWarnings("unchecked")
protected void setBeanType(String beanTypeClassName) {
setBeanType((Class<T>) resolveClass(beanTypeClassName));
}
/**
* Sets the bean type to use for property mapping.
* <p>
* This method is responsible also for setting or updating the property set
* so that it matches the given bean type.
* <p>
* Protected mostly for Designer needs, typically should not be overridden
* or even called.
*
* @param beanType
* the bean type class
*
* @since 8.0.3
*/
protected void setBeanType(Class<T> beanType) {
this.beanType = beanType;
setPropertySet(BeanPropertySet.get(beanType));
}
private Class<?> resolveClass(String qualifiedClassName) {
try {
Class<?> resolvedClass = Class.forName(qualifiedClassName, true,
VaadinServiceClassLoaderUtil.findDefaultClassLoader());
return resolvedClass;
} catch (ClassNotFoundException | SecurityException e) {
throw new IllegalArgumentException(
"Unable to find class " + qualifiedClassName, e);
}
}
@Override
protected void doWriteDesign(Element design, DesignContext designContext) {
Attributes attr = design.attributes();
if (this.beanType != null) {
design.attr(DECLARATIVE_DATA_ITEM_TYPE,
this.beanType.getCanonicalName());
}
DesignAttributeHandler.writeAttribute("selection-allowed", attr,
isReadOnly(), false, Boolean.class, designContext);
Attributes attrs = design.attributes();
Grid<?> defaultInstance = designContext.getDefaultInstance(this);
DesignAttributeHandler.writeAttribute("frozen-columns", attrs,
getFrozenColumnCount(), defaultInstance.getFrozenColumnCount(),
int.class, designContext);
if (HeightMode.ROW.equals(getHeightMode())) {
DesignAttributeHandler.writeAttribute("rows", attrs,
getHeightByRows(), defaultInstance.getHeightByRows(),
double.class, designContext);
}
SelectionMode mode = getSelectionMode();
if (mode != null) {
DesignAttributeHandler.writeAttribute("selection-mode", attrs, mode,
SelectionMode.SINGLE, SelectionMode.class, designContext);
}
writeStructure(design, designContext);
}
@Override
protected T deserializeDeclarativeRepresentation(String item) {
if (item == null) {
return super.deserializeDeclarativeRepresentation(
new String(UUID.randomUUID().toString()));
}
return super.deserializeDeclarativeRepresentation(new String(item));
}
@Override
protected boolean isReadOnly() {
SelectionMode selectionMode = getSelectionMode();
if (SelectionMode.SINGLE.equals(selectionMode)) {
return asSingleSelect().isReadOnly();
} else if (SelectionMode.MULTI.equals(selectionMode)) {
return asMultiSelect().isReadOnly();
}
return false;
}
@Override
protected void setReadOnly(boolean readOnly) {
SelectionMode selectionMode = getSelectionMode();
if (SelectionMode.SINGLE.equals(selectionMode)) {
asSingleSelect().setReadOnly(readOnly);
} else if (SelectionMode.MULTI.equals(selectionMode)) {
asMultiSelect().setReadOnly(readOnly);
}
}
private void readStructure(Element design, DesignContext context) {
if (design.children().isEmpty()) {
return;
}
if (design.children().size() > 1
|| !design.child(0).tagName().equals("table")) {
throw new DesignException(
"Grid needs to have a table element as its only child");
}
Element table = design.child(0);
Elements colgroups = table.getElementsByTag("colgroup");
if (colgroups.size() != 1) {
throw new DesignException(
"Table element in declarative Grid needs to have a"
+ " colgroup defining the columns used in Grid");
}
List<DeclarativeValueProvider<T>> providers = new ArrayList<>();
for (Element col : colgroups.get(0).getElementsByTag("col")) {
String id = DesignAttributeHandler.readAttribute("column-id",
col.attributes(), null, String.class);
// If there is a property with a matching name available,
// map to that
Optional<PropertyDefinition<T, ?>> property = propertySet
.getProperties().filter(p -> p.getName().equals(id))
.findFirst();
Column<T, ?> column;
if (property.isPresent()) {
column = addColumn(id);
} else {
DeclarativeValueProvider<T> provider = new DeclarativeValueProvider<>();
column = new Column<>(provider, new HtmlRenderer());
addColumn(getGeneratedIdentifier(), column);
if (id != null) {
column.setId(id);
}
providers.add(provider);
}
column.readDesign(col, context);
}
for (Element child : table.children()) {
if (child.tagName().equals("thead")) {
getHeader().readDesign(child, context);
} else if (child.tagName().equals("tbody")) {
readData(child, providers);
} else if (child.tagName().equals("tfoot")) {
getFooter().readDesign(child, context);
}
}
// Sync default header captions to column captions
if (getDefaultHeaderRow() != null) {
for (Column<T, ?> c : getColumns()) {
HeaderCell headerCell = getDefaultHeaderRow().getCell(c);
if (headerCell.getCellType() == GridStaticCellType.TEXT) {
c.setCaption(headerCell.getText());
}
}
}
}
protected void readData(Element body,
List<DeclarativeValueProvider<T>> providers) {
getSelectionModel().deselectAll();
List<T> items = new ArrayList<>();
List<T> selectedItems = new ArrayList<>();
for (Element row : body.children()) {
T item = deserializeDeclarativeRepresentation(row.attr("item"));
items.add(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++;
}
}
setItems(items);
selectedItems.forEach(getSelectionModel()::select);
}
private void writeStructure(Element design, DesignContext designContext) {
if (getColumns().isEmpty()) {
return;
}
Element tableElement = design.appendElement("table");
Element colGroup = tableElement.appendElement("colgroup");
getColumns().forEach(column -> column
.writeDesign(colGroup.appendElement("col"), designContext));
// Always write thead. Reads correctly when there no header rows
getHeader().writeDesign(tableElement.appendElement("thead"),
designContext);
if (designContext.shouldWriteData(this)) {
Element bodyElement = tableElement.appendElement("tbody");
writeData(bodyElement, designContext);
}
if (getFooter().getRowCount() > 0) {
getFooter().writeDesign(tableElement.appendElement("tfoot"),
designContext);
}
}
protected void writeData(Element body, DesignContext designContext) {
getDataProvider().fetch(new Query<>())
.forEach(item -> writeRow(body, item, designContext));
}
private void writeRow(Element container, T item, DesignContext context) {
Element tableRow = container.appendElement("tr");
tableRow.attr("item", serializeDeclarativeRepresentation(item));
if (getSelectionModel().isSelected(item)) {
tableRow.attr("selected", "");
}
for (Column<T, ?> column : getColumns()) {
Object value = column.valueProvider.apply(item);
tableRow.appendElement("td")
.append(Optional.ofNullable(value).map(Object::toString)
.map(DesignFormatter::encodeForTextNode)
.orElse(""));
}
}
private SelectionMode getSelectionMode() {
GridSelectionModel<T> selectionModel = getSelectionModel();
SelectionMode mode = null;
if (selectionModel.getClass().equals(SingleSelectionModelImpl.class)) {
mode = SelectionMode.SINGLE;
} else if (selectionModel.getClass()
.equals(MultiSelectionModelImpl.class)) {
mode = SelectionMode.MULTI;
} else if (selectionModel.getClass().equals(NoSelectionModel.class)) {
mode = SelectionMode.NONE;
}
return mode;
}
/**
* Sets a user-defined identifier for given column.
*
* @see Column#setId(String)
*
* @param column
* the column
* @param id
* the user-defined identifier
*/
protected void setColumnId(String id, Column<T, ?> column) {
if (columnIds.containsKey(id)) {
throw new IllegalArgumentException("Duplicate ID for columns");
}
columnIds.put(id, column);
}
@Override
protected Collection<String> getCustomAttributes() {
Collection<String> result = super.getCustomAttributes();
// "rename" for frozen column count
result.add("frozen-column-count");
result.add("frozen-columns");
// "rename" for height-mode
result.add("height-by-rows");
result.add("rows");
// add a selection-mode attribute
result.add("selection-mode");
return result;
}
/**
* Returns a column identified by its internal id. This id should not be
* confused with the user-defined identifier.
*
* @param columnId
* the internal id of column
* @return column identified by internal id
*/
protected Column<T, ?> getColumnByInternalId(String columnId) {
return columnKeys.get(columnId);
}
/**
* Returns the internal id for given column. This id should not be confused
* with the user-defined identifier.
*
* @param column
* the column
* @return internal id of given column
*/
protected String getInternalIdForColumn(Column<T, ?> column) {
return column.getInternalId();
}
private void setSortOrder(List<GridSortOrder<T>> order,
boolean userOriginated) {
Objects.requireNonNull(order, "Sort order list cannot be null");
sortOrder.clear();
if (order.isEmpty()) {
// Grid is not sorted anymore.
getDataCommunicator().setBackEndSorting(Collections.emptyList());
getDataCommunicator().setInMemorySorting(null);
fireEvent(new SortEvent<>(this, new ArrayList<>(sortOrder),
userOriginated));
return;
}
sortOrder.addAll(order);
sort(userOriginated);
}
private void sort(boolean userOriginated) {
// Set sort orders
// In-memory comparator
getDataCommunicator().setInMemorySorting(createSortingComparator());
// Back-end sort properties
List<QuerySortOrder> sortProperties = new ArrayList<>();
sortOrder.stream().map(
order -> order.getSorted().getSortOrder(order.getDirection()))
.forEach(s -> s.forEach(sortProperties::add));
getDataCommunicator().setBackEndSorting(sortProperties);
// Close grid editor if it's open.
if (getEditor().isOpen()) {
getEditor().cancel();
}
fireEvent(new SortEvent<>(this, new ArrayList<>(sortOrder),
userOriginated));
}
/**
* Creates a comparator for grid to sort rows.
*
* @return the comparator based on column sorting information.
*/
protected SerializableComparator<T> createSortingComparator() {
BinaryOperator<SerializableComparator<T>> operator = (comparator1,
comparator2) -> {
/*
* thenComparing is defined to return a serializable comparator as
* long as both original comparators are also serializable
*/
return comparator1.thenComparing(comparator2)::compare;
};
return sortOrder.stream().map(
order -> order.getSorted().getComparator(order.getDirection()))
.reduce((x, y) -> 0, operator);
}
}