/* * Copyright 2000-2013 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.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.Set; import java.util.StringTokenizer; import java.util.logging.Level; import java.util.logging.Logger; import com.vaadin.data.Container; import com.vaadin.data.Item; import com.vaadin.data.Property; import com.vaadin.data.util.ContainerOrderedWrapper; import com.vaadin.data.util.IndexedContainer; import com.vaadin.data.util.converter.Converter; import com.vaadin.data.util.converter.ConverterUtil; import com.vaadin.event.Action; import com.vaadin.event.Action.Handler; import com.vaadin.event.DataBoundTransferable; import com.vaadin.event.ItemClickEvent; import com.vaadin.event.ItemClickEvent.ItemClickListener; import com.vaadin.event.ItemClickEvent.ItemClickNotifier; import com.vaadin.event.MouseEvents.ClickEvent; import com.vaadin.event.dd.DragAndDropEvent; import com.vaadin.event.dd.DragSource; import com.vaadin.event.dd.DropHandler; import com.vaadin.event.dd.DropTarget; import com.vaadin.event.dd.acceptcriteria.ServerSideCriterion; import com.vaadin.server.KeyMapper; import com.vaadin.server.LegacyCommunicationManager; import com.vaadin.server.LegacyPaint; import com.vaadin.server.PaintException; import com.vaadin.server.PaintTarget; import com.vaadin.server.Resource; import com.vaadin.shared.MouseEventDetails; import com.vaadin.shared.ui.MultiSelectMode; import com.vaadin.shared.ui.table.TableConstants; /** *
* Table
is used for representing data or components in a pageable
* and selectable table.
*
* Scalability of the Table is largely dictated by the container. A table does * not have a limit for the number of items and is just as fast with hundreds of * thousands of items as with just a few. The current GWT implementation with * scrolling however limits the number of rows to around 500000, depending on * the browser and the pixel height of rows. *
* ** Components in a Table will not have their caption nor icon rendered. *
* * @author Vaadin Ltd. * @since 3.0 */ @SuppressWarnings({ "deprecation" }) public class Table extends AbstractSelect implements Action.Container, Container.Ordered, Container.Sortable, ItemClickNotifier, DragSource, DropTarget, HasComponents { private transient Logger logger = null; /** * Modes that Table support as drag sourse. */ public enum TableDragMode { /** * Table does not start drag and drop events. HTM5 style events started * by browser may still happen. */ NONE, /** * Table starts drag with a one row only. */ ROW, /** * Table drags selected rows, if drag starts on a selected rows. Else it * starts like in ROW mode. Note, that in Transferable there will still * be only the row on which the drag started, other dragged rows need to * be checked from the source Table. */ MULTIROW } protected static final int CELL_KEY = 0; protected static final int CELL_HEADER = 1; protected static final int CELL_ICON = 2; protected static final int CELL_ITEMID = 3; protected static final int CELL_GENERATED_ROW = 4; protected static final int CELL_FIRSTCOL = 5; public enum Align { /** * Left column alignment. This is the default behaviour. */ LEFT("b"), /** * Center column alignment. */ CENTER("c"), /** * Right column alignment. */ RIGHT("e"); private String alignment; private Align(String alignment) { this.alignment = alignment; } @Override public String toString() { return alignment; } public Align convertStringToAlign(String string) { if (string == null) { return null; } if (string.equals("b")) { return Align.LEFT; } else if (string.equals("c")) { return Align.CENTER; } else if (string.equals("e")) { return Align.RIGHT; } else { return null; } } } /** * @deprecated As of 7.0, use {@link Align#LEFT} instead */ @Deprecated public static final Align ALIGN_LEFT = Align.LEFT; /** * @deprecated As of 7.0, use {@link Align#CENTER} instead */ @Deprecated public static final Align ALIGN_CENTER = Align.CENTER; /** * @deprecated As of 7.0, use {@link Align#RIGHT} instead */ @Deprecated public static final Align ALIGN_RIGHT = Align.RIGHT; public enum ColumnHeaderMode { /** * Column headers are hidden. */ HIDDEN, /** * Property ID:s are used as column headers. */ ID, /** * Column headers are explicitly specified with * {@link #setColumnHeaders(String[])}. */ EXPLICIT, /** * Column headers are explicitly specified with * {@link #setColumnHeaders(String[])}. If a header is not specified for * a given property, its property id is used instead. *
* This is the default behavior.
*/
EXPLICIT_DEFAULTS_ID
}
/**
* @deprecated As of 7.0, use {@link ColumnHeaderMode#HIDDEN} instead
*/
@Deprecated
public static final ColumnHeaderMode COLUMN_HEADER_MODE_HIDDEN = ColumnHeaderMode.HIDDEN;
/**
* @deprecated As of 7.0, use {@link ColumnHeaderMode#ID} instead
*/
@Deprecated
public static final ColumnHeaderMode COLUMN_HEADER_MODE_ID = ColumnHeaderMode.ID;
/**
* @deprecated As of 7.0, use {@link ColumnHeaderMode#EXPLICIT} instead
*/
@Deprecated
public static final ColumnHeaderMode COLUMN_HEADER_MODE_EXPLICIT = ColumnHeaderMode.EXPLICIT;
/**
* @deprecated As of 7.0, use {@link ColumnHeaderMode#EXPLICIT_DEFAULTS_ID}
* instead
*/
@Deprecated
public static final ColumnHeaderMode COLUMN_HEADER_MODE_EXPLICIT_DEFAULTS_ID = ColumnHeaderMode.EXPLICIT_DEFAULTS_ID;
public enum RowHeaderMode {
/**
* Row caption mode: The row headers are hidden. This is the default
* mode.
*/
HIDDEN(null),
/**
* Row caption mode: Items Id-objects toString is used as row caption.
*/
ID(ItemCaptionMode.ID),
/**
* Row caption mode: Item-objects toString is used as row caption.
*/
ITEM(ItemCaptionMode.ITEM),
/**
* Row caption mode: Index of the item is used as item caption. The
* index mode can only be used with the containers implementing the
* {@link com.vaadin.data.Container.Indexed} interface.
*/
INDEX(ItemCaptionMode.INDEX),
/**
* Row caption mode: Item captions are explicitly specified, but if the
* caption is missing, the item id objects toString()
is
* used instead.
*/
EXPLICIT_DEFAULTS_ID(ItemCaptionMode.EXPLICIT_DEFAULTS_ID),
/**
* Row caption mode: Item captions are explicitly specified.
*/
EXPLICIT(ItemCaptionMode.EXPLICIT),
/**
* Row caption mode: Only icons are shown, the captions are hidden.
*/
ICON_ONLY(ItemCaptionMode.ICON_ONLY),
/**
* Row caption mode: Item captions are read from property specified with
* {@link #setItemCaptionPropertyId(Object)}.
*/
PROPERTY(ItemCaptionMode.PROPERTY);
ItemCaptionMode mode;
private RowHeaderMode(ItemCaptionMode mode) {
this.mode = mode;
}
public ItemCaptionMode getItemCaptionMode() {
return mode;
}
}
/**
* @deprecated As of 7.0, use {@link RowHeaderMode#HIDDEN} instead
*/
@Deprecated
public static final RowHeaderMode ROW_HEADER_MODE_HIDDEN = RowHeaderMode.HIDDEN;
/**
* @deprecated As of 7.0, use {@link RowHeaderMode#ID} instead
*/
@Deprecated
public static final RowHeaderMode ROW_HEADER_MODE_ID = RowHeaderMode.ID;
/**
* @deprecated As of 7.0, use {@link RowHeaderMode#ITEM} instead
*/
@Deprecated
public static final RowHeaderMode ROW_HEADER_MODE_ITEM = RowHeaderMode.ITEM;
/**
* @deprecated As of 7.0, use {@link RowHeaderMode#INDEX} instead
*/
@Deprecated
public static final RowHeaderMode ROW_HEADER_MODE_INDEX = RowHeaderMode.INDEX;
/**
* @deprecated As of 7.0, use {@link RowHeaderMode#EXPLICIT_DEFAULTS_ID}
* instead
*/
@Deprecated
public static final RowHeaderMode ROW_HEADER_MODE_EXPLICIT_DEFAULTS_ID = RowHeaderMode.EXPLICIT_DEFAULTS_ID;
/**
* @deprecated As of 7.0, use {@link RowHeaderMode#EXPLICIT} instead
*/
@Deprecated
public static final RowHeaderMode ROW_HEADER_MODE_EXPLICIT = RowHeaderMode.EXPLICIT;
/**
* @deprecated As of 7.0, use {@link RowHeaderMode#ICON_ONLY} instead
*/
@Deprecated
public static final RowHeaderMode ROW_HEADER_MODE_ICON_ONLY = RowHeaderMode.ICON_ONLY;
/**
* @deprecated As of 7.0, use {@link RowHeaderMode#PROPERTY} instead
*/
@Deprecated
public static final RowHeaderMode ROW_HEADER_MODE_PROPERTY = RowHeaderMode.PROPERTY;
/**
* The default rate that table caches rows for smooth scrolling.
*/
private static final double CACHE_RATE_DEFAULT = 2;
private static final String ROW_HEADER_COLUMN_KEY = "0";
private static final Object ROW_HEADER_FAKE_PROPERTY_ID = new UniqueSerializable() {
};
/* Private table extensions to Select */
/**
* True if column collapsing is allowed.
*/
private boolean columnCollapsingAllowed = false;
/**
* True if reordering of columns is allowed on the client side.
*/
private boolean columnReorderingAllowed = false;
/**
* Keymapper for column ids.
*/
private final KeyMapper
* The amount of items in the array must match the amount of properties * identified by {@link #getVisibleColumns()}. The possible values for the * alignments include: *
* Column can either have a fixed width or expand ratio. The latter one set * is used. See @link {@link #setColumnExpandRatio(Object, float)}. * * @param propertyId * colunmns property id * @param width * width to be reserved for colunmns content * @since 4.0.3 */ public void setColumnWidth(Object propertyId, int width) { if (propertyId == null) { // Since propertyId is null, this is the row header. Use the magic // id to store the width of the row header. propertyId = ROW_HEADER_FAKE_PROPERTY_ID; } // Setting column width should remove any expand ratios as well columnExpandRatios.remove(propertyId); if (width < 0) { columnWidths.remove(propertyId); } else { columnWidths.put(propertyId, width); } markAsDirty(); } /** * Sets the column expand ratio for given column. *
* Expand ratios can be defined to customize the way how excess space is * divided among columns. Table can have excess space if it has its width * defined and there is horizontally more space than columns consume * naturally. Excess space is the space that is not used by columns with * explicit width (see {@link #setColumnWidth(Object, int)}) or with natural * width (no width nor expand ratio). * *
* By default (without expand ratios) the excess space is divided * proportionally to columns natural widths. * *
* Only expand ratios of visible columns are used in final calculations. * *
* Column can either have a fixed width or expand ratio. The latter one set * is used. * *
* A column with expand ratio is considered to be minimum width by default * (if no excess space exists). The minimum width is defined by terminal * implementation. * *
* If terminal implementation supports re-sizable columns the column becomes * fixed width column if users resizes the column. * * @param propertyId * columns property id * @param expandRatio * the expandRatio used to divide excess space for this column */ public void setColumnExpandRatio(Object propertyId, float expandRatio) { if (propertyId == null) { // Since propertyId is null, this is the row header. Use the magic // id to store the width of the row header. propertyId = ROW_HEADER_FAKE_PROPERTY_ID; } // Setting the column expand ratio should remove and defined column // width columnWidths.remove(propertyId); if (expandRatio < 0) { columnExpandRatios.remove(propertyId); } else { columnExpandRatios.put(propertyId, expandRatio); } requestRepaint(); } /** * Gets the column expand ratio for a columnd. See * {@link #setColumnExpandRatio(Object, float)} * * @param propertyId * columns property id * @return the expandRatio used to divide excess space for this column */ public float getColumnExpandRatio(Object propertyId) { final Float width = columnExpandRatios.get(propertyId); if (width == null) { return -1; } return width.floatValue(); } /** * Gets the pixel width of column * * @param propertyId * @return width of column or -1 when value not set */ public int getColumnWidth(Object propertyId) { if (propertyId == null) { // Since propertyId is null, this is the row header. Use the magic // id to retrieve the width of the row header. propertyId = ROW_HEADER_FAKE_PROPERTY_ID; } final Integer width = columnWidths.get(propertyId); if (width == null) { return -1; } return width.intValue(); } /** * Gets the page length. * *
* Setting page length 0 disables paging. *
* * @return the Length of one page. */ public int getPageLength() { return pageLength; } /** * Sets the page length. * ** Setting page length 0 disables paging. The page length defaults to 15. *
* ** If Table has width set ({@link #setWidth(float, int)} ) the client side * may update the page length automatically the correct value. *
* * @param pageLength * the length of one page. */ public void setPageLength(int pageLength) { if (pageLength >= 0 && this.pageLength != pageLength) { this.pageLength = pageLength; // Assures the visual refresh refreshRowCache(); } } /** * This method adjusts a possible caching mechanism of table implementation. * ** Table component may fetch and render some rows outside visible area. With * complex tables (for example containing layouts and components), the * client side may become unresponsive. Setting the value lower, UI will * become more responsive. With higher values scrolling in client will hit * server less frequently. * *
* The amount of cached rows will be cacheRate multiplied with pageLength ( * {@link #setPageLength(int)} both below and above visible area.. * * @param cacheRate * a value over 0 (fastest rendering time). Higher value will * cache more rows on server (smoother scrolling). Default value * is 2. */ public void setCacheRate(double cacheRate) { if (cacheRate < 0) { throw new IllegalArgumentException( "cacheRate cannot be less than zero"); } if (this.cacheRate != cacheRate) { this.cacheRate = cacheRate; markAsDirty(); } } /** * @see #setCacheRate(double) * * @return the current cache rate value */ public double getCacheRate() { return cacheRate; } /** * Getter for property currentPageFirstItem. * * @return the Value of property currentPageFirstItem. */ public Object getCurrentPageFirstItemId() { // Priorise index over id if indexes are supported if (items instanceof Container.Indexed) { final int index = getCurrentPageFirstItemIndex(); Object id = null; if (index >= 0 && index < size()) { id = getIdByIndex(index); } if (id != null && !id.equals(currentPageFirstItemId)) { currentPageFirstItemId = id; } } // If there is no item id at all, use the first one if (currentPageFirstItemId == null) { currentPageFirstItemId = firstItemId(); } return currentPageFirstItemId; } /** * Returns the item ID for the item represented by the index given. Assumes * that the current container implements {@link Container.Indexed}. * * See {@link Container.Indexed#getIdByIndex(int)} for more information * about the exceptions that can be thrown. * * @param index * the index for which the item ID should be fetched * @return the item ID for the given index * * @throws ClassCastException * if container does not implement {@link Container.Indexed} * @throws IndexOutOfBoundsException * thrown by {@link Container.Indexed#getIdByIndex(int)} if the * index is invalid */ protected Object getIdByIndex(int index) { return ((Container.Indexed) items).getIdByIndex(index); } /** * Setter for property currentPageFirstItemId. * * @param currentPageFirstItemId * the New value of property currentPageFirstItemId. */ public void setCurrentPageFirstItemId(Object currentPageFirstItemId) { // Gets the corresponding index int index = -1; if (items instanceof Container.Indexed) { index = indexOfId(currentPageFirstItemId); } else { // If the table item container does not have index, we have to // calculates the index by hand Object id = firstItemId(); while (id != null && !id.equals(currentPageFirstItemId)) { index++; id = nextItemId(id); } if (id == null) { index = -1; } } // If the search for item index was successful if (index >= 0) { /* * The table is not capable of displaying an item in the container * as the first if there are not enough items following the selected * item so the whole table (pagelength) is filled. */ int maxIndex = size() - pageLength; if (maxIndex < 0) { maxIndex = 0; } if (index > maxIndex) { // Note that we pass index, not maxIndex, letting // setCurrentPageFirstItemIndex handle the situation. setCurrentPageFirstItemIndex(index); return; } this.currentPageFirstItemId = currentPageFirstItemId; currentPageFirstItemIndex = index; } // Assures the visual refresh refreshRowCache(); } protected int indexOfId(Object itemId) { return ((Container.Indexed) items).indexOfId(itemId); } /** * Gets the icon Resource for the specified column. * * @param propertyId * the propertyId indentifying the column. * @return the icon for the specified column; null if the column has no icon * set, or if the column is not visible. */ public Resource getColumnIcon(Object propertyId) { return columnIcons.get(propertyId); } /** * Sets the icon Resource for the specified column. *
* Throws IllegalArgumentException if the specified column is not visible. *
* * @param propertyId * the propertyId identifying the column. * @param icon * the icon Resource to set. */ public void setColumnIcon(Object propertyId, Resource icon) { if (icon == null) { columnIcons.remove(propertyId); } else { columnIcons.put(propertyId, icon); } markAsDirty(); } /** * Gets the header for the specified column. * * @param propertyId * the propertyId identifying the column. * @return the header for the specified column if it has one. */ public String getColumnHeader(Object propertyId) { if (getColumnHeaderMode() == ColumnHeaderMode.HIDDEN) { return null; } String header = columnHeaders.get(propertyId); if ((header == null && getColumnHeaderMode() == ColumnHeaderMode.EXPLICIT_DEFAULTS_ID) || getColumnHeaderMode() == ColumnHeaderMode.ID) { header = propertyId.toString(); } return header; } /** * Sets the column header for the specified column; * * @param propertyId * the propertyId identifying the column. * @param header * the header to set. */ public void setColumnHeader(Object propertyId, String header) { if (header == null) { columnHeaders.remove(propertyId); } else { columnHeaders.put(propertyId, header); } markAsDirty(); } /** * Gets the specified column's alignment. * * @param propertyId * the propertyID identifying the column. * @return the specified column's alignment if it as one; {@link Align#LEFT} * otherwise. */ public Align getColumnAlignment(Object propertyId) { final Align a = columnAlignments.get(propertyId); return a == null ? Align.LEFT : a; } /** * Sets the specified column's alignment. * ** Throws IllegalArgumentException if the alignment is not one of the * following: {@link Align#LEFT}, {@link Align#CENTER} or * {@link Align#RIGHT} *
* * @param propertyId * the propertyID identifying the column. * @param alignment * the desired alignment. */ public void setColumnAlignment(Object propertyId, Align alignment) { if (alignment == null || alignment == Align.LEFT) { columnAlignments.remove(propertyId); } else { columnAlignments.put(propertyId, alignment); } // Assures the visual refresh. No need to reset the page buffer before // as the content has not changed, only the alignments. refreshRenderedCells(); } /** * Checks if the specified column is collapsed. * * @param propertyId * the propertyID identifying the column. * @return true if the column is collapsed; false otherwise; */ public boolean isColumnCollapsed(Object propertyId) { return collapsedColumns != null && collapsedColumns.contains(propertyId); } /** * Sets whether the specified column is collapsed or not. * * * @param propertyId * the propertyID identifying the column. * @param collapsed * the desired collapsedness. * @throws IllegalStateException * if column collapsing is not allowed */ public void setColumnCollapsed(Object propertyId, boolean collapsed) throws IllegalStateException { if (!isColumnCollapsingAllowed()) { throw new IllegalStateException("Column collapsing not allowed!"); } if (collapsed && noncollapsibleColumns.contains(propertyId)) { throw new IllegalStateException("The column is noncollapsible!"); } if (collapsed) { collapsedColumns.add(propertyId); } else { collapsedColumns.remove(propertyId); } // Assures the visual refresh refreshRowCache(); } /** * Checks if column collapsing is allowed. * * @return true if columns can be collapsed; false otherwise. */ public boolean isColumnCollapsingAllowed() { return columnCollapsingAllowed; } /** * Sets whether column collapsing is allowed or not. * * @param collapsingAllowed * specifies whether column collapsing is allowed. */ public void setColumnCollapsingAllowed(boolean collapsingAllowed) { columnCollapsingAllowed = collapsingAllowed; if (!collapsingAllowed) { collapsedColumns.clear(); } // Assures the visual refresh. No need to reset the page buffer before // as the content has not changed, only the alignments. refreshRenderedCells(); } /** * Sets whether the given column is collapsible. Note that collapsible * columns can only be actually collapsed (via UI or with * {@link #setColumnCollapsed(Object, boolean) setColumnCollapsed()}) if * {@link #isColumnCollapsingAllowed()} is true. By default all columns are * collapsible. * * @param propertyId * the propertyID identifying the column. * @param collapsible * true if the column should be collapsible, false otherwise. */ public void setColumnCollapsible(Object propertyId, boolean collapsible) { if (collapsible) { noncollapsibleColumns.remove(propertyId); } else { noncollapsibleColumns.add(propertyId); collapsedColumns.remove(propertyId); } refreshRowCache(); } /** * Checks if the given column is collapsible. Note that even if this method * returnstrue
, the column can only be actually collapsed (via
* UI or with {@link #setColumnCollapsed(Object, boolean)
* setColumnCollapsed()}) if {@link #isColumnCollapsingAllowed()} is also
* true.
*
* @return true if the column can be collapsed; false otherwise.
*/
public boolean isColumnCollapsible(Object propertyId) {
return !noncollapsibleColumns.contains(propertyId);
}
/**
* Checks if column reordering is allowed.
*
* @return true if columns can be reordered; false otherwise.
*/
public boolean isColumnReorderingAllowed() {
return columnReorderingAllowed;
}
/**
* Sets whether column reordering is allowed or not.
*
* @param columnReorderingAllowed
* specifies whether column reordering is allowed.
*/
public void setColumnReorderingAllowed(boolean columnReorderingAllowed) {
if (columnReorderingAllowed != this.columnReorderingAllowed) {
this.columnReorderingAllowed = columnReorderingAllowed;
markAsDirty();
}
}
/*
* Arranges visible columns according to given columnOrder. Silently ignores
* colimnId:s that are not visible columns, and keeps the internal order of
* visible columns left out of the ordering (trailing). Silently does
* nothing if columnReordering is not allowed.
*/
private void setColumnOrder(Object[] columnOrder) {
if (columnOrder == null || !isColumnReorderingAllowed()) {
return;
}
final LinkedList* The table is not selectable by default. *
* * @return the Value of property selectable. */ public boolean isSelectable() { return selectable; } /** * Setter for property selectable. * ** The table is not selectable by default. *
* * @param selectable * the New value of property selectable. */ public void setSelectable(boolean selectable) { if (this.selectable != selectable) { this.selectable = selectable; markAsDirty(); } } /** * Getter for property columnHeaderMode. * * @return the Value of property columnHeaderMode. */ public ColumnHeaderMode getColumnHeaderMode() { return columnHeaderMode; } /** * Setter for property columnHeaderMode. * * @param columnHeaderMode * the New value of property columnHeaderMode. */ public void setColumnHeaderMode(ColumnHeaderMode columnHeaderMode) { if (columnHeaderMode == null) { throw new IllegalArgumentException( "Column header mode can not be null"); } if (columnHeaderMode != this.columnHeaderMode) { this.columnHeaderMode = columnHeaderMode; markAsDirty(); } } /** * Refreshes the rows in the internal cache. Only if * {@link #resetPageBuffer()} is called before this then all values are * guaranteed to be recreated. */ protected void refreshRenderedCells() { if (!isAttached()) { return; } if (!isContentRefreshesEnabled) { return; } // Collects the basic facts about the table page final int pagelen = getPageLength(); int rows, totalRows; rows = totalRows = size(); int firstIndex = Math .min(getCurrentPageFirstItemIndex(), totalRows - 1); if (rows > 0 && firstIndex >= 0) { rows -= firstIndex; } if (pagelen > 0 && pagelen < rows) { rows = pagelen; } // If "to be painted next" variables are set, use them if (lastToBeRenderedInClient - firstToBeRenderedInClient > 0) { rows = lastToBeRenderedInClient - firstToBeRenderedInClient + 1; } if (firstToBeRenderedInClient >= 0) { if (firstToBeRenderedInClient < totalRows) { firstIndex = firstToBeRenderedInClient; } else { firstIndex = totalRows - 1; } } else { // initial load // #8805 send one extra row in the beginning in case a partial // row is shown on the UI if (firstIndex > 0) { firstIndex = firstIndex - 1; rows = rows + 1; } firstToBeRenderedInClient = firstIndex; } if (totalRows > 0) { if (rows + firstIndex > totalRows) { rows = totalRows - firstIndex; } } else { rows = 0; } // Saves the results to internal buffer pageBuffer = getVisibleCellsNoCache(firstIndex, rows, true); if (rows > 0) { pageBufferFirstIndex = firstIndex; } if (getPageLength() != 0) { removeUnnecessaryRows(); } setRowCacheInvalidated(true); markAsDirty(); maybeThrowCacheUpdateExceptions(); } private void maybeThrowCacheUpdateExceptions() { if (!exceptionsDuringCachePopulation.isEmpty()) { Throwable[] causes = new Throwable[exceptionsDuringCachePopulation .size()]; exceptionsDuringCachePopulation.toArray(causes); exceptionsDuringCachePopulation.clear(); throw new CacheUpdateException(this, "Error during Table cache update.", causes); } } /** * Exception thrown when one or more exceptions occurred during updating of * the Table cache. ** Contains all exceptions which occurred during the cache update. The first * occurred exception is set as the cause of this exception. All occurred * exceptions can be accessed using {@link #getCauses()}. *
* */ public static class CacheUpdateException extends RuntimeException { private Throwable[] causes; private Table table; public CacheUpdateException(Table table, String message, Throwable[] causes) { super(maybeSupplementMessage(message, causes.length), causes[0]); this.table = table; this.causes = causes; } private static String maybeSupplementMessage(String message, int causeCount) { if (causeCount > 1) { return message + " Additional causes not shown."; } else { return message; } } /** * Returns the cause(s) for this exception * * @return the exception(s) which caused this exception */ public Throwable[] getCauses() { return causes; } public Table getTable() { return table; } } /** * Removes rows that fall outside the required cache. */ private void removeUnnecessaryRows() { int minPageBufferIndex = getMinPageBufferIndex(); int maxPageBufferIndex = getMaxPageBufferIndex(); int maxBufferSize = maxPageBufferIndex - minPageBufferIndex + 1; /* * Number of rows that were previously cached. This is not necessarily * the same as pageLength if we do not have enough rows in the * container. */ int currentlyCachedRowCount = pageBuffer[CELL_ITEMID].length; if (currentlyCachedRowCount <= maxBufferSize) { // removal unnecessary return; } /* Figure out which rows to get rid of. */ int firstCacheRowToRemoveInPageBuffer = -1; if (minPageBufferIndex > pageBufferFirstIndex) { firstCacheRowToRemoveInPageBuffer = pageBufferFirstIndex; } else if (maxPageBufferIndex < pageBufferFirstIndex + currentlyCachedRowCount) { firstCacheRowToRemoveInPageBuffer = maxPageBufferIndex + 1; } if (firstCacheRowToRemoveInPageBuffer - pageBufferFirstIndex < currentlyCachedRowCount) { /* * Unregister all components that fall beyond the cache limits after * inserting the new rows. */ unregisterComponentsAndPropertiesInRows( firstCacheRowToRemoveInPageBuffer, currentlyCachedRowCount - firstCacheRowToRemoveInPageBuffer); } } /** * Requests that the Table should be repainted as soon as possible. * * Note that a {@code Table} does not necessarily repaint its contents when * this method has been called. See {@link #refreshRowCache()} for forcing * an update of the contents. * * @deprecated As of 7.0, use {@link #markAsDirty()} instead */ @Deprecated @Override public void requestRepaint() { markAsDirty(); } /** * Requests that the Table should be repainted as soon as possible. * * Note that a {@code Table} does not necessarily repaint its contents when * this method has been called. See {@link #refreshRowCache()} for forcing * an update of the contents. */ @Override public void markAsDirty() { // Overridden only for javadoc super.markAsDirty(); } @Override public void markAsDirtyRecursive() { super.markAsDirtyRecursive(); // Avoid sending a partial repaint (#8714) refreshRowCache(); } private void removeRowsFromCacheAndFillBottom(int firstIndex, int rows) { int totalCachedRows = pageBuffer[CELL_ITEMID].length; int totalRows = size(); int firstIndexInPageBuffer = firstIndex - pageBufferFirstIndex; /* * firstIndexInPageBuffer is the first row to be removed. "rows" rows * after that should be removed. If the page buffer does not contain * that many rows, we only remove the rows that actually are in the page * buffer. */ if (firstIndexInPageBuffer + rows > totalCachedRows) { rows = totalCachedRows - firstIndexInPageBuffer; } /* * Unregister components that will no longer be in the page buffer to * make sure that no components leak. */ unregisterComponentsAndPropertiesInRows(firstIndex, rows); /* * The number of rows that should be in the cache after this operation * is done (pageBuffer currently contains the expanded items). */ int newCachedRowCount = totalCachedRows; if (newCachedRowCount + pageBufferFirstIndex > totalRows) { newCachedRowCount = totalRows - pageBufferFirstIndex; } /* * The index at which we should render the first row that does not come * from the previous page buffer. */ int firstAppendedRowInPageBuffer = totalCachedRows - rows; int firstAppendedRow = firstAppendedRowInPageBuffer + pageBufferFirstIndex; /* * Calculate the maximum number of new rows that we can add to the page * buffer. Less than the rows we removed if the container does not * contain that many items afterwards. */ int maxRowsToRender = (totalRows - firstAppendedRow); int rowsToAdd = rows; if (rowsToAdd > maxRowsToRender) { rowsToAdd = maxRowsToRender; } Object[][] cells = null; if (rowsToAdd > 0) { cells = getVisibleCellsNoCache(firstAppendedRow, rowsToAdd, false); } /* * Create the new cache buffer by copying the first rows from the old * buffer, moving the following rows upwards and appending more rows if * applicable. */ Object[][] newPageBuffer = new Object[pageBuffer.length][newCachedRowCount]; for (int i = 0; i < pageBuffer.length; i++) { for (int row = 0; row < firstIndexInPageBuffer; row++) { // Copy the first rows newPageBuffer[i][row] = pageBuffer[i][row]; } for (int row = firstIndexInPageBuffer; row < firstAppendedRowInPageBuffer; row++) { // Move the rows that were after the expanded rows newPageBuffer[i][row] = pageBuffer[i][row + rows]; } for (int row = firstAppendedRowInPageBuffer; row < newCachedRowCount; row++) { // Add the newly rendered rows. Only used if rowsToAdd > 0 // (cells != null) newPageBuffer[i][row] = cells[i][row - firstAppendedRowInPageBuffer]; } } pageBuffer = newPageBuffer; } private Object[][] getVisibleCellsUpdateCacheRows(int firstIndex, int rows) { Object[][] cells = getVisibleCellsNoCache(firstIndex, rows, false); int cacheIx = firstIndex - pageBufferFirstIndex; // update the new rows in the cache. int totalCachedRows = pageBuffer[CELL_ITEMID].length; int end = Math.min(cacheIx + rows, totalCachedRows); for (int ix = cacheIx; ix < end; ix++) { for (int i = 0; i < pageBuffer.length; i++) { pageBuffer[i][ix] = cells[i][ix - cacheIx]; } } return cells; } /** * @param firstIndex * The position where new rows should be inserted * @param rows * The maximum number of rows that should be inserted at position * firstIndex. Less rows will be inserted if the page buffer is * too small. * @return */ private Object[][] getVisibleCellsInsertIntoCache(int firstIndex, int rows) { getLogger() .log(Level.FINEST, "Insert {0} rows at index {1} to existing page buffer requested", new Object[] { rows, firstIndex }); int minPageBufferIndex = getMinPageBufferIndex(); int maxPageBufferIndex = getMaxPageBufferIndex(); int maxBufferSize = maxPageBufferIndex - minPageBufferIndex + 1; if (getPageLength() == 0) { // If pageLength == 0 then all rows should be rendered maxBufferSize = pageBuffer[CELL_ITEMID].length + rows; } /* * Number of rows that were previously cached. This is not necessarily * the same as maxBufferSize. */ int currentlyCachedRowCount = pageBuffer[CELL_ITEMID].length; /* If rows > size available in page buffer */ if (firstIndex + rows - 1 > maxPageBufferIndex) { rows = maxPageBufferIndex - firstIndex + 1; } /* * "rows" rows will be inserted at firstIndex. Find out how many old * rows fall outside the new buffer so we can unregister components in * the cache. */ /* * if there are rows before the new pageBuffer limits they must be * removed */ int lastCacheRowToRemove = minPageBufferIndex - 1; int rowsFromBeginning = lastCacheRowToRemove - pageBufferFirstIndex + 1; if (lastCacheRowToRemove >= pageBufferFirstIndex) { unregisterComponentsAndPropertiesInRows(pageBufferFirstIndex, rowsFromBeginning); } else { rowsFromBeginning = 0; } /* * the rows that fall outside of the new pageBuffer limits after the new * rows are inserted must also be removed */ int firstCacheRowToRemove = firstIndex; /* * IF there is space remaining in the buffer after the rows have been * inserted, we can keep more rows. */ int numberOfOldRowsAfterInsertedRows = Math.min(pageBufferFirstIndex + currentlyCachedRowCount + rows, maxPageBufferIndex + 1) - (firstIndex + rows - 1); if (numberOfOldRowsAfterInsertedRows > 0) { firstCacheRowToRemove += numberOfOldRowsAfterInsertedRows; } int rowsFromAfter = currentlyCachedRowCount - (firstCacheRowToRemove - pageBufferFirstIndex); if (rowsFromAfter > 0) { /* * Unregister all components that fall beyond the cache limits after * inserting the new rows. */ unregisterComponentsAndPropertiesInRows(firstCacheRowToRemove, rowsFromAfter); } // Calculate the new cache size int newCachedRowCount = maxBufferSize; if (pageBufferFirstIndex + currentlyCachedRowCount + rows - 1 < maxPageBufferIndex) { // there aren't enough rows to fill the whole potential -> use what // there is newCachedRowCount -= maxPageBufferIndex - (pageBufferFirstIndex + currentlyCachedRowCount + rows - 1); } else if (minPageBufferIndex < pageBufferFirstIndex) { newCachedRowCount -= pageBufferFirstIndex - minPageBufferIndex; } /* calculate the internal location of the new rows within the new cache */ int firstIndexInNewPageBuffer = firstIndex - pageBufferFirstIndex - rowsFromBeginning; /* Paint the new rows into a separate buffer */ Object[][] cells = getVisibleCellsNoCache(firstIndex, rows, false); /* * Create the new cache buffer and fill it with the data from the old * buffer as well as the inserted rows. */ Object[][] newPageBuffer = new Object[pageBuffer.length][newCachedRowCount]; for (int i = 0; i < pageBuffer.length; i++) { for (int row = 0; row < firstIndexInNewPageBuffer; row++) { // Copy the first rows newPageBuffer[i][row] = pageBuffer[i][rowsFromBeginning + row]; } for (int row = firstIndexInNewPageBuffer; row < firstIndexInNewPageBuffer + rows; row++) { // Copy the newly created rows newPageBuffer[i][row] = cells[i][row - firstIndexInNewPageBuffer]; } for (int row = firstIndexInNewPageBuffer + rows; row < newCachedRowCount; row++) { // Move the old rows down below the newly inserted rows newPageBuffer[i][row] = pageBuffer[i][rowsFromBeginning + row - rows]; } } pageBuffer = newPageBuffer; pageBufferFirstIndex = Math.max(pageBufferFirstIndex + rowsFromBeginning, minPageBufferIndex); if (getLogger().isLoggable(Level.FINEST)) { getLogger().log( Level.FINEST, "Page Buffer now contains {0} rows ({1}-{2})", new Object[] { pageBuffer[CELL_ITEMID].length, pageBufferFirstIndex, (pageBufferFirstIndex + pageBuffer[CELL_ITEMID].length - 1) }); } return cells; } private int getMaxPageBufferIndex() { int total = size(); if (getPageLength() == 0) { // everything is shown at once, no caching return total - 1; } // Page buffer must not become larger than pageLength*cacheRate after // the current page int maxPageBufferIndex = getCurrentPageFirstItemIndex() + (int) (getPageLength() * (1 + getCacheRate())); if (shouldHideNullSelectionItem()) { --total; } if (maxPageBufferIndex >= total) { maxPageBufferIndex = total - 1; } return maxPageBufferIndex; } private int getMinPageBufferIndex() { if (getPageLength() == 0) { // everything is shown at once, no caching return 0; } // Page buffer must not become larger than pageLength*cacheRate before // the current page int minPageBufferIndex = getCurrentPageFirstItemIndex() - (int) (getPageLength() * getCacheRate()); if (minPageBufferIndex < 0) { minPageBufferIndex = 0; } return minPageBufferIndex; } /** * Render rows with index "firstIndex" to "firstIndex+rows-1" to a new * buffer. * * Reuses values from the current page buffer if the rows are found there. * * @param firstIndex * @param rows * @param replaceListeners * @return */ private Object[][] getVisibleCellsNoCache(int firstIndex, int rows, boolean replaceListeners) { if (getLogger().isLoggable(Level.FINEST)) { getLogger().log(Level.FINEST, "Render visible cells for rows {0}-{1}", new Object[] { firstIndex, (firstIndex + rows - 1) }); } final Object[] colids = getVisibleColumns(); final int cols = colids.length; HashSet* The mode can be one of the following ones: *
toString()
* is used as row caption.
* toString()
* is used as row caption.
* toString()
is used as row header. If caption is explicitly
* specified, it overrides the id-caption.
* Container.Indexed
interface.* Do not call this e.g. if you have updated the data model through a * Property. These types of changes are automatically propagated to the * Table. *
* A typical case when this is needed is if you update a generator (e.g. * CellStyleGenerator) and want to ensure that the rows are redrawn with new * styles. *
* Note that calling this method is not cheap so avoid calling it
* unnecessarily.
*
* @since 6.7.2
*/
public void refreshRowCache() {
resetPageBuffer();
refreshRenderedCells();
}
/**
* Sets the Container that serves as the data source of the viewer. As a
* side-effect the table's selection value is set to null as the old
* selection might not exist in new Container.
* A generated column is a column that exists only in the Table, not as a
* property in the underlying Container. It shows up just as a regular
* column.
*
* A generated column will override a property with the same id, so that the
* generated column is shown instead of the column representing the
* property. Note that getContainerProperty() will still get the real
* property.
*
* Table will not listen to value change events from properties overridden
* by generated columns. If the content of your generated column depends on
* properties that are not directly visible in the table, attach value
* change listener to update the content on all depended properties.
* Otherwise your UI might not get updated as expected.
*
* Also note that getVisibleColumns() will return the generated columns,
* while getContainerPropertyIds() will not.
*
* Note, that some due to historical reasons the name of the method is bit
* misleading. Some items may be partly or totally out of the viewport of
* the table's scrollable area. Actually detecting rows which can be
* actually seen by the end user may be problematic due to the client server
* architecture. Using {@link #getCurrentPageFirstItemId()} combined with
* {@link #getPageLength()} may produce good enough estimates in some
* situations.
*
* @see com.vaadin.ui.Select#getVisibleItemIds()
*/
@Override
public Collection> getVisibleItemIds() {
final LinkedList
* Note that the {@link #isSortEnabled()} state affects what this method
* returns. Disabling sorting causes this method to always return an empty
* collection.
*
* Setting this to false disallows sorting by the user. It is still possible
* to call {@link #sort()}.
*
* Note, that on some clients the mode may not be respected. E.g. on touch
* based devices CTRL/SHIFT base selection method is invalid, so touch based
* browsers always use the {@link MultiSelectMode#SIMPLE}.
*
* @param mode
* The select mode of the table
*/
public void setMultiSelectMode(MultiSelectMode mode) {
multiSelectMode = mode;
markAsDirty();
}
/**
* Returns the select mode in which multi-select is used.
*
* @return The multi select mode
*/
public MultiSelectMode getMultiSelectMode() {
return multiSelectMode;
}
/**
* Lazy loading accept criterion for Table. Accepted target rows are loaded
* from server once per drag and drop operation. Developer must override one
* method that decides on which rows the currently dragged data can be
* dropped.
*
*
* Initially pretty much no data is sent to client. On first required
* criterion check (per drag request) the client side data structure is
* initialized from server and no subsequent requests requests are needed
* during that drag and drop operation.
*/
public static abstract class TableDropCriterion extends ServerSideCriterion {
private Table table;
private Set
* The listener will receive events which contain information about which
* column was clicked and some details about the mouse event.
*
* The listener will receive events which contain information about which
* column was clicked and some details about the mouse event.
*
* The footer can be used to add column related data like sums to the bottom
* of the Table using setColumnFooter(Object propertyId, String footer).
*
* The GeneratedRow data object contains the text that should be
* rendered in the row. The itemId in the container thus works only as a
* placeholder.
*
* If GeneratedRow.setSpanColumns(true) is used, there will be one
* String spanning all columns (use setText("Spanning text")). Otherwise
* you can define one String per visible column.
*
* If GeneratedRow.setRenderAsHtml(true) is used, the strings can
* contain HTML markup, otherwise all strings will be rendered as text
* (the default).
*
* A "v-table-generated-row" CSS class is added to all generated rows.
* For custom styling of a generated row you can combine a RowGenerator
* with a CellStyleGenerator.
*
*
* @param table
* The Table that is being painted
* @param itemId
* The itemId for the row
* @return A GeneratedRow describing how the row should be painted or
* null to paint the row with the contents from the container
*/
public GeneratedRow generateRow(Table table, Object itemId);
}
public static class GeneratedRow implements Serializable {
private boolean htmlContentAllowed = false;
private boolean spanColumns = false;
private String[] text = null;
/**
* Creates a new generated row. If only one string is passed in, columns
* are automatically spanned.
*
* @param text
*/
public GeneratedRow(String... text) {
setHtmlContentAllowed(false);
setSpanColumns(text == null || text.length == 1);
setText(text);
}
/**
* Pass one String if spanColumns is used, one String for each visible
* column otherwise
*/
public void setText(String... text) {
if (text == null || (text.length == 1 && text[0] == null)) {
text = new String[] { "" };
}
this.text = text;
}
protected String[] getText() {
return text;
}
protected Object getValue() {
return getText();
}
protected boolean isHtmlContentAllowed() {
return htmlContentAllowed;
}
/**
* If set to true, all strings passed to {@link #setText(String...)}
* will be rendered as HTML.
*
* @param htmlContentAllowed
*/
public void setHtmlContentAllowed(boolean htmlContentAllowed) {
this.htmlContentAllowed = htmlContentAllowed;
}
protected boolean isSpanColumns() {
return spanColumns;
}
/**
* If set to true, only one string will be rendered, spanning the entire
* row.
*
* @param spanColumns
*/
public void setSpanColumns(boolean spanColumns) {
this.spanColumns = spanColumns;
}
}
/**
* Assigns a row generator to the table. The row generator will be able to
* replace rows in the table when it is rendered.
*
* @param generator
* the new row generator
*/
public void setRowGenerator(RowGenerator generator) {
rowGenerator = generator;
refreshRowCache();
}
/**
* @return the current row generator
*/
public RowGenerator getRowGenerator() {
return rowGenerator;
}
/**
* Sets a converter for a property id.
*
* The converter is used to format the the data for the given property id
* before displaying it in the table.
*
*
* All rows and columns are generated as visible using this method. If the
* new container contains properties that are not meant to be shown you
* should use {@link Table#setContainerDataSource(Container, Collection)}
* instead, especially if the table is editable.
*
* @param newDataSource
* the new data source.
*/
@Override
public void setContainerDataSource(Container newDataSource) {
if (newDataSource == null) {
newDataSource = new IndexedContainer();
}
CollectionItemId
from the Container.
*
* @see com.vaadin.data.Container#removeItem(Object)
*/
@Override
public boolean removeItem(Object itemId) {
final Object nextItemId = nextItemId(itemId);
final boolean ret = super.removeItem(itemId);
if (ret && (itemId != null) && (itemId.equals(currentPageFirstItemId))) {
currentPageFirstItemId = nextItemId;
}
if (!(items instanceof Container.ItemSetChangeNotifier)) {
refreshRowCache();
}
return ret;
}
/**
* Removes a Property specified by the given Property ID from the Container.
*
* @see com.vaadin.data.Container#removeContainerProperty(Object)
*/
@Override
public boolean removeContainerProperty(Object propertyId)
throws UnsupportedOperationException {
// If a visible property is removed, remove the corresponding column
visibleColumns.remove(propertyId);
columnAlignments.remove(propertyId);
columnIcons.remove(propertyId);
columnHeaders.remove(propertyId);
columnFooters.remove(propertyId);
return super.removeContainerProperty(propertyId);
}
/**
* Adds a new property to the table and show it as a visible column.
*
* @param propertyId
* the Id of the proprty.
* @param type
* the class of the property.
* @param defaultValue
* the default value given for all existing items.
* @see com.vaadin.data.Container#addContainerProperty(Object, Class,
* Object)
*/
@Override
public boolean addContainerProperty(Object propertyId, Class> type,
Object defaultValue) throws UnsupportedOperationException {
boolean visibleColAdded = false;
if (!visibleColumns.contains(propertyId)) {
visibleColumns.add(propertyId);
visibleColAdded = true;
}
if (!super.addContainerProperty(propertyId, type, defaultValue)) {
if (visibleColAdded) {
visibleColumns.remove(propertyId);
}
return false;
}
if (!(items instanceof Container.PropertySetChangeNotifier)) {
refreshRowCache();
}
return true;
}
/**
* Adds a new property to the table and show it as a visible column.
*
* @param propertyId
* the Id of the proprty
* @param type
* the class of the property
* @param defaultValue
* the default value given for all existing items
* @param columnHeader
* the Explicit header of the column. If explicit header is not
* needed, this should be set null.
* @param columnIcon
* the Icon of the column. If icon is not needed, this should be
* set null.
* @param columnAlignment
* the Alignment of the column. Null implies align left.
* @throws UnsupportedOperationException
* if the operation is not supported.
* @see com.vaadin.data.Container#addContainerProperty(Object, Class,
* Object)
*/
public boolean addContainerProperty(Object propertyId, Class> type,
Object defaultValue, String columnHeader, Resource columnIcon,
Align columnAlignment) throws UnsupportedOperationException {
if (!this.addContainerProperty(propertyId, type, defaultValue)) {
return false;
}
setColumnAlignment(propertyId, columnAlignment);
setColumnHeader(propertyId, columnHeader);
setColumnIcon(propertyId, columnIcon);
return true;
}
/**
* Adds a generated column to the Table.
* true
if ascending, false
if descending.
*/
public boolean isSortAscending() {
return sortAscending;
}
/**
* Sets the table in ascending order.
*
* @param ascending
* true
if ascending, false
if
* descending.
*/
public void setSortAscending(boolean ascending) {
setSortAscending(ascending, true);
}
/**
* Internal method to set sort ascending. With doSort flag actual sort can
* be bypassed.
*
* @param ascending
* @param doSort
*/
private void setSortAscending(boolean ascending, boolean doSort) {
if (sortAscending != ascending) {
sortAscending = ascending;
if (doSort) {
sort();
// Assures the visual refresh. This should not be necessary as
// sort() calls refreshRowCache
refreshRenderedCells();
}
}
}
/**
* Is sorting disabled altogether.
*
* True iff no sortable columns are given even in the case where data source
* would support this.
*
* @return True iff sorting is disabled.
* @deprecated As of 7.0, use {@link #isSortEnabled()} instead
*/
@Deprecated
public boolean isSortDisabled() {
return !isSortEnabled();
}
/**
* Checks if sorting is enabled.
*
* @return true if sorting by the user is allowed, false otherwise
*/
public boolean isSortEnabled() {
return sortEnabled;
}
/**
* Disables the sorting by the user altogether.
*
* @param sortDisabled
* True iff sorting is disabled.
* @deprecated As of 7.0, use {@link #setSortEnabled(boolean)} instead
*/
@Deprecated
public void setSortDisabled(boolean sortDisabled) {
setSortEnabled(!sortDisabled);
}
/**
* Enables or disables sorting.
*