/* * Copyright 2000-2014 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.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; import com.google.gwt.thirdparty.guava.common.collect.Sets; import com.google.gwt.thirdparty.guava.common.collect.Sets.SetView; import com.vaadin.data.Container; import com.vaadin.data.Container.Indexed; import com.vaadin.data.Container.PropertySetChangeEvent; import com.vaadin.data.Container.PropertySetChangeListener; import com.vaadin.data.Container.PropertySetChangeNotifier; import com.vaadin.data.Container.Sortable; import com.vaadin.data.Item; import com.vaadin.data.Property; import com.vaadin.data.RpcDataProviderExtension; import com.vaadin.data.RpcDataProviderExtension.DataProviderKeyMapper; import com.vaadin.data.fieldgroup.FieldGroup; import com.vaadin.data.fieldgroup.FieldGroup.BindException; import com.vaadin.data.fieldgroup.FieldGroup.CommitException; import com.vaadin.data.fieldgroup.FieldGroupFieldFactory; import com.vaadin.data.sort.Sort; import com.vaadin.data.sort.SortOrder; import com.vaadin.data.util.IndexedContainer; import com.vaadin.data.util.converter.Converter; import com.vaadin.data.util.converter.ConverterUtil; import com.vaadin.event.SelectionChangeEvent; import com.vaadin.event.SelectionChangeEvent.SelectionChangeListener; import com.vaadin.event.SelectionChangeEvent.SelectionChangeNotifier; import com.vaadin.event.SortOrderChangeEvent; import com.vaadin.event.SortOrderChangeEvent.SortOrderChangeListener; import com.vaadin.event.SortOrderChangeEvent.SortOrderChangeNotifier; import com.vaadin.server.AbstractClientConnector; import com.vaadin.server.AbstractExtension; import com.vaadin.server.ErrorHandler; import com.vaadin.server.ErrorMessage; import com.vaadin.server.JsonCodec; import com.vaadin.server.KeyMapper; import com.vaadin.server.VaadinSession; import com.vaadin.shared.ui.grid.EditorRowClientRpc; import com.vaadin.shared.ui.grid.EditorRowServerRpc; import com.vaadin.shared.ui.grid.GridClientRpc; import com.vaadin.shared.ui.grid.GridColumnState; import com.vaadin.shared.ui.grid.GridServerRpc; import com.vaadin.shared.ui.grid.GridState; import com.vaadin.shared.ui.grid.GridState.SharedSelectionMode; import com.vaadin.shared.ui.grid.GridStaticCellType; import com.vaadin.shared.ui.grid.GridStaticSectionState; import com.vaadin.shared.ui.grid.GridStaticSectionState.CellState; import com.vaadin.shared.ui.grid.GridStaticSectionState.RowState; import com.vaadin.shared.ui.grid.HeightMode; import com.vaadin.shared.ui.grid.ScrollDestination; import com.vaadin.shared.ui.grid.SortDirection; import com.vaadin.shared.util.SharedUtil; import com.vaadin.ui.renderer.Renderer; import com.vaadin.ui.renderer.TextRenderer; import com.vaadin.util.ReflectTools; import elemental.json.Json; import elemental.json.JsonArray; import elemental.json.JsonObject; import elemental.json.JsonValue; /** * A grid component for displaying tabular data. *

* Grid is always bound to a {@link Container.Indexed}, but is not a * {@code Container} of any kind in of itself. The contents of the given * Container is displayed with the help of {@link Renderer Renderers}. * *

Headers and Footers

*

* * *

Converters and Renderers

*

* Each column has its own {@link Renderer} that displays data into something * that can be displayed in the browser. That data is first converted with a * {@link com.vaadin.data.util.converter.Converter Converter} into something * that the Renderer can process. This can also be an implicit step - if a * column has a simple data type, like a String, no explicit assignment is * needed. *

* Usually a renderer takes some kind of object, and converts it into a * HTML-formatted string. *

*

 * Grid grid = new Grid(myContainer);
 * Column column = grid.getColumn(STRING_DATE_PROPERTY);
 * column.setConverter(new StringToDateConverter());
 * column.setRenderer(new MyColorfulDateRenderer());
 * 
* *

Lazy Loading

*

* The data is accessed as it is needed by Grid and not any sooner. In other * words, if the given Container is huge, but only the first few rows are * displayed to the user, only those (and a few more, for caching purposes) are * accessed. * *

Selection Modes and Models

*

* Grid supports three selection {@link SelectionMode modes} (single, * multi, none), and comes bundled with one * {@link SelectionModel model} for each of the modes. The distinction * between a selection mode and selection model is as follows: a mode * essentially says whether you can have one, many or no rows selected. The * model, however, has the behavioral details of each. A single selection model * may require that the user deselects one row before selecting another one. A * variant of a multiselect might have a configurable maximum of rows that may * be selected. And so on. *

*

 * Grid grid = new Grid(myContainer);
 * 
 * // uses the bundled SingleSelectionModel class
 * grid.setSelectionMode(SelectionMode.SINGLE);
 * 
 * // changes the behavior to a custom selection model
 * grid.setSelectionModel(new MyTwoSelectionModel());
 * 
* * @since * @author Vaadin Ltd */ public class Grid extends AbstractComponent implements SelectionChangeNotifier, SortOrderChangeNotifier, SelectiveRenderer { /** * Selection modes representing built-in {@link SelectionModel * SelectionModels} that come bundled with {@link Grid}. *

* Passing one of these enums into * {@link Grid#setSelectionMode(SelectionMode)} is equivalent to calling * {@link Grid#setSelectionModel(SelectionModel)} with one of the built-in * implementations of {@link SelectionModel}. * * @see Grid#setSelectionMode(SelectionMode) * @see Grid#setSelectionModel(SelectionModel) */ public enum SelectionMode { /** A SelectionMode that maps to {@link SingleSelectionModel} */ SINGLE { @Override protected SelectionModel createModel() { return new SingleSelectionModel(); } }, /** A SelectionMode that maps to {@link MultiSelectionModel} */ MULTI { @Override protected SelectionModel createModel() { return new MultiSelectionModel(); } }, /** A SelectionMode that maps to {@link NoSelectionModel} */ NONE { @Override protected SelectionModel createModel() { return new NoSelectionModel(); } }; protected abstract SelectionModel createModel(); } /** * The server-side interface that controls Grid's selection state. * * @since * @author Vaadin Ltd */ public interface SelectionModel extends Serializable { /** * Checks whether an item is selected or not. * * @param itemId * the item id to check for * @return true iff the item is selected */ boolean isSelected(Object itemId); /** * Returns a collection of all the currently selected itemIds. * * @return a collection of all the currently selected itemIds */ Collection getSelectedRows(); /** * Injects the current {@link Grid} instance into the SelectionModel. *

* Note: This method should not be called manually. * * @param grid * the Grid in which the SelectionModel currently is, or * null when a selection model is being detached * from a Grid. */ void setGrid(Grid grid); /** * Resets the SelectiomModel to an initial state. *

* Most often this means that the selection state is cleared, but * implementations are free to interpret the "initial state" as they * wish. Some, for example, may want to keep the first selected item as * selected. */ void reset(); /** * A SelectionModel that supports multiple selections to be made. *

* This interface has a contract of having the same behavior, no matter * how the selection model is interacted with. In other words, if * something is forbidden to do in e.g. the user interface, it must also * be forbidden to do in the server-side and client-side APIs. */ public interface Multi extends SelectionModel { /** * Marks items as selected. *

* This method does not clear any previous selection state, only * adds to it. * * @param itemIds * the itemId(s) to mark as selected * @return true if the selection state changed. * false if all the given itemIds already were * selected * @throws IllegalArgumentException * if the itemIds varargs array is * null or given itemIds don't exist in the * container of Grid * @see #deselect(Object...) */ boolean select(Object... itemIds) throws IllegalArgumentException; /** * Marks items as selected. *

* This method does not clear any previous selection state, only * adds to it. * * @param itemIds * the itemIds to mark as selected * @return true if the selection state changed. * false if all the given itemIds already were * selected * @throws IllegalArgumentException * if itemIds is null or given * itemIds don't exist in the container of Grid * @see #deselect(Collection) */ boolean select(Collection itemIds) throws IllegalArgumentException; /** * Marks items as deselected. * * @param itemIds * the itemId(s) to remove from being selected * @return true if the selection state changed. * false if none the given itemIds were * selected previously * @throws IllegalArgumentException * if the itemIds varargs array is * null * @see #select(Object...) */ boolean deselect(Object... itemIds) throws IllegalArgumentException; /** * Marks items as deselected. * * @param itemIds * the itemId(s) to remove from being selected * @return true if the selection state changed. * false if none the given itemIds were * selected previously * @throws IllegalArgumentException * if itemIds is null * @see #select(Collection) */ boolean deselect(Collection itemIds) throws IllegalArgumentException; /** * Marks all the items in the current Container as selected * * @return true iff some items were previously not * selected * @see #deselectAll() */ boolean selectAll(); /** * Marks all the items in the current Container as deselected * * @return true iff some items were previously selected * @see #selectAll() */ boolean deselectAll(); } /** * A SelectionModel that supports for only single rows to be selected at * a time. *

* This interface has a contract of having the same behavior, no matter * how the selection model is interacted with. In other words, if * something is forbidden to do in e.g. the user interface, it must also * be forbidden to do in the server-side and client-side APIs. */ public interface Single extends SelectionModel { /** * Marks an item as selected. * * @param itemIds * the itemId to mark as selected; null for * deselect * @return true if the selection state changed. * false if the itemId already was selected * @throws IllegalStateException * if the selection was illegal. One such reason might * be that the given id was null, indicating a deselect, * but implementation doesn't allow deselecting. * re-selecting something * @throws IllegalArgumentException * if given itemId does not exist in the container of * Grid */ boolean select(Object itemId) throws IllegalStateException, IllegalArgumentException; /** * Gets the item id of the currently selected item. * * @return the item id of the currently selected item, or * null if nothing is selected */ Object getSelectedRow(); } /** * A SelectionModel that does not allow for rows to be selected. *

* This interface has a contract of having the same behavior, no matter * how the selection model is interacted with. In other words, if the * developer is unable to select something programmatically, it is not * allowed for the end-user to select anything, either. */ public interface None extends SelectionModel { /** * {@inheritDoc} * * @return always false. */ @Override public boolean isSelected(Object itemId); /** * {@inheritDoc} * * @return always an empty collection. */ @Override public Collection getSelectedRows(); } } /** * A base class for SelectionModels that contains some of the logic that is * reusable. * * @since * @author Vaadin Ltd */ public static abstract class AbstractSelectionModel implements SelectionModel { protected final LinkedHashSet selection = new LinkedHashSet(); protected Grid grid = null; @Override public boolean isSelected(final Object itemId) { return selection.contains(itemId); } @Override public Collection getSelectedRows() { return new ArrayList(selection); } @Override public void setGrid(final Grid grid) { this.grid = grid; } /** * Sanity check for existence of item id. * * @param itemId * item id to be selected / deselected * * @throws IllegalArgumentException * if item Id doesn't exist in the container of Grid */ protected void checkItemIdExists(Object itemId) throws IllegalArgumentException { if (!grid.getContainerDataSource().containsId(itemId)) { throw new IllegalArgumentException("Given item id (" + itemId + ") does not exist in the container"); } } /** * Sanity check for existence of item ids in given collection. * * @param itemIds * item id collection to be selected / deselected * * @throws IllegalArgumentException * if at least one item id doesn't exist in the container of * Grid */ protected void checkItemIdsExist(Collection itemIds) throws IllegalArgumentException { for (Object itemId : itemIds) { checkItemIdExists(itemId); } } /** * Fires a {@link SelectionChangeEvent} to all the * {@link SelectionChangeListener SelectionChangeListeners} currently * added to the Grid in which this SelectionModel is. *

* Note that this is only a helper method, and routes the call all the * way to Grid. A {@link SelectionModel} is not a * {@link SelectionChangeNotifier} * * @param oldSelection * the complete {@link Collection} of the itemIds that were * selected before this event happened * @param newSelection * the complete {@link Collection} of the itemIds that are * selected after this event happened */ protected void fireSelectionChangeEvent( final Collection oldSelection, final Collection newSelection) { grid.fireSelectionChangeEvent(oldSelection, newSelection); } } /** * A default implementation of a {@link SelectionModel.Single} * * @since * @author Vaadin Ltd */ public static class SingleSelectionModel extends AbstractSelectionModel implements SelectionModel.Single { @Override public boolean select(final Object itemId) { if (itemId == null) { return deselect(getSelectedRow()); } checkItemIdExists(itemId); final Object selectedRow = getSelectedRow(); final boolean modified = selection.add(itemId); if (modified) { final Collection deselected; if (selectedRow != null) { deselectInternal(selectedRow, false); deselected = Collections.singleton(selectedRow); } else { deselected = Collections.emptySet(); } fireSelectionChangeEvent(deselected, selection); } return modified; } private boolean deselect(final Object itemId) { return deselectInternal(itemId, true); } private boolean deselectInternal(final Object itemId, boolean fireEventIfNeeded) { final boolean modified = selection.remove(itemId); if (fireEventIfNeeded && modified) { fireSelectionChangeEvent(Collections.singleton(itemId), Collections.emptySet()); } return modified; } @Override public Object getSelectedRow() { if (selection.isEmpty()) { return null; } else { return selection.iterator().next(); } } /** * Resets the selection state. *

* If an item is selected, it will become deselected. */ @Override public void reset() { deselect(getSelectedRow()); } } /** * A default implementation for a {@link SelectionModel.None} * * @since * @author Vaadin Ltd */ public static class NoSelectionModel implements SelectionModel.None { @Override public void setGrid(final Grid grid) { // NOOP, not needed for anything } @Override public boolean isSelected(final Object itemId) { return false; } @Override public Collection getSelectedRows() { return Collections.emptyList(); } /** * Semantically resets the selection model. *

* Effectively a no-op. */ @Override public void reset() { // NOOP } } /** * A default implementation of a {@link SelectionModel.Multi} * * @since * @author Vaadin Ltd */ public static class MultiSelectionModel extends AbstractSelectionModel implements SelectionModel.Multi { /** * The default selection size limit. * * @see #setSelectionLimit(int) */ public static final int DEFAULT_MAX_SELECTIONS = 1000; private int selectionLimit = DEFAULT_MAX_SELECTIONS; @Override public boolean select(final Object... itemIds) throws IllegalArgumentException { if (itemIds != null) { // select will fire the event return select(Arrays.asList(itemIds)); } else { throw new IllegalArgumentException( "Vararg array of itemIds may not be null"); } } /** * {@inheritDoc} *

* All items might not be selected if the limit set using * {@link #setSelectionLimit(int)} is exceeded. */ @Override public boolean select(final Collection itemIds) throws IllegalArgumentException { if (itemIds == null) { throw new IllegalArgumentException("itemIds may not be null"); } // Sanity check checkItemIdsExist(itemIds); final boolean selectionWillChange = !selection.containsAll(itemIds) && selection.size() < selectionLimit; if (selectionWillChange) { final HashSet oldSelection = new HashSet( selection); if (selection.size() + itemIds.size() >= selectionLimit) { // Add one at a time if there's a risk of overflow Iterator iterator = itemIds.iterator(); while (iterator.hasNext() && selection.size() < selectionLimit) { selection.add(iterator.next()); } } else { selection.addAll(itemIds); } fireSelectionChangeEvent(oldSelection, selection); } return selectionWillChange; } /** * Sets the maximum number of rows that can be selected at once. This is * a mechanism to prevent exhausting server memory in situations where * users select lots of rows. If the limit is reached, newly selected * rows will not become recorded. *

* Old selections are not discarded if the current number of selected * row exceeds the new limit. *

* The default limit is {@value #DEFAULT_MAX_SELECTIONS} rows. * * @param selectionLimit * the non-negative selection limit to set * @throws IllegalArgumentException * if the limit is negative */ public void setSelectionLimit(int selectionLimit) { if (selectionLimit < 0) { throw new IllegalArgumentException( "The selection limit must be non-negative"); } this.selectionLimit = selectionLimit; } /** * Gets the selection limit. * * @see #setSelectionLimit(int) * * @return the selection limit */ public int getSelectionLimit() { return selectionLimit; } @Override public boolean deselect(final Object... itemIds) throws IllegalArgumentException { if (itemIds != null) { // deselect will fire the event return deselect(Arrays.asList(itemIds)); } else { throw new IllegalArgumentException( "Vararg array of itemIds may not be null"); } } @Override public boolean deselect(final Collection itemIds) throws IllegalArgumentException { if (itemIds == null) { throw new IllegalArgumentException("itemIds may not be null"); } final boolean hasCommonElements = !Collections.disjoint(itemIds, selection); if (hasCommonElements) { final HashSet oldSelection = new HashSet( selection); selection.removeAll(itemIds); fireSelectionChangeEvent(oldSelection, selection); } return hasCommonElements; } @Override public boolean selectAll() { // select will fire the event final Indexed container = grid.getContainerDataSource(); if (container != null) { return select(container.getItemIds()); } else if (selection.isEmpty()) { return false; } else { /* * this should never happen (no container but has a selection), * but I guess the only theoretically correct course of * action... */ return deselectAll(); } } @Override public boolean deselectAll() { // deselect will fire the event return deselect(getSelectedRows()); } /** * {@inheritDoc} *

* The returned Collection is in order of selection * – the item that was first selected will be first in the * collection, and so on. Should an item have been selected twice * without being deselected in between, it will have remained in its * original position. */ @Override public Collection getSelectedRows() { // overridden only for JavaDoc return super.getSelectedRows(); } /** * Resets the selection model. *

* Equivalent to calling {@link #deselectAll()} */ @Override public void reset() { deselectAll(); } } /** * Callback interface for generating custom style names for data rows and * cells. * * @see Grid#setCellStyleGenerator(CellStyleGenerator) */ public interface CellStyleGenerator extends Serializable { /** * Called by Grid to generate a style name for a row or cell element. * Row styles are generated when the column parameter is * null, otherwise a cell style is generated. * * @param grid * the source grid * @param itemId * the itemId of the target row * @param propertyId * the propertyId of the target cell, null when * getting row style * @return the style name to add to this cell or row element, or * null to not set any style */ public abstract String getStyle(Grid grid, Object itemId, Object propertyId); } /** * Abstract base class for Grid header and footer sections. * * @param * the type of the rows in the section */ protected static abstract class StaticSection> implements Serializable { /** * Abstract base class for Grid header and footer rows. * * @param * the type of the cells in the row */ abstract static class StaticRow implements Serializable { private RowState rowState = new RowState(); protected StaticSection section; private Map cells = new LinkedHashMap(); private Map, CELLTYPE> cellGroups = new HashMap, CELLTYPE>(); protected StaticRow(StaticSection section) { this.section = section; } protected void addCell(Object propertyId) { CELLTYPE cell = createCell(); cell.setColumnId(section.grid.getColumn(propertyId).getState().id); cells.put(propertyId, cell); rowState.cells.add(cell.getCellState()); } protected void removeCell(Object propertyId) { CELLTYPE cell = cells.remove(propertyId); if (cell != null) { Set cellGroupForCell = getCellGroupForCell(cell); if (cellGroupForCell != null) { removeCellFromGroup(cell, cellGroupForCell); } rowState.cells.remove(cell.getCellState()); } } private void removeCellFromGroup(CELLTYPE cell, Set cellGroup) { String columnId = cell.getColumnId(); for (Set group : rowState.cellGroups.keySet()) { if (group.contains(columnId)) { if (group.size() > 2) { // Update map key correctly CELLTYPE mergedCell = cellGroups.remove(cellGroup); cellGroup.remove(cell); cellGroups.put(cellGroup, mergedCell); group.remove(columnId); } else { rowState.cellGroups.remove(group); cellGroups.remove(cellGroup); } return; } } } /** * Creates and returns a new instance of the cell type. * * @return the created cell */ protected abstract CELLTYPE createCell(); protected RowState getRowState() { return rowState; } /** * Returns the cell for the given property id on this row. If the * column is merged returned cell is the cell for the whole group. * * @param propertyId * the property id of the column * @return the cell for the given property, merged cell for merged * properties, null if not found */ public CELLTYPE getCell(Object propertyId) { CELLTYPE cell = cells.get(propertyId); Set cellGroup = getCellGroupForCell(cell); if (cellGroup != null) { cell = cellGroups.get(cellGroup); } return cell; } /** * Merges columns cells in a row * * @param propertyIds * The property ids of columns to merge * @return The remaining visible cell after the merge */ public CELLTYPE join(Object... propertyIds) { assert propertyIds.length > 1 : "You need to merge at least 2 properties"; Set cells = new HashSet(); for (int i = 0; i < propertyIds.length; ++i) { cells.add(getCell(propertyIds[i])); } return join(cells); } /** * Merges columns cells in a row * * @param cells * The cells to merge. Must be from the same row. * @return The remaining visible cell after the merge */ public CELLTYPE join(CELLTYPE... cells) { assert cells.length > 1 : "You need to merge at least 2 cells"; return join(new HashSet(Arrays.asList(cells))); } protected CELLTYPE join(Set cells) { for (CELLTYPE cell : cells) { if (getCellGroupForCell(cell) != null) { throw new IllegalArgumentException( "Cell already merged"); } else if (!this.cells.containsValue(cell)) { throw new IllegalArgumentException( "Cell does not exist on this row"); } } // Create new cell data for the group CELLTYPE newCell = createCell(); Set columnGroup = new HashSet(); for (CELLTYPE cell : cells) { columnGroup.add(cell.getColumnId()); } rowState.cellGroups.put(columnGroup, newCell.getCellState()); cellGroups.put(cells, newCell); return newCell; } private Set getCellGroupForCell(CELLTYPE cell) { for (Set group : cellGroups.keySet()) { if (group.contains(cell)) { return group; } } return null; } /** * Returns the custom style name for this row. * * @return the style name or null if no style name has been set */ public String getStyleName() { return getRowState().styleName; } /** * Sets a custom style name for this row. * * @param styleName * the style name to set or null to not use any style * name */ public void setStyleName(String styleName) { getRowState().styleName = styleName; } } /** * A header or footer cell. Has a simple textual caption. */ abstract static class StaticCell implements Serializable { private CellState cellState = new CellState(); private StaticRow row; protected StaticCell(StaticRow row) { this.row = row; } void setColumnId(String id) { cellState.columnId = id; } String getColumnId() { return cellState.columnId; } /** * Gets the row where this cell is. * * @return row for this cell */ public StaticRow getRow() { return row; } protected CellState getCellState() { return cellState; } /** * Sets the text displayed in this cell. * * @param text * a plain text caption */ public void setText(String text) { cellState.text = text; cellState.type = GridStaticCellType.TEXT; row.section.markAsDirty(); } /** * Returns the text displayed in this cell. * * @return the plain text caption */ public String getText() { if (cellState.type != GridStaticCellType.TEXT) { throw new IllegalStateException( "Cannot fetch Text from a cell with type " + cellState.type); } return cellState.text; } /** * Returns the HTML content displayed in this cell. * * @return the html * */ public String getHtml() { if (cellState.type != GridStaticCellType.HTML) { throw new IllegalStateException( "Cannot fetch HTML from a cell with type " + cellState.type); } return cellState.html; } /** * Sets the HTML content displayed in this cell. * * @param html * the html to set */ public void setHtml(String html) { cellState.html = html; cellState.type = GridStaticCellType.HTML; row.section.markAsDirty(); } /** * Returns the component displayed in this cell. * * @return the component */ public Component getComponent() { if (cellState.type != GridStaticCellType.WIDGET) { throw new IllegalStateException( "Cannot fetch Component from a cell with type " + cellState.type); } return (Component) cellState.connector; } /** * Sets the component displayed in this cell. * * @param component * the component to set */ public void setComponent(Component component) { component.setParent(row.section.grid); cellState.connector = component; cellState.type = GridStaticCellType.WIDGET; row.section.markAsDirty(); } /** * Returns the custom style name for this cell. * * @return the style name or null if no style name has been set */ public String getStyleName() { return cellState.styleName; } /** * Sets a custom style name for this cell. * * @param styleName * the style name to set or null to not use any style * name */ public void setStyleName(String styleName) { cellState.styleName = styleName; row.section.markAsDirty(); } } protected Grid grid; protected List rows = new ArrayList(); /** * Sets the visibility of the whole section. * * @param visible * true to show this section, false to hide */ public void setVisible(boolean visible) { if (getSectionState().visible != visible) { getSectionState().visible = visible; markAsDirty(); } } /** * Returns the visibility of this section. * * @return true if visible, false otherwise. */ public boolean isVisible() { return getSectionState().visible; } /** * Removes the row at the given position. * * @param index * the position of the row * * @throws IllegalArgumentException * if no row exists at given index * @see #removeRow(StaticRow) * @see #addRowAt(int) * @see #appendRow() * @see #prependRow() */ public ROWTYPE removeRow(int rowIndex) { if (rowIndex >= rows.size() || rowIndex < 0) { throw new IllegalArgumentException("No row at given index " + rowIndex); } ROWTYPE row = rows.remove(rowIndex); getSectionState().rows.remove(rowIndex); markAsDirty(); return row; } /** * Removes the given row from the section. * * @param row * the row to be removed * * @throws IllegalArgumentException * if the row does not exist in this section * @see #removeRow(int) * @see #addRowAt(int) * @see #appendRow() * @see #prependRow() */ public void removeRow(ROWTYPE row) { try { removeRow(rows.indexOf(row)); } catch (IndexOutOfBoundsException e) { throw new IllegalArgumentException( "Section does not contain the given row"); } } /** * Gets row at given index. * * @param rowIndex * 0 based index for row. Counted from top to bottom * @return row at given index */ public ROWTYPE getRow(int rowIndex) { if (rowIndex >= rows.size() || rowIndex < 0) { throw new IllegalArgumentException("No row at given index " + rowIndex); } return rows.get(rowIndex); } /** * Adds a new row at the top of this section. * * @return the new row * @see #appendRow() * @see #addRowAt(int) * @see #removeRow(StaticRow) * @see #removeRow(int) */ public ROWTYPE prependRow() { return addRowAt(0); } /** * Adds a new row at the bottom of this section. * * @return the new row * @see #prependRow() * @see #addRowAt(int) * @see #removeRow(StaticRow) * @see #removeRow(int) */ public ROWTYPE appendRow() { return addRowAt(rows.size()); } /** * Inserts a new row at the given position. * * @param index * the position at which to insert the row * @return the new row * * @throws IndexOutOfBoundsException * if the index is out of bounds * @see #appendRow() * @see #prependRow() * @see #removeRow(StaticRow) * @see #removeRow(int) */ public ROWTYPE addRowAt(int index) { if (index > rows.size() || index < 0) { throw new IllegalArgumentException( "Unable to add row at index " + index); } ROWTYPE row = createRow(); rows.add(index, row); getSectionState().rows.add(index, row.getRowState()); Indexed dataSource = grid.getContainerDataSource(); for (Object id : dataSource.getContainerPropertyIds()) { row.addCell(id); } markAsDirty(); return row; } /** * Gets the amount of rows in this section. * * @return row count */ public int getRowCount() { return rows.size(); } protected abstract GridStaticSectionState getSectionState(); protected abstract ROWTYPE createRow(); /** * Informs the grid that state has changed and it should be redrawn. */ protected void markAsDirty() { grid.markAsDirty(); } /** * Removes a column for given property id from the section. * * @param propertyId * property to be removed */ protected void removeColumn(Object propertyId) { for (ROWTYPE row : rows) { row.removeCell(propertyId); } } /** * Adds a column for given property id to the section. * * @param propertyId * property to be added */ protected void addColumn(Object propertyId) { for (ROWTYPE row : rows) { row.addCell(propertyId); } } /** * Performs a sanity check that section is in correct state. * * @throws IllegalStateException * if merged cells are not i n continuous range */ protected void sanityCheck() throws IllegalStateException { List columnOrder = grid.getState().columnOrder; for (ROWTYPE row : rows) { for (Set cellGroup : row.getRowState().cellGroups .keySet()) { if (!checkCellGroupAndOrder(columnOrder, cellGroup)) { throw new IllegalStateException( "Not all merged cells were in a continuous range."); } } } } private boolean checkCellGroupAndOrder(List columnOrder, Set cellGroup) { if (!columnOrder.containsAll(cellGroup)) { return false; } for (int i = 0; i < columnOrder.size(); ++i) { if (!cellGroup.contains(columnOrder.get(i))) { continue; } for (int j = 1; j < cellGroup.size(); ++j) { if (!cellGroup.contains(columnOrder.get(i + j))) { return false; } } return true; } return false; } } /** * Represents the header section of a Grid. */ protected static class Header extends StaticSection { private HeaderRow defaultRow = null; private final GridStaticSectionState headerState = new GridStaticSectionState(); protected Header(Grid grid) { this.grid = grid; grid.getState(true).header = headerState; HeaderRow row = createRow(); rows.add(row); setDefaultRow(row); getSectionState().rows.add(row.getRowState()); } /** * Sets the default row of this header. The default row is a special * header row providing a user interface for sorting columns. * * @param row * the new default row, or null for no default row * * @throws IllegalArgumentException * this header does not contain the row */ public void setDefaultRow(HeaderRow row) { if (row == defaultRow) { return; } if (row != null && !rows.contains(row)) { throw new IllegalArgumentException( "Cannot set a default row that does not exist in the section"); } if (defaultRow != null) { defaultRow.setDefaultRow(false); } if (row != null) { row.setDefaultRow(true); } defaultRow = row; markAsDirty(); } /** * Returns the current default row of this header. The default row is a * special header row providing a user interface for sorting columns. * * @return the default row or null if no default row set */ public HeaderRow getDefaultRow() { return defaultRow; } @Override protected GridStaticSectionState getSectionState() { return headerState; } @Override protected HeaderRow createRow() { return new HeaderRow(this); } @Override public HeaderRow removeRow(int rowIndex) { HeaderRow row = super.removeRow(rowIndex); if (row == defaultRow) { // Default Header Row was just removed. setDefaultRow(null); } return row; } @Override protected void sanityCheck() throws IllegalStateException { super.sanityCheck(); boolean hasDefaultRow = false; for (HeaderRow row : rows) { if (row.getRowState().defaultRow) { if (!hasDefaultRow) { hasDefaultRow = true; } else { throw new IllegalStateException( "Multiple default rows in header"); } } } } } /** * Represents a header row in Grid. */ public static class HeaderRow extends StaticSection.StaticRow { protected HeaderRow(StaticSection section) { super(section); } private void setDefaultRow(boolean value) { getRowState().defaultRow = value; } @Override protected HeaderCell createCell() { return new HeaderCell(this); } } /** * Represents a header cell in Grid. Can be a merged cell for multiple * columns. */ public static class HeaderCell extends StaticSection.StaticCell { protected HeaderCell(HeaderRow row) { super(row); } } /** * Represents the footer section of a Grid. By default Footer is not * visible. */ protected static class Footer extends StaticSection { private final GridStaticSectionState footerState = new GridStaticSectionState(); protected Footer(Grid grid) { this.grid = grid; grid.getState(true).footer = footerState; } @Override protected GridStaticSectionState getSectionState() { return footerState; } @Override protected FooterRow createRow() { return new FooterRow(this); } @Override protected void sanityCheck() throws IllegalStateException { super.sanityCheck(); } } /** * Represents a footer row in Grid. */ public static class FooterRow extends StaticSection.StaticRow { protected FooterRow(StaticSection section) { super(section); } @Override protected FooterCell createCell() { return new FooterCell(this); } } /** * Represents a footer cell in Grid. */ public static class FooterCell extends StaticSection.StaticCell { protected FooterCell(FooterRow row) { super(row); } } /** * A column in the grid. Can be obtained by calling * {@link Grid#getColumn(Object propertyId)}. */ public static class Column implements Serializable { /** * The state of the column shared to the client */ private final GridColumnState state; /** * The grid this column is associated with */ private final Grid grid; /** * Backing property for column */ private final Object columnProperty; private Converter converter; /** * A check for allowing the {@link #Column(Grid, GridColumnState) * constructor} to call {@link #setConverter(Converter)} with a * null, even if model and renderer aren't compatible. */ private boolean isFirstConverterAssignment = true; /** * Internally used constructor. * * @param grid * The grid this column belongs to. Should not be null. * @param state * the shared state of this column * @param columnProperty * the backing property id for this column */ Column(Grid grid, GridColumnState state, Object columnProperty) { this.grid = grid; this.state = state; this.columnProperty = columnProperty; internalSetRenderer(new TextRenderer()); } /** * Returns the serializable state of this column that is sent to the * client side connector. * * @return the internal state of the column */ GridColumnState getState() { return state; } /** * Return the property id for the backing property of this Column * * @return property id */ public Object getColumnProperty() { return columnProperty; } /** * Returns the caption of the header. By default the header caption is * the property id of the column. * * @return the text in the default row of header, null if no default row * * @throws IllegalStateException * if the column no longer is attached to the grid */ public String getHeaderCaption() throws IllegalStateException { checkColumnIsAttached(); HeaderRow row = grid.getHeader().getDefaultRow(); if (row != null) { return row.getCell(grid.getPropertyIdByColumnId(state.id)) .getText(); } return null; } /** * Sets the caption of the header. * * @param caption * the text to show in the caption * @return the column itself * * @throws IllegalStateException * if the column is no longer attached to any grid */ public Column setHeaderCaption(String caption) throws IllegalStateException { checkColumnIsAttached(); HeaderRow row = grid.getHeader().getDefaultRow(); if (row != null) { row.getCell(grid.getPropertyIdByColumnId(state.id)).setText( caption); } return this; } /** * 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 state.width; } /** * Sets the width (in pixels). * * @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 setWidth(double pixelWidth) throws IllegalStateException, IllegalArgumentException { checkColumnIsAttached(); if (pixelWidth < 0) { throw new IllegalArgumentException( "Pixel width should be greated than 0 (in " + toString() + ")"); } state.width = pixelWidth; grid.markAsDirty(); return this; } /** * Marks the column width as undefined meaning that 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 setWidthUndefined() { checkColumnIsAttached(); state.width = -1; grid.markAsDirty(); return this; } /** * 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 (grid.getColumnByColumnId(state.id) == null) { throw new IllegalStateException("Column no longer exists."); } } /** * Sets this column as the last frozen column in its grid. * * @return the column itself * * @throws IllegalArgumentException * if the column is no longer attached to any grid * @see Grid#setFrozenColumnCount(int) */ public Column setLastFrozenColumn() { checkColumnIsAttached(); grid.setFrozenColumnCount(grid.getState(false).columnOrder .indexOf(this) + 1); return this; } /** * Sets the renderer for this column. *

* If a suitable converter isn't defined explicitly, the session * converter factory is used to find a compatible converter. * * @param renderer * the renderer to use * @return the column itself * * @throws IllegalArgumentException * if no compatible converter could be found * * @see VaadinSession#getConverterFactory() * @see ConverterUtil#getConverter(Class, Class, VaadinSession) * @see #setConverter(Converter) */ public Column setRenderer(Renderer renderer) { if (!internalSetRenderer(renderer)) { throw new IllegalArgumentException( "Could not find a converter for converting from the model type " + getModelType() + " to the renderer presentation type " + renderer.getPresentationType() + " (in " + toString() + ")"); } return this; } /** * Sets the renderer for this column and the converter used to convert * from the property value type to the renderer presentation type. * * @param renderer * the renderer to use, cannot be null * @param converter * the converter to use * @return the column itself * * @throws IllegalArgumentException * if the renderer is already associated with a grid column */ public Column setRenderer(Renderer renderer, Converter converter) { if (renderer.getParent() != null) { throw new IllegalArgumentException( "Cannot set a renderer that is already connected to a grid column (in " + toString() + ")"); } if (getRenderer() != null) { grid.removeExtension(getRenderer()); } grid.addRenderer(renderer); state.rendererConnector = renderer; setConverter(converter); return this; } /** * Sets the converter used to convert from the property value type to * the renderer presentation type. * * @param converter * the converter to use, or {@code null} to not use any * converters * @return the column itself * * @throws IllegalArgumentException * if the types are not compatible */ public Column setConverter(Converter converter) throws IllegalArgumentException { Class modelType = getModelType(); if (converter != null) { if (!converter.getModelType().isAssignableFrom(modelType)) { throw new IllegalArgumentException( "The converter model type " + converter.getModelType() + " is not compatible with the property type " + modelType + " (in " + toString() + ")"); } else if (!getRenderer().getPresentationType() .isAssignableFrom(converter.getPresentationType())) { throw new IllegalArgumentException( "The converter presentation type " + converter.getPresentationType() + " is not compatible with the renderer presentation type " + getRenderer().getPresentationType() + " (in " + toString() + ")"); } } else { /* * Since the converter is null (i.e. will be removed), we need * to know that the renderer and model are compatible. If not, * we can't allow for this to happen. * * The constructor is allowed to call this method with null * without any compatibility checks, therefore we have a special * case for it. */ Class rendererPresentationType = getRenderer() .getPresentationType(); if (!isFirstConverterAssignment && !rendererPresentationType .isAssignableFrom(modelType)) { throw new IllegalArgumentException( "Cannot remove converter, " + "as renderer's presentation type " + rendererPresentationType.getName() + " and column's " + "model " + modelType.getName() + " type aren't " + "directly compatible with each other (in " + toString() + ")"); } } isFirstConverterAssignment = false; @SuppressWarnings("unchecked") Converter castConverter = (Converter) converter; this.converter = castConverter; return this; } /** * Returns the renderer instance used by this column. * * @return the renderer */ public Renderer getRenderer() { return (Renderer) getState().rendererConnector; } /** * Returns the converter instance used by this column. * * @return the converter */ public Converter getConverter() { return converter; } private boolean internalSetRenderer(Renderer renderer) { Converter converter; if (isCompatibleWithProperty(renderer, getConverter())) { // Use the existing converter (possibly none) if types // compatible converter = (Converter) getConverter(); } else { converter = ConverterUtil.getConverter( renderer.getPresentationType(), getModelType(), getSession()); } setRenderer(renderer, converter); return isCompatibleWithProperty(renderer, converter); } private VaadinSession getSession() { UI ui = grid.getUI(); return ui != null ? ui.getSession() : null; } private boolean isCompatibleWithProperty(Renderer renderer, Converter converter) { Class type; if (converter == null) { type = getModelType(); } else { type = converter.getPresentationType(); } return renderer.getPresentationType().isAssignableFrom(type); } private Class getModelType() { return grid.getContainerDataSource().getType( grid.getPropertyIdByColumnId(state.id)); } /** * Should sorting controls be available for the column * * @param sortable * true if the sorting controls should be * visible. * @return the column itself */ public Column setSortable(boolean sortable) { checkColumnIsAttached(); state.sortable = sortable; grid.markAsDirty(); return this; } /** * Are the sorting controls visible in the column header */ public boolean isSortable() { return state.sortable; } @Override public String toString() { return getClass().getSimpleName() + "[propertyId:" + grid.getPropertyIdByColumnId(state.id) + "]"; } } /** * An abstract base class for server-side Grid renderers. * {@link com.vaadin.client.ui.grid.Renderer Grid renderers}. This class * currently extends the AbstractExtension superclass, but this fact should * be regarded as an implementation detail and subject to change in a future * major or minor Vaadin revision. * * @param * the type this renderer knows how to present */ public static abstract class AbstractRenderer extends AbstractExtension implements Renderer { private final Class presentationType; protected AbstractRenderer(Class presentationType) { this.presentationType = presentationType; } /** * This method is inherited from AbstractExtension but should never be * called directly with an AbstractRenderer. */ @Deprecated @Override protected Class getSupportedParentType() { return Grid.class; } /** * This method is inherited from AbstractExtension but should never be * called directly with an AbstractRenderer. */ @Deprecated @Override protected void extend(AbstractClientConnector target) { super.extend(target); } @Override public Class getPresentationType() { return presentationType; } @Override public JsonValue encode(T value) { return encode(value, getPresentationType()); } /** * Encodes the given value to JSON. *

* This is a helper method that can be invoked by an * {@link #encode(Object) encode(T)} override if serializing a value of * type other than {@link #getPresentationType() the presentation type} * is desired. For instance, a {@code Renderer} could first turn a * date value into a formatted string and return * {@code encode(dateString, String.class)}. * * @param value * the value to be encoded * @param type * the type of the value * @return a JSON representation of the given value */ protected JsonValue encode(U value, Class type) { return JsonCodec.encode(value, null, type, getUI().getConnectorTracker()).getEncodedValue(); } /** * Gets the item id for a row key. *

* A key is used to identify a particular row on both a server and a * client. This method can be used to get the item id for the row key * that the client has sent. * * @param rowKey * the row key for which to retrieve an item id * @return the item id corresponding to {@code key} */ protected Object getItemId(String rowKey) { if (getParent() instanceof Grid) { Grid grid = (Grid) getParent(); return grid.getKeyMapper().getItemId(rowKey); } else { throw new IllegalStateException( "Renderers can be used only with Grid"); } } } /** * The data source attached to the grid */ private Container.Indexed datasource; /** * Property id to column instance mapping */ private final Map columns = new HashMap(); /** * Key generator for column server-to-client communication */ private final KeyMapper columnKeys = new KeyMapper(); /** * The current sort order */ private final List sortOrder = new ArrayList(); /** * Property listener for listening to changes in data source properties. */ private final PropertySetChangeListener propertyListener = new PropertySetChangeListener() { @Override public void containerPropertySetChange(PropertySetChangeEvent event) { Collection properties = new HashSet(event.getContainer() .getContainerPropertyIds()); // Cleanup columns that are no longer in grid List removedColumns = new LinkedList(); for (Object columnId : columns.keySet()) { if (!properties.contains(columnId)) { removedColumns.add(columnId); } } for (Object columnId : removedColumns) { removeColumn(columnId); columnKeys.remove(columnId); } datasourceExtension.propertiesRemoved(removedColumns); // Add new columns HashSet addedPropertyIds = new HashSet(); for (Object propertyId : properties) { if (!columns.containsKey(propertyId)) { appendColumn(propertyId); addedPropertyIds.add(propertyId); } } datasourceExtension.propertiesAdded(addedPropertyIds); if (getFrozenColumnCount() > columns.size()) { setFrozenColumnCount(columns.size()); } // Update sortable columns if (event.getContainer() instanceof Sortable) { Collection sortableProperties = ((Sortable) event .getContainer()).getSortableContainerPropertyIds(); for (Entry columnEntry : columns.entrySet()) { columnEntry.getValue().setSortable( sortableProperties.contains(columnEntry.getKey())); } } } }; private RpcDataProviderExtension datasourceExtension; /** * The selection model that is currently in use. Never null * after the constructor has been run. */ private SelectionModel selectionModel; /** * Used to know whether selection change events originate from the server or * the client so the selection change handler knows whether the changes * should be sent to the client. */ private boolean applyingSelectionFromClient; private final Header header = new Header(this); private final Footer footer = new Footer(this); private Object editedItemId = null; private FieldGroup editorRowFieldGroup = new FieldGroup(); private HashSet uneditableProperties = new HashSet(); private ErrorHandler editorRowErrorHandler; private CellStyleGenerator cellStyleGenerator; /** * true if Grid is using the internal IndexedContainer created * in Grid() constructor, or false if the user has set their * own Container. * * @see #setContainerDataSource() * @see #Grid() */ private boolean defaultContainer = true; private static final Method SELECTION_CHANGE_METHOD = ReflectTools .findMethod(SelectionChangeListener.class, "selectionChange", SelectionChangeEvent.class); private static final Method SORT_ORDER_CHANGE_METHOD = ReflectTools .findMethod(SortOrderChangeListener.class, "sortOrderChange", SortOrderChangeEvent.class); /** * Creates a new Grid with a new {@link IndexedContainer} as the datasource. */ public Grid() { internalSetContainerDataSource(new IndexedContainer()); initGrid(); } /** * Creates a new Grid using the given datasource. * * @param datasource * the data source for the grid */ public Grid(final Container.Indexed datasource) { setContainerDataSource(datasource); initGrid(); } /** * Grid initial setup */ private void initGrid() { setSelectionMode(SelectionMode.MULTI); addSelectionChangeListener(new SelectionChangeListener() { @Override public void selectionChange(SelectionChangeEvent event) { if (applyingSelectionFromClient) { /* * Avoid sending changes back to the client if they * originated from the client. Instead, the RPC handler is * responsible for keeping track of the resulting selection * state and notifying the client if it doens't match the * expectation. */ return; } /* * The rows are pinned here to ensure that the client gets the * correct key from server when the selected row is first * loaded. * * Once the client has gotten info that it is supposed to select * a row, it will pin the data from the client side as well and * it will be unpinned once it gets deselected. Nothing on the * server side should ever unpin anything from KeyMapper. * Pinning is mostly a client feature and is only used when * selecting something from the server side. */ for (Object addedItemId : event.getAdded()) { if (!getKeyMapper().isPinned(addedItemId)) { getKeyMapper().pin(addedItemId); } } getState().selectedKeys = getKeyMapper().getKeys( getSelectedRows()); } }); registerRpc(new GridServerRpc() { @Override public void selectionChange(List selection) { Collection receivedSelection = getKeyMapper() .getItemIds(selection); final HashSet receivedSelectionSet = new HashSet( receivedSelection); final HashSet previousSelectionSet = new HashSet( getSelectedRows()); applyingSelectionFromClient = true; try { SelectionModel selectionModel = getSelectionModel(); SetView removedItemIds = Sets.difference( previousSelectionSet, receivedSelectionSet); if (!removedItemIds.isEmpty()) { if (removedItemIds.size() == 1) { deselect(removedItemIds.iterator().next()); } else { assert selectionModel instanceof SelectionModel.Multi : "Got multiple deselections, but the selection model is not a SelectionModel.Multi"; ((SelectionModel.Multi) selectionModel) .deselect(removedItemIds); } } SetView addedItemIds = Sets.difference( receivedSelectionSet, previousSelectionSet); if (!addedItemIds.isEmpty()) { if (addedItemIds.size() == 1) { select(addedItemIds.iterator().next()); } else { assert selectionModel instanceof SelectionModel.Multi : "Got multiple selections, but the selection model is not a SelectionModel.Multi"; ((SelectionModel.Multi) selectionModel) .select(addedItemIds); } } } finally { applyingSelectionFromClient = false; } Collection actualSelection = getSelectedRows(); // Make sure all selected rows are pinned for (Object itemId : actualSelection) { if (!getKeyMapper().isPinned(itemId)) { getKeyMapper().pin(itemId); } } // Don't mark as dirty since this might be the expected state getState(false).selectedKeys = getKeyMapper().getKeys( actualSelection); JsonObject diffState = getUI().getConnectorTracker() .getDiffState(Grid.this); final String diffstateKey = "selectedKeys"; assert diffState.hasKey(diffstateKey) : "Field name has changed"; if (receivedSelection.equals(actualSelection)) { /* * We ended up with the same selection state that the client * sent us. There's nothing to send back to the client, just * update the diffstate so subsequent changes will be * detected. */ JsonArray diffSelected = Json.createArray(); for (String rowKey : getState(false).selectedKeys) { diffSelected.set(diffSelected.length(), rowKey); } diffState.put(diffstateKey, diffSelected); } else { /* * Actual selection is not what the client expects. Make * sure the client gets a state change event by clearing the * diffstate and marking as dirty */ diffState.remove(diffstateKey); markAsDirty(); } } @Override public void sort(String[] columnIds, SortDirection[] directions, boolean userOriginated) { assert columnIds.length == directions.length; List order = new ArrayList( columnIds.length); for (int i = 0; i < columnIds.length; i++) { Object propertyId = getPropertyIdByColumnId(columnIds[i]); order.add(new SortOrder(propertyId, directions[i])); } setSortOrder(order, userOriginated); } @Override public void selectAll() { assert getSelectionModel() instanceof SelectionModel.Multi : "Not a multi selection model!"; ((SelectionModel.Multi) getSelectionModel()).selectAll(); } }); registerRpc(new EditorRowServerRpc() { @Override public void bind(int rowIndex) { try { Object id = getContainerDataSource().getIdByIndex(rowIndex); doEditItem(id); getEditorRowRpc().confirmBind(); } catch (Exception e) { handleError(e); } } @Override public void cancel(int rowIndex) { try { // For future proofing even though cannot currently fail doCancelEditorRow(); } catch (Exception e) { handleError(e); } } @Override public void save(int rowIndex) { try { saveEditorRow(); getEditorRowRpc().confirmSave(); } catch (Exception e) { handleError(e); } } private void handleError(Exception e) { ErrorHandler handler = getEditorRowErrorHandler(); if (handler == null) { handler = com.vaadin.server.ErrorEvent .findErrorHandler(Grid.this); } handler.error(new ConnectorErrorEvent(Grid.this, e)); } }); } @Override public void beforeClientResponse(boolean initial) { try { header.sanityCheck(); footer.sanityCheck(); } catch (Exception e) { e.printStackTrace(); setComponentError(new ErrorMessage() { @Override public ErrorLevel getErrorLevel() { return ErrorLevel.CRITICAL; } @Override public String getFormattedHtmlMessage() { return "Incorrectly merged cells"; } }); } super.beforeClientResponse(initial); } /** * Sets the grid data source. * * @param container * The container data source. Cannot be null. * @throws IllegalArgumentException * if the data source is null */ public void setContainerDataSource(Container.Indexed container) { defaultContainer = false; internalSetContainerDataSource(container); } private void internalSetContainerDataSource(Container.Indexed container) { if (container == null) { throw new IllegalArgumentException( "Cannot set the datasource to null"); } if (datasource == container) { return; } // Remove old listeners if (datasource instanceof PropertySetChangeNotifier) { ((PropertySetChangeNotifier) datasource) .removePropertySetChangeListener(propertyListener); } if (datasourceExtension != null) { removeExtension(datasourceExtension); } columnKeys.removeAll(); datasource = container; resetEditorRow(); // // Adjust sort order // if (container instanceof Container.Sortable) { // If the container is sortable, go through the current sort order // and match each item to the sortable properties of the new // container. If the new container does not support an item in the // current sort order, that item is removed from the current sort // order list. Collection sortableProps = ((Container.Sortable) getContainerDataSource()) .getSortableContainerPropertyIds(); Iterator i = sortOrder.iterator(); while (i.hasNext()) { if (!sortableProps.contains(i.next().getPropertyId())) { i.remove(); } } sort(false); } else { // If the new container is not sortable, we'll just re-set the sort // order altogether. clearSortOrder(); } datasourceExtension = new RpcDataProviderExtension(container); datasourceExtension.extend(this, columnKeys); /* * selectionModel == null when the invocation comes from the * constructor. */ if (selectionModel != null) { selectionModel.reset(); } // Listen to changes in properties and remove columns if needed if (datasource instanceof PropertySetChangeNotifier) { ((PropertySetChangeNotifier) datasource) .addPropertySetChangeListener(propertyListener); } /* * activeRowHandler will be updated by the client-side request that * occurs on container change - no need to actively re-insert any * ValueChangeListeners at this point. */ setFrozenColumnCount(0); if (columns.isEmpty()) { // Add columns for (Object propertyId : datasource.getContainerPropertyIds()) { Column column = appendColumn(propertyId); // Initial sorting is defined by container if (datasource instanceof Sortable) { column.setSortable(((Sortable) datasource) .getSortableContainerPropertyIds().contains( propertyId)); } } } else { Collection properties = datasource.getContainerPropertyIds(); for (Object property : columns.keySet()) { if (!properties.contains(property)) { throw new IllegalStateException( "Found at least one column in Grid that does not exist in the given container: " + property + " with the header \"" + getColumn(property).getHeaderCaption() + "\""); } } } } /** * Returns the grid data source. * * @return the container data source of the grid */ public Container.Indexed getContainerDataSource() { return datasource; } /** * Returns a column based on the property id * * @param propertyId * the property id of the column * @return the column or null if not found */ public Column getColumn(Object propertyId) { return columns.get(propertyId); } /** * Returns a copy of currently configures columns in their current visual * order in this Grid. * * @return unmodifiable copy of current columns in visual order */ public List getColumns() { List columns = new ArrayList(); for (String columnId : getState(false).columnOrder) { columns.add(getColumnByColumnId(columnId)); } return Collections.unmodifiableList(columns); } /** * Adds a new Column to Grid. Also adds the property to container with data * type String, if property for column does not exist in it. Default value * for the new property is an empty String. *

* Note that adding a new property is only done for the default container * that Grid sets up with the default constructor. * * @param propertyId * the property id of the new column * @return the new column * * @throws IllegalStateException * if column for given property already exists in this grid */ public Column addColumn(Object propertyId) throws IllegalStateException { if (datasource.getContainerPropertyIds().contains(propertyId) && !columns.containsKey(propertyId)) { appendColumn(propertyId); } else { addColumnProperty(propertyId, String.class, ""); } return getColumn(propertyId); } /** * Adds a new Column to Grid. This function makes sure that the property * with the given id and data type exists in the container. If property does * not exists, it will be created. *

* Default value for the new property is 0 if type is Integer, Double and * Float. If type is String, default value is an empty string. For all other * types the default value is null. *

* Note that adding a new property is only done for the default container * that Grid sets up with the default constructor. * * @param propertyId * the property id of the new column * @param type * the data type for the new property * @return the new column * * @throws IllegalStateException * if column for given property already exists in this grid or * property already exists in the container with wrong type */ public Column addColumn(Object propertyId, Class type) { addColumnProperty(propertyId, type, null); return getColumn(propertyId); } protected void addColumnProperty(Object propertyId, Class type, Object defaultValue) throws IllegalStateException { if (!defaultContainer) { throw new IllegalStateException( "Container for this Grid is not a default container from Grid() constructor"); } if (!columns.containsKey(propertyId)) { if (!datasource.getContainerPropertyIds().contains(propertyId)) { datasource.addContainerProperty(propertyId, type, defaultValue); } else { Property containerProperty = datasource .getContainerProperty(datasource.firstItemId(), propertyId); if (containerProperty.getType() == type) { appendColumn(propertyId); } else { throw new IllegalStateException( "DataSource already has the given property " + propertyId + " with a different type"); } } } else { throw new IllegalStateException( "Grid already has a column for property " + propertyId); } } /** * Removes all columns from this Grid. */ public void removeAllColumns() { Set properties = new HashSet(columns.keySet()); for (Object propertyId : properties) { removeColumn(propertyId); } } /** * Used internally by the {@link Grid} to get a {@link Column} by * referencing its generated state id. Also used by {@link Column} to verify * if it has been detached from the {@link Grid}. * * @param columnId * the client id generated for the column when the column is * added to the grid * @return the column with the id or null if not found */ Column getColumnByColumnId(String columnId) { Object propertyId = getPropertyIdByColumnId(columnId); return getColumn(propertyId); } /** * Used internally by the {@link Grid} to get a property id by referencing * the columns generated state id. * * @param columnId * The state id of the column * @return The column instance or null if not found */ Object getPropertyIdByColumnId(String columnId) { return columnKeys.get(columnId); } @Override protected GridState getState() { return (GridState) super.getState(); } @Override protected GridState getState(boolean markAsDirty) { return (GridState) super.getState(markAsDirty); } /** * Creates a new column based on a property id and appends it as the last * column. * * @param datasourcePropertyId * The property id of a property in the datasource */ private Column appendColumn(Object datasourcePropertyId) { if (datasourcePropertyId == null) { throw new IllegalArgumentException("Property id cannot be null"); } assert datasource.getContainerPropertyIds().contains( datasourcePropertyId) : "Datasource should contain the property id"; GridColumnState columnState = new GridColumnState(); columnState.id = columnKeys.key(datasourcePropertyId); Column column = new Column(this, columnState, datasourcePropertyId); columns.put(datasourcePropertyId, column); getState().columns.add(columnState); getState().columnOrder.add(columnState.id); header.addColumn(datasourcePropertyId); footer.addColumn(datasourcePropertyId); column.setHeaderCaption(SharedUtil.camelCaseToHumanFriendly(String .valueOf(datasourcePropertyId))); return column; } /** * Removes a column from Grid based on a property id. * * @param propertyId * The property id of column to be removed */ public void removeColumn(Object propertyId) { header.removeColumn(propertyId); footer.removeColumn(propertyId); Column column = columns.remove(propertyId); getState().columnOrder.remove(columnKeys.key(propertyId)); getState().columns.remove(column.getState()); removeExtension(column.getRenderer()); } /** * 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 propertyIds * properties in the order columns should be */ public void setColumnOrder(Object... propertyIds) { List columnOrder = new ArrayList(); for (Object propertyId : propertyIds) { if (columns.containsKey(propertyId)) { columnOrder.add(columnKeys.key(propertyId)); } else { throw new IllegalArgumentException( "Grid does not contain column for property " + String.valueOf(propertyId)); } } List stateColumnOrder = getState().columnOrder; if (stateColumnOrder.size() != columnOrder.size()) { stateColumnOrder.removeAll(columnOrder); columnOrder.addAll(stateColumnOrder); } getState().columnOrder = columnOrder; } /** * 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. *

* The default value is 0. * * @param numberOfColumns * the number of columns that should be frozen * * @throws IllegalArgumentException * if the column count is < 0 or > the number of visible columns */ public void setFrozenColumnCount(int numberOfColumns) { if (numberOfColumns < -1 || numberOfColumns > columns.size()) { throw new IllegalArgumentException( "count must be between -1 and the current number of columns (" + columns + ")"); } 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. * * @see #setFrozenColumnCount(int) * * @return the number of frozen columns */ public int getFrozenColumnCount() { return getState(false).frozenColumnCount; } /** * Scrolls to a certain item, using {@link ScrollDestination#ANY}. * * @param itemId * id of item to scroll to. * @throws IllegalArgumentException * if the provided id is not recognized by the data source. */ public void scrollTo(Object itemId) throws IllegalArgumentException { scrollTo(itemId, ScrollDestination.ANY); } /** * Scrolls to a certain item, using user-specified scroll destination. * * @param itemId * id of item to scroll to. * @param destination * value specifying desired position of scrolled-to row. * @throws IllegalArgumentException * if the provided id is not recognized by the data source. */ public void scrollTo(Object itemId, ScrollDestination destination) throws IllegalArgumentException { int row = datasource.indexOfId(itemId); if (row == -1) { throw new IllegalArgumentException( "Item with specified ID does not exist in data source"); } GridClientRpc clientRPC = getRpcProxy(GridClientRpc.class); clientRPC.scrollToRow(row, destination); } /** * Scrolls to the beginning of the first data row. */ public void scrollToStart() { GridClientRpc clientRPC = getRpcProxy(GridClientRpc.class); clientRPC.scrollToStart(); } /** * Scrolls to the end of the last data row. */ public void scrollToEnd() { GridClientRpc clientRPC = getRpcProxy(GridClientRpc.class); clientRPC.scrollToEnd(); } /** * Sets the number of rows that should be visible in Grid's body, while * {@link #getHeightMode()} is {@link HeightMode#ROW}. *

* If Grid is currently not in {@link HeightMode#ROW}, the given value is * remembered, and applied once the mode is applied. * * @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 null 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#isInifinite(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().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} *

* Note: This method will change the widget's size in the browser * only if {@link #getHeightMode()} returns {@link HeightMode#CSS}. * * @see #setHeightMode(HeightMode) */ @Override public void setHeight(float height, Unit unit) { super.setHeight(height, unit); } /** * Defines the mode in which the Grid widget's height is calculated. *

* If {@link HeightMode#CSS} is given, Grid will respect the values given * via a {@code setHeight}-method, and behave as a traditional Component. *

* If {@link HeightMode#ROW} is given, Grid will make sure that the body * will display as many rows as {@link #getHeightByRows()} defines. * Note: 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 an 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. *

* Defaults to {@link HeightMode#CSS}. * * @return the current HeightMode */ public HeightMode getHeightMode() { return getState(false).heightMode; } /* Selection related methods: */ /** * Takes a new {@link SelectionModel} into use. *

* The SelectionModel that is previously in use will have all its items * deselected. *

* If the given SelectionModel is already in use, this method does nothing. * * @param selectionModel * the new SelectionModel to use * @throws IllegalArgumentException * if {@code selectionModel} is null */ public void setSelectionModel(SelectionModel selectionModel) throws IllegalArgumentException { if (selectionModel == null) { throw new IllegalArgumentException( "Selection model may not be null"); } if (this.selectionModel != selectionModel) { // this.selectionModel is null on init if (this.selectionModel != null) { this.selectionModel.reset(); this.selectionModel.setGrid(null); } this.selectionModel = selectionModel; this.selectionModel.setGrid(this); this.selectionModel.reset(); if (selectionModel.getClass().equals(SingleSelectionModel.class)) { getState().selectionMode = SharedSelectionMode.SINGLE; } else if (selectionModel.getClass().equals( MultiSelectionModel.class)) { getState().selectionMode = SharedSelectionMode.MULTI; } else if (selectionModel.getClass().equals(NoSelectionModel.class)) { getState().selectionMode = SharedSelectionMode.NONE; } else { throw new UnsupportedOperationException("Grid currently " + "supports only its own bundled selection models"); } } } /** * Returns the currently used {@link SelectionModel}. * * @return the currently used SelectionModel */ public SelectionModel getSelectionModel() { return selectionModel; } /** * Changes the Grid's selection mode. *

* Grid supports three selection modes: multiselect, single select and no * selection, and this is a conveniency method for choosing between one of * them. *

* Technically, this method is a shortcut that can be used instead of * calling {@code setSelectionModel} with a specific SelectionModel * instance. Grid comes with three built-in SelectionModel classes, and the * {@link SelectionMode} enum represents each of them. *

* Essentially, the two following method calls are equivalent: *

*

     * grid.setSelectionMode(SelectionMode.MULTI);
     * grid.setSelectionModel(new MultiSelectionMode());
     * 
* * * @param selectionMode * the selection mode to switch to * @return The {@link SelectionModel} instance that was taken into use * @throws IllegalArgumentException * if {@code selectionMode} is null * @see SelectionModel */ public SelectionModel setSelectionMode(final SelectionMode selectionMode) throws IllegalArgumentException { if (selectionMode == null) { throw new IllegalArgumentException("selection mode may not be null"); } final SelectionModel newSelectionModel = selectionMode.createModel(); setSelectionModel(newSelectionModel); return newSelectionModel; } /** * Checks whether an item is selected or not. * * @param itemId * the item id to check for * @return true iff the item is selected */ // keep this javadoc in sync with SelectionModel.isSelected public boolean isSelected(Object itemId) { return selectionModel.isSelected(itemId); } /** * Returns a collection of all the currently selected itemIds. *

* This method is a shorthand that is forwarded to the object that is * returned by {@link #getSelectionModel()}. * * @return a collection of all the currently selected itemIds */ // keep this javadoc in sync with SelectionModel.getSelectedRows public Collection getSelectedRows() { return getSelectionModel().getSelectedRows(); } /** * Gets the item id of the currently selected item. *

* This method is a shorthand that is forwarded to the object that is * returned by {@link #getSelectionModel()}. Only * {@link SelectionModel.Single} is supported. * * @return the item id of the currently selected item, or null * if nothing is selected * @throws IllegalStateException * if the object that is returned by * {@link #getSelectionModel()} is not an instance of * {@link SelectionModel.Single} */ // keep this javadoc in sync with SelectionModel.Single.getSelectedRow public Object getSelectedRow() throws IllegalStateException { if (selectionModel instanceof SelectionModel.Single) { return ((SelectionModel.Single) selectionModel).getSelectedRow(); } else { throw new IllegalStateException(Grid.class.getSimpleName() + " does not support the 'getSelectedRow' shortcut method " + "unless the selection model implements " + SelectionModel.Single.class.getName() + ". The current one does not (" + selectionModel.getClass().getName() + ")"); } } /** * Marks an item as selected. *

* This method is a shorthand that is forwarded to the object that is * returned by {@link #getSelectionModel()}. Only * {@link SelectionModel.Single} or {@link SelectionModel.Multi} are * supported. * * * @param itemIds * the itemId to mark as selected * @return true if the selection state changed. * false if the itemId already was selected * @throws IllegalArgumentException * if the {@code itemId} doesn't exist in the currently active * Container * @throws IllegalStateException * if the selection was illegal. One such reason might be that * the implementation already had an item selected, and that * needs to be explicitly deselected before re-selecting * something * @throws IllegalStateException * if the object that is returned by * {@link #getSelectionModel()} does not implement * {@link SelectionModel.Single} or {@link SelectionModel.Multi} */ // keep this javadoc in sync with SelectionModel.Single.select public boolean select(Object itemId) throws IllegalArgumentException, IllegalStateException { if (selectionModel instanceof SelectionModel.Single) { return ((SelectionModel.Single) selectionModel).select(itemId); } else if (selectionModel instanceof SelectionModel.Multi) { return ((SelectionModel.Multi) selectionModel).select(itemId); } else { throw new IllegalStateException(Grid.class.getSimpleName() + " does not support the 'select' shortcut method " + "unless the selection model implements " + SelectionModel.Single.class.getName() + " or " + SelectionModel.Multi.class.getName() + ". The current one does not (" + selectionModel.getClass().getName() + ")."); } } /** * Marks an item as deselected. *

* This method is a shorthand that is forwarded to the object that is * returned by {@link #getSelectionModel()}. Only * {@link SelectionModel.Single} and {@link SelectionModel.Multi} are * supported. * * @param itemId * the itemId to remove from being selected * @return true if the selection state changed. * false if the itemId already was selected * @throws IllegalArgumentException * if the {@code itemId} doesn't exist in the currently active * Container * @throws IllegalStateException * if the deselection was illegal. One such reason might be that * the implementation already had an item selected, and that * needs to be explicitly deselected before re-selecting * something * @throws IllegalStateException * if the object that is returned by * {@link #getSelectionModel()} does not implement * {@link SelectionModel.Single} or {@link SelectionModel.Multi} */ // keep this javadoc in sync with SelectionModel.Single.deselect public boolean deselect(Object itemId) throws IllegalStateException { if (selectionModel instanceof SelectionModel.Single) { if (isSelected(itemId)) { return ((SelectionModel.Single) selectionModel).select(null); } return false; } else if (selectionModel instanceof SelectionModel.Multi) { return ((SelectionModel.Multi) selectionModel).deselect(itemId); } else { throw new IllegalStateException(Grid.class.getSimpleName() + " does not support the 'deselect' shortcut method " + "unless the selection model implements " + SelectionModel.Single.class.getName() + " or " + SelectionModel.Multi.class.getName() + ". The current one does not (" + selectionModel.getClass().getName() + ")."); } } /** * Fires a selection change event. *

* Note: This is not a method that should be called by * application logic. This method is publicly accessible only so that * {@link SelectionModel SelectionModels} would be able to inform Grid of * these events. * * @param addedSelections * the selections that were added by this event * @param removedSelections * the selections that were removed by this event */ public void fireSelectionChangeEvent(Collection oldSelection, Collection newSelection) { fireEvent(new SelectionChangeEvent(this, oldSelection, newSelection)); } @Override public void addSelectionChangeListener(SelectionChangeListener listener) { addListener(SelectionChangeEvent.class, listener, SELECTION_CHANGE_METHOD); } @Override public void removeSelectionChangeListener(SelectionChangeListener listener) { removeListener(SelectionChangeEvent.class, listener, SELECTION_CHANGE_METHOD); } /** * Gets the * {@link com.vaadin.data.RpcDataProviderExtension.DataProviderKeyMapper * DataProviderKeyMapper} being used by the data source. * * @return the key mapper being used by the data source */ DataProviderKeyMapper getKeyMapper() { return datasourceExtension.getKeyMapper(); } /** * Adds a renderer to this grid's connector hierarchy. * * @param renderer * the renderer to add */ void addRenderer(Renderer renderer) { addExtension(renderer); } /** * Sets the current sort order using the fluid Sort API. Read the * documentation for {@link Sort} for more information. * * @param s * a sort instance */ public void sort(Sort s) { setSortOrder(s.build()); } /** * Sort this Grid in ascending order by a specified property. * * @param propertyId * a property ID */ public void sort(Object propertyId) { sort(propertyId, SortDirection.ASCENDING); } /** * Sort this Grid in user-specified {@link SortOrder} by a property. * * @param propertyId * a property ID * @param direction * a sort order value (ascending/descending) */ public void sort(Object propertyId, SortDirection direction) { sort(Sort.by(propertyId, direction)); } /** * Clear the current sort order, and re-sort the grid. */ public void clearSortOrder() { sortOrder.clear(); sort(false); } /** * Sets the sort order to use. This method throws * {@link IllegalStateException} if the attached container is not a * {@link Container.Sortable}, and {@link IllegalArgumentException} if a * property in the list is not recognized by the container, or if the * 'order' parameter is null. * * @param order * a sort order list. */ public void setSortOrder(List order) { setSortOrder(order, false); } private void setSortOrder(List order, boolean userOriginated) { if (!(getContainerDataSource() instanceof Container.Sortable)) { throw new IllegalStateException( "Attached container is not sortable (does not implement Container.Sortable)"); } if (order == null) { throw new IllegalArgumentException("Order list may not be null!"); } sortOrder.clear(); Collection sortableProps = ((Container.Sortable) getContainerDataSource()) .getSortableContainerPropertyIds(); for (SortOrder o : order) { if (!sortableProps.contains(o.getPropertyId())) { throw new IllegalArgumentException( "Property " + o.getPropertyId() + " does not exist or is not sortable in the current container"); } } sortOrder.addAll(order); sort(false); } /** * Get the current sort order list. * * @return a sort order list */ public List getSortOrder() { return Collections.unmodifiableList(sortOrder); } /** * Apply sorting to data source. */ private void sort(boolean userOriginated) { Container c = getContainerDataSource(); if (c instanceof Container.Sortable) { Container.Sortable cs = (Container.Sortable) c; final int items = sortOrder.size(); Object[] propertyIds = new Object[items]; boolean[] directions = new boolean[items]; String[] columnKeys = new String[items]; SortDirection[] stateDirs = new SortDirection[items]; for (int i = 0; i < items; ++i) { SortOrder order = sortOrder.get(i); columnKeys[i] = this.columnKeys.key(order.getPropertyId()); stateDirs[i] = order.getDirection(); propertyIds[i] = order.getPropertyId(); switch (order.getDirection()) { case ASCENDING: directions[i] = true; break; case DESCENDING: directions[i] = false; break; default: throw new IllegalArgumentException("getDirection() of " + order + " returned an unexpected value"); } } cs.sort(propertyIds, directions); fireEvent(new SortOrderChangeEvent(this, new ArrayList( sortOrder), userOriginated)); getState().sortColumns = columnKeys; getState(false).sortDirs = stateDirs; } else { throw new IllegalStateException( "Container is not sortable (does not implement Container.Sortable)"); } } /** * 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 void addSortOrderChangeListener(SortOrderChangeListener listener) { addListener(SortOrderChangeEvent.class, listener, SORT_ORDER_CHANGE_METHOD); } /** * Removes a sort order change listener previously added using * {@link #addSortOrderChangeListener(SortOrderChangeListener)}. * * @param listener * the sort order change listener to remove */ @Override public void removeSortOrderChangeListener(SortOrderChangeListener listener) { removeListener(SortOrderChangeEvent.class, listener, SORT_ORDER_CHANGE_METHOD); } /* Grid Headers */ /** * Returns the header section of this grid. The default header contains a * single row displaying the column captions. * * @return the header */ protected Header getHeader() { return header; } /** * Gets the header row at given index. * * @param rowIndex * 0 based index for row. Counted from top to bottom * @return header row at given index * @throws IllegalArgumentException * if no row exists at given index */ public HeaderRow getHeaderRow(int rowIndex) { return header.getRow(rowIndex); } /** * 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). * * @param index * the position at which to insert the row * @return the new row * * @throws IllegalArgumentException * if the index is less than 0 or greater than row count * @see #appendHeaderRow() * @see #prependHeaderRow() * @see #removeHeaderRow(HeaderRow) * @see #removeHeaderRow(int) */ public HeaderRow addHeaderRowAt(int index) { return header.addRowAt(index); } /** * Adds a new row at the bottom of the header section. * * @return the new row * @see #prependHeaderRow() * @see #addHeaderRowAt(int) * @see #removeHeaderRow(HeaderRow) * @see #removeHeaderRow(int) */ public HeaderRow appendHeaderRow() { return header.appendRow(); } /** * Returns the current default row of the header section. The default row is * a special header row providing a user interface for sorting columns. * Setting a header text for column updates cells in the default header. * * @return the default row or null if no default row set */ public HeaderRow getDefaultHeaderRow() { return header.getDefaultRow(); } /** * Gets the row count for the header section. * * @return row count */ public int getHeaderRowCount() { return header.getRowCount(); } /** * Adds a new row at the top of the header section. * * @return the new row * @see #appendHeaderRow() * @see #addHeaderRowAt(int) * @see #removeHeaderRow(HeaderRow) * @see #removeHeaderRow(int) */ public HeaderRow prependHeaderRow() { return header.prependRow(); } /** * Removes the given row from the header section. * * @param row * the row to be removed * * @throws IllegalArgumentException * if the row does not exist in this section * @see #removeHeaderRow(int) * @see #addHeaderRowAt(int) * @see #appendHeaderRow() * @see #prependHeaderRow() */ public void removeHeaderRow(HeaderRow row) { header.removeRow(row); } /** * Removes the row at the given position from the header section. * * @param index * the position of the row * * @throws IllegalArgumentException * if no row exists at given index * @see #removeHeaderRow(HeaderRow) * @see #addHeaderRowAt(int) * @see #appendHeaderRow() * @see #prependHeaderRow() */ public void removeHeaderRow(int rowIndex) { header.removeRow(rowIndex); } /** * Sets the default row of the header. The default row is a special header * row providing a user interface for sorting columns. * * @param row * the new default row, or null for no default row * * @throws IllegalArgumentException * header does not contain the row */ public void setDefaultHeaderRow(HeaderRow row) { header.setDefaultRow(row); } /** * Sets the visibility of the header section. * * @param visible * true to show header section, false to hide */ public void setHeaderVisible(boolean visible) { header.setVisible(visible); } /** * Returns the visibility of the header section. * * @return true if visible, false otherwise. */ public boolean isHeaderVisible() { return header.isVisible(); } /* Grid Footers */ /** * Returns the footer section of this grid. The default header contains a * single row displaying the column captions. * * @return the footer */ protected Footer getFooter() { return footer; } /** * Gets the footer row at given index. * * @param rowIndex * 0 based index for row. Counted from top to bottom * @return footer row at given index * @throws IllegalArgumentException * if no row exists at given index */ public FooterRow getFooterRow(int rowIndex) { return footer.getRow(rowIndex); } /** * 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). * * @param index * the position at which to insert the row * @return the new row * * @throws IllegalArgumentException * if the index is less than 0 or greater than row count * @see #appendFooterRow() * @see #prependFooterRow() * @see #removeFooterRow(FooterRow) * @see #removeFooterRow(int) */ public FooterRow addFooterRowAt(int index) { return footer.addRowAt(index); } /** * Adds a new row at the bottom of the footer section. * * @return the new row * @see #prependFooterRow() * @see #addFooterRowAt(int) * @see #removeFooterRow(FooterRow) * @see #removeFooterRow(int) */ public FooterRow appendFooterRow() { return footer.appendRow(); } /** * Gets the row count for the footer. * * @return row count */ public int getFooterRowCount() { return footer.getRowCount(); } /** * Adds a new row at the top of the footer section. * * @return the new row * @see #appendFooterRow() * @see #addFooterRowAt(int) * @see #removeFooterRow(FooterRow) * @see #removeFooterRow(int) */ public FooterRow prependFooterRow() { return footer.prependRow(); } /** * Removes the given row from the footer section. * * @param row * the row to be removed * * @throws IllegalArgumentException * if the row does not exist in this section * @see #removeFooterRow(int) * @see #addFooterRowAt(int) * @see #appendFooterRow() * @see #prependFooterRow() */ public void removeFooterRow(FooterRow row) { footer.removeRow(row); } /** * Removes the row at the given position from the footer section. * * @param index * the position of the row * * @throws IllegalArgumentException * if no row exists at given index * @see #removeFooterRow(FooterRow) * @see #addFooterRowAt(int) * @see #appendFooterRow() * @see #prependFooterRow() */ public void removeFooterRow(int rowIndex) { footer.removeRow(rowIndex); } /** * Sets the visibility of the footer section. * * @param visible * true to show footer section, false to hide */ public void setFooterVisible(boolean visible) { footer.setVisible(visible); } /** * Returns the visibility of the footer section. * * @return true if visible, false otherwise. */ public boolean isFooterVisible() { return footer.isVisible(); } @Override public Iterator iterator() { List componentList = new ArrayList(); Header header = getHeader(); for (int i = 0; i < header.getRowCount(); ++i) { HeaderRow row = header.getRow(i); for (Object propId : columns.keySet()) { HeaderCell cell = row.getCell(propId); if (cell.getCellState().type == GridStaticCellType.WIDGET) { componentList.add(cell.getComponent()); } } } Footer footer = getFooter(); for (int i = 0; i < footer.getRowCount(); ++i) { FooterRow row = footer.getRow(i); for (Object propId : columns.keySet()) { FooterCell cell = row.getCell(propId); if (cell.getCellState().type == GridStaticCellType.WIDGET) { componentList.add(cell.getComponent()); } } } componentList.addAll(getEditorRowFields()); return componentList.iterator(); } @Override public boolean isRendered(Component childComponent) { if (getEditorRowFields().contains(childComponent)) { // Only render editor row fields if the editor is open return isEditorRowActive(); } else { // TODO Header and footer components should also only be rendered if // the header/footer is visible return true; } } EditorRowClientRpc getEditorRowRpc() { return getRpcProxy(EditorRowClientRpc.class); } /** * Sets the cell style generator that is used for generating styles for rows * and cells. * * @param cellStyleGenerator * the cell style generator to set, or null to * remove a previously set generator */ public void setCellStyleGenerator(CellStyleGenerator cellStyleGenerator) { this.cellStyleGenerator = cellStyleGenerator; getState().hasCellStyleGenerator = (cellStyleGenerator != null); datasourceExtension.refreshCache(); } /** * Gets the cell style generator that is used for generating styles for rows * and cells. * * @return the cell style generator, or null if no generator is * set */ public CellStyleGenerator getCellStyleGenerator() { return cellStyleGenerator; } /** * Adds a row to the underlying container. The order of the parameters * should match the current visible column order. *

* Please note that it's generally only safe to use this method during * initialization. After Grid has been initialized and the visible column * order might have been changed, it's better to instead add items directly * to the underlying container and use {@link Item#getItemProperty(Object)} * to make sure each value is assigned to the intended property. * * @param values * the cell values of the new row, in the same order as the * visible column order, not null. * @return the item id of the new row * @throws IllegalArgumentException * if values is null * @throws IllegalArgumentException * if its length does not match the number of visible columns * @throws IllegalArgumentException * if a parameter value is not an instance of the corresponding * property type * @throws UnsupportedOperationException * if the container does not support adding new items */ public Object addRow(Object... values) { if (values == null) { throw new IllegalArgumentException("Values cannot be null"); } Indexed dataSource = getContainerDataSource(); List columnOrder = getState(false).columnOrder; if (values.length != columnOrder.size()) { throw new IllegalArgumentException("There are " + columnOrder.size() + " visible columns, but " + values.length + " cell values were provided."); } // First verify all parameter types for (int i = 0; i < columnOrder.size(); i++) { Object propertyId = getPropertyIdByColumnId(columnOrder.get(i)); Class propertyType = dataSource.getType(propertyId); if (values[i] != null && !propertyType.isInstance(values[i])) { throw new IllegalArgumentException("Parameter " + i + "(" + values[i] + ") is not an instance of " + propertyType.getCanonicalName()); } } Object itemId = dataSource.addItem(); try { Item item = dataSource.getItem(itemId); for (int i = 0; i < columnOrder.size(); i++) { Object propertyId = getPropertyIdByColumnId(columnOrder.get(i)); Property property = item.getItemProperty(propertyId); property.setValue(values[i]); } } catch (RuntimeException e) { try { dataSource.removeItem(itemId); } catch (Exception e2) { getLogger().log(Level.SEVERE, "Error recovering from exception in addRow", e); } throw e; } return itemId; } private static Logger getLogger() { return Logger.getLogger(Grid.class.getName()); } /** * Sets whether or not the editor row feature is enabled for this grid. * * @param isEnabled * true to enable the feature, false * otherwise * @throws IllegalStateException * if an item is currently being edited * @see #getEditedItemId() */ public void setEditorRowEnabled(boolean isEnabled) throws IllegalStateException { if (isEditorRowActive()) { throw new IllegalStateException( "Cannot disable the editor row while an item (" + getEditedItemId() + ") is being edited."); } if (isEditorRowEnabled() != isEnabled) { getState().editorRowEnabled = isEnabled; } } /** * Checks whether the editor row feature is enabled for this grid. * * @return true iff the editor row feature is enabled for this * grid * @see #getEditedItemId() */ public boolean isEditorRowEnabled() { return getState(false).editorRowEnabled; } /** * Gets the id of the item that is currently being edited. * * @return the id of the item that is currently being edited, or * null if no item is being edited at the moment */ public Object getEditedItemId() { return editedItemId; } /** * Gets the field group that is backing the editor row of this grid. * * @return the backing field group */ public FieldGroup getEditorRowFieldGroup() { return editorRowFieldGroup; } /** * Sets the field group that is backing this editor row. * * @param fieldGroup * the backing field group */ public void setEditorRowFieldGroup(FieldGroup fieldGroup) { editorRowFieldGroup = fieldGroup; if (isEditorRowActive()) { editorRowFieldGroup.setItemDataSource(getContainerDataSource() .getItem(editedItemId)); } } /** * Returns whether an item is currently being edited in the editor row. * * @return true iff the editor row is editing an item */ public boolean isEditorRowActive() { return editedItemId != null; } /** * Sets a property editable or not. *

* In order for a user to edit a particular value with a Field, it needs to * be both non-readonly and editable. *

* The difference between read-only and uneditable is that the read-only * state is propagated back into the property, while the editable property * is internal metadata for the editor row. * * @param propertyId * the id of the property to set as editable state * @param editable * whether or not {@code propertyId} chould be editable */ public void setPropertyEditable(Object propertyId, boolean editable) { checkPropertyExists(propertyId); if (getEditorRowField(propertyId) != null) { getEditorRowField(propertyId).setReadOnly(!editable); } if (editable) { uneditableProperties.remove(propertyId); } else { uneditableProperties.add(propertyId); } } /** * Checks whether a property is editable through the editor row. *

* This only checks whether the property is configured as uneditable in the * editor row. The property's or field's readonly status will ultimately * decide whether the value can be edited or not. * * @param propertyId * the id of the property to check for editable status * @return true iff the property is editable according to this * editor row */ public boolean isPropertyEditable(Object propertyId) { checkPropertyExists(propertyId); return !uneditableProperties.contains(propertyId); } private void checkPropertyExists(Object propertyId) { if (!getContainerDataSource().getContainerPropertyIds().contains( propertyId)) { throw new IllegalArgumentException("Property with id " + propertyId + " is not in the current Container"); } } /** * Gets the field component that represents a property in the editor row. If * the property is not yet bound to a field, null is returned. *

* When {@link #editItem(Object) editItem} is called, fields are * automatically created and bound for any unbound properties. * * @param propertyId * the property id of the property for which to find the field * @return the bound field or null if not bound */ public Field getEditorRowField(Object propertyId) { return editorRowFieldGroup.getField(propertyId); } /** * Opens the editor row for the provided item. * * @param itemId * the id of the item to edit * @throws IllegalStateException * if the editor row is not enabled * @throws IllegalArgumentException * if the {@code itemId} is not in the backing container * @see #setEditorRowEnabled(boolean) */ public void editItem(Object itemId) throws IllegalStateException, IllegalArgumentException { doEditItem(itemId); getEditorRowRpc().bind(getContainerDataSource().indexOfId(itemId)); } protected void doEditItem(Object itemId) { if (!isEditorRowEnabled()) { throw new IllegalStateException("Editor row is not enabled"); } Item item = getContainerDataSource().getItem(itemId); if (item == null) { throw new IllegalArgumentException("Item with id " + itemId + " not found in current container"); } editorRowFieldGroup.setItemDataSource(item); editedItemId = itemId; for (Object propertyId : item.getItemPropertyIds()) { final Field editor; if (editorRowFieldGroup.getUnboundPropertyIds() .contains(propertyId)) { editor = editorRowFieldGroup.buildAndBind(propertyId); } else { editor = editorRowFieldGroup.getField(propertyId); } getColumn(propertyId).getState().editorConnector = editor; if (editor != null) { editor.setReadOnly(!isPropertyEditable(propertyId)); if (editor.getParent() != Grid.this) { assert editor.getParent() == null; editor.setParent(Grid.this); } } } } /** * Binds the field with the given propertyId from the current item. If an * item has not been set then the binding is postponed until the item is set * using {@link #editItem(Object)}. *

* This method also adds validators when applicable. *

* Note: This is a pass-through call to the backing field group. * * @param field * The field to bind * @param propertyId * The propertyId to bind to the field * @throws BindException * If the property id is already bound to another field by this * field binder */ public void bindEditorRowField(Object propertyId, Field field) throws BindException { editorRowFieldGroup.bind(field, propertyId); } /** * Saves all changes done to the bound fields. *

* Note: This is a pass-through call to the backing field group. * * @throws CommitException * If the commit was aborted * * @see FieldGroup#commit() */ public void saveEditorRow() throws CommitException { editorRowFieldGroup.commit(); } /** * Cancels the currently active edit if any. */ public void cancelEditorRow() { if (isEditorRowActive()) { getEditorRowRpc().cancel( getContainerDataSource().indexOfId(editedItemId)); doCancelEditorRow(); } } protected void doCancelEditorRow() { editedItemId = null; } void resetEditorRow() { if (isEditorRowActive()) { /* * Simply force cancel the editing; throwing here would just make * Grid.setContainerDataSource semantics more complicated. */ cancelEditorRow(); } for (Field editor : getEditorRowFields()) { editor.setParent(null); } editedItemId = null; editorRowFieldGroup = new FieldGroup(); uneditableProperties = new HashSet(); } /** * Gets a collection of all fields bound to the editor row of this grid. *

* All non-editable fields (either readonly or uneditable) are in read-only * mode. *

* When {@link #editItem(Object) editItem} is called, fields are * automatically created and bound to any unbound properties. * * @return a collection of all the fields bound to this editor row */ Collection> getEditorRowFields() { return editorRowFieldGroup.getFields(); } /** * Sets the field factory for the {@link FieldGroup}. The field factory is * only used when {@link FieldGroup} creates a new field. *

* Note: This is a pass-through call to the backing field group. * * @param fieldFactory * The field factory to use */ public void setEditorRowFieldFactory(FieldGroupFieldFactory fieldFactory) { editorRowFieldGroup.setFieldFactory(fieldFactory); } /** * Returns the error handler of this editor row. * * @return the error handler or null if there is no dedicated error handler * * @see #setEditorRowErrorHandler(ErrorHandler) * @see ClientConnector#getErrorHandler() */ public ErrorHandler getEditorRowErrorHandler() { return editorRowErrorHandler; } /** * Sets the error handler for this editor row. The error handler is invoked * for exceptions thrown while processing client requests; specifically when * {@link #saveEditorRow()} triggered by the client throws a * CommitException. If the error handler is not set, one is looked up via * Grid. * * @param errorHandler * the error handler to use * * @see ClientConnector#setErrorHandler(ErrorHandler) * @see ErrorEvent#findErrorHandler(ClientConnector) */ public void setEditorRowErrorHandler(ErrorHandler errorHandler) { editorRowErrorHandler = errorHandler; } /** * Builds a field using the given caption and binds it to the given property * id using the field binder. Ensures the new field is of the given type. *

* Note: This is a pass-through call to the backing field group. * * @param propertyId * The property id to bind to. Must be present in the field * finder * @param fieldType * The type of field that we want to create * @throws BindException * If the field could not be created * @return The created and bound field. Can be any type of {@link Field} . */ public > T buildAndBind(Object propertyId, Class fieldType) throws BindException { return editorRowFieldGroup.buildAndBind(null, propertyId, fieldType); } }