diff options
author | Artur Signell <artur@vaadin.com> | 2012-08-13 18:34:33 +0300 |
---|---|---|
committer | Artur Signell <artur@vaadin.com> | 2012-08-13 19:18:33 +0300 |
commit | e85d933b25cc3c5cc85eb7eb4b13b950fd8e1569 (patch) | |
tree | 9ab6f13f7188cab44bbd979b1cf620f15328a03f /server/src/com/vaadin/ui/Table.java | |
parent | 14dd4d0b28c76eb994b181a4570f3adec53342e6 (diff) | |
download | vaadin-framework-e85d933b25cc3c5cc85eb7eb4b13b950fd8e1569.tar.gz vaadin-framework-e85d933b25cc3c5cc85eb7eb4b13b950fd8e1569.zip |
Moved server files to a server src folder (#9299)
Diffstat (limited to 'server/src/com/vaadin/ui/Table.java')
-rw-r--r-- | server/src/com/vaadin/ui/Table.java | 5449 |
1 files changed, 5449 insertions, 0 deletions
diff --git a/server/src/com/vaadin/ui/Table.java b/server/src/com/vaadin/ui/Table.java new file mode 100644 index 0000000000..39b7fb7473 --- /dev/null +++ b/server/src/com/vaadin/ui/Table.java @@ -0,0 +1,5449 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +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.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.shared.MouseEventDetails; +import com.vaadin.terminal.KeyMapper; +import com.vaadin.terminal.LegacyPaint; +import com.vaadin.terminal.PaintException; +import com.vaadin.terminal.PaintTarget; +import com.vaadin.terminal.Resource; +import com.vaadin.terminal.gwt.client.ui.table.VScrollTable; + +/** + * <p> + * <code>Table</code> is used for representing data or components in a pageable + * and selectable table. + * </p> + * + * <p> + * 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. + * </p> + * + * <p> + * Components in a Table will not have their caption nor icon rendered. + * </p> + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @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. <b>This is the default behaviour. </b> + */ + 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 from 7.0, use {@link Align#LEFT} instead + */ + @Deprecated + public static final Align ALIGN_LEFT = Align.LEFT; + + /** + * @deprecated from 7.0, use {@link Align#CENTER} instead + */ + @Deprecated + public static final Align ALIGN_CENTER = Align.CENTER; + + /** + * @deprecated from 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. + * <p> + * <b>This is the default behavior. </b> + */ + EXPLICIT_DEFAULTS_ID + } + + /** + * @deprecated from 7.0, use {@link ColumnHeaderMode#HIDDEN} instead + */ + @Deprecated + public static final ColumnHeaderMode COLUMN_HEADER_MODE_HIDDEN = ColumnHeaderMode.HIDDEN; + + /** + * @deprecated from 7.0, use {@link ColumnHeaderMode#ID} instead + */ + @Deprecated + public static final ColumnHeaderMode COLUMN_HEADER_MODE_ID = ColumnHeaderMode.ID; + + /** + * @deprecated from 7.0, use {@link ColumnHeaderMode#EXPLICIT} instead + */ + @Deprecated + public static final ColumnHeaderMode COLUMN_HEADER_MODE_EXPLICIT = ColumnHeaderMode.EXPLICIT; + + /** + * @deprecated from 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. <b>This is the default + * mode. </b> + */ + 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 <code>toString()</code> 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 from 7.0, use {@link RowHeaderMode#HIDDEN} instead + */ + @Deprecated + public static final RowHeaderMode ROW_HEADER_MODE_HIDDEN = RowHeaderMode.HIDDEN; + + /** + * @deprecated from 7.0, use {@link RowHeaderMode#ID} instead + */ + @Deprecated + public static final RowHeaderMode ROW_HEADER_MODE_ID = RowHeaderMode.ID; + + /** + * @deprecated from 7.0, use {@link RowHeaderMode#ITEM} instead + */ + @Deprecated + public static final RowHeaderMode ROW_HEADER_MODE_ITEM = RowHeaderMode.ITEM; + + /** + * @deprecated from 7.0, use {@link RowHeaderMode#INDEX} instead + */ + @Deprecated + public static final RowHeaderMode ROW_HEADER_MODE_INDEX = RowHeaderMode.INDEX; + + /** + * @deprecated from 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 from 7.0, use {@link RowHeaderMode#EXPLICIT} instead + */ + @Deprecated + public static final RowHeaderMode ROW_HEADER_MODE_EXPLICIT = RowHeaderMode.EXPLICIT; + + /** + * @deprecated from 7.0, use {@link RowHeaderMode#ICON_ONLY} instead + */ + @Deprecated + public static final RowHeaderMode ROW_HEADER_MODE_ICON_ONLY = RowHeaderMode.ICON_ONLY; + + /** + * @deprecated from 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<Object> columnIdMap = new KeyMapper<Object>(); + + /** + * Holds visible column propertyIds - in order. + */ + private LinkedList<Object> visibleColumns = new LinkedList<Object>(); + + /** + * Holds noncollapsible columns. + */ + private HashSet<Object> noncollapsibleColumns = new HashSet<Object>(); + + /** + * Holds propertyIds of currently collapsed columns. + */ + private final HashSet<Object> collapsedColumns = new HashSet<Object>(); + + /** + * Holds headers for visible columns (by propertyId). + */ + private final HashMap<Object, String> columnHeaders = new HashMap<Object, String>(); + + /** + * Holds footers for visible columns (by propertyId). + */ + private final HashMap<Object, String> columnFooters = new HashMap<Object, String>(); + + /** + * Holds icons for visible columns (by propertyId). + */ + private final HashMap<Object, Resource> columnIcons = new HashMap<Object, Resource>(); + + /** + * Holds alignments for visible columns (by propertyId). + */ + private HashMap<Object, Align> columnAlignments = new HashMap<Object, Align>(); + + /** + * Holds column widths in pixels (Integer) or expand ratios (Float) for + * visible columns (by propertyId). + */ + private final HashMap<Object, Object> columnWidths = new HashMap<Object, Object>(); + + /** + * Holds column generators + */ + private final HashMap<Object, ColumnGenerator> columnGenerators = new LinkedHashMap<Object, ColumnGenerator>(); + + /** + * Holds value of property pageLength. 0 disables paging. + */ + private int pageLength = 15; + + /** + * Id the first item on the current page. + */ + private Object currentPageFirstItemId = null; + + /** + * Index of the first item on the current page. + */ + private int currentPageFirstItemIndex = 0; + + /** + * Holds value of property selectable. + */ + private boolean selectable = false; + + /** + * Holds value of property columnHeaderMode. + */ + private ColumnHeaderMode columnHeaderMode = ColumnHeaderMode.EXPLICIT_DEFAULTS_ID; + + /** + * Holds value of property rowHeaderMode. + */ + private RowHeaderMode rowHeaderMode = RowHeaderMode.EXPLICIT_DEFAULTS_ID; + + /** + * Should the Table footer be visible? + */ + private boolean columnFootersVisible = false; + + /** + * Page contents buffer used in buffered mode. + */ + private Object[][] pageBuffer = null; + + /** + * Set of properties listened - the list is kept to release the listeners + * later. + */ + private HashSet<Property<?>> listenedProperties = null; + + /** + * Set of visible components - the is used for needsRepaint calculation. + */ + private HashSet<Component> visibleComponents = null; + + /** + * List of action handlers. + */ + private LinkedList<Handler> actionHandlers = null; + + /** + * Action mapper. + */ + private KeyMapper<Action> actionMapper = null; + + /** + * Table cell editor factory. + */ + private TableFieldFactory fieldFactory = DefaultFieldFactory.get(); + + /** + * Is table editable. + */ + private boolean editable = false; + + /** + * Current sorting direction. + */ + private boolean sortAscending = true; + + /** + * Currently table is sorted on this propertyId. + */ + private Object sortContainerPropertyId = null; + + /** + * Is table sorting by the user enabled. + */ + private boolean sortEnabled = true; + + /** + * Number of rows explicitly requested by the client to be painted on next + * paint. This is -1 if no request by the client is made. Painting the + * component will automatically reset this to -1. + */ + private int reqRowsToPaint = -1; + + /** + * Index of the first rows explicitly requested by the client to be painted. + * This is -1 if no request by the client is made. Painting the component + * will automatically reset this to -1. + */ + private int reqFirstRowToPaint = -1; + + private int firstToBeRenderedInClient = -1; + + private int lastToBeRenderedInClient = -1; + + private boolean isContentRefreshesEnabled = true; + + private int pageBufferFirstIndex; + + private boolean containerChangeToBeRendered = false; + + /** + * Table cell specific style generator + */ + private CellStyleGenerator cellStyleGenerator = null; + + /** + * Table cell specific tooltip generator + */ + private ItemDescriptionGenerator itemDescriptionGenerator; + + /* + * EXPERIMENTAL feature: will tell the client to re-calculate column widths + * if set to true. Currently no setter: extend to enable. + */ + protected boolean alwaysRecalculateColumnWidths = false; + + private double cacheRate = CACHE_RATE_DEFAULT; + + private TableDragMode dragMode = TableDragMode.NONE; + + private DropHandler dropHandler; + + private MultiSelectMode multiSelectMode = MultiSelectMode.DEFAULT; + + private boolean rowCacheInvalidated; + + private RowGenerator rowGenerator = null; + + private final Map<Field<?>, Property<?>> associatedProperties = new HashMap<Field<?>, Property<?>>(); + + private boolean painted = false; + + private HashMap<Object, Converter<String, Object>> propertyValueConverters = new HashMap<Object, Converter<String, Object>>(); + + /** + * Set to true if the client-side should be informed that the key mapper has + * been reset so it can avoid sending back references to keys that are no + * longer present. + */ + private boolean keyMapperReset; + + /* Table constructors */ + + /** + * Creates a new empty table. + */ + public Table() { + setRowHeaderMode(ROW_HEADER_MODE_HIDDEN); + } + + /** + * Creates a new empty table with caption. + * + * @param caption + */ + public Table(String caption) { + this(); + setCaption(caption); + } + + /** + * Creates a new table with caption and connect it to a Container. + * + * @param caption + * @param dataSource + */ + public Table(String caption, Container dataSource) { + this(); + setCaption(caption); + setContainerDataSource(dataSource); + } + + /* Table functionality */ + + /** + * Gets the array of visible column id:s, including generated columns. + * + * <p> + * The columns are show in the order of their appearance in this array. + * </p> + * + * @return an array of currently visible propertyIds and generated column + * ids. + */ + public Object[] getVisibleColumns() { + if (visibleColumns == null) { + return null; + } + return visibleColumns.toArray(); + } + + /** + * Sets the array of visible column property id:s. + * + * <p> + * The columns are show in the order of their appearance in this array. + * </p> + * + * @param visibleColumns + * the Array of shown property id:s. + */ + public void setVisibleColumns(Object[] visibleColumns) { + + // Visible columns must exist + if (visibleColumns == null) { + throw new NullPointerException( + "Can not set visible columns to null value"); + } + + // TODO add error check that no duplicate identifiers exist + + // Checks that the new visible columns contains no nulls and properties + // exist + final Collection<?> properties = getContainerPropertyIds(); + for (int i = 0; i < visibleColumns.length; i++) { + if (visibleColumns[i] == null) { + throw new NullPointerException("Ids must be non-nulls"); + } else if (!properties.contains(visibleColumns[i]) + && !columnGenerators.containsKey(visibleColumns[i])) { + throw new IllegalArgumentException( + "Ids must exist in the Container or as a generated column , missing id: " + + visibleColumns[i]); + } + } + + // If this is called before the constructor is finished, it might be + // uninitialized + final LinkedList<Object> newVC = new LinkedList<Object>(); + for (int i = 0; i < visibleColumns.length; i++) { + newVC.add(visibleColumns[i]); + } + + // Removes alignments, icons and headers from hidden columns + if (this.visibleColumns != null) { + boolean disabledHere = disableContentRefreshing(); + try { + for (final Iterator<Object> i = this.visibleColumns.iterator(); i + .hasNext();) { + final Object col = i.next(); + if (!newVC.contains(col)) { + setColumnHeader(col, null); + setColumnAlignment(col, (Align) null); + setColumnIcon(col, null); + } + } + } finally { + if (disabledHere) { + enableContentRefreshing(false); + } + } + } + + this.visibleColumns = newVC; + + // Assures visual refresh + refreshRowCache(); + } + + /** + * Gets the headers of the columns. + * + * <p> + * The headers match the property id:s given my the set visible column + * headers. The table must be set in either + * {@link #COLUMN_HEADER_MODE_EXPLICIT} or + * {@link #COLUMN_HEADER_MODE_EXPLICIT_DEFAULTS_ID} mode to show the + * headers. In the defaults mode any nulls in the headers array are replaced + * with id.toString(). + * </p> + * + * @return the Array of column headers. + */ + public String[] getColumnHeaders() { + if (columnHeaders == null) { + return null; + } + final String[] headers = new String[visibleColumns.size()]; + int i = 0; + for (final Iterator<Object> it = visibleColumns.iterator(); it + .hasNext(); i++) { + headers[i] = getColumnHeader(it.next()); + } + return headers; + } + + /** + * Sets the headers of the columns. + * + * <p> + * The headers match the property id:s given my the set visible column + * headers. The table must be set in either + * {@link #COLUMN_HEADER_MODE_EXPLICIT} or + * {@link #COLUMN_HEADER_MODE_EXPLICIT_DEFAULTS_ID} mode to show the + * headers. In the defaults mode any nulls in the headers array are replaced + * with id.toString() outputs when rendering. + * </p> + * + * @param columnHeaders + * the Array of column headers that match the + * {@link #getVisibleColumns()} method. + */ + public void setColumnHeaders(String[] columnHeaders) { + + if (columnHeaders.length != visibleColumns.size()) { + throw new IllegalArgumentException( + "The length of the headers array must match the number of visible columns"); + } + + this.columnHeaders.clear(); + int i = 0; + for (final Iterator<Object> it = visibleColumns.iterator(); it + .hasNext() && i < columnHeaders.length; i++) { + this.columnHeaders.put(it.next(), columnHeaders[i]); + } + + requestRepaint(); + } + + /** + * Gets the icons of the columns. + * + * <p> + * The icons in headers match the property id:s given my the set visible + * column headers. The table must be set in either + * {@link #COLUMN_HEADER_MODE_EXPLICIT} or + * {@link #COLUMN_HEADER_MODE_EXPLICIT_DEFAULTS_ID} mode to show the headers + * with icons. + * </p> + * + * @return the Array of icons that match the {@link #getVisibleColumns()}. + */ + public Resource[] getColumnIcons() { + if (columnIcons == null) { + return null; + } + final Resource[] icons = new Resource[visibleColumns.size()]; + int i = 0; + for (final Iterator<Object> it = visibleColumns.iterator(); it + .hasNext(); i++) { + icons[i] = columnIcons.get(it.next()); + } + + return icons; + } + + /** + * Sets the icons of the columns. + * + * <p> + * The icons in headers match the property id:s given my the set visible + * column headers. The table must be set in either + * {@link #COLUMN_HEADER_MODE_EXPLICIT} or + * {@link #COLUMN_HEADER_MODE_EXPLICIT_DEFAULTS_ID} mode to show the headers + * with icons. + * </p> + * + * @param columnIcons + * the Array of icons that match the {@link #getVisibleColumns()} + * . + */ + public void setColumnIcons(Resource[] columnIcons) { + + if (columnIcons.length != visibleColumns.size()) { + throw new IllegalArgumentException( + "The length of the icons array must match the number of visible columns"); + } + + this.columnIcons.clear(); + int i = 0; + for (final Iterator<Object> it = visibleColumns.iterator(); it + .hasNext() && i < columnIcons.length; i++) { + this.columnIcons.put(it.next(), columnIcons[i]); + } + + requestRepaint(); + } + + /** + * Gets the array of column alignments. + * + * <p> + * The items in the array must match the properties identified by + * {@link #getVisibleColumns()}. The possible values for the alignments + * include: + * <ul> + * <li>{@link Align#LEFT}: Left alignment</li> + * <li>{@link Align#CENTER}: Centered</li> + * <li>{@link Align#RIGHT}: Right alignment</li> + * </ul> + * The alignments default to {@link Align#LEFT}: any null values are + * rendered as align lefts. + * </p> + * + * @return the Column alignments array. + */ + public Align[] getColumnAlignments() { + if (columnAlignments == null) { + return null; + } + final Align[] alignments = new Align[visibleColumns.size()]; + int i = 0; + for (final Iterator<Object> it = visibleColumns.iterator(); it + .hasNext(); i++) { + alignments[i] = getColumnAlignment(it.next()); + } + + return alignments; + } + + /** + * Sets the column alignments. + * + * <p> + * The amount of items in the array must match the amount of properties + * identified by {@link #getVisibleColumns()}. The possible values for the + * alignments include: + * <ul> + * <li>{@link Align#LEFT}: Left alignment</li> + * <li>{@link Align#CENTER}: Centered</li> + * <li>{@link Align#RIGHT}: Right alignment</li> + * </ul> + * The alignments default to {@link Align#LEFT} + * </p> + * + * @param columnAlignments + * the Column alignments array. + */ + public void setColumnAlignments(Align... columnAlignments) { + + if (columnAlignments.length != visibleColumns.size()) { + throw new IllegalArgumentException( + "The length of the alignments array must match the number of visible columns"); + } + + // Resets the alignments + final HashMap<Object, Align> newCA = new HashMap<Object, Align>(); + int i = 0; + for (final Iterator<Object> it = visibleColumns.iterator(); it + .hasNext() && i < columnAlignments.length; i++) { + newCA.put(it.next(), columnAlignments[i]); + } + this.columnAlignments = newCA; + + // Assures the visual refresh. No need to reset the page buffer before + // as the content has not changed, only the alignments. + refreshRenderedCells(); + } + + /** + * Sets columns width (in pixels). Theme may not necessary respect very + * small or very big values. Setting width to -1 (default) means that theme + * will make decision of width. + * + * <p> + * 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; + } + if (width < 0) { + columnWidths.remove(propertyId); + } else { + columnWidths.put(propertyId, Integer.valueOf(width)); + } + requestRepaint(); + } + + /** + * Sets the column expand ratio for given column. + * <p> + * 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). + * + * <p> + * By default (without expand ratios) the excess space is divided + * proportionally to columns natural widths. + * + * <p> + * Only expand ratios of visible columns are used in final calculations. + * + * <p> + * Column can either have a fixed width or expand ratio. The latter one set + * is used. + * + * <p> + * 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. + * + * <p> + * 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 (expandRatio < 0) { + columnWidths.remove(propertyId); + } else { + columnWidths.put(propertyId, new Float(expandRatio)); + } + } + + public float getColumnExpandRatio(Object propertyId) { + final Object width = columnWidths.get(propertyId); + if (width == null || !(width instanceof Float)) { + return -1; + } + final Float value = (Float) width; + return value.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 Object width = columnWidths.get(propertyId); + if (width == null || !(width instanceof Integer)) { + return -1; + } + final Integer value = (Integer) width; + return value.intValue(); + } + + /** + * Gets the page length. + * + * <p> + * Setting page length 0 disables paging. + * </p> + * + * @return the Length of one page. + */ + public int getPageLength() { + return pageLength; + } + + /** + * Sets the page length. + * + * <p> + * Setting page length 0 disables paging. The page length defaults to 15. + * </p> + * + * <p> + * If Table has width set ({@link #setWidth(float, int)} ) the client side + * may update the page length automatically the correct value. + * </p> + * + * @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. + * + * <p> + * 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. + * + * <p> + * 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; + requestRepaint(); + } + } + + /** + * @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; + } + + 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. + * <p> + * Throws IllegalArgumentException if the specified column is not visible. + * </p> + * + * @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); + } + + requestRepaint(); + } + + /** + * 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); + } + + requestRepaint(); + } + + /** + * Gets the specified column's alignment. + * + * @param propertyId + * the propertyID identifying the column. + * @return the specified column's alignment if it as one; null otherwise. + */ + public Align getColumnAlignment(Object propertyId) { + final Align a = columnAlignments.get(propertyId); + return a == null ? Align.LEFT : a; + } + + /** + * Sets the specified column's alignment. + * + * <p> + * Throws IllegalArgumentException if the alignment is not one of the + * following: {@link Align#LEFT}, {@link Align#CENTER} or + * {@link Align#RIGHT} + * </p> + * + * @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 + * returns <code>true</code>, 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; + requestRepaint(); + } + } + + /* + * 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<Object> newOrder = new LinkedList<Object>(); + for (int i = 0; i < columnOrder.length; i++) { + if (columnOrder[i] != null + && visibleColumns.contains(columnOrder[i])) { + visibleColumns.remove(columnOrder[i]); + newOrder.add(columnOrder[i]); + } + } + for (final Iterator<Object> it = visibleColumns.iterator(); it + .hasNext();) { + final Object columnId = it.next(); + if (!newOrder.contains(columnId)) { + newOrder.add(columnId); + } + } + visibleColumns = newOrder; + + // Assure visual refresh + refreshRowCache(); + } + + /** + * Getter for property currentPageFirstItem. + * + * @return the Value of property currentPageFirstItem. + */ + public int getCurrentPageFirstItemIndex() { + return currentPageFirstItemIndex; + } + + void setCurrentPageFirstItemIndex(int newIndex, boolean needsPageBufferReset) { + + if (newIndex < 0) { + newIndex = 0; + } + + /* + * minimize Container.size() calls which may be expensive. For example + * it may cause sql query. + */ + final int size = size(); + + /* + * 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; + } + + /* + * FIXME #7607 Take somehow into account the case where we want to + * scroll to the bottom so that the last row is completely visible even + * if (table height) / (row height) is not an integer. Reverted the + * original fix because of #8662 regression. + */ + if (newIndex > maxIndex) { + newIndex = maxIndex; + } + + // Refresh first item id + if (items instanceof Container.Indexed) { + try { + currentPageFirstItemId = getIdByIndex(newIndex); + } catch (final IndexOutOfBoundsException e) { + currentPageFirstItemId = null; + } + currentPageFirstItemIndex = newIndex; + } else { + + // For containers not supporting indexes, we must iterate the + // container forwards / backwards + // next available item forward or backward + + currentPageFirstItemId = firstItemId(); + + // Go forwards in the middle of the list (respect borders) + while (currentPageFirstItemIndex < newIndex + && !isLastId(currentPageFirstItemId)) { + currentPageFirstItemIndex++; + currentPageFirstItemId = nextItemId(currentPageFirstItemId); + } + + // If we did hit the border + if (isLastId(currentPageFirstItemId)) { + currentPageFirstItemIndex = size - 1; + } + + // Go backwards in the middle of the list (respect borders) + while (currentPageFirstItemIndex > newIndex + && !isFirstId(currentPageFirstItemId)) { + currentPageFirstItemIndex--; + currentPageFirstItemId = prevItemId(currentPageFirstItemId); + } + + // If we did hit the border + if (isFirstId(currentPageFirstItemId)) { + currentPageFirstItemIndex = 0; + } + + // Go forwards once more + while (currentPageFirstItemIndex < newIndex + && !isLastId(currentPageFirstItemId)) { + currentPageFirstItemIndex++; + currentPageFirstItemId = nextItemId(currentPageFirstItemId); + } + + // If for some reason we do hit border again, override + // the user index request + if (isLastId(currentPageFirstItemId)) { + newIndex = currentPageFirstItemIndex = size - 1; + } + } + if (needsPageBufferReset) { + // Assures the visual refresh + refreshRowCache(); + } + } + + /** + * Setter for property currentPageFirstItem. + * + * @param newIndex + * the New value of property currentPageFirstItem. + */ + public void setCurrentPageFirstItemIndex(int newIndex) { + setCurrentPageFirstItemIndex(newIndex, true); + } + + /** + * Getter for property pageBuffering. + * + * @deprecated functionality is not needed in ajax rendering model + * + * @return the Value of property pageBuffering. + */ + @Deprecated + public boolean isPageBufferingEnabled() { + return true; + } + + /** + * Setter for property pageBuffering. + * + * @deprecated functionality is not needed in ajax rendering model + * + * @param pageBuffering + * the New value of property pageBuffering. + */ + @Deprecated + public void setPageBufferingEnabled(boolean pageBuffering) { + + } + + /** + * Getter for property selectable. + * + * <p> + * The table is not selectable by default. + * </p> + * + * @return the Value of property selectable. + */ + public boolean isSelectable() { + return selectable; + } + + /** + * Setter for property selectable. + * + * <p> + * The table is not selectable by default. + * </p> + * + * @param selectable + * the New value of property selectable. + */ + public void setSelectable(boolean selectable) { + if (this.selectable != selectable) { + this.selectable = selectable; + requestRepaint(); + } + } + + /** + * 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; + requestRepaint(); + } + + } + + /** + * 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 (getParent() == null) { + 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; + } + + setRowCacheInvalidated(true); + requestRepaint(); + } + + /** + * 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 requestRepaint() { + // Overridden only for javadoc + super.requestRepaint(); + } + + @Override + public void requestRepaintAll() { + super.requestRepaintAll(); + + // 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().finest( + "Insert " + rows + " rows at index " + firstIndex + + " to existing page buffer requested"); + + // Page buffer must not become larger than pageLength*cacheRate before + // or after the current page + int minPageBufferIndex = getCurrentPageFirstItemIndex() + - (int) (getPageLength() * getCacheRate()); + if (minPageBufferIndex < 0) { + minPageBufferIndex = 0; + } + + int maxPageBufferIndex = getCurrentPageFirstItemIndex() + + (int) (getPageLength() * (1 + getCacheRate())); + int maxBufferSize = maxPageBufferIndex - minPageBufferIndex; + + if (getPageLength() == 0) { + // If pageLength == 0 then all rows should be rendered + maxBufferSize = pageBuffer[0].length + rows; + } + /* + * 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; + + /* + * firstIndexInPageBuffer is the offset in pageBuffer where the new rows + * will be inserted (firstIndex is the index in the whole table). + * + * E.g. scrolled down to row 1000: firstIndex==1010, + * pageBufferFirstIndex==1000 -> cacheIx==10 + */ + int firstIndexInPageBuffer = firstIndex - pageBufferFirstIndex; + + /* If rows > size available in page buffer */ + if (firstIndexInPageBuffer + rows > maxBufferSize) { + rows = maxBufferSize - firstIndexInPageBuffer; + } + + /* + * "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. + */ + + /* All rows until the insertion point remain, always. */ + int firstCacheRowToRemoveInPageBuffer = firstIndexInPageBuffer; + + /* + * IF there is space remaining in the buffer after the rows have been + * inserted, we can keep more rows. + */ + int numberOfOldRowsAfterInsertedRows = maxBufferSize + - firstIndexInPageBuffer - rows; + if (numberOfOldRowsAfterInsertedRows > 0) { + firstCacheRowToRemoveInPageBuffer += numberOfOldRowsAfterInsertedRows; + } + + if (firstCacheRowToRemoveInPageBuffer <= currentlyCachedRowCount) { + /* + * Unregister all components that fall beyond the cache limits after + * inserting the new rows. + */ + unregisterComponentsAndPropertiesInRows( + firstCacheRowToRemoveInPageBuffer + pageBufferFirstIndex, + currentlyCachedRowCount - firstCacheRowToRemoveInPageBuffer + + pageBufferFirstIndex); + } + + // Calculate the new cache size + int newCachedRowCount = currentlyCachedRowCount; + if (maxBufferSize == 0 || currentlyCachedRowCount < maxBufferSize) { + newCachedRowCount = currentlyCachedRowCount + rows; + if (maxBufferSize > 0 && newCachedRowCount > maxBufferSize) { + newCachedRowCount = maxBufferSize; + } + } + + /* 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 < firstIndexInPageBuffer; row++) { + // Copy the first rows + newPageBuffer[i][row] = pageBuffer[i][row]; + } + for (int row = firstIndexInPageBuffer; row < firstIndexInPageBuffer + + rows; row++) { + // Copy the newly created rows + newPageBuffer[i][row] = cells[i][row - firstIndexInPageBuffer]; + } + for (int row = firstIndexInPageBuffer + rows; row < newCachedRowCount; row++) { + // Move the old rows down below the newly inserted rows + newPageBuffer[i][row] = pageBuffer[i][row - rows]; + } + } + pageBuffer = newPageBuffer; + getLogger().finest( + "Page Buffer now contains " + + pageBuffer[CELL_ITEMID].length + + " rows (" + + pageBufferFirstIndex + + "-" + + (pageBufferFirstIndex + + pageBuffer[CELL_ITEMID].length - 1) + ")"); + return cells; + } + + /** + * 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) { + getLogger().finest( + "Render visible cells for rows " + firstIndex + "-" + + (firstIndex + rows - 1)); + final Object[] colids = getVisibleColumns(); + final int cols = colids.length; + + HashSet<Property<?>> oldListenedProperties = listenedProperties; + HashSet<Component> oldVisibleComponents = visibleComponents; + + if (replaceListeners) { + // initialize the listener collections, this should only be done if + // the entire cache is refreshed (through refreshRenderedCells) + listenedProperties = new HashSet<Property<?>>(); + visibleComponents = new HashSet<Component>(); + } + + Object[][] cells = new Object[cols + CELL_FIRSTCOL][rows]; + if (rows == 0) { + unregisterPropertiesAndComponents(oldListenedProperties, + oldVisibleComponents); + return cells; + } + + // Gets the first item id + Object id; + if (items instanceof Container.Indexed) { + id = getIdByIndex(firstIndex); + } else { + id = firstItemId(); + for (int i = 0; i < firstIndex; i++) { + id = nextItemId(id); + } + } + + final RowHeaderMode headmode = getRowHeaderMode(); + final boolean[] iscomponent = new boolean[cols]; + for (int i = 0; i < cols; i++) { + iscomponent[i] = columnGenerators.containsKey(colids[i]) + || Component.class.isAssignableFrom(getType(colids[i])); + } + int firstIndexNotInCache; + if (pageBuffer != null && pageBuffer[CELL_ITEMID].length > 0) { + firstIndexNotInCache = pageBufferFirstIndex + + pageBuffer[CELL_ITEMID].length; + } else { + firstIndexNotInCache = -1; + } + + // Creates the page contents + int filledRows = 0; + for (int i = 0; i < rows && id != null; i++) { + cells[CELL_ITEMID][i] = id; + cells[CELL_KEY][i] = itemIdMapper.key(id); + if (headmode != ROW_HEADER_MODE_HIDDEN) { + switch (headmode) { + case INDEX: + cells[CELL_HEADER][i] = String.valueOf(i + firstIndex + 1); + break; + default: + cells[CELL_HEADER][i] = getItemCaption(id); + } + cells[CELL_ICON][i] = getItemIcon(id); + } + + GeneratedRow generatedRow = rowGenerator != null ? rowGenerator + .generateRow(this, id) : null; + cells[CELL_GENERATED_ROW][i] = generatedRow; + + for (int j = 0; j < cols; j++) { + if (isColumnCollapsed(colids[j])) { + continue; + } + Property<?> p = null; + Object value = ""; + boolean isGeneratedRow = generatedRow != null; + boolean isGeneratedColumn = columnGenerators + .containsKey(colids[j]); + boolean isGenerated = isGeneratedRow || isGeneratedColumn; + + if (!isGenerated) { + p = getContainerProperty(id, colids[j]); + } + + if (isGeneratedRow) { + if (generatedRow.isSpanColumns() && j > 0) { + value = null; + } else if (generatedRow.isSpanColumns() && j == 0 + && generatedRow.getValue() instanceof Component) { + value = generatedRow.getValue(); + } else if (generatedRow.getText().length > j) { + value = generatedRow.getText()[j]; + } + } else { + // check in current pageBuffer already has row + int index = firstIndex + i; + if (p != null || isGenerated) { + int indexInOldBuffer = index - pageBufferFirstIndex; + if (index < firstIndexNotInCache + && index >= pageBufferFirstIndex + && pageBuffer[CELL_GENERATED_ROW][indexInOldBuffer] == null + && id.equals(pageBuffer[CELL_ITEMID][indexInOldBuffer])) { + // we already have data in our cache, + // recycle it instead of fetching it via + // getValue/getPropertyValue + value = pageBuffer[CELL_FIRSTCOL + j][indexInOldBuffer]; + if (!isGeneratedColumn && iscomponent[j] + || !(value instanceof Component)) { + listenProperty(p, oldListenedProperties); + } + } else { + if (isGeneratedColumn) { + ColumnGenerator cg = columnGenerators + .get(colids[j]); + value = cg.generateCell(this, id, colids[j]); + if (value != null + && !(value instanceof Component) + && !(value instanceof String)) { + // Avoid errors if a generator returns + // something + // other than a Component or a String + value = value.toString(); + } + } else if (iscomponent[j]) { + value = p.getValue(); + listenProperty(p, oldListenedProperties); + } else if (p != null) { + value = getPropertyValue(id, colids[j], p); + /* + * If returned value is Component (via + * fieldfactory or overridden getPropertyValue) + * we excpect it to listen property value + * changes. Otherwise if property emits value + * change events, table will start to listen + * them and refresh content when needed. + */ + if (!(value instanceof Component)) { + listenProperty(p, oldListenedProperties); + } + } else { + value = getPropertyValue(id, colids[j], null); + } + } + } + } + + if (value instanceof Component) { + registerComponent((Component) value); + } + cells[CELL_FIRSTCOL + j][i] = value; + } + + // Gets the next item id + if (items instanceof Container.Indexed) { + int index = firstIndex + i + 1; + if (index < size()) { + id = getIdByIndex(index); + } else { + id = null; + } + } else { + id = nextItemId(id); + } + + filledRows++; + } + + // Assures that all the rows of the cell-buffer are valid + if (filledRows != cells[0].length) { + final Object[][] temp = new Object[cells.length][filledRows]; + for (int i = 0; i < cells.length; i++) { + for (int j = 0; j < filledRows; j++) { + temp[i][j] = cells[i][j]; + } + } + cells = temp; + } + + unregisterPropertiesAndComponents(oldListenedProperties, + oldVisibleComponents); + + return cells; + } + + protected void registerComponent(Component component) { + getLogger().finest( + "Registered " + component.getClass().getSimpleName() + ": " + + component.getCaption()); + if (component.getParent() != this) { + component.setParent(this); + } + visibleComponents.add(component); + } + + private void listenProperty(Property<?> p, + HashSet<Property<?>> oldListenedProperties) { + if (p instanceof Property.ValueChangeNotifier) { + if (oldListenedProperties == null + || !oldListenedProperties.contains(p)) { + ((Property.ValueChangeNotifier) p).addListener(this); + } + /* + * register listened properties, so we can do proper cleanup to free + * memory. Essential if table has loads of data and it is used for a + * long time. + */ + listenedProperties.add(p); + + } + } + + /** + * @param firstIx + * Index of the first row to process. Global index, not relative + * to page buffer. + * @param count + */ + private void unregisterComponentsAndPropertiesInRows(int firstIx, int count) { + getLogger().finest( + "Unregistering components in rows " + firstIx + "-" + + (firstIx + count - 1)); + Object[] colids = getVisibleColumns(); + if (pageBuffer != null && pageBuffer[CELL_ITEMID].length > 0) { + int bufSize = pageBuffer[CELL_ITEMID].length; + int ix = firstIx - pageBufferFirstIndex; + ix = ix < 0 ? 0 : ix; + if (ix < bufSize) { + count = count > bufSize - ix ? bufSize - ix : count; + for (int i = 0; i < count; i++) { + for (int c = 0; c < colids.length; c++) { + Object cellVal = pageBuffer[CELL_FIRSTCOL + c][i + ix]; + if (cellVal instanceof Component + && visibleComponents.contains(cellVal)) { + visibleComponents.remove(cellVal); + unregisterComponent((Component) cellVal); + } else { + Property<?> p = getContainerProperty( + pageBuffer[CELL_ITEMID][i + ix], colids[c]); + if (p instanceof ValueChangeNotifier + && listenedProperties.contains(p)) { + listenedProperties.remove(p); + ((ValueChangeNotifier) p).removeListener(this); + } + } + } + } + } + } + } + + /** + * Helper method to remove listeners and maintain correct component + * hierarchy. Detaches properties and components if those are no more + * rendered in client. + * + * @param oldListenedProperties + * set of properties that where listened in last render + * @param oldVisibleComponents + * set of components that where attached in last render + */ + private void unregisterPropertiesAndComponents( + HashSet<Property<?>> oldListenedProperties, + HashSet<Component> oldVisibleComponents) { + if (oldVisibleComponents != null) { + for (final Iterator<Component> i = oldVisibleComponents.iterator(); i + .hasNext();) { + Component c = i.next(); + if (!visibleComponents.contains(c)) { + unregisterComponent(c); + } + } + } + + if (oldListenedProperties != null) { + for (final Iterator<Property<?>> i = oldListenedProperties + .iterator(); i.hasNext();) { + Property.ValueChangeNotifier o = (ValueChangeNotifier) i.next(); + if (!listenedProperties.contains(o)) { + o.removeListener(this); + } + } + } + } + + /** + * This method cleans up a Component that has been generated when Table is + * in editable mode. The component needs to be detached from its parent and + * if it is a field, it needs to be detached from its property data source + * in order to allow garbage collection to take care of removing the unused + * component from memory. + * + * Override this method and getPropertyValue(Object, Object, Property) with + * custom logic if you need to deal with buffered fields. + * + * @see #getPropertyValue(Object, Object, Property) + * + * @param oldVisibleComponents + * a set of components that should be unregistered. + */ + protected void unregisterComponent(Component component) { + getLogger().finest( + "Unregistered " + component.getClass().getSimpleName() + ": " + + component.getCaption()); + component.setParent(null); + /* + * Also remove property data sources to unregister listeners keeping the + * fields in memory. + */ + if (component instanceof Field) { + Field<?> field = (Field<?>) component; + Property<?> associatedProperty = associatedProperties + .remove(component); + if (associatedProperty != null + && field.getPropertyDataSource() == associatedProperty) { + // Remove the property data source only if it's the one we + // added in getPropertyValue + field.setPropertyDataSource(null); + } + } + } + + /** + * Refreshes the current page contents. + * + * @deprecated should not need to be used + */ + @Deprecated + public void refreshCurrentPage() { + + } + + /** + * Sets the row header mode. + * <p> + * The mode can be one of the following ones: + * <ul> + * <li>{@link #ROW_HEADER_MODE_HIDDEN}: The row captions are hidden.</li> + * <li>{@link #ROW_HEADER_MODE_ID}: Items Id-objects <code>toString()</code> + * is used as row caption. + * <li>{@link #ROW_HEADER_MODE_ITEM}: Item-objects <code>toString()</code> + * is used as row caption. + * <li>{@link #ROW_HEADER_MODE_PROPERTY}: Property set with + * {@link #setItemCaptionPropertyId(Object)} is used as row header. + * <li>{@link #ROW_HEADER_MODE_EXPLICIT_DEFAULTS_ID}: Items Id-objects + * <code>toString()</code> is used as row header. If caption is explicitly + * specified, it overrides the id-caption. + * <li>{@link #ROW_HEADER_MODE_EXPLICIT}: The row headers must be explicitly + * specified.</li> + * <li>{@link #ROW_HEADER_MODE_INDEX}: The index of the item is used as row + * caption. The index mode can only be used with the containers implementing + * <code>Container.Indexed</code> interface.</li> + * </ul> + * The default value is {@link #ROW_HEADER_MODE_HIDDEN} + * </p> + * + * @param mode + * the One of the modes listed above. + */ + public void setRowHeaderMode(RowHeaderMode mode) { + if (mode != null) { + rowHeaderMode = mode; + if (mode != RowHeaderMode.HIDDEN) { + setItemCaptionMode(mode.getItemCaptionMode()); + } + // Assures the visual refresh. No need to reset the page buffer + // before + // as the content has not changed, only the alignments. + refreshRenderedCells(); + } + } + + /** + * Gets the row header mode. + * + * @return the Row header mode. + * @see #setRowHeaderMode(int) + */ + public RowHeaderMode getRowHeaderMode() { + return rowHeaderMode; + } + + /** + * Adds the new row to table and fill the visible cells (except generated + * columns) with given values. + * + * @param cells + * the Object array that is used for filling the visible cells + * new row. The types must be settable to visible column property + * types. + * @param itemId + * the Id the new row. If null, a new id is automatically + * assigned. If given, the table cant already have a item with + * given id. + * @return Returns item id for the new row. Returns null if operation fails. + */ + public Object addItem(Object[] cells, Object itemId) + throws UnsupportedOperationException { + + // remove generated columns from the list of columns being assigned + final LinkedList<Object> availableCols = new LinkedList<Object>(); + for (Iterator<Object> it = visibleColumns.iterator(); it.hasNext();) { + Object id = it.next(); + if (!columnGenerators.containsKey(id)) { + availableCols.add(id); + } + } + // Checks that a correct number of cells are given + if (cells.length != availableCols.size()) { + return null; + } + + // Creates new item + Item item; + if (itemId == null) { + itemId = items.addItem(); + if (itemId == null) { + return null; + } + item = items.getItem(itemId); + } else { + item = items.addItem(itemId); + } + if (item == null) { + return null; + } + + // Fills the item properties + for (int i = 0; i < availableCols.size(); i++) { + item.getItemProperty(availableCols.get(i)).setValue(cells[i]); + } + + if (!(items instanceof Container.ItemSetChangeNotifier)) { + refreshRowCache(); + } + + return itemId; + } + + /** + * Discards and recreates the internal row cache. Call this if you make + * changes that affect the rows but the information about the changes are + * not automatically propagated to the Table. + * <p> + * 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. + * <p> + * 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. + * <p> + * <i>Note that calling this method is not cheap so avoid calling it + * unnecessarily.</i> + * + * @since 6.7.2 + */ + public void refreshRowCache() { + resetPageBuffer(); + refreshRenderedCells(); + } + + @Override + public void setContainerDataSource(Container newDataSource) { + + disableContentRefreshing(); + + if (newDataSource == null) { + newDataSource = new IndexedContainer(); + } + + // Assures that the data source is ordered by making unordered + // containers ordered by wrapping them + if (newDataSource instanceof Container.Ordered) { + super.setContainerDataSource(newDataSource); + } else { + super.setContainerDataSource(new ContainerOrderedWrapper( + newDataSource)); + } + + // Resets page position + currentPageFirstItemId = null; + currentPageFirstItemIndex = 0; + + // Resets column properties + if (collapsedColumns != null) { + collapsedColumns.clear(); + } + + // columnGenerators 'override' properties, don't add the same id twice + Collection<Object> col = new LinkedList<Object>(); + for (Iterator<?> it = getContainerPropertyIds().iterator(); it + .hasNext();) { + Object id = it.next(); + if (columnGenerators == null || !columnGenerators.containsKey(id)) { + col.add(id); + } + } + // generators added last + if (columnGenerators != null && columnGenerators.size() > 0) { + col.addAll(columnGenerators.keySet()); + } + + setVisibleColumns(col.toArray()); + + // Assure visual refresh + resetPageBuffer(); + + enableContentRefreshing(true); + } + + /** + * Gets items ids from a range of key values + * + * @param startRowKey + * The start key + * @param endRowKey + * The end key + * @return + */ + private LinkedHashSet<Object> getItemIdsInRange(Object itemId, + final int length) { + LinkedHashSet<Object> ids = new LinkedHashSet<Object>(); + for (int i = 0; i < length; i++) { + assert itemId != null; // should not be null unless client-server + // are out of sync + ids.add(itemId); + itemId = nextItemId(itemId); + } + return ids; + } + + /** + * Handles selection if selection is a multiselection + * + * @param variables + * The variables + */ + private void handleSelectedItems(Map<String, Object> variables) { + final String[] ka = (String[]) variables.get("selected"); + final String[] ranges = (String[]) variables.get("selectedRanges"); + + Set<Object> renderedButNotSelectedItemIds = getCurrentlyRenderedItemIds(); + + @SuppressWarnings("unchecked") + HashSet<Object> newValue = new LinkedHashSet<Object>( + (Collection<Object>) getValue()); + + if (variables.containsKey("clearSelections")) { + // the client side has instructed to swipe all previous selections + newValue.clear(); + } + + /* + * Then add (possibly some of them back) rows that are currently + * selected on the client side (the ones that the client side is aware + * of). + */ + for (int i = 0; i < ka.length; i++) { + // key to id + final Object id = itemIdMapper.get(ka[i]); + if (!isNullSelectionAllowed() + && (id == null || id == getNullSelectionItemId())) { + // skip empty selection if nullselection is not allowed + requestRepaint(); + } else if (id != null && containsId(id)) { + newValue.add(id); + renderedButNotSelectedItemIds.remove(id); + } + } + + /* Add range items aka shift clicked multiselection areas */ + if (ranges != null) { + for (String range : ranges) { + String[] split = range.split("-"); + Object startItemId = itemIdMapper.get(split[0]); + int length = Integer.valueOf(split[1]); + LinkedHashSet<Object> itemIdsInRange = getItemIdsInRange( + startItemId, length); + newValue.addAll(itemIdsInRange); + renderedButNotSelectedItemIds.removeAll(itemIdsInRange); + } + } + /* + * finally clear all currently rendered rows (the ones that the client + * side counterpart is aware of) that the client didn't send as selected + */ + newValue.removeAll(renderedButNotSelectedItemIds); + + if (!isNullSelectionAllowed() && newValue.isEmpty()) { + // empty selection not allowed, keep old value + requestRepaint(); + return; + } + + setValue(newValue, true); + + } + + private Set<Object> getCurrentlyRenderedItemIds() { + HashSet<Object> ids = new HashSet<Object>(); + if (pageBuffer != null) { + for (int i = 0; i < pageBuffer[CELL_ITEMID].length; i++) { + ids.add(pageBuffer[CELL_ITEMID][i]); + } + } + return ids; + } + + /* Component basics */ + + /** + * Invoked when the value of a variable has changed. + * + * @see com.vaadin.ui.Select#changeVariables(java.lang.Object, + * java.util.Map) + */ + + @Override + public void changeVariables(Object source, Map<String, Object> variables) { + + boolean clientNeedsContentRefresh = false; + + handleClickEvent(variables); + + handleColumnResizeEvent(variables); + + handleColumnWidthUpdates(variables); + + disableContentRefreshing(); + + if (!isSelectable() && variables.containsKey("selected")) { + // Not-selectable is a special case, AbstractSelect does not support + // TODO could be optimized. + variables = new HashMap<String, Object>(variables); + variables.remove("selected"); + } + + /* + * The AbstractSelect cannot handle the multiselection properly, instead + * we handle it ourself + */ + else if (isSelectable() && isMultiSelect() + && variables.containsKey("selected") + && multiSelectMode == MultiSelectMode.DEFAULT) { + handleSelectedItems(variables); + variables = new HashMap<String, Object>(variables); + variables.remove("selected"); + } + + super.changeVariables(source, variables); + + // Client might update the pagelength if Table height is fixed + if (variables.containsKey("pagelength")) { + // Sets pageLength directly to avoid repaint that setter causes + pageLength = (Integer) variables.get("pagelength"); + } + + // Page start index + if (variables.containsKey("firstvisible")) { + final Integer value = (Integer) variables.get("firstvisible"); + if (value != null) { + setCurrentPageFirstItemIndex(value.intValue(), false); + } + } + + // Sets requested firstrow and rows for the next paint + if (variables.containsKey("reqfirstrow") + || variables.containsKey("reqrows")) { + + try { + firstToBeRenderedInClient = ((Integer) variables + .get("firstToBeRendered")).intValue(); + lastToBeRenderedInClient = ((Integer) variables + .get("lastToBeRendered")).intValue(); + } catch (Exception e) { + // FIXME: Handle exception + getLogger().log(Level.FINER, + "Could not parse the first and/or last rows.", e); + } + + // respect suggested rows only if table is not otherwise updated + // (row caches emptied by other event) + if (!containerChangeToBeRendered) { + Integer value = (Integer) variables.get("reqfirstrow"); + if (value != null) { + reqFirstRowToPaint = value.intValue(); + } + value = (Integer) variables.get("reqrows"); + if (value != null) { + reqRowsToPaint = value.intValue(); + // sanity check + if (reqFirstRowToPaint + reqRowsToPaint > size()) { + reqRowsToPaint = size() - reqFirstRowToPaint; + } + } + } + getLogger().finest( + "Client wants rows " + reqFirstRowToPaint + "-" + + (reqFirstRowToPaint + reqRowsToPaint - 1)); + clientNeedsContentRefresh = true; + } + + if (isSortEnabled()) { + // Sorting + boolean doSort = false; + if (variables.containsKey("sortcolumn")) { + final String colId = (String) variables.get("sortcolumn"); + if (colId != null && !"".equals(colId) && !"null".equals(colId)) { + final Object id = columnIdMap.get(colId); + setSortContainerPropertyId(id, false); + doSort = true; + } + } + if (variables.containsKey("sortascending")) { + final boolean state = ((Boolean) variables.get("sortascending")) + .booleanValue(); + if (state != sortAscending) { + setSortAscending(state, false); + doSort = true; + } + } + if (doSort) { + this.sort(); + resetPageBuffer(); + } + } + + // Dynamic column hide/show and order + // Update visible columns + if (isColumnCollapsingAllowed()) { + if (variables.containsKey("collapsedcolumns")) { + try { + final Object[] ids = (Object[]) variables + .get("collapsedcolumns"); + for (final Iterator<Object> it = visibleColumns.iterator(); it + .hasNext();) { + setColumnCollapsed(it.next(), false); + } + for (int i = 0; i < ids.length; i++) { + setColumnCollapsed(columnIdMap.get(ids[i].toString()), + true); + } + } catch (final Exception e) { + // FIXME: Handle exception + getLogger().log(Level.FINER, + "Could not determine column collapsing state", e); + } + clientNeedsContentRefresh = true; + } + } + if (isColumnReorderingAllowed()) { + if (variables.containsKey("columnorder")) { + try { + final Object[] ids = (Object[]) variables + .get("columnorder"); + // need a real Object[], ids can be a String[] + final Object[] idsTemp = new Object[ids.length]; + for (int i = 0; i < ids.length; i++) { + idsTemp[i] = columnIdMap.get(ids[i].toString()); + } + setColumnOrder(idsTemp); + if (hasListeners(ColumnReorderEvent.class)) { + fireEvent(new ColumnReorderEvent(this)); + } + } catch (final Exception e) { + // FIXME: Handle exception + getLogger().log(Level.FINER, + "Could not determine column reordering state", e); + } + clientNeedsContentRefresh = true; + } + } + + enableContentRefreshing(clientNeedsContentRefresh); + + // Actions + if (variables.containsKey("action")) { + final StringTokenizer st = new StringTokenizer( + (String) variables.get("action"), ","); + if (st.countTokens() == 2) { + final Object itemId = itemIdMapper.get(st.nextToken()); + final Action action = actionMapper.get(st.nextToken()); + + if (action != null && (itemId == null || containsId(itemId)) + && actionHandlers != null) { + for (Handler ah : actionHandlers) { + ah.handleAction(action, this, itemId); + } + } + } + } + + } + + /** + * Handles click event + * + * @param variables + */ + private void handleClickEvent(Map<String, Object> variables) { + + // Item click event + if (variables.containsKey("clickEvent")) { + String key = (String) variables.get("clickedKey"); + Object itemId = itemIdMapper.get(key); + Object propertyId = null; + String colkey = (String) variables.get("clickedColKey"); + // click is not necessary on a property + if (colkey != null) { + propertyId = columnIdMap.get(colkey); + } + MouseEventDetails evt = MouseEventDetails + .deSerialize((String) variables.get("clickEvent")); + Item item = getItem(itemId); + if (item != null) { + fireEvent(new ItemClickEvent(this, item, itemId, propertyId, + evt)); + } + } + + // Header click event + else if (variables.containsKey("headerClickEvent")) { + + MouseEventDetails details = MouseEventDetails + .deSerialize((String) variables.get("headerClickEvent")); + + Object cid = variables.get("headerClickCID"); + Object propertyId = null; + if (cid != null) { + propertyId = columnIdMap.get(cid.toString()); + } + fireEvent(new HeaderClickEvent(this, propertyId, details)); + } + + // Footer click event + else if (variables.containsKey("footerClickEvent")) { + MouseEventDetails details = MouseEventDetails + .deSerialize((String) variables.get("footerClickEvent")); + + Object cid = variables.get("footerClickCID"); + Object propertyId = null; + if (cid != null) { + propertyId = columnIdMap.get(cid.toString()); + } + fireEvent(new FooterClickEvent(this, propertyId, details)); + } + } + + /** + * Handles the column resize event sent by the client. + * + * @param variables + */ + private void handleColumnResizeEvent(Map<String, Object> variables) { + if (variables.containsKey("columnResizeEventColumn")) { + Object cid = variables.get("columnResizeEventColumn"); + Object propertyId = null; + if (cid != null) { + propertyId = columnIdMap.get(cid.toString()); + + Object prev = variables.get("columnResizeEventPrev"); + int previousWidth = -1; + if (prev != null) { + previousWidth = Integer.valueOf(prev.toString()); + } + + Object curr = variables.get("columnResizeEventCurr"); + int currentWidth = -1; + if (curr != null) { + currentWidth = Integer.valueOf(curr.toString()); + } + + fireColumnResizeEvent(propertyId, previousWidth, currentWidth); + } + } + } + + private void fireColumnResizeEvent(Object propertyId, int previousWidth, + int currentWidth) { + /* + * Update the sizes on the server side. If a column previously had a + * expand ratio and the user resized the column then the expand ratio + * will be turned into a static pixel size. + */ + setColumnWidth(propertyId, currentWidth); + + fireEvent(new ColumnResizeEvent(this, propertyId, previousWidth, + currentWidth)); + } + + private void handleColumnWidthUpdates(Map<String, Object> variables) { + if (variables.containsKey("columnWidthUpdates")) { + String[] events = (String[]) variables.get("columnWidthUpdates"); + for (String str : events) { + String[] eventDetails = str.split(":"); + Object propertyId = columnIdMap.get(eventDetails[0]); + if (propertyId == null) { + propertyId = ROW_HEADER_FAKE_PROPERTY_ID; + } + int width = Integer.valueOf(eventDetails[1]); + setColumnWidth(propertyId, width); + } + } + } + + /** + * Go to mode where content updates are not done. This is due we want to + * bypass expensive content for some reason (like when we know we may have + * other content changes on their way). + * + * @return true if content refresh flag was enabled prior this call + */ + protected boolean disableContentRefreshing() { + boolean wasDisabled = isContentRefreshesEnabled; + isContentRefreshesEnabled = false; + return wasDisabled; + } + + /** + * Go to mode where content content refreshing has effect. + * + * @param refreshContent + * true if content refresh needs to be done + */ + protected void enableContentRefreshing(boolean refreshContent) { + isContentRefreshesEnabled = true; + if (refreshContent) { + refreshRenderedCells(); + // Ensure that client gets a response + requestRepaint(); + } + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.AbstractSelect#paintContent(com.vaadin. + * terminal.PaintTarget) + */ + + @Override + public void paintContent(PaintTarget target) throws PaintException { + /* + * Body actions - Actions which has the target null and can be invoked + * by right clicking on the table body. + */ + final Set<Action> actionSet = findAndPaintBodyActions(target); + + final Object[][] cells = getVisibleCells(); + int rows = findNumRowsToPaint(target, cells); + + int total = size(); + if (shouldHideNullSelectionItem()) { + total--; + rows--; + } + + // Table attributes + paintTableAttributes(target, rows, total); + + paintVisibleColumnOrder(target); + + // Rows + if (isPartialRowUpdate() && painted && !target.isFullRepaint()) { + paintPartialRowUpdate(target, actionSet); + /* + * Send the page buffer indexes to ensure that the client side stays + * in sync. Otherwise we _might_ have the situation where the client + * side discards too few or too many rows, causing out of sync + * issues. + * + * This could probably be done for full repaints also to simplify + * the client side. + */ + int pageBufferLastIndex = pageBufferFirstIndex + + pageBuffer[CELL_ITEMID].length - 1; + target.addAttribute(VScrollTable.ATTRIBUTE_PAGEBUFFER_FIRST, + pageBufferFirstIndex); + target.addAttribute(VScrollTable.ATTRIBUTE_PAGEBUFFER_LAST, + pageBufferLastIndex); + } else if (target.isFullRepaint() || isRowCacheInvalidated()) { + paintRows(target, cells, actionSet); + setRowCacheInvalidated(false); + } + + paintSorting(target); + + resetVariablesAndPageBuffer(target); + + // Actions + paintActions(target, actionSet); + + paintColumnOrder(target); + + // Available columns + paintAvailableColumns(target); + + paintVisibleColumns(target); + + if (keyMapperReset) { + keyMapperReset = false; + target.addAttribute(VScrollTable.ATTRIBUTE_KEY_MAPPER_RESET, true); + } + + if (dropHandler != null) { + dropHandler.getAcceptCriterion().paint(target); + } + + painted = true; + } + + private void setRowCacheInvalidated(boolean invalidated) { + rowCacheInvalidated = invalidated; + } + + protected boolean isRowCacheInvalidated() { + return rowCacheInvalidated; + } + + private void paintPartialRowUpdate(PaintTarget target, Set<Action> actionSet) + throws PaintException { + paintPartialRowUpdates(target, actionSet); + paintPartialRowAdditions(target, actionSet); + } + + private void paintPartialRowUpdates(PaintTarget target, + Set<Action> actionSet) throws PaintException { + final boolean[] iscomponent = findCellsWithComponents(); + + int firstIx = getFirstUpdatedItemIndex(); + int count = getUpdatedRowCount(); + + target.startTag("urows"); + target.addAttribute("firsturowix", firstIx); + target.addAttribute("numurows", count); + + // Partial row updates bypass the normal caching mechanism. + Object[][] cells = getVisibleCellsUpdateCacheRows(firstIx, count); + for (int indexInRowbuffer = 0; indexInRowbuffer < count; indexInRowbuffer++) { + final Object itemId = cells[CELL_ITEMID][indexInRowbuffer]; + + if (shouldHideNullSelectionItem()) { + // Remove null selection item if null selection is not allowed + continue; + } + + paintRow(target, cells, isEditable(), actionSet, iscomponent, + indexInRowbuffer, itemId); + } + target.endTag("urows"); + } + + private void paintPartialRowAdditions(PaintTarget target, + Set<Action> actionSet) throws PaintException { + final boolean[] iscomponent = findCellsWithComponents(); + + int firstIx = getFirstAddedItemIndex(); + int count = getAddedRowCount(); + + target.startTag("prows"); + + if (!shouldHideAddedRows()) { + getLogger().finest( + "Paint rows for add. Index: " + firstIx + ", count: " + + count + "."); + + // Partial row additions bypass the normal caching mechanism. + Object[][] cells = getVisibleCellsInsertIntoCache(firstIx, count); + if (cells[0].length < count) { + // delete the rows below, since they will fall beyond the cache + // page. + target.addAttribute("delbelow", true); + count = cells[0].length; + } + + for (int indexInRowbuffer = 0; indexInRowbuffer < count; indexInRowbuffer++) { + final Object itemId = cells[CELL_ITEMID][indexInRowbuffer]; + if (shouldHideNullSelectionItem()) { + // Remove null selection item if null selection is not + // allowed + continue; + } + + paintRow(target, cells, isEditable(), actionSet, iscomponent, + indexInRowbuffer, itemId); + } + } else { + getLogger().finest( + "Paint rows for remove. Index: " + firstIx + ", count: " + + count + "."); + removeRowsFromCacheAndFillBottom(firstIx, count); + target.addAttribute("hide", true); + } + + target.addAttribute("firstprowix", firstIx); + target.addAttribute("numprows", count); + target.endTag("prows"); + } + + /** + * Subclass and override this to enable partial row updates and additions, + * which bypass the normal caching mechanism. This is useful for e.g. + * TreeTable. + * + * @return true if this update is a partial row update, false if not. For + * plain Table it is always false. + */ + protected boolean isPartialRowUpdate() { + return false; + } + + /** + * Subclass and override this to enable partial row additions, bypassing the + * normal caching mechanism. This is useful for e.g. TreeTable, where + * expanding a node should only fetch and add the items inside of that node. + * + * @return The index of the first added item. For plain Table it is always + * 0. + */ + protected int getFirstAddedItemIndex() { + return 0; + } + + /** + * Subclass and override this to enable partial row additions, bypassing the + * normal caching mechanism. This is useful for e.g. TreeTable, where + * expanding a node should only fetch and add the items inside of that node. + * + * @return the number of rows to be added, starting at the index returned by + * {@link #getFirstAddedItemIndex()}. For plain Table it is always + * 0. + */ + protected int getAddedRowCount() { + return 0; + } + + /** + * Subclass and override this to enable removing of rows, bypassing the + * normal caching and lazy loading mechanism. This is useful for e.g. + * TreeTable, when you need to hide certain rows as a node is collapsed. + * + * This should return true if the rows pointed to by + * {@link #getFirstAddedItemIndex()} and {@link #getAddedRowCount()} should + * be hidden instead of added. + * + * @return whether the rows to add (see {@link #getFirstAddedItemIndex()} + * and {@link #getAddedRowCount()}) should be added or hidden. For + * plain Table it is always false. + */ + protected boolean shouldHideAddedRows() { + return false; + } + + /** + * Subclass and override this to enable partial row updates, bypassing the + * normal caching and lazy loading mechanism. This is useful for updating + * the state of certain rows, e.g. in the TreeTable the collapsed state of a + * single node is updated using this mechanism. + * + * @return the index of the first item to be updated. For plain Table it is + * always 0. + */ + protected int getFirstUpdatedItemIndex() { + return 0; + } + + /** + * Subclass and override this to enable partial row updates, bypassing the + * normal caching and lazy loading mechanism. This is useful for updating + * the state of certain rows, e.g. in the TreeTable the collapsed state of a + * single node is updated using this mechanism. + * + * @return the number of rows to update, starting at the index returned by + * {@link #getFirstUpdatedItemIndex()}. For plain table it is always + * 0. + */ + protected int getUpdatedRowCount() { + return 0; + } + + private void paintTableAttributes(PaintTarget target, int rows, int total) + throws PaintException { + paintTabIndex(target); + paintDragMode(target); + paintSelectMode(target); + + if (cacheRate != CACHE_RATE_DEFAULT) { + target.addAttribute("cr", cacheRate); + } + + target.addAttribute("cols", getVisibleColumns().length); + target.addAttribute("rows", rows); + + target.addAttribute("firstrow", + (reqFirstRowToPaint >= 0 ? reqFirstRowToPaint + : firstToBeRenderedInClient)); + target.addAttribute("totalrows", total); + if (getPageLength() != 0) { + target.addAttribute("pagelength", getPageLength()); + } + if (areColumnHeadersEnabled()) { + target.addAttribute("colheaders", true); + } + if (rowHeadersAreEnabled()) { + target.addAttribute("rowheaders", true); + } + + target.addAttribute("colfooters", columnFootersVisible); + + // The cursors are only shown on pageable table + if (getCurrentPageFirstItemIndex() != 0 || getPageLength() > 0) { + target.addVariable(this, "firstvisible", + getCurrentPageFirstItemIndex()); + } + } + + /** + * Resets and paints "to be painted next" variables. Also reset pageBuffer + */ + private void resetVariablesAndPageBuffer(PaintTarget target) + throws PaintException { + reqFirstRowToPaint = -1; + reqRowsToPaint = -1; + containerChangeToBeRendered = false; + target.addVariable(this, "reqrows", reqRowsToPaint); + target.addVariable(this, "reqfirstrow", reqFirstRowToPaint); + } + + private boolean areColumnHeadersEnabled() { + return getColumnHeaderMode() != ColumnHeaderMode.HIDDEN; + } + + private void paintVisibleColumns(PaintTarget target) throws PaintException { + target.startTag("visiblecolumns"); + if (rowHeadersAreEnabled()) { + target.startTag("column"); + target.addAttribute("cid", ROW_HEADER_COLUMN_KEY); + paintColumnWidth(target, ROW_HEADER_FAKE_PROPERTY_ID); + target.endTag("column"); + } + final Collection<?> sortables = getSortableContainerPropertyIds(); + for (Object colId : visibleColumns) { + if (colId != null) { + target.startTag("column"); + target.addAttribute("cid", columnIdMap.key(colId)); + final String head = getColumnHeader(colId); + target.addAttribute("caption", (head != null ? head : "")); + final String foot = getColumnFooter(colId); + target.addAttribute("fcaption", (foot != null ? foot : "")); + if (isColumnCollapsed(colId)) { + target.addAttribute("collapsed", true); + } + if (areColumnHeadersEnabled()) { + if (getColumnIcon(colId) != null) { + target.addAttribute("icon", getColumnIcon(colId)); + } + if (sortables.contains(colId)) { + target.addAttribute("sortable", true); + } + } + if (!Align.LEFT.equals(getColumnAlignment(colId))) { + target.addAttribute("align", getColumnAlignment(colId) + .toString()); + } + paintColumnWidth(target, colId); + target.endTag("column"); + } + } + target.endTag("visiblecolumns"); + } + + private void paintAvailableColumns(PaintTarget target) + throws PaintException { + if (columnCollapsingAllowed) { + final HashSet<Object> collapsedCols = new HashSet<Object>(); + for (Object colId : visibleColumns) { + if (isColumnCollapsed(colId)) { + collapsedCols.add(colId); + } + } + final String[] collapsedKeys = new String[collapsedCols.size()]; + int nextColumn = 0; + for (Object colId : visibleColumns) { + if (isColumnCollapsed(colId)) { + collapsedKeys[nextColumn++] = columnIdMap.key(colId); + } + } + target.addVariable(this, "collapsedcolumns", collapsedKeys); + + final String[] noncollapsibleKeys = new String[noncollapsibleColumns + .size()]; + nextColumn = 0; + for (Object colId : noncollapsibleColumns) { + noncollapsibleKeys[nextColumn++] = columnIdMap.key(colId); + } + target.addVariable(this, "noncollapsiblecolumns", + noncollapsibleKeys); + } + + } + + private void paintActions(PaintTarget target, final Set<Action> actionSet) + throws PaintException { + if (!actionSet.isEmpty()) { + target.addVariable(this, "action", ""); + target.startTag("actions"); + for (Action a : actionSet) { + target.startTag("action"); + if (a.getCaption() != null) { + target.addAttribute("caption", a.getCaption()); + } + if (a.getIcon() != null) { + target.addAttribute("icon", a.getIcon()); + } + target.addAttribute("key", actionMapper.key(a)); + target.endTag("action"); + } + target.endTag("actions"); + } + } + + private void paintColumnOrder(PaintTarget target) throws PaintException { + if (columnReorderingAllowed) { + final String[] colorder = new String[visibleColumns.size()]; + int i = 0; + for (Object colId : visibleColumns) { + colorder[i++] = columnIdMap.key(colId); + } + target.addVariable(this, "columnorder", colorder); + } + } + + private void paintSorting(PaintTarget target) throws PaintException { + // Sorting + if (getContainerDataSource() instanceof Container.Sortable) { + target.addVariable(this, "sortcolumn", + columnIdMap.key(sortContainerPropertyId)); + target.addVariable(this, "sortascending", sortAscending); + } + } + + private void paintRows(PaintTarget target, final Object[][] cells, + final Set<Action> actionSet) throws PaintException { + final boolean[] iscomponent = findCellsWithComponents(); + + target.startTag("rows"); + // cells array contains all that are supposed to be visible on client, + // but we'll start from the one requested by client + int start = 0; + if (reqFirstRowToPaint != -1 && firstToBeRenderedInClient != -1) { + start = reqFirstRowToPaint - firstToBeRenderedInClient; + } + int end = cells[0].length; + if (reqRowsToPaint != -1) { + end = start + reqRowsToPaint; + } + // sanity check + if (lastToBeRenderedInClient != -1 && lastToBeRenderedInClient < end) { + end = lastToBeRenderedInClient + 1; + } + if (start > cells[CELL_ITEMID].length || start < 0) { + start = 0; + } + if (end > cells[CELL_ITEMID].length) { + end = cells[CELL_ITEMID].length; + } + + for (int indexInRowbuffer = start; indexInRowbuffer < end; indexInRowbuffer++) { + final Object itemId = cells[CELL_ITEMID][indexInRowbuffer]; + + if (shouldHideNullSelectionItem()) { + // Remove null selection item if null selection is not allowed + continue; + } + + paintRow(target, cells, isEditable(), actionSet, iscomponent, + indexInRowbuffer, itemId); + } + target.endTag("rows"); + } + + private boolean[] findCellsWithComponents() { + final boolean[] isComponent = new boolean[visibleColumns.size()]; + int ix = 0; + for (Object columnId : visibleColumns) { + if (columnGenerators.containsKey(columnId)) { + isComponent[ix++] = true; + } else { + final Class<?> colType = getType(columnId); + isComponent[ix++] = colType != null + && Component.class.isAssignableFrom(colType); + } + } + return isComponent; + } + + private void paintVisibleColumnOrder(PaintTarget target) { + // Visible column order + final ArrayList<String> visibleColOrder = new ArrayList<String>(); + for (Object columnId : visibleColumns) { + if (!isColumnCollapsed(columnId)) { + visibleColOrder.add(columnIdMap.key(columnId)); + } + } + target.addAttribute("vcolorder", visibleColOrder.toArray()); + } + + private Set<Action> findAndPaintBodyActions(PaintTarget target) { + Set<Action> actionSet = new LinkedHashSet<Action>(); + if (actionHandlers != null) { + final ArrayList<String> keys = new ArrayList<String>(); + for (Handler ah : actionHandlers) { + // Getting actions for the null item, which in this case means + // the body item + final Action[] actions = ah.getActions(null, this); + if (actions != null) { + for (Action action : actions) { + actionSet.add(action); + keys.add(actionMapper.key(action)); + } + } + } + target.addAttribute("alb", keys.toArray()); + } + return actionSet; + } + + private boolean shouldHideNullSelectionItem() { + return !isNullSelectionAllowed() && getNullSelectionItemId() != null + && containsId(getNullSelectionItemId()); + } + + private int findNumRowsToPaint(PaintTarget target, final Object[][] cells) + throws PaintException { + int rows; + if (reqRowsToPaint >= 0) { + rows = reqRowsToPaint; + } else { + rows = cells[0].length; + if (alwaysRecalculateColumnWidths) { + // TODO experimental feature for now: tell the client to + // recalculate column widths. + // We'll only do this for paints that do not originate from + // table scroll/cache requests (i.e when reqRowsToPaint<0) + target.addAttribute("recalcWidths", true); + } + } + return rows; + } + + private void paintSelectMode(PaintTarget target) throws PaintException { + if (multiSelectMode != MultiSelectMode.DEFAULT) { + target.addAttribute("multiselectmode", multiSelectMode.ordinal()); + } + if (isSelectable()) { + target.addAttribute("selectmode", (isMultiSelect() ? "multi" + : "single")); + } else { + target.addAttribute("selectmode", "none"); + } + if (!isNullSelectionAllowed()) { + target.addAttribute("nsa", false); + } + + // selection support + // The select variable is only enabled if selectable + if (isSelectable()) { + target.addVariable(this, "selected", findSelectedKeys()); + } + } + + private String[] findSelectedKeys() { + LinkedList<String> selectedKeys = new LinkedList<String>(); + if (isMultiSelect()) { + HashSet<?> sel = new HashSet<Object>((Set<?>) getValue()); + Collection<?> vids = getVisibleItemIds(); + for (Iterator<?> it = vids.iterator(); it.hasNext();) { + Object id = it.next(); + if (sel.contains(id)) { + selectedKeys.add(itemIdMapper.key(id)); + } + } + } else { + Object value = getValue(); + if (value == null) { + value = getNullSelectionItemId(); + } + if (value != null) { + selectedKeys.add(itemIdMapper.key(value)); + } + } + return selectedKeys.toArray(new String[selectedKeys.size()]); + } + + private void paintDragMode(PaintTarget target) throws PaintException { + if (dragMode != TableDragMode.NONE) { + target.addAttribute("dragmode", dragMode.ordinal()); + } + } + + private void paintTabIndex(PaintTarget target) throws PaintException { + // The tab ordering number + if (getTabIndex() > 0) { + target.addAttribute("tabindex", getTabIndex()); + } + } + + private void paintColumnWidth(PaintTarget target, final Object columnId) + throws PaintException { + if (columnWidths.containsKey(columnId)) { + if (getColumnWidth(columnId) > -1) { + target.addAttribute("width", + String.valueOf(getColumnWidth(columnId))); + } else { + target.addAttribute("er", getColumnExpandRatio(columnId)); + } + } + } + + private boolean rowHeadersAreEnabled() { + return getRowHeaderMode() != ROW_HEADER_MODE_HIDDEN; + } + + private void paintRow(PaintTarget target, final Object[][] cells, + final boolean iseditable, final Set<Action> actionSet, + final boolean[] iscomponent, int indexInRowbuffer, + final Object itemId) throws PaintException { + target.startTag("tr"); + + paintRowAttributes(target, cells, actionSet, indexInRowbuffer, itemId); + + // cells + int currentColumn = 0; + for (final Iterator<Object> it = visibleColumns.iterator(); it + .hasNext(); currentColumn++) { + final Object columnId = it.next(); + if (columnId == null || isColumnCollapsed(columnId)) { + continue; + } + /* + * For each cell, if a cellStyleGenerator is specified, get the + * specific style for the cell. If there is any, add it to the + * target. + */ + if (cellStyleGenerator != null) { + String cellStyle = cellStyleGenerator + .getStyle(itemId, columnId); + if (cellStyle != null && !cellStyle.equals("")) { + target.addAttribute("style-" + columnIdMap.key(columnId), + cellStyle); + } + } + + if ((iscomponent[currentColumn] || iseditable || cells[CELL_GENERATED_ROW][indexInRowbuffer] != null) + && Component.class.isInstance(cells[CELL_FIRSTCOL + + currentColumn][indexInRowbuffer])) { + final Component c = (Component) cells[CELL_FIRSTCOL + + currentColumn][indexInRowbuffer]; + if (c == null) { + target.addText(""); + paintCellTooltips(target, itemId, columnId); + } else { + LegacyPaint.paint(c, target); + } + } else { + target.addText((String) cells[CELL_FIRSTCOL + currentColumn][indexInRowbuffer]); + paintCellTooltips(target, itemId, columnId); + } + } + + target.endTag("tr"); + } + + private void paintCellTooltips(PaintTarget target, Object itemId, + Object columnId) throws PaintException { + if (itemDescriptionGenerator != null) { + String itemDescription = itemDescriptionGenerator + .generateDescription(this, itemId, columnId); + if (itemDescription != null && !itemDescription.equals("")) { + target.addAttribute("descr-" + columnIdMap.key(columnId), + itemDescription); + } + } + } + + private void paintRowTooltips(PaintTarget target, Object itemId) + throws PaintException { + if (itemDescriptionGenerator != null) { + String rowDescription = itemDescriptionGenerator + .generateDescription(this, itemId, null); + if (rowDescription != null && !rowDescription.equals("")) { + target.addAttribute("rowdescr", rowDescription); + } + } + } + + private void paintRowAttributes(PaintTarget target, final Object[][] cells, + final Set<Action> actionSet, int indexInRowbuffer, + final Object itemId) throws PaintException { + // tr attributes + + paintRowIcon(target, cells, indexInRowbuffer); + paintRowHeader(target, cells, indexInRowbuffer); + paintGeneratedRowInfo(target, cells, indexInRowbuffer); + target.addAttribute("key", + Integer.parseInt(cells[CELL_KEY][indexInRowbuffer].toString())); + + if (isSelected(itemId)) { + target.addAttribute("selected", true); + } + + // Actions + if (actionHandlers != null) { + final ArrayList<String> keys = new ArrayList<String>(); + for (Handler ah : actionHandlers) { + final Action[] aa = ah.getActions(itemId, this); + if (aa != null) { + for (int ai = 0; ai < aa.length; ai++) { + final String key = actionMapper.key(aa[ai]); + actionSet.add(aa[ai]); + keys.add(key); + } + } + } + target.addAttribute("al", keys.toArray()); + } + + /* + * For each row, if a cellStyleGenerator is specified, get the specific + * style for the cell, using null as propertyId. If there is any, add it + * to the target. + */ + if (cellStyleGenerator != null) { + String rowStyle = cellStyleGenerator.getStyle(itemId, null); + if (rowStyle != null && !rowStyle.equals("")) { + target.addAttribute("rowstyle", rowStyle); + } + } + + paintRowTooltips(target, itemId); + + paintRowAttributes(target, itemId); + } + + private void paintGeneratedRowInfo(PaintTarget target, Object[][] cells, + int indexInRowBuffer) throws PaintException { + GeneratedRow generatedRow = (GeneratedRow) cells[CELL_GENERATED_ROW][indexInRowBuffer]; + if (generatedRow != null) { + target.addAttribute("gen_html", generatedRow.isHtmlContentAllowed()); + target.addAttribute("gen_span", generatedRow.isSpanColumns()); + target.addAttribute("gen_widget", + generatedRow.getValue() instanceof Component); + } + } + + protected void paintRowHeader(PaintTarget target, Object[][] cells, + int indexInRowbuffer) throws PaintException { + if (rowHeadersAreEnabled()) { + if (cells[CELL_HEADER][indexInRowbuffer] != null) { + target.addAttribute("caption", + (String) cells[CELL_HEADER][indexInRowbuffer]); + } + } + + } + + protected void paintRowIcon(PaintTarget target, final Object[][] cells, + int indexInRowbuffer) throws PaintException { + if (rowHeadersAreEnabled() + && cells[CELL_ICON][indexInRowbuffer] != null) { + target.addAttribute("icon", + (Resource) cells[CELL_ICON][indexInRowbuffer]); + } + } + + /** + * A method where extended Table implementations may add their custom + * attributes for rows. + * + * @param target + * @param itemId + */ + protected void paintRowAttributes(PaintTarget target, Object itemId) + throws PaintException { + + } + + /** + * Gets the cached visible table contents. + * + * @return the cached visible table contents. + */ + private Object[][] getVisibleCells() { + if (pageBuffer == null) { + refreshRenderedCells(); + } + return pageBuffer; + } + + /** + * Gets the value of property. + * + * By default if the table is editable the fieldFactory is used to create + * editors for table cells. Otherwise formatPropertyValue is used to format + * the value representation. + * + * @param rowId + * the Id of the row (same as item Id). + * @param colId + * the Id of the column. + * @param property + * the Property to be presented. + * @return Object Either formatted value or Component for field. + * @see #setTableFieldFactory(TableFieldFactory) + */ + protected Object getPropertyValue(Object rowId, Object colId, + Property property) { + if (isEditable() && fieldFactory != null) { + final Field<?> f = fieldFactory.createField( + getContainerDataSource(), rowId, colId, this); + if (f != null) { + // Remember that we have made this association so we can remove + // it when the component is removed + associatedProperties.put(f, property); + bindPropertyToField(rowId, colId, property, f); + return f; + } + } + + return formatPropertyValue(rowId, colId, property); + } + + /** + * Binds an item property to a field generated by TableFieldFactory. The + * default behavior is to bind property straight to Field. If + * Property.Viewer type property (e.g. PropertyFormatter) is already set for + * field, the property is bound to that Property.Viewer. + * + * @param rowId + * @param colId + * @param property + * @param field + * @since 6.7.3 + */ + protected void bindPropertyToField(Object rowId, Object colId, + Property property, Field field) { + // check if field has a property that is Viewer set. In that case we + // expect developer has e.g. PropertyFormatter that he wishes to use and + // assign the property to the Viewer instead. + boolean hasFilterProperty = field.getPropertyDataSource() != null + && (field.getPropertyDataSource() instanceof Property.Viewer); + if (hasFilterProperty) { + ((Property.Viewer) field.getPropertyDataSource()) + .setPropertyDataSource(property); + } else { + field.setPropertyDataSource(property); + } + } + + /** + * Formats table cell property values. By default the property.toString() + * and return a empty string for null properties. + * + * @param rowId + * the Id of the row (same as item Id). + * @param colId + * the Id of the column. + * @param property + * the Property to be formatted. + * @return the String representation of property and its value. + * @since 3.1 + */ + protected String formatPropertyValue(Object rowId, Object colId, + Property<?> property) { + if (property == null) { + return ""; + } + Converter<String, Object> converter = null; + + if (hasConverter(colId)) { + converter = getConverter(colId); + } else { + ConverterUtil.getConverter(String.class, property.getType(), + getApplication()); + } + Object value = property.getValue(); + if (converter != null) { + return converter.convertToPresentation(value, getLocale()); + } + return (null != value) ? value.toString() : ""; + } + + /* Action container */ + + /** + * Registers a new action handler for this container + * + * @see com.vaadin.event.Action.Container#addActionHandler(Action.Handler) + */ + + @Override + public void addActionHandler(Action.Handler actionHandler) { + + if (actionHandler != null) { + + if (actionHandlers == null) { + actionHandlers = new LinkedList<Handler>(); + actionMapper = new KeyMapper<Action>(); + } + + if (!actionHandlers.contains(actionHandler)) { + actionHandlers.add(actionHandler); + // Assures the visual refresh. No need to reset the page buffer + // before as the content has not changed, only the action + // handlers. + refreshRenderedCells(); + } + + } + } + + /** + * Removes a previously registered action handler for the contents of this + * container. + * + * @see com.vaadin.event.Action.Container#removeActionHandler(Action.Handler) + */ + + @Override + public void removeActionHandler(Action.Handler actionHandler) { + + if (actionHandlers != null && actionHandlers.contains(actionHandler)) { + + actionHandlers.remove(actionHandler); + + if (actionHandlers.isEmpty()) { + actionHandlers = null; + actionMapper = null; + } + + // Assures the visual refresh. No need to reset the page buffer + // before as the content has not changed, only the action + // handlers. + refreshRenderedCells(); + } + } + + /** + * Removes all action handlers + */ + public void removeAllActionHandlers() { + actionHandlers = null; + actionMapper = null; + // Assures the visual refresh. No need to reset the page buffer + // before as the content has not changed, only the action + // handlers. + refreshRenderedCells(); + } + + /* Property value change listening support */ + + /** + * Notifies this listener that the Property's value has changed. + * + * Also listens changes in rendered items to refresh content area. + * + * @see com.vaadin.data.Property.ValueChangeListener#valueChange(Property.ValueChangeEvent) + */ + + @Override + public void valueChange(Property.ValueChangeEvent event) { + if (event.getProperty() == this + || event.getProperty() == getPropertyDataSource()) { + super.valueChange(event); + } else { + refreshRowCache(); + containerChangeToBeRendered = true; + } + requestRepaint(); + } + + /** + * Clears the current page buffer. Call this before + * {@link #refreshRenderedCells()} to ensure that all content is updated + * from the properties. + */ + protected void resetPageBuffer() { + firstToBeRenderedInClient = -1; + lastToBeRenderedInClient = -1; + reqFirstRowToPaint = -1; + reqRowsToPaint = -1; + pageBuffer = null; + } + + /** + * Notifies the component that it is connected to an application. + * + * @see com.vaadin.ui.Component#attach() + */ + + @Override + public void attach() { + super.attach(); + + refreshRenderedCells(); + } + + /** + * Notifies the component that it is detached from the application + * + * @see com.vaadin.ui.Component#detach() + */ + + @Override + public void detach() { + super.detach(); + } + + /** + * Removes all Items from the Container. + * + * @see com.vaadin.data.Container#removeAllItems() + */ + + @Override + public boolean removeAllItems() { + currentPageFirstItemId = null; + currentPageFirstItemIndex = 0; + return super.removeAllItems(); + } + + /** + * Removes the Item identified by <code>ItemId</code> 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. + * <p> + * 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. + * </p> + * <p> + * 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. + * </p> + * <p> + * 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. + * </p> + * <p> + * Also note that getVisibleColumns() will return the generated columns, + * while getContainerPropertyIds() will not. + * </p> + * + * @param id + * the id of the column to be added + * @param generatedColumn + * the {@link ColumnGenerator} to use for this column + */ + public void addGeneratedColumn(Object id, ColumnGenerator generatedColumn) { + if (generatedColumn == null) { + throw new IllegalArgumentException( + "Can not add null as a GeneratedColumn"); + } + if (columnGenerators.containsKey(id)) { + throw new IllegalArgumentException( + "Can not add the same GeneratedColumn twice, id:" + id); + } else { + columnGenerators.put(id, generatedColumn); + /* + * add to visible column list unless already there (overriding + * column from DS) + */ + if (!visibleColumns.contains(id)) { + visibleColumns.add(id); + } + refreshRowCache(); + } + } + + /** + * Returns the ColumnGenerator used to generate the given column. + * + * @param columnId + * The id of the generated column + * @return The ColumnGenerator used for the given columnId or null. + */ + public ColumnGenerator getColumnGenerator(Object columnId) + throws IllegalArgumentException { + return columnGenerators.get(columnId); + } + + /** + * Removes a generated column previously added with addGeneratedColumn. + * + * @param columnId + * id of the generated column to remove + * @return true if the column could be removed (existed in the Table) + */ + public boolean removeGeneratedColumn(Object columnId) { + if (columnGenerators.containsKey(columnId)) { + columnGenerators.remove(columnId); + // remove column from visibleColumns list unless it exists in + // container (generator previously overrode this column) + if (!items.getContainerPropertyIds().contains(columnId)) { + visibleColumns.remove(columnId); + } + refreshRowCache(); + return true; + } else { + return false; + } + } + + /** + * Returns item identifiers of the items which are currently rendered on the + * client. + * <p> + * 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<Object> visible = new LinkedList<Object>(); + + final Object[][] cells = getVisibleCells(); + // may be null if the table has not been rendered yet (e.g. not attached + // to a layout) + if (null != cells) { + for (int i = 0; i < cells[CELL_ITEMID].length; i++) { + visible.add(cells[CELL_ITEMID][i]); + } + } + + return visible; + } + + /** + * Container datasource item set change. Table must flush its buffers on + * change. + * + * @see com.vaadin.data.Container.ItemSetChangeListener#containerItemSetChange(com.vaadin.data.Container.ItemSetChangeEvent) + */ + + @Override + public void containerItemSetChange(Container.ItemSetChangeEvent event) { + super.containerItemSetChange(event); + + // super method clears the key map, must inform client about this to + // avoid getting invalid keys back (#8584) + keyMapperReset = true; + + // ensure that page still has first item in page, ignore buffer refresh + // (forced in this method) + setCurrentPageFirstItemIndex(getCurrentPageFirstItemIndex(), false); + refreshRowCache(); + } + + /** + * Container datasource property set change. Table must flush its buffers on + * change. + * + * @see com.vaadin.data.Container.PropertySetChangeListener#containerPropertySetChange(com.vaadin.data.Container.PropertySetChangeEvent) + */ + + @Override + public void containerPropertySetChange( + Container.PropertySetChangeEvent event) { + disableContentRefreshing(); + super.containerPropertySetChange(event); + + // sanitetize visibleColumns. note that we are not adding previously + // non-existing properties as columns + Collection<?> containerPropertyIds = getContainerDataSource() + .getContainerPropertyIds(); + + LinkedList<Object> newVisibleColumns = new LinkedList<Object>( + visibleColumns); + for (Iterator<Object> iterator = newVisibleColumns.iterator(); iterator + .hasNext();) { + Object id = iterator.next(); + if (!(containerPropertyIds.contains(id) || columnGenerators + .containsKey(id))) { + iterator.remove(); + } + } + setVisibleColumns(newVisibleColumns.toArray()); + // same for collapsed columns + for (Iterator<Object> iterator = collapsedColumns.iterator(); iterator + .hasNext();) { + Object id = iterator.next(); + if (!(containerPropertyIds.contains(id) || columnGenerators + .containsKey(id))) { + iterator.remove(); + } + } + + resetPageBuffer(); + enableContentRefreshing(true); + } + + /** + * Adding new items is not supported. + * + * @throws UnsupportedOperationException + * if set to true. + * @see com.vaadin.ui.Select#setNewItemsAllowed(boolean) + */ + + @Override + public void setNewItemsAllowed(boolean allowNewOptions) + throws UnsupportedOperationException { + if (allowNewOptions) { + throw new UnsupportedOperationException(); + } + } + + /** + * Gets the ID of the Item following the Item that corresponds to itemId. + * + * @see com.vaadin.data.Container.Ordered#nextItemId(java.lang.Object) + */ + + @Override + public Object nextItemId(Object itemId) { + return ((Container.Ordered) items).nextItemId(itemId); + } + + /** + * Gets the ID of the Item preceding the Item that corresponds to the + * itemId. + * + * @see com.vaadin.data.Container.Ordered#prevItemId(java.lang.Object) + */ + + @Override + public Object prevItemId(Object itemId) { + return ((Container.Ordered) items).prevItemId(itemId); + } + + /** + * Gets the ID of the first Item in the Container. + * + * @see com.vaadin.data.Container.Ordered#firstItemId() + */ + + @Override + public Object firstItemId() { + return ((Container.Ordered) items).firstItemId(); + } + + /** + * Gets the ID of the last Item in the Container. + * + * @see com.vaadin.data.Container.Ordered#lastItemId() + */ + + @Override + public Object lastItemId() { + return ((Container.Ordered) items).lastItemId(); + } + + /** + * Tests if the Item corresponding to the given Item ID is the first Item in + * the Container. + * + * @see com.vaadin.data.Container.Ordered#isFirstId(java.lang.Object) + */ + + @Override + public boolean isFirstId(Object itemId) { + return ((Container.Ordered) items).isFirstId(itemId); + } + + /** + * Tests if the Item corresponding to the given Item ID is the last Item in + * the Container. + * + * @see com.vaadin.data.Container.Ordered#isLastId(java.lang.Object) + */ + + @Override + public boolean isLastId(Object itemId) { + return ((Container.Ordered) items).isLastId(itemId); + } + + /** + * Adds new item after the given item. + * + * @see com.vaadin.data.Container.Ordered#addItemAfter(java.lang.Object) + */ + + @Override + public Object addItemAfter(Object previousItemId) + throws UnsupportedOperationException { + Object itemId = ((Container.Ordered) items) + .addItemAfter(previousItemId); + if (!(items instanceof Container.ItemSetChangeNotifier)) { + refreshRowCache(); + } + return itemId; + } + + /** + * Adds new item after the given item. + * + * @see com.vaadin.data.Container.Ordered#addItemAfter(java.lang.Object, + * java.lang.Object) + */ + + @Override + public Item addItemAfter(Object previousItemId, Object newItemId) + throws UnsupportedOperationException { + Item item = ((Container.Ordered) items).addItemAfter(previousItemId, + newItemId); + if (!(items instanceof Container.ItemSetChangeNotifier)) { + refreshRowCache(); + } + return item; + } + + /** + * Sets the TableFieldFactory that is used to create editor for table cells. + * + * The TableFieldFactory is only used if the Table is editable. By default + * the DefaultFieldFactory is used. + * + * @param fieldFactory + * the field factory to set. + * @see #isEditable + * @see DefaultFieldFactory + */ + public void setTableFieldFactory(TableFieldFactory fieldFactory) { + this.fieldFactory = fieldFactory; + + // Assure visual refresh + refreshRowCache(); + } + + /** + * Gets the TableFieldFactory that is used to create editor for table cells. + * + * The FieldFactory is only used if the Table is editable. + * + * @return TableFieldFactory used to create the Field instances. + * @see #isEditable + */ + public TableFieldFactory getTableFieldFactory() { + return fieldFactory; + } + + /** + * Is table editable. + * + * If table is editable a editor of type Field is created for each table + * cell. The assigned FieldFactory is used to create the instances. + * + * To provide custom editors for table cells create a class implementins the + * FieldFactory interface, and assign it to table, and set the editable + * property to true. + * + * @return true if table is editable, false oterwise. + * @see Field + * @see FieldFactory + * + */ + public boolean isEditable() { + return editable; + } + + /** + * Sets the editable property. + * + * If table is editable a editor of type Field is created for each table + * cell. The assigned FieldFactory is used to create the instances. + * + * To provide custom editors for table cells create a class implementins the + * FieldFactory interface, and assign it to table, and set the editable + * property to true. + * + * @param editable + * true if table should be editable by user. + * @see Field + * @see FieldFactory + * + */ + public void setEditable(boolean editable) { + this.editable = editable; + + // Assure visual refresh + refreshRowCache(); + } + + /** + * Sorts the table. + * + * @throws UnsupportedOperationException + * if the container data source does not implement + * Container.Sortable + * @see com.vaadin.data.Container.Sortable#sort(java.lang.Object[], + * boolean[]) + * + */ + + @Override + public void sort(Object[] propertyId, boolean[] ascending) + throws UnsupportedOperationException { + final Container c = getContainerDataSource(); + if (c instanceof Container.Sortable) { + final int pageIndex = getCurrentPageFirstItemIndex(); + ((Container.Sortable) c).sort(propertyId, ascending); + setCurrentPageFirstItemIndex(pageIndex); + refreshRowCache(); + + } else if (c != null) { + throw new UnsupportedOperationException( + "Underlying Data does not allow sorting"); + } + } + + /** + * Sorts the table by currently selected sorting column. + * + * @throws UnsupportedOperationException + * if the container data source does not implement + * Container.Sortable + */ + public void sort() { + if (getSortContainerPropertyId() == null) { + return; + } + sort(new Object[] { sortContainerPropertyId }, + new boolean[] { sortAscending }); + } + + /** + * Gets the container property IDs, which can be used to sort the item. + * <p> + * Note that the {@link #isSortEnabled()} state affects what this method + * returns. Disabling sorting causes this method to always return an empty + * collection. + * </p> + * + * @see com.vaadin.data.Container.Sortable#getSortableContainerPropertyIds() + */ + + @Override + public Collection<?> getSortableContainerPropertyIds() { + final Container c = getContainerDataSource(); + if (c instanceof Container.Sortable && isSortEnabled()) { + return ((Container.Sortable) c).getSortableContainerPropertyIds(); + } else { + return Collections.EMPTY_LIST; + } + } + + /** + * Gets the currently sorted column property ID. + * + * @return the Container property id of the currently sorted column. + */ + public Object getSortContainerPropertyId() { + return sortContainerPropertyId; + } + + /** + * Sets the currently sorted column property id. + * + * @param propertyId + * the Container property id of the currently sorted column. + */ + public void setSortContainerPropertyId(Object propertyId) { + setSortContainerPropertyId(propertyId, true); + } + + /** + * Internal method to set currently sorted column property id. With doSort + * flag actual sorting may be bypassed. + * + * @param propertyId + * @param doSort + */ + private void setSortContainerPropertyId(Object propertyId, boolean doSort) { + if ((sortContainerPropertyId != null && !sortContainerPropertyId + .equals(propertyId)) + || (sortContainerPropertyId == null && propertyId != null)) { + sortContainerPropertyId = propertyId; + + if (doSort) { + sort(); + // Assures the visual refresh. This should not be necessary as + // sort() calls refreshRowCache + refreshRenderedCells(); + } + } + } + + /** + * Is the table currently sorted in ascending order. + * + * @return <code>true</code> if ascending, <code>false</code> if descending. + */ + public boolean isSortAscending() { + return sortAscending; + } + + /** + * Sets the table in ascending order. + * + * @param ascending + * <code>true</code> if ascending, <code>false</code> 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 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 Use {@link #setSortEnabled(boolean)} instead + */ + @Deprecated + public void setSortDisabled(boolean sortDisabled) { + setSortEnabled(!sortDisabled); + } + + /** + * Enables or disables sorting. + * <p> + * Setting this to false disallows sorting by the user. It is still possible + * to call {@link #sort()}. + * </p> + * + * @param sortEnabled + * true to allow the user to sort the table, false to disallow it + */ + public void setSortEnabled(boolean sortEnabled) { + if (this.sortEnabled != sortEnabled) { + this.sortEnabled = sortEnabled; + requestRepaint(); + } + } + + /** + * Used to create "generated columns"; columns that exist only in the Table, + * not in the underlying Container. Implement this interface and pass it to + * Table.addGeneratedColumn along with an id for the column to be generated. + * + */ + public interface ColumnGenerator extends Serializable { + + /** + * Called by Table when a cell in a generated column needs to be + * generated. + * + * @param source + * the source Table + * @param itemId + * the itemId (aka rowId) for the of the cell to be generated + * @param columnId + * the id for the generated column (as specified in + * addGeneratedColumn) + * @return A {@link Component} that should be rendered in the cell or a + * {@link String} that should be displayed in the cell. Other + * return values are not supported. + */ + public abstract Object generateCell(Table source, Object itemId, + Object columnId); + } + + /** + * Set cell style generator for Table. + * + * @param cellStyleGenerator + * New cell style generator or null to remove generator. + */ + public void setCellStyleGenerator(CellStyleGenerator cellStyleGenerator) { + this.cellStyleGenerator = cellStyleGenerator; + // Assures the visual refresh. No need to reset the page buffer + // before as the content has not changed, only the style generators + refreshRenderedCells(); + + } + + /** + * Get the current cell style generator. + * + */ + public CellStyleGenerator getCellStyleGenerator() { + return cellStyleGenerator; + } + + /** + * Allow to define specific style on cells (and rows) contents. Implements + * this interface and pass it to Table.setCellStyleGenerator. Row styles are + * generated when porpertyId is null. The CSS class name that will be added + * to the cell content is <tt>v-table-cell-content-[style name]</tt>, and + * the row style will be <tt>v-table-row-[style name]</tt>. + */ + public interface CellStyleGenerator extends Serializable { + + /** + * Called by Table when a cell (and row) is painted. + * + * @param itemId + * The itemId of the painted cell + * @param propertyId + * The propertyId of the cell, null when getting row style + * @return The style name to add to this cell or row. (the CSS class + * name will be v-table-cell-content-[style name], or + * v-table-row-[style name] for rows) + */ + public abstract String getStyle(Object itemId, Object propertyId); + } + + @Override + public void addListener(ItemClickListener listener) { + addListener(VScrollTable.ITEM_CLICK_EVENT_ID, ItemClickEvent.class, + listener, ItemClickEvent.ITEM_CLICK_METHOD); + } + + @Override + public void removeListener(ItemClickListener listener) { + removeListener(VScrollTable.ITEM_CLICK_EVENT_ID, ItemClickEvent.class, + listener); + } + + // Identical to AbstractCompoenentContainer.setEnabled(); + + @Override + public void setEnabled(boolean enabled) { + super.setEnabled(enabled); + if (getParent() != null && !getParent().isEnabled()) { + // some ancestor still disabled, don't update children + return; + } else { + requestRepaintAll(); + } + } + + /** + * Sets the drag start mode of the Table. Drag start mode controls how Table + * behaves as a drag source. + * + * @param newDragMode + */ + public void setDragMode(TableDragMode newDragMode) { + dragMode = newDragMode; + requestRepaint(); + } + + /** + * @return the current start mode of the Table. Drag start mode controls how + * Table behaves as a drag source. + */ + public TableDragMode getDragMode() { + return dragMode; + } + + /** + * Concrete implementation of {@link DataBoundTransferable} for data + * transferred from a table. + * + * @see {@link DataBoundTransferable}. + * + * @since 6.3 + */ + public class TableTransferable extends DataBoundTransferable { + + protected TableTransferable(Map<String, Object> rawVariables) { + super(Table.this, rawVariables); + Object object = rawVariables.get("itemId"); + if (object != null) { + setData("itemId", itemIdMapper.get((String) object)); + } + object = rawVariables.get("propertyId"); + if (object != null) { + setData("propertyId", columnIdMap.get((String) object)); + } + } + + @Override + public Object getItemId() { + return getData("itemId"); + } + + @Override + public Object getPropertyId() { + return getData("propertyId"); + } + + @Override + public Table getSourceComponent() { + return (Table) super.getSourceComponent(); + } + + } + + @Override + public TableTransferable getTransferable(Map<String, Object> rawVariables) { + TableTransferable transferable = new TableTransferable(rawVariables); + return transferable; + } + + @Override + public DropHandler getDropHandler() { + return dropHandler; + } + + public void setDropHandler(DropHandler dropHandler) { + this.dropHandler = dropHandler; + } + + @Override + public AbstractSelectTargetDetails translateDropTargetDetails( + Map<String, Object> clientVariables) { + return new AbstractSelectTargetDetails(clientVariables); + } + + /** + * Sets the behavior of how the multi-select mode should behave when the + * table is both selectable and in multi-select mode. + * <p> + * 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; + requestRepaint(); + } + + /** + * 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. + * + * <p> + * 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<Object> allowedItemIds; + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.event.dd.acceptcriteria.ServerSideCriterion#getIdentifier + * () + */ + + @Override + protected String getIdentifier() { + return TableDropCriterion.class.getCanonicalName(); + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.event.dd.acceptcriteria.AcceptCriterion#accepts(com.vaadin + * .event.dd.DragAndDropEvent) + */ + @Override + @SuppressWarnings("unchecked") + public boolean accept(DragAndDropEvent dragEvent) { + AbstractSelectTargetDetails dropTargetData = (AbstractSelectTargetDetails) dragEvent + .getTargetDetails(); + table = (Table) dragEvent.getTargetDetails().getTarget(); + Collection<?> visibleItemIds = table.getVisibleItemIds(); + allowedItemIds = getAllowedItemIds(dragEvent, table, + (Collection<Object>) visibleItemIds); + + return allowedItemIds.contains(dropTargetData.getItemIdOver()); + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.event.dd.acceptcriteria.AcceptCriterion#paintResponse( + * com.vaadin.terminal.PaintTarget) + */ + + @Override + public void paintResponse(PaintTarget target) throws PaintException { + /* + * send allowed nodes to client so subsequent requests can be + * avoided + */ + Object[] array = allowedItemIds.toArray(); + for (int i = 0; i < array.length; i++) { + String key = table.itemIdMapper.key(array[i]); + array[i] = key; + } + target.addAttribute("allowedIds", array); + } + + /** + * @param dragEvent + * @param table + * the table for which the allowed item identifiers are + * defined + * @param visibleItemIds + * the list of currently rendered item identifiers, accepted + * item id's need to be detected only for these visible items + * @return the set of identifiers for items on which the dragEvent will + * be accepted + */ + protected abstract Set<Object> getAllowedItemIds( + DragAndDropEvent dragEvent, Table table, + Collection<Object> visibleItemIds); + + } + + /** + * Click event fired when clicking on the Table headers. The event includes + * a reference the the Table the event originated from, the property id of + * the column which header was pressed and details about the mouse event + * itself. + */ + public static class HeaderClickEvent extends ClickEvent { + public static final Method HEADER_CLICK_METHOD; + + static { + try { + // Set the header click method + HEADER_CLICK_METHOD = HeaderClickListener.class + .getDeclaredMethod("headerClick", + new Class[] { HeaderClickEvent.class }); + } catch (final java.lang.NoSuchMethodException e) { + // This should never happen + throw new java.lang.RuntimeException(e); + } + } + + // The property id of the column which header was pressed + private final Object columnPropertyId; + + public HeaderClickEvent(Component source, Object propertyId, + MouseEventDetails details) { + super(source, details); + columnPropertyId = propertyId; + } + + /** + * Gets the property id of the column which header was pressed + * + * @return The column propety id + */ + public Object getPropertyId() { + return columnPropertyId; + } + } + + /** + * Click event fired when clicking on the Table footers. The event includes + * a reference the the Table the event originated from, the property id of + * the column which header was pressed and details about the mouse event + * itself. + */ + public static class FooterClickEvent extends ClickEvent { + public static final Method FOOTER_CLICK_METHOD; + + static { + try { + // Set the header click method + FOOTER_CLICK_METHOD = FooterClickListener.class + .getDeclaredMethod("footerClick", + new Class[] { FooterClickEvent.class }); + } catch (final java.lang.NoSuchMethodException e) { + // This should never happen + throw new java.lang.RuntimeException(e); + } + } + + // The property id of the column which header was pressed + private final Object columnPropertyId; + + /** + * Constructor + * + * @param source + * The source of the component + * @param propertyId + * The propertyId of the column + * @param details + * The mouse details of the click + */ + public FooterClickEvent(Component source, Object propertyId, + MouseEventDetails details) { + super(source, details); + columnPropertyId = propertyId; + } + + /** + * Gets the property id of the column which header was pressed + * + * @return The column propety id + */ + public Object getPropertyId() { + return columnPropertyId; + } + } + + /** + * Interface for the listener for column header mouse click events. The + * headerClick method is called when the user presses a header column cell. + */ + public interface HeaderClickListener extends Serializable { + + /** + * Called when a user clicks a header column cell + * + * @param event + * The event which contains information about the column and + * the mouse click event + */ + public void headerClick(HeaderClickEvent event); + } + + /** + * Interface for the listener for column footer mouse click events. The + * footerClick method is called when the user presses a footer column cell. + */ + public interface FooterClickListener extends Serializable { + + /** + * Called when a user clicks a footer column cell + * + * @param event + * The event which contains information about the column and + * the mouse click event + */ + public void footerClick(FooterClickEvent event); + } + + /** + * Adds a header click listener which handles the click events when the user + * clicks on a column header cell in the Table. + * <p> + * The listener will receive events which contain information about which + * column was clicked and some details about the mouse event. + * </p> + * + * @param listener + * The handler which should handle the header click events. + */ + public void addListener(HeaderClickListener listener) { + addListener(VScrollTable.HEADER_CLICK_EVENT_ID, HeaderClickEvent.class, + listener, HeaderClickEvent.HEADER_CLICK_METHOD); + } + + /** + * Removes a header click listener + * + * @param listener + * The listener to remove. + */ + public void removeListener(HeaderClickListener listener) { + removeListener(VScrollTable.HEADER_CLICK_EVENT_ID, + HeaderClickEvent.class, listener); + } + + /** + * Adds a footer click listener which handles the click events when the user + * clicks on a column footer cell in the Table. + * <p> + * The listener will receive events which contain information about which + * column was clicked and some details about the mouse event. + * </p> + * + * @param listener + * The handler which should handle the footer click events. + */ + public void addListener(FooterClickListener listener) { + addListener(VScrollTable.FOOTER_CLICK_EVENT_ID, FooterClickEvent.class, + listener, FooterClickEvent.FOOTER_CLICK_METHOD); + } + + /** + * Removes a footer click listener + * + * @param listener + * The listener to remove. + */ + public void removeListener(FooterClickListener listener) { + removeListener(VScrollTable.FOOTER_CLICK_EVENT_ID, + FooterClickEvent.class, listener); + } + + /** + * Gets the footer caption beneath the rows + * + * @param propertyId + * The propertyId of the column * + * @return The caption of the footer or NULL if not set + */ + public String getColumnFooter(Object propertyId) { + return columnFooters.get(propertyId); + } + + /** + * Sets the column footer caption. The column footer caption is the text + * displayed beneath the column if footers have been set visible. + * + * @param propertyId + * The properyId of the column + * + * @param footer + * The caption of the footer + */ + public void setColumnFooter(Object propertyId, String footer) { + if (footer == null) { + columnFooters.remove(propertyId); + } else { + columnFooters.put(propertyId, footer); + } + + requestRepaint(); + } + + /** + * Sets the footer visible in the bottom of the table. + * <p> + * The footer can be used to add column related data like sums to the bottom + * of the Table using setColumnFooter(Object propertyId, String footer). + * </p> + * + * @param visible + * Should the footer be visible + */ + public void setFooterVisible(boolean visible) { + if (visible != columnFootersVisible) { + columnFootersVisible = visible; + requestRepaint(); + } + } + + /** + * Is the footer currently visible? + * + * @return Returns true if visible else false + */ + public boolean isFooterVisible() { + return columnFootersVisible; + } + + /** + * This event is fired when a column is resized. The event contains the + * columns property id which was fired, the previous width of the column and + * the width of the column after the resize. + */ + public static class ColumnResizeEvent extends Component.Event { + public static final Method COLUMN_RESIZE_METHOD; + + static { + try { + COLUMN_RESIZE_METHOD = ColumnResizeListener.class + .getDeclaredMethod("columnResize", + new Class[] { ColumnResizeEvent.class }); + } catch (final java.lang.NoSuchMethodException e) { + // This should never happen + throw new java.lang.RuntimeException(e); + } + } + + private final int previousWidth; + private final int currentWidth; + private final Object columnPropertyId; + + /** + * Constructor + * + * @param source + * The source of the event + * @param propertyId + * The columns property id + * @param previous + * The width in pixels of the column before the resize event + * @param current + * The width in pixels of the column after the resize event + */ + public ColumnResizeEvent(Component source, Object propertyId, + int previous, int current) { + super(source); + previousWidth = previous; + currentWidth = current; + columnPropertyId = propertyId; + } + + /** + * Get the column property id of the column that was resized. + * + * @return The column property id + */ + public Object getPropertyId() { + return columnPropertyId; + } + + /** + * Get the width in pixels of the column before the resize event + * + * @return Width in pixels + */ + public int getPreviousWidth() { + return previousWidth; + } + + /** + * Get the width in pixels of the column after the resize event + * + * @return Width in pixels + */ + public int getCurrentWidth() { + return currentWidth; + } + } + + /** + * Interface for listening to column resize events. + */ + public interface ColumnResizeListener extends Serializable { + + /** + * This method is triggered when the column has been resized + * + * @param event + * The event which contains the column property id, the + * previous width of the column and the current width of the + * column + */ + public void columnResize(ColumnResizeEvent event); + } + + /** + * Adds a column resize listener to the Table. A column resize listener is + * called when a user resizes a columns width. + * + * @param listener + * The listener to attach to the Table + */ + public void addListener(ColumnResizeListener listener) { + addListener(VScrollTable.COLUMN_RESIZE_EVENT_ID, + ColumnResizeEvent.class, listener, + ColumnResizeEvent.COLUMN_RESIZE_METHOD); + } + + /** + * Removes a column resize listener from the Table. + * + * @param listener + * The listener to remove + */ + public void removeListener(ColumnResizeListener listener) { + removeListener(VScrollTable.COLUMN_RESIZE_EVENT_ID, + ColumnResizeEvent.class, listener); + } + + /** + * This event is fired when a columns are reordered by the end user user. + */ + public static class ColumnReorderEvent extends Component.Event { + public static final Method METHOD; + + static { + try { + METHOD = ColumnReorderListener.class.getDeclaredMethod( + "columnReorder", + new Class[] { ColumnReorderEvent.class }); + } catch (final java.lang.NoSuchMethodException e) { + // This should never happen + throw new java.lang.RuntimeException(e); + } + } + + /** + * Constructor + * + * @param source + * The source of the event + */ + public ColumnReorderEvent(Component source) { + super(source); + } + + } + + /** + * Interface for listening to column reorder events. + */ + public interface ColumnReorderListener extends Serializable { + + /** + * This method is triggered when the column has been reordered + * + * @param event + */ + public void columnReorder(ColumnReorderEvent event); + } + + /** + * Adds a column reorder listener to the Table. A column reorder listener is + * called when a user reorders columns. + * + * @param listener + * The listener to attach to the Table + */ + public void addListener(ColumnReorderListener listener) { + addListener(VScrollTable.COLUMN_REORDER_EVENT_ID, + ColumnReorderEvent.class, listener, ColumnReorderEvent.METHOD); + } + + /** + * Removes a column reorder listener from the Table. + * + * @param listener + * The listener to remove + */ + public void removeListener(ColumnReorderListener listener) { + removeListener(VScrollTable.COLUMN_REORDER_EVENT_ID, + ColumnReorderEvent.class, listener); + } + + /** + * Set the item description generator which generates tooltips for cells and + * rows in the Table + * + * @param generator + * The generator to use or null to disable + */ + public void setItemDescriptionGenerator(ItemDescriptionGenerator generator) { + if (generator != itemDescriptionGenerator) { + itemDescriptionGenerator = generator; + // Assures the visual refresh. No need to reset the page buffer + // before as the content has not changed, only the descriptions + refreshRenderedCells(); + } + } + + /** + * Get the item description generator which generates tooltips for cells and + * rows in the Table. + */ + public ItemDescriptionGenerator getItemDescriptionGenerator() { + return itemDescriptionGenerator; + } + + /** + * Row generators can be used to replace certain items in a table with a + * generated string. The generator is called each time the table is + * rendered, which means that new strings can be generated each time. + * + * Row generators can be used for e.g. summary rows or grouping of items. + */ + public interface RowGenerator extends Serializable { + /** + * Called for every row that is painted in the Table. Returning a + * GeneratedRow object will cause the row to be painted based on the + * contents of the GeneratedRow. A generated row is by default styled + * similarly to a header or footer row. + * <p> + * 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. + * <p> + * 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. + * <p> + * If GeneratedRow.setRenderAsHtml(true) is used, the strings can + * contain HTML markup, otherwise all strings will be rendered as text + * (the default). + * <p> + * 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. + * <p> + * + * @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. + * <p> + * The converter is used to format the the data for the given property id + * before displaying it in the table. + * </p> + * + * @param propertyId + * The propertyId to format using the converter + * @param converter + * The converter to use for the property id + */ + public void setConverter(Object propertyId, Converter<String, ?> converter) { + if (!getContainerPropertyIds().contains(propertyId)) { + throw new IllegalArgumentException("PropertyId " + propertyId + + " must be in the container"); + } + // FIXME: This check should be here but primitive types like Boolean + // formatter for boolean property must be handled + + // if (!converter.getSourceType().isAssignableFrom(getType(propertyId))) + // { + // throw new IllegalArgumentException("Property type (" + // + getType(propertyId) + // + ") must match converter source type (" + // + converter.getSourceType() + ")"); + // } + propertyValueConverters.put(propertyId, + (Converter<String, Object>) converter); + refreshRowCache(); + } + + /** + * Checks if there is a converter set explicitly for the given property id. + * + * @param propertyId + * The propertyId to check + * @return true if a converter has been set for the property id, false + * otherwise + */ + protected boolean hasConverter(Object propertyId) { + return propertyValueConverters.containsKey(propertyId); + } + + /** + * Returns the converter used to format the given propertyId. + * + * @param propertyId + * The propertyId to check + * @return The converter used to format the propertyId or null if no + * converter has been set + */ + public Converter<String, Object> getConverter(Object propertyId) { + return propertyValueConverters.get(propertyId); + } + + @Override + public void setVisible(boolean visible) { + if (visible) { + // We need to ensure that the rows are sent to the client when the + // Table is made visible if it has been rendered as invisible. + setRowCacheInvalidated(true); + } + super.setVisible(visible); + } + + @Override + public Iterator<Component> iterator() { + return getComponentIterator(); + } + + @Override + public Iterator<Component> getComponentIterator() { + if (visibleComponents == null) { + Collection<Component> empty = Collections.emptyList(); + return empty.iterator(); + } + + return visibleComponents.iterator(); + } + + @Override + public boolean isComponentVisible(Component childComponent) { + return true; + } + + private final Logger getLogger() { + if (logger == null) { + logger = Logger.getLogger(Table.class.getName()); + } + return logger; + } +} |