diff options
author | Artur Signell <artur@vaadin.com> | 2016-08-18 21:58:46 +0300 |
---|---|---|
committer | Artur Signell <artur@vaadin.com> | 2016-08-20 00:08:47 +0300 |
commit | 65370e12a0605926cb80e205c2b0e74fefe83e5b (patch) | |
tree | 013b7a60741db3c902bb1a8ea29733aa117bd0b2 /compatibility-server | |
parent | 34852cdb88c6c27b1341684204d78db0fdd061a6 (diff) | |
download | vaadin-framework-65370e12a0605926cb80e205c2b0e74fefe83e5b.tar.gz vaadin-framework-65370e12a0605926cb80e205c2b0e74fefe83e5b.zip |
Move Table/TreeTable to compatibility package
Change-Id: Ic9f2badf8688c32d704be67519c0f4c9a3da0e28
Diffstat (limited to 'compatibility-server')
47 files changed, 17508 insertions, 0 deletions
diff --git a/compatibility-server/pom.xml b/compatibility-server/pom.xml index 38dcc07f21..4f6d26fe5f 100644 --- a/compatibility-server/pom.xml +++ b/compatibility-server/pom.xml @@ -38,6 +38,16 @@ <artifactId>javax.servlet-api</artifactId> <scope>provided</scope> </dependency> + <dependency> + <groupId>commons-lang</groupId> + <artifactId>commons-lang</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.hsqldb</groupId> + <artifactId>hsqldb</artifactId> + <scope>test</scope> + </dependency> <!-- Bean Validation API --> <dependency> <groupId>javax.validation</groupId> diff --git a/compatibility-server/src/main/java/com/vaadin/ui/DefaultFieldFactory.java b/compatibility-server/src/main/java/com/vaadin/ui/DefaultFieldFactory.java new file mode 100644 index 0000000000..2ee5ecf00e --- /dev/null +++ b/compatibility-server/src/main/java/com/vaadin/ui/DefaultFieldFactory.java @@ -0,0 +1,114 @@ +/* + * Copyright 2000-2016 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.ui; + +import java.text.Normalizer.Form; +import java.util.Date; + +import com.vaadin.data.Container; +import com.vaadin.data.Property; +import com.vaadin.shared.util.SharedUtil; +import com.vaadin.v7.ui.LegacyCheckBox; +import com.vaadin.v7.ui.LegacyDateField; +import com.vaadin.v7.ui.LegacyField; +import com.vaadin.v7.ui.LegacyTextField; + +/** + * This class contains a basic implementation for {@link TableFieldFactory}. The + * class is singleton, use {@link #get()} method to get reference to the + * instance. + * + * <p> + * There are also some static helper methods available for custom built field + * factories. + * + */ +public class DefaultFieldFactory implements TableFieldFactory { + + private static final DefaultFieldFactory instance = new DefaultFieldFactory(); + + /** + * Singleton method to get an instance of DefaultFieldFactory. + * + * @return an instance of DefaultFieldFactory + */ + public static DefaultFieldFactory get() { + return instance; + } + + protected DefaultFieldFactory() { + } + + @Override + public LegacyField createField(Container container, Object itemId, + Object propertyId, Component uiContext) { + Property containerProperty = container.getContainerProperty(itemId, + propertyId); + Class<?> type = containerProperty.getType(); + LegacyField<?> field = createFieldByPropertyType(type); + field.setCaption(createCaptionByPropertyId(propertyId)); + return field; + } + + /** + * If name follows method naming conventions, convert the name to spaced + * upper case text. For example, convert "firstName" to "First Name" + * + * @param propertyId + * @return the formatted caption string + */ + public static String createCaptionByPropertyId(Object propertyId) { + return SharedUtil.propertyIdToHumanFriendly(propertyId); + } + + /** + * Creates fields based on the property type. + * <p> + * The default field type is {@link LegacyTextField}. Other field types + * generated by this method: + * <p> + * <b>Boolean</b>: {@link CheckBox}.<br/> + * <b>Date</b>: {@link LegacyDateField}(resolution: day).<br/> + * <b>Item</b>: {@link Form}. <br/> + * <b>default field type</b>: {@link LegacyTextField}. + * <p> + * + * @param type + * the type of the property + * @return the most suitable generic {@link LegacyField} for given type + */ + public static LegacyField<?> createFieldByPropertyType(Class<?> type) { + // Null typed properties can not be edited + if (type == null) { + return null; + } + + // Date field + if (Date.class.isAssignableFrom(type)) { + final LegacyDateField df = new LegacyDateField(); + df.setResolution(LegacyDateField.RESOLUTION_DAY); + return df; + } + + // Boolean field + if (Boolean.class.isAssignableFrom(type)) { + return new LegacyCheckBox(); + } + + return new LegacyTextField(); + } + +} diff --git a/compatibility-server/src/main/java/com/vaadin/ui/Table.java b/compatibility-server/src/main/java/com/vaadin/ui/Table.java new file mode 100644 index 0000000000..13333146f2 --- /dev/null +++ b/compatibility-server/src/main/java/com/vaadin/ui/Table.java @@ -0,0 +1,6533 @@ +/* + * Copyright 2000-2016 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.vaadin.ui; + +import java.io.Serializable; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.StringTokenizer; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.jsoup.nodes.Element; +import org.jsoup.select.Elements; + +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.event.Action; +import com.vaadin.event.Action.Handler; +import com.vaadin.event.ContextClickEvent; +import com.vaadin.event.DataBoundTransferable; +import com.vaadin.event.ItemClickEvent; +import com.vaadin.event.ItemClickEvent.ItemClickListener; +import com.vaadin.event.ItemClickEvent.ItemClickNotifier; +import com.vaadin.event.MouseEvents.ClickEvent; +import com.vaadin.event.dd.DragAndDropEvent; +import com.vaadin.event.dd.DragSource; +import com.vaadin.event.dd.DropHandler; +import com.vaadin.event.dd.DropTarget; +import com.vaadin.event.dd.acceptcriteria.ServerSideCriterion; +import com.vaadin.server.KeyMapper; +import com.vaadin.server.LegacyCommunicationManager; +import com.vaadin.server.LegacyPaint; +import com.vaadin.server.PaintException; +import com.vaadin.server.PaintTarget; +import com.vaadin.server.Resource; +import com.vaadin.shared.MouseEventDetails; +import com.vaadin.shared.ui.MultiSelectMode; +import com.vaadin.shared.ui.table.CollapseMenuContent; +import com.vaadin.shared.ui.table.TableConstants; +import com.vaadin.shared.ui.table.TableConstants.Section; +import com.vaadin.shared.ui.table.TableServerRpc; +import com.vaadin.shared.ui.table.TableState; +import com.vaadin.shared.util.SharedUtil; +import com.vaadin.ui.declarative.DesignAttributeHandler; +import com.vaadin.ui.declarative.DesignContext; +import com.vaadin.ui.declarative.DesignException; +import com.vaadin.ui.declarative.DesignFormatter; +import com.vaadin.util.ReflectTools; +import com.vaadin.v7.data.util.converter.LegacyConverter; +import com.vaadin.v7.data.util.converter.LegacyConverterUtil; +import com.vaadin.v7.ui.LegacyField; + +/** + * <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. + * @since 3.0 + */ +@SuppressWarnings({ "deprecation" }) +public class Table extends AbstractSelect implements Action.Container, + Container.Ordered, Container.Sortable, ItemClickNotifier, DragSource, + DropTarget, HasComponents, HasChildMeasurementHint { + + 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 As of 7.0, use {@link Align#LEFT} instead + */ + @Deprecated + public static final Align ALIGN_LEFT = Align.LEFT; + + /** + * @deprecated As of 7.0, use {@link Align#CENTER} instead + */ + @Deprecated + public static final Align ALIGN_CENTER = Align.CENTER; + + /** + * @deprecated As of 7.0, use {@link Align#RIGHT} instead + */ + @Deprecated + public static final Align ALIGN_RIGHT = Align.RIGHT; + + public enum ColumnHeaderMode { + /** + * Column headers are hidden. + */ + HIDDEN, + /** + * Property ID:s are used as column headers. + */ + ID, + /** + * Column headers are explicitly specified with + * {@link #setColumnHeaders(String[])}. + */ + EXPLICIT, + /** + * Column headers are explicitly specified with + * {@link #setColumnHeaders(String[])}. If a header is not specified for + * a given property, its property id is used instead. + * <p> + * <b>This is the default behavior. </b> + */ + EXPLICIT_DEFAULTS_ID + } + + /** + * @deprecated As of 7.0, use {@link ColumnHeaderMode#HIDDEN} instead + */ + @Deprecated + public static final ColumnHeaderMode COLUMN_HEADER_MODE_HIDDEN = ColumnHeaderMode.HIDDEN; + + /** + * @deprecated As of 7.0, use {@link ColumnHeaderMode#ID} instead + */ + @Deprecated + public static final ColumnHeaderMode COLUMN_HEADER_MODE_ID = ColumnHeaderMode.ID; + + /** + * @deprecated As of 7.0, use {@link ColumnHeaderMode#EXPLICIT} instead + */ + @Deprecated + public static final ColumnHeaderMode COLUMN_HEADER_MODE_EXPLICIT = ColumnHeaderMode.EXPLICIT; + + /** + * @deprecated As of 7.0, use {@link ColumnHeaderMode#EXPLICIT_DEFAULTS_ID} + * instead + */ + @Deprecated + public static final ColumnHeaderMode COLUMN_HEADER_MODE_EXPLICIT_DEFAULTS_ID = ColumnHeaderMode.EXPLICIT_DEFAULTS_ID; + + public enum RowHeaderMode { + /** + * Row caption mode: The row headers are hidden. <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 As of 7.0, use {@link RowHeaderMode#HIDDEN} instead + */ + @Deprecated + public static final RowHeaderMode ROW_HEADER_MODE_HIDDEN = RowHeaderMode.HIDDEN; + + /** + * @deprecated As of 7.0, use {@link RowHeaderMode#ID} instead + */ + @Deprecated + public static final RowHeaderMode ROW_HEADER_MODE_ID = RowHeaderMode.ID; + + /** + * @deprecated As of 7.0, use {@link RowHeaderMode#ITEM} instead + */ + @Deprecated + public static final RowHeaderMode ROW_HEADER_MODE_ITEM = RowHeaderMode.ITEM; + + /** + * @deprecated As of 7.0, use {@link RowHeaderMode#INDEX} instead + */ + @Deprecated + public static final RowHeaderMode ROW_HEADER_MODE_INDEX = RowHeaderMode.INDEX; + + /** + * @deprecated As of 7.0, use {@link RowHeaderMode#EXPLICIT_DEFAULTS_ID} + * instead + */ + @Deprecated + public static final RowHeaderMode ROW_HEADER_MODE_EXPLICIT_DEFAULTS_ID = RowHeaderMode.EXPLICIT_DEFAULTS_ID; + + /** + * @deprecated As of 7.0, use {@link RowHeaderMode#EXPLICIT} instead + */ + @Deprecated + public static final RowHeaderMode ROW_HEADER_MODE_EXPLICIT = RowHeaderMode.EXPLICIT; + + /** + * @deprecated As of 7.0, use {@link RowHeaderMode#ICON_ONLY} instead + */ + @Deprecated + public static final RowHeaderMode ROW_HEADER_MODE_ICON_ONLY = RowHeaderMode.ICON_ONLY; + + /** + * @deprecated As of 7.0, use {@link RowHeaderMode#PROPERTY} instead + */ + @Deprecated + public static final RowHeaderMode ROW_HEADER_MODE_PROPERTY = RowHeaderMode.PROPERTY; + + /** + * The default rate that table caches rows for smooth scrolling. + */ + private static final double CACHE_RATE_DEFAULT = 2; + + private static final String ROW_HEADER_COLUMN_KEY = "0"; + private static final Object ROW_HEADER_FAKE_PROPERTY_ID = new UniqueSerializable() { + }; + + /** + * How layout manager should behave when measuring Table's child components + */ + private ChildMeasurementHint childMeasurementHint = ChildMeasurementHint.MEASURE_ALWAYS; + + /* 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 for visible columns (by propertyId). + */ + private final HashMap<Object, Integer> columnWidths = new HashMap<Object, Integer>(); + + /** + * Holds column expand rations for visible columns (by propertyId). + */ + private final HashMap<Object, Float> columnExpandRatios = new HashMap<Object, Float>(); + + /** + * 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; + + /* + * If all rows get removed then scroll position of the previous container + * can be restored after re-adding/replacing rows via addAll(). This + * resolves #14581. + */ + private int repairOnReAddAllRowsDataScrollPositionItemIndex = -1; + + /** + * Index of the first item on the current page. + */ + private int currentPageFirstItemIndex = 0; + + /** + * Index of the "first" item on the last page if a user has used + * setCurrentPageFirstItemIndex to scroll down. -1 if not set. + */ + private int currentPageFirstItemIndexOnLastPage = -1; + + /** + * Holds value of property selectable. + */ + private Boolean selectable; + + /** + * 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<LegacyField<?>, Property<?>> associatedProperties = new HashMap<LegacyField<?>, Property<?>>(); + + private boolean painted = false; + + private HashMap<Object, LegacyConverter<String, Object>> propertyValueConverters = new HashMap<Object, LegacyConverter<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; + + private List<Throwable> exceptionsDuringCachePopulation = new ArrayList<Throwable>(); + + private boolean isBeingPainted; + + /* Table constructors */ + + /** + * Creates a new empty table. + */ + public Table() { + setRowHeaderMode(ROW_HEADER_MODE_HIDDEN); + + registerRpc(new TableServerRpc() { + + @Override + public void contextClick(String rowKey, String colKey, + Section section, MouseEventDetails details) { + Object itemId = itemIdMapper.get(rowKey); + Object propertyId = columnIdMap.get(colKey); + fireEvent(new TableContextClickEvent(Table.this, details, + itemId, propertyId, section)); + } + }); + } + + /** + * 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"); + } + + final LinkedList<Object> newVC = new LinkedList<Object>(); + + // Checks that the new visible columns contains no nulls, properties + // exist and that there are no duplicates before adding them to newVC. + 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]); + } else if (newVC.contains(visibleColumns[i])) { + throw new IllegalArgumentException( + "Ids must be unique, duplicate id: " + + visibleColumns[i]); + } else { + newVC.add(visibleColumns[i]); + } + } + + this.visibleColumns = newVC; + + // Assures visual refresh + refreshRowCache(); + } + + /** + * Gets the headers of the columns. + * + * <p> + * The headers match the property id:s given by 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 by 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]); + } + + markAsDirty(); + } + + /** + * Gets the icons of the columns. + * + * <p> + * The icons in headers match the property id:s given by 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 by 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]); + } + + markAsDirty(); + } + + /** + * 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 necessarily 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 + * columns property id + * @param width + * width to be reserved for columns content + * @since 4.0.3 + */ + public void setColumnWidth(Object propertyId, int width) { + if (propertyId == null) { + // Since propertyId is null, this is the row header. Use the magic + // id to store the width of the row header. + propertyId = ROW_HEADER_FAKE_PROPERTY_ID; + } + + // Setting column width should remove any expand ratios as well + columnExpandRatios.remove(propertyId); + + if (width < 0) { + columnWidths.remove(propertyId); + } else { + columnWidths.put(propertyId, width); + } + markAsDirty(); + } + + /** + * Sets the column expand ratio for given column. + * <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 (propertyId == null) { + // Since propertyId is null, this is the row header. Use the magic + // id to store the width of the row header. + propertyId = ROW_HEADER_FAKE_PROPERTY_ID; + } + + // Setting the column expand ratio should remove and defined column + // width + columnWidths.remove(propertyId); + + if (expandRatio < 0) { + columnExpandRatios.remove(propertyId); + } else { + columnExpandRatios.put(propertyId, expandRatio); + } + + requestRepaint(); + } + + /** + * Gets the column expand ratio for a column. See + * {@link #setColumnExpandRatio(Object, float)} + * + * @param propertyId + * columns property id + * @return the expandRatio used to divide excess space for this column + */ + public float getColumnExpandRatio(Object propertyId) { + final Float width = columnExpandRatios.get(propertyId); + if (width == null) { + return -1; + } + return width.floatValue(); + } + + /** + * Gets the pixel width of column + * + * @param propertyId + * @return width of column or -1 when value not set + */ + public int getColumnWidth(Object propertyId) { + if (propertyId == null) { + // Since propertyId is null, this is the row header. Use the magic + // id to retrieve the width of the row header. + propertyId = ROW_HEADER_FAKE_PROPERTY_ID; + } + final Integer width = columnWidths.get(propertyId); + if (width == null) { + return -1; + } + return width.intValue(); + } + + /** + * Gets the page length. + * + * <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 height set ({@link #setHeight(float, Unit)} ) 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; + markAsDirty(); + } + } + + /** + * @see #setCacheRate(double) + * + * @return the current cache rate value + */ + public double getCacheRate() { + return cacheRate; + } + + /** + * Getter for property currentPageFirstItem. + * + * @return the Value of property currentPageFirstItem. + */ + public Object getCurrentPageFirstItemId() { + + // Prioritise index over id if indexes are supported + if (items instanceof Container.Indexed) { + final int index = getCurrentPageFirstItemIndex(); + Object id = null; + if (index >= 0 && index < size()) { + id = getIdByIndex(index); + } + if (id != null && !id.equals(currentPageFirstItemId)) { + currentPageFirstItemId = id; + } + } + + // If there is no item id at all, use the first one + if (currentPageFirstItemId == null) { + currentPageFirstItemId = firstItemId(); + } + + return currentPageFirstItemId; + } + + /** + * Returns the item ID for the item represented by the index given. Assumes + * that the current container implements {@link Container.Indexed}. + * + * See {@link Container.Indexed#getIdByIndex(int)} for more information + * about the exceptions that can be thrown. + * + * @param index + * the index for which the item ID should be fetched + * @return the item ID for the given index + * + * @throws ClassCastException + * if container does not implement {@link Container.Indexed} + * @throws IndexOutOfBoundsException + * thrown by {@link Container.Indexed#getIdByIndex(int)} if the + * index is invalid + */ + protected Object getIdByIndex(int index) { + return ((Container.Indexed) items).getIdByIndex(index); + } + + /** + * Setter for property currentPageFirstItemId. + * + * @param currentPageFirstItemId + * the New value of property currentPageFirstItemId. + */ + public void setCurrentPageFirstItemId(Object currentPageFirstItemId) { + + // Gets the corresponding index + int index = -1; + if (items instanceof Container.Indexed) { + index = indexOfId(currentPageFirstItemId); + } else { + // If the table item container does not have index, we have to + // calculates the index by hand + Object id = firstItemId(); + while (id != null && !id.equals(currentPageFirstItemId)) { + index++; + id = nextItemId(id); + } + if (id == null) { + index = -1; + } + } + + // If the search for item index was successful + if (index >= 0) { + /* + * The table is not capable of displaying an item in the container + * as the first if there are not enough items following the selected + * item so the whole table (pagelength) is filled. + */ + int maxIndex = size() - pageLength; + if (maxIndex < 0) { + maxIndex = 0; + } + + if (index > maxIndex) { + // Note that we pass index, not maxIndex, letting + // setCurrentPageFirstItemIndex handle the situation. + setCurrentPageFirstItemIndex(index); + return; + } + + this.currentPageFirstItemId = currentPageFirstItemId; + currentPageFirstItemIndex = index; + } + + // Assures the visual refresh + refreshRowCache(); + + } + + protected int indexOfId(Object itemId) { + return ((Container.Indexed) items).indexOfId(itemId); + } + + /** + * Gets the icon Resource for the specified column. + * + * @param propertyId + * the propertyId identifying 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); + } + + markAsDirty(); + } + + /** + * Gets the header for the specified column. + * + * @param propertyId + * the propertyId identifying the column. + * @return the header for the specified column if it has one. + */ + public String getColumnHeader(Object propertyId) { + if (getColumnHeaderMode() == ColumnHeaderMode.HIDDEN) { + return null; + } + + String header = columnHeaders.get(propertyId); + if ((header == null + && getColumnHeaderMode() == ColumnHeaderMode.EXPLICIT_DEFAULTS_ID) + || getColumnHeaderMode() == ColumnHeaderMode.ID) { + header = propertyId.toString(); + } + + return header; + } + + /** + * Sets the column header for the specified column; + * + * @param propertyId + * the propertyId identifying the column. + * @param header + * the header to set. + */ + public void setColumnHeader(Object propertyId, String header) { + + if (header == null) { + columnHeaders.remove(propertyId); + } else { + columnHeaders.put(propertyId, header); + } + + markAsDirty(); + } + + /** + * Gets the specified column's alignment. + * + * @param propertyId + * the propertyID identifying the column. + * @return the specified column's alignment if it as one; {@link Align#LEFT} + * otherwise. + */ + public Align getColumnAlignment(Object propertyId) { + final Align a = columnAlignments.get(propertyId); + return a == null ? Align.LEFT : a; + } + + /** + * Sets the specified column's alignment. + * + * <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 + * @throws IllegalArgumentException + * if the property id does not exist + */ + 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 (!getContainerPropertyIds().contains(propertyId) + && !columnGenerators.containsKey(propertyId)) { + throw new IllegalArgumentException("Property '" + propertyId + + "' was not found in the container"); + } + + if (collapsed) { + if (collapsedColumns.add(propertyId)) { + fireColumnCollapseEvent(propertyId); + } + } else { + if (collapsedColumns.remove(propertyId)) { + fireColumnCollapseEvent(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; + markAsDirty(); + } + } + + /* + * Arranges visible columns according to given columnOrder. Silently ignores + * colimnId:s that are not visible columns, and keeps the internal order of + * visible columns left out of the ordering (trailing). Silently does + * nothing if columnReordering is not allowed. + */ + private void setColumnOrder(Object[] columnOrder) { + if (columnOrder == null || !isColumnReorderingAllowed()) { + return; + } + final LinkedList<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; + } + + /* + * If the new index is on the last page we set the index to be the first + * item on that last page and make a note of the real index for the + * client side to be able to move the scroll position to the correct + * position. + */ + int indexOnLastPage = -1; + if (newIndex > maxIndex) { + indexOnLastPage = newIndex; + newIndex = maxIndex; + } + + // Refresh first item id + if (items instanceof Container.Indexed) { + try { + currentPageFirstItemId = getIdByIndex(newIndex); + } catch (final IndexOutOfBoundsException e) { + currentPageFirstItemId = null; + } + currentPageFirstItemIndex = newIndex; + + if (needsPageBufferReset) { + /* + * The flag currentPageFirstItemIndexOnLastPage denotes a user + * set scrolling position on the last page via + * setCurrentPageFirstItemIndex() and shouldn't be changed by + * the table component internally changing the firstvisible item + * on lazy row fetching. Doing so would make the scrolling + * position not be updated correctly when the lazy rows are + * finally rendered. + */ + + boolean isLastRowPossiblyPartiallyVisible = true; + if (indexOnLastPage != -1) { + /* + * If the requested row was greater than maxIndex, the last + * row should be fully visible (See + * TestCurrentPageFirstItem). + */ + isLastRowPossiblyPartiallyVisible = false; + } + + int extraRows = isLastRowPossiblyPartiallyVisible ? 0 : 1; + currentPageFirstItemIndexOnLastPage = currentPageFirstItemIndex + + extraRows; + } else { + currentPageFirstItemIndexOnLastPage = -1; + } + + } 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); + } + + /** + * Returns whether table is selectable. + * + * <p> + * The table is not selectable until it's explicitly set as selectable or at + * least one {@link ValueChangeListener} is added. + * </p> + * + * @return whether table is selectable. + */ + public boolean isSelectable() { + if (selectable == null) { + return hasListeners(ValueChangeEvent.class); + } + return selectable; + } + + /** + * Setter for property selectable. + * + * <p> + * The table is not selectable until it's explicitly set as selectable via + * this method or alternatively at least one {@link ValueChangeListener} is + * added. + * </p> + * + * @param selectable + * the New value of property selectable. + */ + public void setSelectable(boolean selectable) { + if (!SharedUtil.equals(this.selectable, selectable)) { + this.selectable = selectable; + markAsDirty(); + } + } + + /** + * Getter for property columnHeaderMode. + * + * @return the Value of property columnHeaderMode. + */ + public ColumnHeaderMode getColumnHeaderMode() { + return columnHeaderMode; + } + + /** + * Setter for property columnHeaderMode. + * + * @param columnHeaderMode + * the New value of property columnHeaderMode. + */ + public void setColumnHeaderMode(ColumnHeaderMode columnHeaderMode) { + if (columnHeaderMode == null) { + throw new IllegalArgumentException( + "Column header mode can not be null"); + } + if (columnHeaderMode != this.columnHeaderMode) { + this.columnHeaderMode = columnHeaderMode; + markAsDirty(); + } + + } + + /** + * Refreshes the rows in the internal cache. Only if + * {@link #resetPageBuffer()} is called before this then all values are + * guaranteed to be recreated. + */ + protected void refreshRenderedCells() { + if (!isAttached()) { + return; + } + + if (!isContentRefreshesEnabled) { + return; + } + + // Collects the basic facts about the table page + final int pagelen = getPageLength(); + int rows, totalRows; + rows = totalRows = size(); + int firstIndex = Math.min(getCurrentPageFirstItemIndex(), + totalRows - 1); + if (rows > 0 && firstIndex >= 0) { + rows -= firstIndex; + } + if (pagelen > 0 && pagelen < rows) { + rows = pagelen; + } + + // If "to be painted next" variables are set, use them + if (lastToBeRenderedInClient - firstToBeRenderedInClient > 0) { + rows = lastToBeRenderedInClient - firstToBeRenderedInClient + 1; + } + if (firstToBeRenderedInClient >= 0) { + if (firstToBeRenderedInClient < totalRows) { + firstIndex = firstToBeRenderedInClient; + } else { + firstIndex = totalRows - 1; + } + } else { + // initial load + + // #8805 send one extra row in the beginning in case a partial + // row is shown on the UI + if (firstIndex > 0) { + firstIndex = firstIndex - 1; + rows = rows + 1; + } + firstToBeRenderedInClient = firstIndex; + } + if (totalRows > 0) { + if (rows + firstIndex > totalRows) { + rows = totalRows - firstIndex; + } + } else { + rows = 0; + } + + // Saves the results to internal buffer + pageBuffer = getVisibleCellsNoCache(firstIndex, rows, true); + + if (rows > 0) { + pageBufferFirstIndex = firstIndex; + } + if (getPageLength() != 0) { + removeUnnecessaryRows(); + } + + setRowCacheInvalidated(true); + markAsDirty(); + maybeThrowCacheUpdateExceptions(); + + } + + private void maybeThrowCacheUpdateExceptions() { + if (!exceptionsDuringCachePopulation.isEmpty()) { + Throwable[] causes = new Throwable[exceptionsDuringCachePopulation + .size()]; + exceptionsDuringCachePopulation.toArray(causes); + + exceptionsDuringCachePopulation.clear(); + throw new CacheUpdateException(this, + "Error during Table cache update.", causes); + } + + } + + /** + * Exception thrown when one or more exceptions occurred during updating of + * the Table cache. + * <p> + * Contains all exceptions which occurred during the cache update. The first + * occurred exception is set as the cause of this exception. All occurred + * exceptions can be accessed using {@link #getCauses()}. + * </p> + * + */ + public static class CacheUpdateException extends RuntimeException { + private Throwable[] causes; + private Table table; + + public CacheUpdateException(Table table, String message, + Throwable[] causes) { + super(maybeSupplementMessage(message, causes.length), causes[0]); + this.table = table; + this.causes = causes; + } + + private static String maybeSupplementMessage(String message, + int causeCount) { + if (causeCount > 1) { + return message + " Additional causes not shown."; + } else { + return message; + } + } + + /** + * Returns the cause(s) for this exception + * + * @return the exception(s) which caused this exception + */ + public Throwable[] getCauses() { + return causes; + } + + public Table getTable() { + return table; + } + + } + + /** + * Removes rows that fall outside the required cache. + */ + private void removeUnnecessaryRows() { + int minPageBufferIndex = getMinPageBufferIndex(); + int maxPageBufferIndex = getMaxPageBufferIndex(); + + int maxBufferSize = maxPageBufferIndex - minPageBufferIndex + 1; + + /* + * Number of rows that were previously cached. This is not necessarily + * the same as pageLength if we do not have enough rows in the + * container. + */ + int currentlyCachedRowCount = pageBuffer[CELL_ITEMID].length; + + if (currentlyCachedRowCount <= maxBufferSize) { + // removal unnecessary + return; + } + + /* Figure out which rows to get rid of. */ + int firstCacheRowToRemoveInPageBuffer = -1; + if (minPageBufferIndex > pageBufferFirstIndex) { + firstCacheRowToRemoveInPageBuffer = pageBufferFirstIndex; + } else if (maxPageBufferIndex < pageBufferFirstIndex + + currentlyCachedRowCount) { + firstCacheRowToRemoveInPageBuffer = maxPageBufferIndex + 1; + } + + if (firstCacheRowToRemoveInPageBuffer + - pageBufferFirstIndex < currentlyCachedRowCount) { + /* + * Unregister all components that fall beyond the cache limits after + * inserting the new rows. + */ + unregisterComponentsAndPropertiesInRows( + firstCacheRowToRemoveInPageBuffer, currentlyCachedRowCount + - firstCacheRowToRemoveInPageBuffer); + } + } + + /** + * Requests that the Table should be repainted as soon as possible. + * + * Note that a {@code Table} does not necessarily repaint its contents when + * this method has been called. See {@link #refreshRowCache()} for forcing + * an update of the contents. + * + * @deprecated As of 7.0, use {@link #markAsDirty()} instead + */ + + @Deprecated + @Override + public void requestRepaint() { + markAsDirty(); + } + + /** + * Requests that the Table should be repainted as soon as possible. + * + * Note that a {@code Table} does not necessarily repaint its contents when + * this method has been called. See {@link #refreshRowCache()} for forcing + * an update of the contents. + */ + + @Override + public void markAsDirty() { + // Overridden only for javadoc + super.markAsDirty(); + } + + @Override + public void markAsDirtyRecursive() { + super.markAsDirtyRecursive(); + + // Avoid sending a partial repaint (#8714) + refreshRowCache(); + } + + private void removeRowsFromCacheAndFillBottom(int firstIndex, int rows) { + int totalCachedRows = pageBuffer[CELL_ITEMID].length; + int totalRows = size(); + int firstIndexInPageBuffer = firstIndex - pageBufferFirstIndex; + + /* + * firstIndexInPageBuffer is the first row to be removed. "rows" rows + * after that should be removed. If the page buffer does not contain + * that many rows, we only remove the rows that actually are in the page + * buffer. + */ + if (firstIndexInPageBuffer + rows > totalCachedRows) { + rows = totalCachedRows - firstIndexInPageBuffer; + } + + /* + * Unregister components that will no longer be in the page buffer to + * make sure that no components leak. + */ + unregisterComponentsAndPropertiesInRows(firstIndex, rows); + + /* + * The number of rows that should be in the cache after this operation + * is done (pageBuffer currently contains the expanded items). + */ + int newCachedRowCount = totalCachedRows; + if (newCachedRowCount + pageBufferFirstIndex > totalRows) { + newCachedRowCount = totalRows - pageBufferFirstIndex; + } + + /* + * The index at which we should render the first row that does not come + * from the previous page buffer. + */ + int firstAppendedRowInPageBuffer = totalCachedRows - rows; + int firstAppendedRow = firstAppendedRowInPageBuffer + + pageBufferFirstIndex; + + /* + * Calculate the maximum number of new rows that we can add to the page + * buffer. Less than the rows we removed if the container does not + * contain that many items afterwards. + */ + int maxRowsToRender = (totalRows - firstAppendedRow); + int rowsToAdd = rows; + if (rowsToAdd > maxRowsToRender) { + rowsToAdd = maxRowsToRender; + } + + Object[][] cells = null; + if (rowsToAdd > 0) { + cells = getVisibleCellsNoCache(firstAppendedRow, rowsToAdd, false); + } + /* + * Create the new cache buffer by copying the first rows from the old + * buffer, moving the following rows upwards and appending more rows if + * applicable. + */ + Object[][] newPageBuffer = new Object[pageBuffer.length][newCachedRowCount]; + + for (int i = 0; i < pageBuffer.length; i++) { + for (int row = 0; row < firstIndexInPageBuffer; row++) { + // Copy the first rows + newPageBuffer[i][row] = pageBuffer[i][row]; + } + for (int row = firstIndexInPageBuffer; row < firstAppendedRowInPageBuffer; row++) { + // Move the rows that were after the expanded rows + newPageBuffer[i][row] = pageBuffer[i][row + rows]; + } + for (int row = firstAppendedRowInPageBuffer; row < newCachedRowCount; row++) { + // Add the newly rendered rows. Only used if rowsToAdd > 0 + // (cells != null) + newPageBuffer[i][row] = cells[i][row + - firstAppendedRowInPageBuffer]; + } + } + pageBuffer = newPageBuffer; + } + + private Object[][] getVisibleCellsUpdateCacheRows(int firstIndex, + int rows) { + Object[][] cells = getVisibleCellsNoCache(firstIndex, rows, false); + int cacheIx = firstIndex - pageBufferFirstIndex; + // update the new rows in the cache. + int totalCachedRows = pageBuffer[CELL_ITEMID].length; + int end = Math.min(cacheIx + rows, totalCachedRows); + for (int ix = cacheIx; ix < end; ix++) { + for (int i = 0; i < pageBuffer.length; i++) { + pageBuffer[i][ix] = cells[i][ix - cacheIx]; + } + } + return cells; + } + + /** + * @param firstIndex + * The position where new rows should be inserted + * @param rows + * The maximum number of rows that should be inserted at position + * firstIndex. Less rows will be inserted if the page buffer is + * too small. + * @return + */ + private Object[][] getVisibleCellsInsertIntoCache(int firstIndex, + int rows) { + getLogger().log(Level.FINEST, + "Insert {0} rows at index {1} to existing page buffer requested", + new Object[] { rows, firstIndex }); + + int minPageBufferIndex = getMinPageBufferIndex(); + int maxPageBufferIndex = getMaxPageBufferIndex(); + + int maxBufferSize = maxPageBufferIndex - minPageBufferIndex + 1; + + if (getPageLength() == 0) { + // If pageLength == 0 then all rows should be rendered + maxBufferSize = pageBuffer[CELL_ITEMID].length + rows; + } + /* + * Number of rows that were previously cached. This is not necessarily + * the same as maxBufferSize. + */ + int currentlyCachedRowCount = pageBuffer[CELL_ITEMID].length; + + /* If rows > size available in page buffer */ + if (firstIndex + rows - 1 > maxPageBufferIndex) { + rows = maxPageBufferIndex - firstIndex + 1; + } + + /* + * "rows" rows will be inserted at firstIndex. Find out how many old + * rows fall outside the new buffer so we can unregister components in + * the cache. + */ + + /* + * if there are rows before the new pageBuffer limits they must be + * removed + */ + int lastCacheRowToRemove = minPageBufferIndex - 1; + int rowsFromBeginning = lastCacheRowToRemove - pageBufferFirstIndex + 1; + if (lastCacheRowToRemove >= pageBufferFirstIndex) { + unregisterComponentsAndPropertiesInRows(pageBufferFirstIndex, + rowsFromBeginning); + } else { + rowsFromBeginning = 0; + } + + /* + * the rows that fall outside of the new pageBuffer limits after the new + * rows are inserted must also be removed + */ + int firstCacheRowToRemove = firstIndex; + /* + * IF there is space remaining in the buffer after the rows have been + * inserted, we can keep more rows. + */ + int numberOfOldRowsAfterInsertedRows = Math.min( + pageBufferFirstIndex + currentlyCachedRowCount + rows, + maxPageBufferIndex + 1) - (firstIndex + rows - 1); + if (numberOfOldRowsAfterInsertedRows > 0) { + firstCacheRowToRemove += numberOfOldRowsAfterInsertedRows; + } + int rowsFromAfter = currentlyCachedRowCount + - (firstCacheRowToRemove - pageBufferFirstIndex); + + if (rowsFromAfter > 0) { + /* + * Unregister all components that fall beyond the cache limits after + * inserting the new rows. + */ + unregisterComponentsAndPropertiesInRows(firstCacheRowToRemove, + rowsFromAfter); + } + + // Calculate the new cache size + int newCachedRowCount = maxBufferSize; + if (pageBufferFirstIndex + currentlyCachedRowCount + rows + - 1 < maxPageBufferIndex) { + // there aren't enough rows to fill the whole potential -> use what + // there is + newCachedRowCount -= maxPageBufferIndex - (pageBufferFirstIndex + + currentlyCachedRowCount + rows - 1); + } else if (minPageBufferIndex < pageBufferFirstIndex) { + newCachedRowCount -= pageBufferFirstIndex - minPageBufferIndex; + } + /* + * calculate the internal location of the new rows within the new cache + */ + int firstIndexInNewPageBuffer = firstIndex - pageBufferFirstIndex + - rowsFromBeginning; + + /* Paint the new rows into a separate buffer */ + Object[][] cells = getVisibleCellsNoCache(firstIndex, rows, false); + + /* + * Create the new cache buffer and fill it with the data from the old + * buffer as well as the inserted rows. + */ + Object[][] newPageBuffer = new Object[pageBuffer.length][newCachedRowCount]; + + for (int i = 0; i < pageBuffer.length; i++) { + for (int row = 0; row < firstIndexInNewPageBuffer; row++) { + // Copy the first rows + newPageBuffer[i][row] = pageBuffer[i][rowsFromBeginning + row]; + } + for (int row = firstIndexInNewPageBuffer; row < firstIndexInNewPageBuffer + + rows; row++) { + // Copy the newly created rows + newPageBuffer[i][row] = cells[i][row + - firstIndexInNewPageBuffer]; + } + for (int row = firstIndexInNewPageBuffer + + rows; row < newCachedRowCount; row++) { + // Move the old rows down below the newly inserted rows + newPageBuffer[i][row] = pageBuffer[i][rowsFromBeginning + row + - rows]; + } + } + pageBuffer = newPageBuffer; + pageBufferFirstIndex = Math.max( + pageBufferFirstIndex + rowsFromBeginning, minPageBufferIndex); + if (getLogger().isLoggable(Level.FINEST)) { + getLogger().log(Level.FINEST, + "Page Buffer now contains {0} rows ({1}-{2})", + new Object[] { pageBuffer[CELL_ITEMID].length, + pageBufferFirstIndex, (pageBufferFirstIndex + + pageBuffer[CELL_ITEMID].length - 1) }); + } + return cells; + } + + private int getMaxPageBufferIndex() { + int total = size(); + if (getPageLength() == 0) { + // everything is shown at once, no caching + return total - 1; + } + // Page buffer must not become larger than pageLength*cacheRate after + // the current page + int maxPageBufferIndex = getCurrentPageFirstItemIndex() + + (int) (getPageLength() * (1 + getCacheRate())); + if (shouldHideNullSelectionItem()) { + --total; + } + if (maxPageBufferIndex >= total) { + maxPageBufferIndex = total - 1; + } + return maxPageBufferIndex; + } + + private int getMinPageBufferIndex() { + if (getPageLength() == 0) { + // everything is shown at once, no caching + return 0; + } + // Page buffer must not become larger than pageLength*cacheRate before + // the current page + int minPageBufferIndex = getCurrentPageFirstItemIndex() + - (int) (getPageLength() * getCacheRate()); + if (minPageBufferIndex < 0) { + minPageBufferIndex = 0; + } + return minPageBufferIndex; + } + + /** + * Render rows with index "firstIndex" to "firstIndex+rows-1" to a new + * buffer. + * + * Reuses values from the current page buffer if the rows are found there. + * + * @param firstIndex + * @param rows + * @param replaceListeners + * @return + */ + private Object[][] getVisibleCellsNoCache(int firstIndex, int rows, + boolean replaceListeners) { + if (getLogger().isLoggable(Level.FINEST)) { + getLogger().log(Level.FINEST, + "Render visible cells for rows {0}-{1}", + new Object[] { firstIndex, (firstIndex + rows - 1) }); + } + final Object[] colids = getVisibleColumns(); + final int cols = colids.length; + + HashSet<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; + } + + 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; + if (items instanceof Container.Indexed) { + // more efficient implementation for containers supporting access by + // index + + List<?> itemIds = getItemIds(firstIndex, rows); + for (int i = 0; i < rows && i < itemIds.size(); i++) { + Object id = itemIds.get(i); + if (id == null) { + throw new IllegalStateException( + "Null itemId returned from container"); + } + // Start by parsing the values, id should already be set + parseItemIdToCells(cells, id, i, firstIndex, headmode, cols, + colids, firstIndexNotInCache, iscomponent, + oldListenedProperties); + + filledRows++; + } + } else { + // slow back-up implementation for cases where the container does + // not support access by index + + // Gets the first item id + Object id = firstItemId(); + for (int i = 0; i < firstIndex; i++) { + id = nextItemId(id); + } + for (int i = 0; i < rows && id != null; i++) { + // Start by parsing the values, id should already be set + parseItemIdToCells(cells, id, i, firstIndex, headmode, cols, + colids, firstIndexNotInCache, iscomponent, + oldListenedProperties); + + // Gets the next item id for non indexed container + 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 List<Object> getItemIds(int firstIndex, int rows) { + return (List<Object>) ((Container.Indexed) items).getItemIds(firstIndex, + rows); + } + + /** + * Update a cache array for a row, register any relevant listeners etc. + * + * This is an internal method extracted from + * {@link #getVisibleCellsNoCache(int, int, boolean)} and should be removed + * when the Table is rewritten. + */ + private void parseItemIdToCells(Object[][] cells, Object id, int i, + int firstIndex, RowHeaderMode headmode, int cols, Object[] colids, + int firstIndexNotInCache, boolean[] iscomponent, + HashSet<Property<?>> oldListenedProperties) { + + 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: + try { + cells[CELL_HEADER][i] = getItemCaption(id); + } catch (Exception e) { + exceptionsDuringCachePopulation.add(e); + cells[CELL_HEADER][i] = ""; + } + } + try { + cells[CELL_ICON][i] = getItemIcon(id); + } catch (Exception e) { + exceptionsDuringCachePopulation.add(e); + cells[CELL_ICON][i] = null; + } + } + + 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) { + try { + p = getContainerProperty(id, colids[j]); + } catch (Exception e) { + exceptionsDuringCachePopulation.add(e); + value = null; + } + } + + 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 if 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]); + try { + value = cg.generateCell(this, id, colids[j]); + } catch (Exception e) { + exceptionsDuringCachePopulation.add(e); + value = null; + } + 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]) { + try { + value = p.getValue(); + } catch (Exception e) { + exceptionsDuringCachePopulation.add(e); + value = null; + } + listenProperty(p, oldListenedProperties); + } else if (p != null) { + try { + value = getPropertyValue(id, colids[j], p); + } catch (Exception e) { + exceptionsDuringCachePopulation.add(e); + value = null; + } + /* + * If returned value is Component (via fieldfactory + * or overridden getPropertyValue) we expect 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 { + try { + value = getPropertyValue(id, colids[j], null); + } catch (Exception e) { + exceptionsDuringCachePopulation.add(e); + value = null; + } + } + } + } + } + + if (value instanceof Component) { + registerComponent((Component) value); + } + cells[CELL_FIRSTCOL + j][i] = value; + } + } + + protected void registerComponent(Component component) { + getLogger().log(Level.FINEST, "Registered {0}: {1}", new Object[] { + component.getClass().getSimpleName(), component.getCaption() }); + if (!equals(component.getParent())) { + 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) { + if (getLogger().isLoggable(Level.FINEST)) { + getLogger().log(Level.FINEST, + "Unregistering components in rows {0}-{1}", + new Object[] { 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 component + * component that should be unregistered. + */ + protected void unregisterComponent(Component component) { + getLogger().log(Level.FINEST, "Unregistered {0}: {1}", new Object[] { + component.getClass().getSimpleName(), component.getCaption() }); + component.setParent(null); + /* + * Also remove property data sources to unregister listeners keeping the + * fields in memory. + */ + if (component instanceof LegacyField) { + LegacyField<?> field = (LegacyField<?>) 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); + } + } + } + + /** + * 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 + */ + 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 cannot 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(); + } + + /** + * Sets the Container that serves as the data source of the viewer. As a + * side-effect the table's selection value is set to null as the old + * selection might not exist in new Container.<br> + * <br> + * All rows and columns are generated as visible using this method. If the + * new container contains properties that are not meant to be shown you + * should use {@link Table#setContainerDataSource(Container, Collection)} + * instead, especially if the table is editable. + * <p> + * Keeps propertyValueConverters if the corresponding id exists in the new + * data source and is of a compatible type. + * </p> + * + * @param newDataSource + * the new data source. + */ + @Override + public void setContainerDataSource(Container newDataSource) { + if (newDataSource == null) { + newDataSource = new IndexedContainer(); + } + + Collection<Object> generated; + if (columnGenerators != null) { + generated = columnGenerators.keySet(); + } else { + generated = Collections.emptyList(); + } + List<Object> visibleIds = new ArrayList<Object>(); + if (generated.isEmpty()) { + visibleIds.addAll(newDataSource.getContainerPropertyIds()); + } else { + for (Object id : newDataSource.getContainerPropertyIds()) { + // don't add duplicates + if (!generated.contains(id)) { + visibleIds.add(id); + } + } + // generated columns to the end + visibleIds.addAll(generated); + } + setContainerDataSource(newDataSource, visibleIds); + } + + /** + * Sets the container data source and the columns that will be visible. + * Columns are shown in the collection's iteration order. + * <p> + * Keeps propertyValueConverters if the corresponding id exists in the new + * data source and is of a compatible type. + * </p> + * + * @see Table#setContainerDataSource(Container) + * @see Table#setVisibleColumns(Object[]) + * @see Table#setConverter(Object, Converter<String, ?>) + * + * @param newDataSource + * the new data source. + * @param visibleIds + * IDs of the visible columns + */ + public void setContainerDataSource(Container newDataSource, + Collection<?> visibleIds) { + + disableContentRefreshing(); + + if (newDataSource == null) { + newDataSource = new IndexedContainer(); + } + if (visibleIds == null) { + visibleIds = new ArrayList<Object>(); + } + + // Retain propertyValueConverters if their corresponding ids are + // properties of the new + // data source and are of a compatible type + if (propertyValueConverters != null) { + Collection<?> newPropertyIds = newDataSource + .getContainerPropertyIds(); + LinkedList<Object> retainableValueConverters = new LinkedList<Object>(); + for (Object propertyId : newPropertyIds) { + LegacyConverter<String, ?> converter = getConverter(propertyId); + if (converter != null) { + if (typeIsCompatible(converter.getModelType(), + newDataSource.getType(propertyId))) { + retainableValueConverters.add(propertyId); + } + } + } + propertyValueConverters.keySet() + .retainAll(retainableValueConverters); + } + + // 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(); + } + + // don't add the same id twice + Collection<Object> col = new LinkedList<Object>(); + for (Iterator<?> it = visibleIds.iterator(); it.hasNext();) { + Object id = it.next(); + if (!col.contains(id)) { + col.add(id); + } + } + + setVisibleColumns(col.toArray()); + + // Assure visual refresh + resetPageBuffer(); + + enableContentRefreshing(true); + } + + /** + * Checks if class b can be safely assigned to class a. + * + * @param a + * @param b + * @return + */ + private boolean typeIsCompatible(Class<?> a, Class<?> b) { + // TODO Implement this check properly + // Basically we need to do a a.isAssignableFrom(b) + // with special considerations for primitive types. + return true; + } + + /** + * Gets items ids from a range of key values + * + * @param itemId + * The start key + * @param length + * amount of items to be retrieved + * @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 + markAsDirty(); + } 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 + markAsDirty(); + 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(); + int size = size(); + // sanity check + + if (reqFirstRowToPaint >= size) { + reqFirstRowToPaint = size; + } + + if (reqFirstRowToPaint + reqRowsToPaint > size) { + reqRowsToPaint = size - reqFirstRowToPaint; + } + } + } + if (getLogger().isLoggable(Level.FINEST)) { + getLogger().log(Level.FINEST, "Client wants rows {0}-{1}", + new Object[] { 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"); + Set<Object> idSet = new HashSet<Object>(); + for (Object id : ids) { + idSet.add(columnIdMap.get(id.toString())); + } + for (final Iterator<Object> it = visibleColumns + .iterator(); it.hasNext();) { + Object propertyId = it.next(); + if (isColumnCollapsed(propertyId)) { + if (!idSet.contains(propertyId)) { + setColumnCollapsed(propertyId, false); + } + } else if (idSet.contains(propertyId)) { + setColumnCollapsed(propertyId, 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 fireColumnCollapseEvent(Object propertyId) { + fireEvent(new ColumnCollapseEvent(this, propertyId)); + } + + 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 + markAsDirty(); + } + } + + @Override + public void beforeClientResponse(boolean initial) { + super.beforeClientResponse(initial); + + // Ensure pageBuffer is filled before sending the response to avoid + // calls to markAsDirty during paint + getVisibleCells(); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.AbstractSelect#paintContent(com.vaadin. + * terminal.PaintTarget) + */ + + @Override + public void paintContent(PaintTarget target) throws PaintException { + isBeingPainted = true; + try { + doPaintContent(target); + } finally { + isBeingPainted = false; + } + } + + private void doPaintContent(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); + } else if (target.isFullRepaint() || isRowCacheInvalidated()) { + paintRows(target, cells, actionSet); + setRowCacheInvalidated(false); + } + + /* + * 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. + */ + int pageBufferLastIndex = pageBufferFirstIndex + + pageBuffer[CELL_ITEMID].length - 1; + target.addAttribute(TableConstants.ATTRIBUTE_PAGEBUFFER_FIRST, + pageBufferFirstIndex); + target.addAttribute(TableConstants.ATTRIBUTE_PAGEBUFFER_LAST, + pageBufferLastIndex); + + paintSorting(target); + + resetVariablesAndPageBuffer(target); + + // Actions + paintActions(target, actionSet); + + paintColumnOrder(target); + + // Available columns + paintAvailableColumns(target); + + paintVisibleColumns(target); + + if (keyMapperReset) { + keyMapperReset = false; + target.addAttribute(TableConstants.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"); + maybeThrowCacheUpdateExceptions(); + } + + 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().log(Level.FINEST, + "Paint rows for add. Index: {0}, count: {1}.", + new Object[] { firstIx, 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().log(Level.FINEST, + "Paint rows for remove. Index: {0}, count: {1}.", + new Object[] { firstIx, count }); + removeRowsFromCacheAndFillBottom(firstIx, count); + target.addAttribute("hide", true); + } + + target.addAttribute("firstprowix", firstIx); + target.addAttribute("numprows", count); + target.endTag("prows"); + maybeThrowCacheUpdateExceptions(); + } + + /** + * 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); + paintTableChildLayoutMeasureMode(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()); + target.addVariable(this, "firstvisibleonlastpage", + currentPageFirstItemIndexOnLastPage); + } + } + + /** + * 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); + paintColumnExpandRatio(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); + paintColumnExpandRatio(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)) { + target.addAttribute("width", getColumnWidth(columnId)); + } + } + + private void paintColumnExpandRatio(PaintTarget target, + final Object columnId) throws PaintException { + if (columnExpandRatios.containsKey(columnId)) { + target.addAttribute("er", getColumnExpandRatio(columnId)); + } + } + + private void paintTableChildLayoutMeasureMode(PaintTarget target) + throws PaintException { + target.addAttribute("measurehint", getChildMeasurementHint().ordinal()); + } + + /** + * Checks whether row headers are visible. + * + * @return {@code false} if row headers are hidden, {@code true} otherwise + * @since 7.3.9 + */ + protected boolean rowHeadersAreEnabled() { + return getRowHeaderMode() != RowHeaderMode.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(this, 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 || !LegacyCommunicationManager + .isComponentVisibleToClient(c)) { + target.addText(""); + } 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(this, 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 LegacyField<?> 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 LegacyField. 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, LegacyField 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 ""; + } + LegacyConverter<String, Object> converter = null; + + if (hasConverter(colId)) { + converter = getConverter(colId); + } else { + converter = (LegacyConverter) LegacyConverterUtil.getConverter( + String.class, property.getType(), getSession()); + } + Object value = property.getValue(); + if (converter != null) { + return converter.convertToPresentation(value, String.class, + 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 (equals(event.getProperty()) + || event.getProperty() == getPropertyDataSource()) { + super.valueChange(event); + } else { + refreshRowCache(); + containerChangeToBeRendered = true; + } + markAsDirty(); + } + + /** + * 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); + // If a propertyValueConverter was defined for the property, remove it. + propertyValueConverters.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 property. + * @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 property + * @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) { + if (isBeingPainted) { + return; + } + + super.containerItemSetChange(event); + + // super method clears the key map, must inform client about this to + // avoid getting invalid keys back (#8584) + keyMapperReset = true; + + int currentFirstItemIndex = getCurrentPageFirstItemIndex(); + + if (event.getContainer().size() == 0) { + repairOnReAddAllRowsDataScrollPositionItemIndex = getCurrentPageFirstItemIndex(); + } else { + if (repairOnReAddAllRowsDataScrollPositionItemIndex != -1) { + currentFirstItemIndex = repairOnReAddAllRowsDataScrollPositionItemIndex; + /* + * Reset repairOnReAddAllRowsDataScrollPositionItemIndex. + * + * Next string should be commented (removed) if we want to have + * possibility to restore scroll position during adding items to + * container one by one via add() but not only addAll(). The + * problem in this case: we cannot track what happened between + * add() and add()... So it is ambiguous where to stop restore + * scroll position. + */ + repairOnReAddAllRowsDataScrollPositionItemIndex = -1; + } + } + + // ensure that page still has first item in page, ignore buffer refresh + // (forced in this method) + setCurrentPageFirstItemIndex(currentFirstItemIndex, 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) { + if (isBeingPainted) { + return; + } + + disableContentRefreshing(); + super.containerPropertySetChange(event); + + // sanitize 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 LegacyField instances. + * @see #isEditable + */ + public TableFieldFactory getTableFieldFactory() { + return fieldFactory; + } + + /** + * Is table editable. + * + * If table is editable a editor of type LegacyField 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 implementing the + * FieldFactory interface, and assign it to table, and set the editable + * property to true. + * + * @return true if table is editable, false otherwise. + * @see LegacyField + * @see FieldFactory + * + */ + public boolean isEditable() { + return editable; + } + + /** + * Sets the editable property. + * + * If table is editable a editor of type LegacyField 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 implementing 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 LegacyField + * @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(); + boolean refreshingPreviouslyEnabled = disableContentRefreshing(); + ((Container.Sortable) c).sort(propertyId, ascending); + setCurrentPageFirstItemIndex(pageIndex); + if (refreshingPreviouslyEnabled) { + enableContentRefreshing(true); + } + if (propertyId.length > 0 && ascending.length > 0) { + // The first propertyId is the primary sorting criterion, + // therefore the sort indicator should be there + sortAscending = ascending[0]; + sortContainerPropertyId = propertyId[0]; + } else { + sortAscending = true; + sortContainerPropertyId = null; + } + } 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 As of 7.0, use {@link #isSortEnabled()} instead + */ + @Deprecated + public boolean isSortDisabled() { + return !isSortEnabled(); + } + + /** + * Checks if sorting is enabled. + * + * @return true if sorting by the user is allowed, false otherwise + */ + public boolean isSortEnabled() { + return sortEnabled; + } + + /** + * Disables the sorting by the user altogether. + * + * @param sortDisabled + * True iff sorting is disabled. + * @deprecated As of 7.0, use {@link #setSortEnabled(boolean)} instead + */ + @Deprecated + public void setSortDisabled(boolean sortDisabled) { + setSortEnabled(!sortDisabled); + } + + /** + * Enables or disables sorting. + * <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; + markAsDirty(); + } + } + + /** + * 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 source + * the source Table + * @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(Table source, Object itemId, + Object propertyId); + } + + @Override + public void addItemClickListener(ItemClickListener listener) { + addListener(TableConstants.ITEM_CLICK_EVENT_ID, ItemClickEvent.class, + listener, ItemClickEvent.ITEM_CLICK_METHOD); + } + + /** + * @deprecated As of 7.0, replaced by + * {@link #addItemClickListener(ItemClickListener)} + **/ + @Override + @Deprecated + public void addListener(ItemClickListener listener) { + addItemClickListener(listener); + } + + @Override + public void removeItemClickListener(ItemClickListener listener) { + removeListener(TableConstants.ITEM_CLICK_EVENT_ID, ItemClickEvent.class, + listener); + } + + /** + * @deprecated As of 7.0, replaced by + * {@link #removeItemClickListener(ItemClickListener)} + **/ + @Override + @Deprecated + public void removeListener(ItemClickListener listener) { + removeItemClickListener(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 { + markAsDirtyRecursive(); + } + } + + /** + * 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; + markAsDirty(); + } + + /** + * @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; + markAsDirty(); + } + + /** + * Returns the select mode in which multi-select is used. + * + * @return The multi select mode + */ + public MultiSelectMode getMultiSelectMode() { + return multiSelectMode; + } + + /** + * Lazy loading accept criterion for Table. Accepted target rows are loaded + * from server once per drag and drop operation. Developer must override one + * method that decides on which rows the currently dragged data can be + * dropped. + * + * <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.server.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 property 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 property 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 addHeaderClickListener(HeaderClickListener listener) { + addListener(TableConstants.HEADER_CLICK_EVENT_ID, + HeaderClickEvent.class, listener, + HeaderClickEvent.HEADER_CLICK_METHOD); + } + + /** + * @deprecated As of 7.0, replaced by + * {@link #addHeaderClickListener(HeaderClickListener)} + **/ + @Deprecated + public void addListener(HeaderClickListener listener) { + addHeaderClickListener(listener); + } + + /** + * Removes a header click listener + * + * @param listener + * The listener to remove. + */ + public void removeHeaderClickListener(HeaderClickListener listener) { + removeListener(TableConstants.HEADER_CLICK_EVENT_ID, + HeaderClickEvent.class, listener); + } + + /** + * @deprecated As of 7.0, replaced by + * {@link #removeHeaderClickListener(HeaderClickListener)} + **/ + @Deprecated + public void removeListener(HeaderClickListener listener) { + removeHeaderClickListener(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 addFooterClickListener(FooterClickListener listener) { + addListener(TableConstants.FOOTER_CLICK_EVENT_ID, + FooterClickEvent.class, listener, + FooterClickEvent.FOOTER_CLICK_METHOD); + } + + /** + * @deprecated As of 7.0, replaced by + * {@link #addFooterClickListener(FooterClickListener)} + **/ + @Deprecated + public void addListener(FooterClickListener listener) { + addFooterClickListener(listener); + } + + /** + * Removes a footer click listener + * + * @param listener + * The listener to remove. + */ + public void removeFooterClickListener(FooterClickListener listener) { + removeListener(TableConstants.FOOTER_CLICK_EVENT_ID, + FooterClickEvent.class, listener); + } + + /** + * @deprecated As of 7.0, replaced by + * {@link #removeFooterClickListener(FooterClickListener)} + **/ + @Deprecated + public void removeListener(FooterClickListener listener) { + removeFooterClickListener(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); + } + + markAsDirty(); + } + + /** + * 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; + markAsDirty(); + } + } + + /** + * 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 addColumnResizeListener(ColumnResizeListener listener) { + addListener(TableConstants.COLUMN_RESIZE_EVENT_ID, + ColumnResizeEvent.class, listener, + ColumnResizeEvent.COLUMN_RESIZE_METHOD); + } + + /** + * @deprecated As of 7.0, replaced by + * {@link #addColumnResizeListener(ColumnResizeListener)} + **/ + @Deprecated + public void addListener(ColumnResizeListener listener) { + addColumnResizeListener(listener); + } + + /** + * Removes a column resize listener from the Table. + * + * @param listener + * The listener to remove + */ + public void removeColumnResizeListener(ColumnResizeListener listener) { + removeListener(TableConstants.COLUMN_RESIZE_EVENT_ID, + ColumnResizeEvent.class, listener); + } + + /** + * @deprecated As of 7.0, replaced by + * {@link #removeColumnResizeListener(ColumnResizeListener)} + **/ + @Deprecated + public void removeListener(ColumnResizeListener listener) { + removeColumnResizeListener(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); + } + + /** + * This event is fired when the collapse state of a column changes. + * + * @since 7.6 + */ + public static class ColumnCollapseEvent extends Component.Event { + + public static final Method METHOD = ReflectTools.findMethod( + ColumnCollapseListener.class, "columnCollapseStateChange", + ColumnCollapseEvent.class); + private Object propertyId; + + /** + * Constructor + * + * @param source + * The source of the event + * @param propertyId + * The id of the column + */ + public ColumnCollapseEvent(Component source, Object propertyId) { + super(source); + this.propertyId = propertyId; + } + + /** + * Gets the id of the column whose collapse state changed + * + * @return the property id of the column + */ + public Object getPropertyId() { + return propertyId; + } + } + + /** + * Interface for listening to column collapse events. + * + * @since 7.6 + */ + public interface ColumnCollapseListener extends Serializable { + + /** + * This method is triggered when the collapse state for a column has + * changed + * + * @param event + */ + public void columnCollapseStateChange(ColumnCollapseEvent 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 addColumnReorderListener(ColumnReorderListener listener) { + addListener(TableConstants.COLUMN_REORDER_EVENT_ID, + ColumnReorderEvent.class, listener, ColumnReorderEvent.METHOD); + } + + /** + * @deprecated As of 7.0, replaced by + * {@link #addColumnReorderListener(ColumnReorderListener)} + **/ + @Deprecated + public void addListener(ColumnReorderListener listener) { + addColumnReorderListener(listener); + } + + /** + * Removes a column reorder listener from the Table. + * + * @param listener + * The listener to remove + */ + public void removeColumnReorderListener(ColumnReorderListener listener) { + removeListener(TableConstants.COLUMN_REORDER_EVENT_ID, + ColumnReorderEvent.class, listener); + } + + /** + * @deprecated As of 7.0, replaced by + * {@link #removeColumnReorderListener(ColumnReorderListener)} + **/ + @Deprecated + public void removeListener(ColumnReorderListener listener) { + removeColumnReorderListener(listener); + } + + /** + * Adds a column collapse listener to the Table. A column collapse listener + * is called when the collapsed state of a column changes. + * + * @since 7.6 + * + * @param listener + * The listener to attach + */ + public void addColumnCollapseListener(ColumnCollapseListener listener) { + addListener(TableConstants.COLUMN_COLLAPSE_EVENT_ID, + ColumnCollapseEvent.class, listener, + ColumnCollapseEvent.METHOD); + } + + /** + * Removes a column reorder listener from the Table. + * + * @since 7.6 + * @param listener + * The listener to remove + */ + public void removeColumnCollapseListener(ColumnCollapseListener listener) { + removeListener(TableConstants.COLUMN_COLLAPSE_EVENT_ID, + ColumnCollapseEvent.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, + LegacyConverter<String, ?> converter) { + if (!getContainerPropertyIds().contains(propertyId)) { + throw new IllegalArgumentException( + "PropertyId " + propertyId + " must be in the container"); + } + + if (!typeIsCompatible(converter.getModelType(), getType(propertyId))) { + throw new IllegalArgumentException( + "Property type (" + getType(propertyId) + + ") must match converter source type (" + + converter.getModelType() + ")"); + } + propertyValueConverters.put(propertyId, + (LegacyConverter<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 LegacyConverter<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() { + if (visibleComponents == null) { + Collection<Component> empty = Collections.emptyList(); + return empty.iterator(); + } + return visibleComponents.iterator(); + } + + /** + * @deprecated As of 7.0, use {@link #iterator()} instead. + */ + @Deprecated + public Iterator<Component> getComponentIterator() { + return iterator(); + } + + @Override + public void readDesign(Element design, DesignContext context) { + super.readDesign(design, context); + + if (design.hasAttr("sortable")) { + setSortEnabled(DesignAttributeHandler.readAttribute("sortable", + design.attributes(), boolean.class)); + } + + readColumns(design); + readHeader(design); + readBody(design, context); + readFooter(design); + } + + private void readColumns(Element design) { + Element colgroup = design.select("> table > colgroup").first(); + + if (colgroup != null) { + int i = 0; + List<Object> pIds = new ArrayList<Object>(); + for (Element col : colgroup.children()) { + if (!col.tagName().equals("col")) { + throw new DesignException("invalid column"); + } + + String id = DesignAttributeHandler.readAttribute("property-id", + col.attributes(), "property-" + i++, String.class); + pIds.add(id); + + addContainerProperty(id, String.class, null); + + if (col.hasAttr("width")) { + setColumnWidth(id, DesignAttributeHandler.readAttribute( + "width", col.attributes(), Integer.class)); + } + if (col.hasAttr("center")) { + setColumnAlignment(id, Align.CENTER); + } else if (col.hasAttr("right")) { + setColumnAlignment(id, Align.RIGHT); + } + if (col.hasAttr("expand")) { + if (col.attr("expand").isEmpty()) { + setColumnExpandRatio(id, 1); + } else { + setColumnExpandRatio(id, + DesignAttributeHandler.readAttribute("expand", + col.attributes(), float.class)); + } + } + if (col.hasAttr("collapsible")) { + setColumnCollapsible(id, + DesignAttributeHandler.readAttribute("collapsible", + col.attributes(), boolean.class)); + } + if (col.hasAttr("collapsed")) { + setColumnCollapsed(id, DesignAttributeHandler.readAttribute( + "collapsed", col.attributes(), boolean.class)); + } + } + setVisibleColumns(pIds.toArray()); + } + } + + private void readFooter(Element design) { + readHeaderOrFooter(design, false); + } + + private void readHeader(Element design) { + readHeaderOrFooter(design, true); + } + + @Override + protected void readItems(Element design, DesignContext context) { + // Do nothing - header/footer and inline data must be handled after + // colgroup. + } + + private void readHeaderOrFooter(Element design, boolean header) { + String selector = header ? "> table > thead" : "> table > tfoot"; + Element elem = design.select(selector).first(); + if (elem != null) { + if (!header) { + setFooterVisible(true); + } + if (elem.children().size() != 1) { + throw new DesignException( + "Table header and footer should contain exactly one <tr> element"); + } + Element tr = elem.child(0); + Elements elems = tr.children(); + Collection<?> propertyIds = visibleColumns; + if (elems.size() != propertyIds.size()) { + throw new DesignException( + "Table header and footer should contain as many items as there" + + " are columns in the Table."); + } + Iterator<?> propertyIt = propertyIds.iterator(); + for (Element e : elems) { + String columnValue = DesignFormatter + .decodeFromTextNode(e.html()); + Object propertyId = propertyIt.next(); + if (header) { + setColumnHeader(propertyId, columnValue); + if (e.hasAttr("icon")) { + setColumnIcon(propertyId, + DesignAttributeHandler.readAttribute("icon", + e.attributes(), Resource.class)); + } + } else { + setColumnFooter(propertyId, columnValue); + } + } + } + } + + protected void readBody(Element design, DesignContext context) { + Element tbody = design.select("> table > tbody").first(); + if (tbody == null) { + return; + } + + Set<String> selected = new HashSet<String>(); + for (Element tr : tbody.children()) { + readItem(tr, selected, context); + } + } + + @Override + protected Object readItem(Element tr, Set<String> selected, + DesignContext context) { + Elements cells = tr.children(); + if (visibleColumns.size() != cells.size()) { + throw new DesignException( + "Wrong number of columns in a Table row. Expected " + + visibleColumns.size() + ", was " + cells.size() + + "."); + } + Object[] data = new String[cells.size()]; + for (int c = 0; c < cells.size(); ++c) { + data[c] = DesignFormatter.decodeFromTextNode(cells.get(c).html()); + } + + Object itemId = addItem(data, + tr.hasAttr("item-id") ? tr.attr("item-id") : null); + + if (itemId == null) { + throw new DesignException("Failed to add a Table row: " + data); + } + + return itemId; + } + + @Override + public void writeDesign(Element design, DesignContext context) { + Table def = context.getDefaultInstance(this); + + DesignAttributeHandler.writeAttribute("sortable", design.attributes(), + isSortEnabled(), def.isSortEnabled(), boolean.class); + + Element table = null; + boolean hasColumns = getVisibleColumns().length != 0; + if (hasColumns) { + table = design.appendElement("table"); + writeColumns(table, def); + writeHeader(table, def); + } + super.writeDesign(design, context); + if (hasColumns) { + writeFooter(table); + } + } + + private void writeColumns(Element table, Table def) { + Object[] columns = getVisibleColumns(); + if (columns.length == 0) { + return; + } + + Element colgroup = table.appendElement("colgroup"); + for (Object id : columns) { + Element col = colgroup.appendElement("col"); + + col.attr("property-id", id.toString()); + + if (getColumnAlignment(id) == Align.CENTER) { + col.attr("center", true); + } else if (getColumnAlignment(id) == Align.RIGHT) { + col.attr("right", true); + } + + DesignAttributeHandler.writeAttribute("width", col.attributes(), + getColumnWidth(id), def.getColumnWidth(null), int.class); + + DesignAttributeHandler.writeAttribute("expand", col.attributes(), + getColumnExpandRatio(id), def.getColumnExpandRatio(null), + float.class); + + DesignAttributeHandler.writeAttribute("collapsible", + col.attributes(), isColumnCollapsible(id), + def.isColumnCollapsible(null), boolean.class); + + DesignAttributeHandler.writeAttribute("collapsed", col.attributes(), + isColumnCollapsed(id), def.isColumnCollapsed(null), + boolean.class); + } + } + + private void writeHeader(Element table, Table def) { + Object[] columns = getVisibleColumns(); + if (columns.length == 0 + || (columnIcons.isEmpty() && columnHeaders.isEmpty())) { + return; + } + + Element header = table.appendElement("thead").appendElement("tr"); + for (Object id : columns) { + Element th = header.appendElement("th"); + th.html(getColumnHeader(id)); + DesignAttributeHandler.writeAttribute("icon", th.attributes(), + getColumnIcon(id), def.getColumnIcon(null), Resource.class); + } + + } + + private void writeFooter(Element table) { + Object[] columns = getVisibleColumns(); + if (columns.length == 0 || columnFooters.isEmpty()) { + return; + } + + Element footer = table.appendElement("tfoot").appendElement("tr"); + for (Object id : columns) { + footer.appendElement("td").text(getColumnFooter(id)); + } + } + + @Override + protected void writeItems(Element design, DesignContext context) { + if (getVisibleColumns().length == 0) { + return; + } + Element tbody = design.child(0).appendElement("tbody"); + super.writeItems(tbody, context); + } + + @Override + protected Element writeItem(Element tbody, Object itemId, + DesignContext context) { + Element tr = tbody.appendElement("tr"); + tr.attr("item-id", String.valueOf(itemId)); + Item item = getItem(itemId); + for (Object id : getVisibleColumns()) { + Element td = tr.appendElement("td"); + Object value = item.getItemProperty(id).getValue(); + td.html(value != null ? value.toString() : ""); + } + return tr; + } + + @Override + protected Collection<String> getCustomAttributes() { + Collection<String> result = super.getCustomAttributes(); + result.add("sortable"); + result.add("sort-enabled"); + result.add("sort-disabled"); + result.add("footer-visible"); + result.add("item-caption-mode"); + result.add("current-page-first-item-id"); + result.add("current-page-first-item-index"); + return result; + } + + /** + * ContextClickEvent for the Table Component. + * + * @since 7.6 + */ + public static class TableContextClickEvent extends ContextClickEvent { + + private final Object itemId; + private final Object propertyId; + private final Section section; + + public TableContextClickEvent(Table source, + MouseEventDetails mouseEventDetails, Object itemId, + Object propertyId, Section section) { + super(source, mouseEventDetails); + + this.itemId = itemId; + this.propertyId = propertyId; + this.section = section; + } + + /** + * Returns the item id of context clicked row. + * + * @return item id of clicked row; <code>null</code> if header, footer + * or empty area of Table + */ + public Object getItemId() { + return itemId; + } + + /** + * Returns the property id of context clicked column. + * + * @return property id; or <code>null</code> if we've clicked on the + * empty area of the Table + */ + public Object getPropertyId() { + return propertyId; + } + + /** + * Returns the clicked section of Table. + * + * @return section of Table + */ + public Section getSection() { + return section; + } + + @Override + public Table getComponent() { + return (Table) super.getComponent(); + } + } + + @Override + protected TableState getState() { + return getState(true); + } + + @Override + protected TableState getState(boolean markAsDirty) { + return (TableState) super.getState(markAsDirty); + } + + private final Logger getLogger() { + if (logger == null) { + logger = Logger.getLogger(Table.class.getName()); + } + return logger; + } + + @Override + public void setChildMeasurementHint(ChildMeasurementHint hint) { + if (hint == null) { + childMeasurementHint = ChildMeasurementHint.MEASURE_ALWAYS; + } else { + childMeasurementHint = hint; + } + } + + @Override + public ChildMeasurementHint getChildMeasurementHint() { + return childMeasurementHint; + } + + /** + * Sets whether only collapsible columns should be shown to the user in the + * column collapse menu. The default is + * {@link CollapseMenuContent#ALL_COLUMNS}. + * + * + * @since 7.6 + * @param content + * the desired collapsible menu content setting + */ + public void setCollapseMenuContent(CollapseMenuContent content) { + getState().collapseMenuContent = content; + } + + /** + * Checks whether only collapsible columns are shown to the user in the + * column collapse menu. The default is + * {@link CollapseMenuContent#ALL_COLUMNS} . + * + * @since 7.6 + * @return the current collapsible menu content setting + */ + public CollapseMenuContent getCollapseMenuContent() { + return getState(false).collapseMenuContent; + } +} diff --git a/compatibility-server/src/main/java/com/vaadin/ui/TableFieldFactory.java b/compatibility-server/src/main/java/com/vaadin/ui/TableFieldFactory.java new file mode 100644 index 0000000000..54f8bc5110 --- /dev/null +++ b/compatibility-server/src/main/java/com/vaadin/ui/TableFieldFactory.java @@ -0,0 +1,56 @@ +/* + * Copyright 2000-2016 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.ui; + +import java.io.Serializable; + +import com.vaadin.data.Container; +import com.vaadin.v7.ui.LegacyField; + +/** + * Factory interface for creating new LegacyField-instances based on Container + * (datasource), item id, property id and uiContext (the component responsible + * for displaying fields). Currently this interface is used by {@link Table}, + * but might later be used by some other components for {@link LegacyField} + * generation. + * + * <p> + * + * @author Vaadin Ltd. + * @since 6.0 + * @see FormFieldFactory + */ +public interface TableFieldFactory extends Serializable { + /** + * Creates a field based on the Container, item id, property id and the + * component responsible for displaying the field (most commonly + * {@link Table}). + * + * @param container + * the Container where the property belongs to. + * @param itemId + * the item Id. + * @param propertyId + * the Id of the property. + * @param uiContext + * the component where the field is presented. + * @return A field suitable for editing the specified data or null if the + * property should not be editable. + */ + LegacyField<?> createField(Container container, Object itemId, + Object propertyId, Component uiContext); + +} diff --git a/compatibility-server/src/main/java/com/vaadin/ui/TreeTable.java b/compatibility-server/src/main/java/com/vaadin/ui/TreeTable.java new file mode 100644 index 0000000000..2e69a0071d --- /dev/null +++ b/compatibility-server/src/main/java/com/vaadin/ui/TreeTable.java @@ -0,0 +1,985 @@ +/* + * Copyright 2000-2016 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.vaadin.ui; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.Stack; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.jsoup.nodes.Element; + +import com.vaadin.data.Collapsible; +import com.vaadin.data.Container; +import com.vaadin.data.Container.Hierarchical; +import com.vaadin.data.Container.ItemSetChangeEvent; +import com.vaadin.data.util.ContainerHierarchicalWrapper; +import com.vaadin.data.util.HierarchicalContainer; +import com.vaadin.data.util.HierarchicalContainerOrderedWrapper; +import com.vaadin.server.PaintException; +import com.vaadin.server.PaintTarget; +import com.vaadin.server.Resource; +import com.vaadin.shared.ui.treetable.TreeTableConstants; +import com.vaadin.shared.ui.treetable.TreeTableState; +import com.vaadin.ui.Tree.CollapseEvent; +import com.vaadin.ui.Tree.CollapseListener; +import com.vaadin.ui.Tree.ExpandEvent; +import com.vaadin.ui.Tree.ExpandListener; +import com.vaadin.ui.declarative.DesignAttributeHandler; +import com.vaadin.ui.declarative.DesignContext; +import com.vaadin.ui.declarative.DesignException; + +/** + * TreeTable extends the {@link Table} component so that it can also visualize a + * hierarchy of its Items in a similar manner that {@link Tree} does. The tree + * hierarchy is always displayed in the first actual column of the TreeTable. + * <p> + * The TreeTable supports the usual {@link Table} features like lazy loading, so + * it should be no problem to display lots of items at once. Only required rows + * and some cache rows are sent to the client. + * <p> + * TreeTable supports standard {@link Hierarchical} container interfaces, but + * also a more fine tuned version - {@link Collapsible}. A container + * implementing the {@link Collapsible} interface stores the collapsed/expanded + * state internally and can this way scale better on the server side than with + * standard Hierarchical implementations. Developer must however note that + * {@link Collapsible} containers can not be shared among several users as they + * share UI state in the container. + */ +@SuppressWarnings({ "serial" }) +public class TreeTable extends Table implements Hierarchical { + + private interface ContainerStrategy extends Serializable { + public int size(); + + public boolean isNodeOpen(Object itemId); + + public int getDepth(Object itemId); + + public void toggleChildVisibility(Object itemId); + + public Object getIdByIndex(int index); + + public int indexOfId(Object id); + + public Object nextItemId(Object itemId); + + public Object lastItemId(); + + public Object prevItemId(Object itemId); + + public boolean isLastId(Object itemId); + + public Collection<?> getItemIds(); + + public void containerItemSetChange(ItemSetChangeEvent event); + } + + private abstract class AbstractStrategy implements ContainerStrategy { + + /** + * Consider adding getDepth to {@link Collapsible}, might help + * scalability with some container implementations. + */ + + @Override + public int getDepth(Object itemId) { + int depth = 0; + Hierarchical hierarchicalContainer = getContainerDataSource(); + while (!hierarchicalContainer.isRoot(itemId)) { + depth++; + itemId = hierarchicalContainer.getParent(itemId); + } + return depth; + } + + @Override + public void containerItemSetChange(ItemSetChangeEvent event) { + } + + } + + /** + * This strategy is used if current container implements {@link Collapsible} + * . + * + * open-collapsed logic diverted to container, otherwise use default + * implementations. + */ + private class CollapsibleStrategy extends AbstractStrategy { + + private Collapsible c() { + return (Collapsible) getContainerDataSource(); + } + + @Override + public void toggleChildVisibility(Object itemId) { + c().setCollapsed(itemId, !c().isCollapsed(itemId)); + } + + @Override + public boolean isNodeOpen(Object itemId) { + return !c().isCollapsed(itemId); + } + + @Override + public int size() { + return TreeTable.super.size(); + } + + @Override + public Object getIdByIndex(int index) { + return TreeTable.super.getIdByIndex(index); + } + + @Override + public int indexOfId(Object id) { + return TreeTable.super.indexOfId(id); + } + + @Override + public boolean isLastId(Object itemId) { + // using the default impl + return TreeTable.super.isLastId(itemId); + } + + @Override + public Object lastItemId() { + // using the default impl + return TreeTable.super.lastItemId(); + } + + @Override + public Object nextItemId(Object itemId) { + return TreeTable.super.nextItemId(itemId); + } + + @Override + public Object prevItemId(Object itemId) { + return TreeTable.super.prevItemId(itemId); + } + + @Override + public Collection<?> getItemIds() { + return TreeTable.super.getItemIds(); + } + + } + + /** + * Strategy for Hierarchical but not Collapsible container like + * {@link HierarchicalContainer}. + * + * Store collapsed/open states internally, fool Table to use preorder when + * accessing items from container via Ordered/Indexed methods. + */ + private class HierarchicalStrategy extends AbstractStrategy { + + private final HashSet<Object> openItems = new HashSet<Object>(); + + @Override + public boolean isNodeOpen(Object itemId) { + return openItems.contains(itemId); + } + + @Override + public int size() { + return getPreOrder().size(); + } + + @Override + public Collection<Object> getItemIds() { + return Collections.unmodifiableCollection(getPreOrder()); + } + + @Override + public boolean isLastId(Object itemId) { + if (itemId == null) { + return false; + } + + return itemId.equals(lastItemId()); + } + + @Override + public Object lastItemId() { + if (getPreOrder().size() > 0) { + return getPreOrder().get(getPreOrder().size() - 1); + } else { + return null; + } + } + + @Override + public Object nextItemId(Object itemId) { + int indexOf = getPreOrder().indexOf(itemId); + if (indexOf == -1) { + return null; + } + indexOf++; + if (indexOf == getPreOrder().size()) { + return null; + } else { + return getPreOrder().get(indexOf); + } + } + + @Override + public Object prevItemId(Object itemId) { + int indexOf = getPreOrder().indexOf(itemId); + indexOf--; + if (indexOf < 0) { + return null; + } else { + return getPreOrder().get(indexOf); + } + } + + @Override + public void toggleChildVisibility(Object itemId) { + boolean removed = openItems.remove(itemId); + if (!removed) { + openItems.add(itemId); + getLogger().log(Level.FINEST, "Item {0} is now expanded", + itemId); + } else { + getLogger().log(Level.FINEST, "Item {0} is now collapsed", + itemId); + } + clearPreorderCache(); + } + + private void clearPreorderCache() { + preOrder = null; // clear preorder cache + } + + List<Object> preOrder; + + /** + * Preorder of ids currently visible + * + * @return + */ + private List<Object> getPreOrder() { + if (preOrder == null) { + preOrder = new ArrayList<Object>(); + Collection<?> rootItemIds = getContainerDataSource() + .rootItemIds(); + for (Object id : rootItemIds) { + preOrder.add(id); + addVisibleChildTree(id); + } + } + return preOrder; + } + + private void addVisibleChildTree(Object id) { + if (isNodeOpen(id)) { + Collection<?> children = getContainerDataSource() + .getChildren(id); + if (children != null) { + for (Object childId : children) { + preOrder.add(childId); + addVisibleChildTree(childId); + } + } + } + + } + + @Override + public int indexOfId(Object id) { + return getPreOrder().indexOf(id); + } + + @Override + public Object getIdByIndex(int index) { + return getPreOrder().get(index); + } + + @Override + public void containerItemSetChange(ItemSetChangeEvent event) { + // preorder becomes invalid on sort, item additions etc. + clearPreorderCache(); + super.containerItemSetChange(event); + } + + } + + /** + * Creates an empty TreeTable with a default container. + */ + public TreeTable() { + super(null, new HierarchicalContainer()); + } + + /** + * Creates an empty TreeTable with a default container. + * + * @param caption + * the caption for the TreeTable + */ + public TreeTable(String caption) { + this(); + setCaption(caption); + } + + /** + * Creates a TreeTable instance with given captions and data source. + * + * @param caption + * the caption for the component + * @param dataSource + * the dataSource that is used to list items in the component + */ + public TreeTable(String caption, Container dataSource) { + super(caption, dataSource); + } + + private ContainerStrategy cStrategy; + private Object focusedRowId = null; + private Object hierarchyColumnId; + + /** + * The item id that was expanded or collapsed during this request. Reset at + * the end of paint and only used for determining if a partial or full paint + * should be done. + * + * Can safely be reset to null whenever a change occurs that would prevent a + * partial update from rendering the correct result, e.g. rows added or + * removed during an expand operation. + */ + private Object toggledItemId; + private boolean animationsEnabled; + private boolean clearFocusedRowPending; + + /** + * If the container does not send item set change events, always do a full + * repaint instead of a partial update when expanding/collapsing nodes. + */ + private boolean containerSupportsPartialUpdates; + + private ContainerStrategy getContainerStrategy() { + if (cStrategy == null) { + if (getContainerDataSource() instanceof Collapsible) { + cStrategy = new CollapsibleStrategy(); + } else { + cStrategy = new HierarchicalStrategy(); + } + } + return cStrategy; + } + + @Override + protected void paintRowAttributes(PaintTarget target, Object itemId) + throws PaintException { + super.paintRowAttributes(target, itemId); + target.addAttribute("depth", getContainerStrategy().getDepth(itemId)); + if (getContainerDataSource().areChildrenAllowed(itemId)) { + target.addAttribute("ca", true); + target.addAttribute("open", + getContainerStrategy().isNodeOpen(itemId)); + } + } + + @Override + protected void paintRowIcon(PaintTarget target, Object[][] cells, + int indexInRowbuffer) throws PaintException { + // always paint if present (in parent only if row headers visible) + if (getRowHeaderMode() == ROW_HEADER_MODE_HIDDEN) { + Resource itemIcon = getItemIcon( + cells[CELL_ITEMID][indexInRowbuffer]); + if (itemIcon != null) { + target.addAttribute("icon", itemIcon); + } + } else if (cells[CELL_ICON][indexInRowbuffer] != null) { + target.addAttribute("icon", + (Resource) cells[CELL_ICON][indexInRowbuffer]); + } + } + + @Override + protected boolean rowHeadersAreEnabled() { + if (getRowHeaderMode() == RowHeaderMode.ICON_ONLY) { + return false; + } + return super.rowHeadersAreEnabled(); + } + + @Override + public void changeVariables(Object source, Map<String, Object> variables) { + super.changeVariables(source, variables); + + if (variables.containsKey("toggleCollapsed")) { + String object = (String) variables.get("toggleCollapsed"); + Object itemId = itemIdMapper.get(object); + toggledItemId = itemId; + toggleChildVisibility(itemId, false); + if (variables.containsKey("selectCollapsed")) { + // ensure collapsed is selected unless opened with selection + // head + if (isSelectable()) { + select(itemId); + } + } + } else if (variables.containsKey("focusParent")) { + String key = (String) variables.get("focusParent"); + Object refId = itemIdMapper.get(key); + Object itemId = getParent(refId); + focusParent(itemId); + } + } + + private void focusParent(Object itemId) { + boolean inView = false; + Object inPageId = getCurrentPageFirstItemId(); + for (int i = 0; inPageId != null && i < getPageLength(); i++) { + if (inPageId.equals(itemId)) { + inView = true; + break; + } + inPageId = nextItemId(inPageId); + i++; + } + if (!inView) { + setCurrentPageFirstItemId(itemId); + } + // Select the row if it is selectable. + if (isSelectable()) { + if (isMultiSelect()) { + setValue(Collections.singleton(itemId)); + } else { + setValue(itemId); + } + } + setFocusedRow(itemId); + } + + private void setFocusedRow(Object itemId) { + focusedRowId = itemId; + if (focusedRowId == null) { + // Must still inform the client that the focusParent request has + // been processed + clearFocusedRowPending = true; + } + markAsDirty(); + } + + @Override + public void paintContent(PaintTarget target) throws PaintException { + if (focusedRowId != null) { + target.addAttribute("focusedRow", itemIdMapper.key(focusedRowId)); + focusedRowId = null; + } else if (clearFocusedRowPending) { + // Must still inform the client that the focusParent request has + // been processed + target.addAttribute("clearFocusPending", true); + clearFocusedRowPending = false; + } + target.addAttribute("animate", animationsEnabled); + if (hierarchyColumnId != null) { + Object[] visibleColumns2 = getVisibleColumns(); + for (int i = 0; i < visibleColumns2.length; i++) { + Object object = visibleColumns2[i]; + if (hierarchyColumnId.equals(object)) { + target.addAttribute( + TreeTableConstants.ATTRIBUTE_HIERARCHY_COLUMN_INDEX, + i); + break; + } + } + } + super.paintContent(target); + toggledItemId = null; + } + + /* + * Override methods for partial row updates and additions when expanding / + * collapsing nodes. + */ + + @Override + protected boolean isPartialRowUpdate() { + return toggledItemId != null && containerSupportsPartialUpdates + && !isRowCacheInvalidated(); + } + + @Override + protected int getFirstAddedItemIndex() { + return indexOfId(toggledItemId) + 1; + } + + @Override + protected int getAddedRowCount() { + return countSubNodesRecursively(getContainerDataSource(), + toggledItemId); + } + + private int countSubNodesRecursively(Hierarchical hc, Object itemId) { + int count = 0; + // we need the number of children for toggledItemId no matter if its + // collapsed or expanded. Other items' children are only counted if the + // item is expanded. + if (getContainerStrategy().isNodeOpen(itemId) + || itemId == toggledItemId) { + Collection<?> children = hc.getChildren(itemId); + if (children != null) { + count += children != null ? children.size() : 0; + for (Object id : children) { + count += countSubNodesRecursively(hc, id); + } + } + } + return count; + } + + @Override + protected int getFirstUpdatedItemIndex() { + return indexOfId(toggledItemId); + } + + @Override + protected int getUpdatedRowCount() { + return 1; + } + + @Override + protected boolean shouldHideAddedRows() { + return !getContainerStrategy().isNodeOpen(toggledItemId); + } + + private void toggleChildVisibility(Object itemId, + boolean forceFullRefresh) { + getContainerStrategy().toggleChildVisibility(itemId); + // ensure that page still has first item in page, DON'T clear the + // caches. + setCurrentPageFirstItemIndex(getCurrentPageFirstItemIndex(), false); + + if (isCollapsed(itemId)) { + fireCollapseEvent(itemId); + } else { + fireExpandEvent(itemId); + } + + if (containerSupportsPartialUpdates && !forceFullRefresh) { + markAsDirty(); + } else { + // For containers that do not send item set change events, always do + // full repaint instead of partial row update. + refreshRowCache(); + } + } + + @Override + public int size() { + return getContainerStrategy().size(); + } + + @Override + public Hierarchical getContainerDataSource() { + return (Hierarchical) super.getContainerDataSource(); + } + + @Override + public void setContainerDataSource(Container newDataSource) { + cStrategy = null; + + // FIXME: This disables partial updates until TreeTable is fixed so it + // does not change component hierarchy during paint + containerSupportsPartialUpdates = (newDataSource instanceof ItemSetChangeNotifier) + && false; + + if (newDataSource != null && !(newDataSource instanceof Hierarchical)) { + newDataSource = new ContainerHierarchicalWrapper(newDataSource); + } + + if (newDataSource != null && !(newDataSource instanceof Ordered)) { + newDataSource = new HierarchicalContainerOrderedWrapper( + (Hierarchical) newDataSource); + } + + super.setContainerDataSource(newDataSource); + } + + @Override + public void containerItemSetChange( + com.vaadin.data.Container.ItemSetChangeEvent event) { + // Can't do partial repaints if items are added or removed during the + // expand/collapse request + toggledItemId = null; + getContainerStrategy().containerItemSetChange(event); + super.containerItemSetChange(event); + } + + @Override + protected Object getIdByIndex(int index) { + return getContainerStrategy().getIdByIndex(index); + } + + @Override + protected int indexOfId(Object itemId) { + return getContainerStrategy().indexOfId(itemId); + } + + @Override + public Object nextItemId(Object itemId) { + return getContainerStrategy().nextItemId(itemId); + } + + @Override + public Object lastItemId() { + return getContainerStrategy().lastItemId(); + } + + @Override + public Object prevItemId(Object itemId) { + return getContainerStrategy().prevItemId(itemId); + } + + @Override + public boolean isLastId(Object itemId) { + return getContainerStrategy().isLastId(itemId); + } + + @Override + public Collection<?> getItemIds() { + return getContainerStrategy().getItemIds(); + } + + @Override + public boolean areChildrenAllowed(Object itemId) { + return getContainerDataSource().areChildrenAllowed(itemId); + } + + @Override + public Collection<?> getChildren(Object itemId) { + return getContainerDataSource().getChildren(itemId); + } + + @Override + public Object getParent(Object itemId) { + return getContainerDataSource().getParent(itemId); + } + + @Override + public boolean hasChildren(Object itemId) { + return getContainerDataSource().hasChildren(itemId); + } + + @Override + public boolean isRoot(Object itemId) { + return getContainerDataSource().isRoot(itemId); + } + + @Override + public Collection<?> rootItemIds() { + return getContainerDataSource().rootItemIds(); + } + + @Override + public boolean setChildrenAllowed(Object itemId, boolean areChildrenAllowed) + throws UnsupportedOperationException { + return getContainerDataSource().setChildrenAllowed(itemId, + areChildrenAllowed); + } + + @Override + public boolean setParent(Object itemId, Object newParentId) + throws UnsupportedOperationException { + return getContainerDataSource().setParent(itemId, newParentId); + } + + /** + * Sets the Item specified by given identifier as collapsed or expanded. If + * the Item is collapsed, its children are not displayed to the user. + * + * @param itemId + * the identifier of the Item + * @param collapsed + * true if the Item should be collapsed, false if expanded + */ + public void setCollapsed(Object itemId, boolean collapsed) { + if (isCollapsed(itemId) != collapsed) { + if (null == toggledItemId && !isRowCacheInvalidated() + && getVisibleItemIds().contains(itemId)) { + // optimization: partial refresh if only one item is + // collapsed/expanded + toggledItemId = itemId; + toggleChildVisibility(itemId, false); + } else { + // make sure a full refresh takes place - otherwise neither + // partial nor full repaint of table content is performed + toggledItemId = null; + toggleChildVisibility(itemId, true); + } + } + } + + /** + * Checks if Item with given identifier is collapsed in the UI. + * + * <p> + * + * @param itemId + * the identifier of the checked Item + * @return true if the Item with given id is collapsed + * @see Collapsible#isCollapsed(Object) + */ + public boolean isCollapsed(Object itemId) { + return !getContainerStrategy().isNodeOpen(itemId); + } + + /** + * Explicitly sets the column in which the TreeTable visualizes the + * hierarchy. If hierarchyColumnId is not set, the hierarchy is visualized + * in the first visible column. + * + * @param hierarchyColumnId + */ + public void setHierarchyColumn(Object hierarchyColumnId) { + this.hierarchyColumnId = hierarchyColumnId; + } + + /** + * @return the identifier of column into which the hierarchy will be + * visualized or null if the column is not explicitly defined. + */ + public Object getHierarchyColumnId() { + return hierarchyColumnId; + } + + /** + * Adds an expand listener. + * + * @param listener + * the Listener to be added. + */ + public void addExpandListener(ExpandListener listener) { + addListener(ExpandEvent.class, listener, ExpandListener.EXPAND_METHOD); + } + + /** + * @deprecated As of 7.0, replaced by + * {@link #addExpandListener(ExpandListener)} + **/ + @Deprecated + public void addListener(ExpandListener listener) { + addExpandListener(listener); + } + + /** + * Removes an expand listener. + * + * @param listener + * the Listener to be removed. + */ + public void removeExpandListener(ExpandListener listener) { + removeListener(ExpandEvent.class, listener, + ExpandListener.EXPAND_METHOD); + } + + /** + * @deprecated As of 7.0, replaced by + * {@link #removeExpandListener(ExpandListener)} + **/ + @Deprecated + public void removeListener(ExpandListener listener) { + removeExpandListener(listener); + } + + /** + * Emits an expand event. + * + * @param itemId + * the item id. + */ + protected void fireExpandEvent(Object itemId) { + fireEvent(new ExpandEvent(this, itemId)); + } + + /** + * Adds a collapse listener. + * + * @param listener + * the Listener to be added. + */ + public void addCollapseListener(CollapseListener listener) { + addListener(CollapseEvent.class, listener, + CollapseListener.COLLAPSE_METHOD); + } + + /** + * @deprecated As of 7.0, replaced by + * {@link #addCollapseListener(CollapseListener)} + **/ + @Deprecated + public void addListener(CollapseListener listener) { + addCollapseListener(listener); + } + + /** + * Removes a collapse listener. + * + * @param listener + * the Listener to be removed. + */ + public void removeCollapseListener(CollapseListener listener) { + removeListener(CollapseEvent.class, listener, + CollapseListener.COLLAPSE_METHOD); + } + + /** + * @deprecated As of 7.0, replaced by + * {@link #removeCollapseListener(CollapseListener)} + **/ + @Deprecated + public void removeListener(CollapseListener listener) { + removeCollapseListener(listener); + } + + /** + * Emits a collapse event. + * + * @param itemId + * the item id. + */ + protected void fireCollapseEvent(Object itemId) { + fireEvent(new CollapseEvent(this, itemId)); + } + + /** + * @return true if animations are enabled + */ + public boolean isAnimationsEnabled() { + return animationsEnabled; + } + + /** + * Animations can be enabled by passing true to this method. Currently + * expanding rows slide in from the top and collapsing rows slide out the + * same way. NOTE! not supported in Internet Explorer 6 or 7. + * + * @param animationsEnabled + * true or false whether to enable animations or not. + */ + public void setAnimationsEnabled(boolean animationsEnabled) { + this.animationsEnabled = animationsEnabled; + markAsDirty(); + } + + private static final Logger getLogger() { + return Logger.getLogger(TreeTable.class.getName()); + } + + @Override + protected List<Object> getItemIds(int firstIndex, int rows) { + List<Object> itemIds = new ArrayList<Object>(); + for (int i = firstIndex; i < firstIndex + rows; i++) { + itemIds.add(getIdByIndex(i)); + } + return itemIds; + } + + @Override + protected void readBody(Element design, DesignContext context) { + Element tbody = design.select("> table > tbody").first(); + if (tbody == null) { + return; + } + + Set<String> selected = new HashSet<String>(); + Stack<Object> parents = new Stack<Object>(); + int lastDepth = -1; + + for (Element tr : tbody.children()) { + int depth = DesignAttributeHandler.readAttribute("depth", + tr.attributes(), 0, int.class); + + if (depth < 0 || depth > lastDepth + 1) { + throw new DesignException( + "Malformed TreeTable item hierarchy at " + tr + + ": last depth was " + lastDepth); + } else if (depth <= lastDepth) { + for (int d = depth; d <= lastDepth; d++) { + parents.pop(); + } + } + + Object itemId = readItem(tr, selected, context); + setParent(itemId, !parents.isEmpty() ? parents.peek() : null); + parents.push(itemId); + lastDepth = depth; + } + } + + @Override + protected Object readItem(Element tr, Set<String> selected, + DesignContext context) { + Object itemId = super.readItem(tr, selected, context); + + if (tr.hasAttr("collapsed")) { + boolean collapsed = DesignAttributeHandler + .readAttribute("collapsed", tr.attributes(), boolean.class); + setCollapsed(itemId, collapsed); + } + + return itemId; + } + + @Override + protected void writeItems(Element design, DesignContext context) { + if (getVisibleColumns().length == 0) { + return; + } + Element tbody = design.child(0).appendElement("tbody"); + writeItems(tbody, rootItemIds(), 0, context); + } + + protected void writeItems(Element tbody, Collection<?> itemIds, int depth, + DesignContext context) { + for (Object itemId : itemIds) { + Element tr = writeItem(tbody, itemId, context); + DesignAttributeHandler.writeAttribute("depth", tr.attributes(), + depth, 0, int.class); + + if (getChildren(itemId) != null) { + writeItems(tbody, getChildren(itemId), depth + 1, context); + } + } + } + + @Override + protected Element writeItem(Element tbody, Object itemId, + DesignContext context) { + Element tr = super.writeItem(tbody, itemId, context); + DesignAttributeHandler.writeAttribute("collapsed", tr.attributes(), + isCollapsed(itemId), true, boolean.class); + return tr; + } + + @Override + protected TreeTableState getState() { + return (TreeTableState) super.getState(); + } +} diff --git a/compatibility-server/src/test/java/com/vaadin/data/util/sqlcontainer/AllTests.java b/compatibility-server/src/test/java/com/vaadin/data/util/sqlcontainer/AllTests.java new file mode 100644 index 0000000000..9c3f6c0eaa --- /dev/null +++ b/compatibility-server/src/test/java/com/vaadin/data/util/sqlcontainer/AllTests.java @@ -0,0 +1,24 @@ +package com.vaadin.data.util.sqlcontainer; + +import org.junit.runner.RunWith; +import org.junit.runners.Suite; +import org.junit.runners.Suite.SuiteClasses; + +import com.vaadin.data.util.sqlcontainer.connection.J2EEConnectionPoolTest; +import com.vaadin.data.util.sqlcontainer.connection.SimpleJDBCConnectionPoolTest; +import com.vaadin.data.util.sqlcontainer.filters.BetweenTest; +import com.vaadin.data.util.sqlcontainer.filters.LikeTest; +import com.vaadin.data.util.sqlcontainer.generator.SQLGeneratorsTest; +import com.vaadin.data.util.sqlcontainer.query.FreeformQueryTest; +import com.vaadin.data.util.sqlcontainer.query.QueryBuilderTest; +import com.vaadin.data.util.sqlcontainer.query.TableQueryTest; + +@RunWith(Suite.class) +@SuiteClasses({ SimpleJDBCConnectionPoolTest.class, + J2EEConnectionPoolTest.class, LikeTest.class, QueryBuilderTest.class, + FreeformQueryTest.class, RowIdTest.class, SQLContainerTest.class, + SQLContainerTableQueryTest.class, ColumnPropertyTest.class, + TableQueryTest.class, SQLGeneratorsTest.class, UtilTest.class, + TicketTest.class, BetweenTest.class, ReadOnlyRowIdTest.class }) +public class AllTests { +} diff --git a/compatibility-server/src/test/java/com/vaadin/data/util/sqlcontainer/ColumnPropertyTest.java b/compatibility-server/src/test/java/com/vaadin/data/util/sqlcontainer/ColumnPropertyTest.java new file mode 100644 index 0000000000..edc56035e1 --- /dev/null +++ b/compatibility-server/src/test/java/com/vaadin/data/util/sqlcontainer/ColumnPropertyTest.java @@ -0,0 +1,318 @@ +package com.vaadin.data.util.sqlcontainer; + +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.Arrays; + +import org.easymock.EasyMock; +import org.junit.Assert; +import org.junit.Test; + +import com.vaadin.data.Property.ReadOnlyException; +import com.vaadin.data.util.sqlcontainer.ColumnProperty.NotNullableException; +import com.vaadin.data.util.sqlcontainer.query.QueryDelegate; + +public class ColumnPropertyTest { + + @Test + public void constructor_legalParameters_shouldSucceed() { + ColumnProperty cp = new ColumnProperty("NAME", false, true, true, false, + "Ville", String.class); + Assert.assertNotNull(cp); + } + + @Test(expected = IllegalArgumentException.class) + public void constructor_missingPropertyId_shouldFail() { + new ColumnProperty(null, false, true, true, false, "Ville", + String.class); + } + + @Test(expected = IllegalArgumentException.class) + public void constructor_missingType_shouldFail() { + new ColumnProperty("NAME", false, true, true, false, "Ville", null); + } + + @Test + public void getValue_defaultValue_returnsVille() { + ColumnProperty cp = new ColumnProperty("NAME", false, true, true, false, + "Ville", String.class); + Assert.assertEquals("Ville", cp.getValue()); + } + + @Test + public void setValue_readWriteNullable_returnsKalle() { + ColumnProperty cp = new ColumnProperty("NAME", false, true, true, false, + "Ville", String.class); + SQLContainer container = EasyMock.createMock(SQLContainer.class); + RowItem owner = new RowItem(container, new RowId(new Object[] { 1 }), + Arrays.asList(cp)); + container.itemChangeNotification(owner); + EasyMock.replay(container); + cp.setValue("Kalle"); + Assert.assertEquals("Kalle", cp.getValue()); + EasyMock.verify(container); + } + + @Test(expected = ReadOnlyException.class) + public void setValue_readOnlyNullable_shouldFail() { + ColumnProperty cp = new ColumnProperty("NAME", true, true, true, false, + "Ville", String.class); + SQLContainer container = EasyMock.createMock(SQLContainer.class); + new RowItem(container, new RowId(new Object[] { 1 }), + Arrays.asList(cp)); + EasyMock.replay(container); + cp.setValue("Kalle"); + EasyMock.verify(container); + } + + @Test + public void setValue_readWriteNullable_nullShouldWork() { + ColumnProperty cp = new ColumnProperty("NAME", false, true, true, false, + "Ville", String.class); + SQLContainer container = EasyMock.createMock(SQLContainer.class); + RowItem owner = new RowItem(container, new RowId(new Object[] { 1 }), + Arrays.asList(cp)); + container.itemChangeNotification(owner); + EasyMock.replay(container); + cp.setValue(null); + Assert.assertNull(cp.getValue()); + EasyMock.verify(container); + } + + @Test(expected = NotNullableException.class) + public void setValue_readWriteNotNullable_nullShouldFail() { + ColumnProperty cp = new ColumnProperty("NAME", false, true, false, + false, "Ville", String.class); + SQLContainer container = EasyMock.createMock(SQLContainer.class); + RowItem owner = new RowItem(container, new RowId(new Object[] { 1 }), + Arrays.asList(cp)); + container.itemChangeNotification(owner); + EasyMock.replay(container); + cp.setValue(null); + Assert.assertNotNull(cp.getValue()); + EasyMock.verify(container); + } + + @Test + public void getType_normal_returnsStringClass() { + ColumnProperty cp = new ColumnProperty("NAME", false, true, true, false, + "Ville", String.class); + Assert.assertSame(String.class, cp.getType()); + } + + @Test + public void isReadOnly_readWriteNullable_returnsTrue() { + ColumnProperty cp = new ColumnProperty("NAME", false, true, true, false, + "Ville", String.class); + Assert.assertFalse(cp.isReadOnly()); + } + + @Test + public void isReadOnly_readOnlyNullable_returnsTrue() { + ColumnProperty cp = new ColumnProperty("NAME", true, true, true, false, + "Ville", String.class); + Assert.assertTrue(cp.isReadOnly()); + } + + @Test + public void setReadOnly_readOnlyChangeAllowed_shouldSucceed() { + ColumnProperty cp = new ColumnProperty("NAME", false, true, true, false, + "Ville", String.class); + cp.setReadOnly(true); + Assert.assertTrue(cp.isReadOnly()); + } + + @Test + public void setReadOnly_readOnlyChangeDisallowed_shouldFail() { + ColumnProperty cp = new ColumnProperty("NAME", false, false, true, + false, "Ville", String.class); + cp.setReadOnly(true); + Assert.assertFalse(cp.isReadOnly()); + } + + @Test + public void getPropertyId_normal_returnsNAME() { + ColumnProperty cp = new ColumnProperty("NAME", false, false, true, + false, "Ville", String.class); + Assert.assertEquals("NAME", cp.getPropertyId()); + } + + @Test + public void isModified_valueModified_returnsTrue() { + ColumnProperty cp = new ColumnProperty("NAME", false, true, true, false, + "Ville", String.class); + SQLContainer container = EasyMock.createMock(SQLContainer.class); + RowItem owner = new RowItem(container, new RowId(new Object[] { 1 }), + Arrays.asList(cp)); + container.itemChangeNotification(owner); + EasyMock.replay(container); + cp.setValue("Kalle"); + Assert.assertEquals("Kalle", cp.getValue()); + Assert.assertTrue(cp.isModified()); + EasyMock.verify(container); + } + + @Test + public void isModified_valueNotModified_returnsFalse() { + ColumnProperty cp = new ColumnProperty("NAME", false, false, true, + false, "Ville", String.class); + Assert.assertFalse(cp.isModified()); + } + + @Test + public void setValue_nullOnNullable_shouldWork() { + ColumnProperty cp = new ColumnProperty("NAME", false, true, true, false, + "asdf", String.class); + SQLContainer container = EasyMock.createMock(SQLContainer.class); + new RowItem(container, new RowId(new Object[] { 1 }), + Arrays.asList(cp)); + cp.setValue(null); + Assert.assertNull(cp.getValue()); + } + + @Test + public void setValue_resetTonullOnNullable_shouldWork() { + ColumnProperty cp = new ColumnProperty("NAME", false, true, true, false, + null, String.class); + SQLContainer container = EasyMock.createMock(SQLContainer.class); + new RowItem(container, new RowId(new Object[] { 1 }), + Arrays.asList(cp)); + cp.setValue("asdf"); + Assert.assertEquals("asdf", cp.getValue()); + cp.setValue(null); + Assert.assertNull(cp.getValue()); + } + + @Test + public void setValue_sendsItemChangeNotification() throws SQLException { + + class TestContainer extends SQLContainer { + Object value = null; + boolean modified = false; + + public TestContainer(QueryDelegate delegate) throws SQLException { + super(delegate); + } + + @Override + public void itemChangeNotification(RowItem changedItem) { + ColumnProperty cp = (ColumnProperty) changedItem + .getItemProperty("NAME"); + value = cp.getValue(); + modified = cp.isModified(); + } + } + + ColumnProperty property = new ColumnProperty("NAME", false, true, true, + false, "Ville", String.class); + + Statement statement = EasyMock.createNiceMock(Statement.class); + EasyMock.replay(statement); + + ResultSetMetaData metadata = EasyMock + .createNiceMock(ResultSetMetaData.class); + EasyMock.replay(metadata); + + ResultSet resultSet = EasyMock.createNiceMock(ResultSet.class); + EasyMock.expect(resultSet.getStatement()).andReturn(statement); + EasyMock.expect(resultSet.getMetaData()).andReturn(metadata); + EasyMock.replay(resultSet); + + QueryDelegate delegate = EasyMock.createNiceMock(QueryDelegate.class); + EasyMock.expect(delegate.getResults(0, 1)).andReturn(resultSet); + EasyMock.replay(delegate); + + TestContainer container = new TestContainer(delegate); + + new RowItem(container, new RowId(new Object[] { 1 }), + Arrays.asList(property)); + + property.setValue("Kalle"); + Assert.assertEquals("Kalle", container.value); + Assert.assertTrue(container.modified); + } + + @Test + public void versionColumnsShouldNotBeInValueMap_shouldReturnFalse() { + ColumnProperty property = new ColumnProperty("NAME", false, true, true, + false, "Ville", String.class); + property.setVersionColumn(true); + + Assert.assertFalse(property.isPersistent()); + } + + @Test + public void neverWritableColumnsShouldNotBeInValueMap_shouldReturnFalse() { + ColumnProperty property = new ColumnProperty("NAME", true, false, true, + false, "Ville", String.class); + + Assert.assertFalse(property.isPersistent()); + } + + @Test + public void writableColumnsShouldBeInValueMap_shouldReturnTrue() { + ColumnProperty property = new ColumnProperty("NAME", false, true, true, + false, "Ville", String.class); + + Assert.assertTrue(property.isPersistent()); + } + + @Test + public void writableButReadOnlyColumnsShouldNotBeInValueMap_shouldReturnFalse() { + ColumnProperty property = new ColumnProperty("NAME", true, true, true, + false, "Ville", String.class); + + Assert.assertFalse(property.isPersistent()); + } + + @Test + public void primKeysShouldBeRowIdentifiers_shouldReturnTrue() { + ColumnProperty property = new ColumnProperty("NAME", false, true, true, + true, "Ville", String.class); + + Assert.assertTrue(property.isRowIdentifier()); + } + + @Test + public void versionColumnsShouldBeRowIdentifiers_shouldReturnTrue() { + ColumnProperty property = new ColumnProperty("NAME", false, true, true, + false, "Ville", String.class); + property.setVersionColumn(true); + + Assert.assertTrue(property.isRowIdentifier()); + } + + @Test + public void nonPrimKeyOrVersionColumnsShouldBeNotRowIdentifiers_shouldReturnFalse() { + ColumnProperty property = new ColumnProperty("NAME", false, true, true, + false, "Ville", String.class); + + Assert.assertFalse(property.isRowIdentifier()); + } + + @Test + public void getOldValueShouldReturnPreviousValue_shouldReturnVille() { + ColumnProperty property = new ColumnProperty("NAME", false, true, true, + false, "Ville", String.class); + + // Here we really don't care about the container management, but in + // order to set the value for a column the owner (RowItem) must be set + // and to create the owner we must have a container... + ArrayList<ColumnProperty> properties = new ArrayList<ColumnProperty>(); + properties.add(property); + + SQLContainer container = EasyMock.createNiceMock(SQLContainer.class); + RowItem rowItem = new RowItem(container, new RowId(new Object[] { 1 }), + Arrays.asList(property)); + + property.setValue("Kalle"); + // Just check that the new value was actually set... + Assert.assertEquals("Kalle", property.getValue()); + // Assert that old value is the original value... + Assert.assertEquals("Ville", property.getOldValue()); + } + +} diff --git a/compatibility-server/src/test/java/com/vaadin/data/util/sqlcontainer/DataGenerator.java b/compatibility-server/src/test/java/com/vaadin/data/util/sqlcontainer/DataGenerator.java new file mode 100644 index 0000000000..8ce298c065 --- /dev/null +++ b/compatibility-server/src/test/java/com/vaadin/data/util/sqlcontainer/DataGenerator.java @@ -0,0 +1,133 @@ +package com.vaadin.data.util.sqlcontainer; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; + +import org.junit.Assert; +import org.junit.Test; + +import com.vaadin.data.util.sqlcontainer.SQLTestsConstants.DB; +import com.vaadin.data.util.sqlcontainer.connection.JDBCConnectionPool; + +public class DataGenerator { + + public static void addPeopleToDatabase(JDBCConnectionPool connectionPool) + throws SQLException { + Connection conn = connectionPool.reserveConnection(); + Statement statement = conn.createStatement(); + try { + statement.execute("drop table PEOPLE"); + if (SQLTestsConstants.db == DB.ORACLE) { + statement.execute("drop sequence people_seq"); + } + } catch (SQLException e) { + // Will fail if table doesn't exist, which is OK. + conn.rollback(); + } + statement.execute(SQLTestsConstants.peopleFirst); + if (SQLTestsConstants.peopleSecond != null) { + statement.execute(SQLTestsConstants.peopleSecond); + } + if (SQLTestsConstants.db == DB.ORACLE) { + statement.execute(SQLTestsConstants.peopleThird); + } + if (SQLTestsConstants.db == DB.MSSQL) { + statement.executeUpdate("insert into people values('Ville', '23')"); + statement.executeUpdate("insert into people values('Kalle', '7')"); + statement.executeUpdate("insert into people values('Pelle', '18')"); + statement.executeUpdate("insert into people values('Börje', '64')"); + } else { + statement.executeUpdate( + "insert into people values(default, 'Ville', '23')"); + statement.executeUpdate( + "insert into people values(default, 'Kalle', '7')"); + statement.executeUpdate( + "insert into people values(default, 'Pelle', '18')"); + statement.executeUpdate( + "insert into people values(default, 'Börje', '64')"); + } + statement.close(); + statement = conn.createStatement(); + ResultSet rs = statement.executeQuery("select * from PEOPLE"); + Assert.assertTrue(rs.next()); + statement.close(); + conn.commit(); + connectionPool.releaseConnection(conn); + } + + public static void addFiveThousandPeople(JDBCConnectionPool connectionPool) + throws SQLException { + Connection conn = connectionPool.reserveConnection(); + Statement statement = conn.createStatement(); + for (int i = 4; i < 5000; i++) { + if (SQLTestsConstants.db == DB.MSSQL) { + statement.executeUpdate("insert into people values('Person " + i + + "', '" + i % 99 + "')"); + } else { + statement.executeUpdate( + "insert into people values(default, 'Person " + i + + "', '" + i % 99 + "')"); + } + } + statement.close(); + conn.commit(); + connectionPool.releaseConnection(conn); + } + + public static void addVersionedData(JDBCConnectionPool connectionPool) + throws SQLException { + Connection conn = connectionPool.reserveConnection(); + Statement statement = conn.createStatement(); + try { + statement.execute("DROP TABLE VERSIONED"); + if (SQLTestsConstants.db == DB.ORACLE) { + statement.execute("drop sequence versioned_seq"); + statement.execute("drop sequence versioned_version"); + } + } catch (SQLException e) { + // Will fail if table doesn't exist, which is OK. + conn.rollback(); + } + for (String stmtString : SQLTestsConstants.versionStatements) { + statement.execute(stmtString); + } + if (SQLTestsConstants.db == DB.MSSQL) { + statement.executeUpdate( + "insert into VERSIONED values('Junk', default)"); + } else { + statement.executeUpdate( + "insert into VERSIONED values(default, 'Junk', default)"); + } + statement.close(); + statement = conn.createStatement(); + ResultSet rs = statement.executeQuery("select * from VERSIONED"); + Assert.assertTrue(rs.next()); + statement.close(); + conn.commit(); + connectionPool.releaseConnection(conn); + } + + public static void createGarbage(JDBCConnectionPool connectionPool) + throws SQLException { + Connection conn = connectionPool.reserveConnection(); + Statement statement = conn.createStatement(); + try { + statement.execute("drop table GARBAGE"); + if (SQLTestsConstants.db == DB.ORACLE) { + statement.execute("drop sequence garbage_seq"); + } + } catch (SQLException e) { + // Will fail if table doesn't exist, which is OK. + conn.rollback(); + } + statement.execute(SQLTestsConstants.createGarbage); + if (SQLTestsConstants.db == DB.ORACLE) { + statement.execute(SQLTestsConstants.createGarbageSecond); + statement.execute(SQLTestsConstants.createGarbageThird); + } + conn.commit(); + connectionPool.releaseConnection(conn); + } +} diff --git a/compatibility-server/src/test/java/com/vaadin/data/util/sqlcontainer/FreeformQueryUtil.java b/compatibility-server/src/test/java/com/vaadin/data/util/sqlcontainer/FreeformQueryUtil.java new file mode 100644 index 0000000000..8688c9ae64 --- /dev/null +++ b/compatibility-server/src/test/java/com/vaadin/data/util/sqlcontainer/FreeformQueryUtil.java @@ -0,0 +1,68 @@ +package com.vaadin.data.util.sqlcontainer; + +import java.util.List; + +import org.junit.Test; + +import com.vaadin.data.Container.Filter; +import com.vaadin.data.util.sqlcontainer.SQLTestsConstants.DB; +import com.vaadin.data.util.sqlcontainer.query.generator.StatementHelper; +import com.vaadin.data.util.sqlcontainer.query.generator.filter.QueryBuilder; + +public class FreeformQueryUtil { + + public static StatementHelper getQueryWithFilters(List<Filter> filters, + int offset, int limit) { + StatementHelper sh = new StatementHelper(); + if (SQLTestsConstants.db == DB.MSSQL) { + if (limit > 1) { + offset++; + limit--; + } + StringBuilder query = new StringBuilder(); + query.append("SELECT * FROM (SELECT row_number() OVER ("); + query.append("ORDER BY \"ID\" ASC"); + query.append(") AS rownum, * FROM \"PEOPLE\""); + + if (!filters.isEmpty()) { + query.append( + QueryBuilder.getWhereStringForFilters(filters, sh)); + } + query.append(") AS a WHERE a.rownum BETWEEN ").append(offset) + .append(" AND ").append(Integer.toString(offset + limit)); + sh.setQueryString(query.toString()); + return sh; + } else if (SQLTestsConstants.db == DB.ORACLE) { + if (limit > 1) { + offset++; + limit--; + } + StringBuilder query = new StringBuilder(); + query.append("SELECT * FROM (SELECT x.*, ROWNUM AS " + + "\"rownum\" FROM (SELECT * FROM \"PEOPLE\""); + if (!filters.isEmpty()) { + query.append( + QueryBuilder.getWhereStringForFilters(filters, sh)); + } + query.append(") x) WHERE \"rownum\" BETWEEN ? AND ?"); + sh.addParameterValue(offset); + sh.addParameterValue(offset + limit); + sh.setQueryString(query.toString()); + return sh; + } else { + StringBuilder query = new StringBuilder("SELECT * FROM people"); + if (!filters.isEmpty()) { + query.append( + QueryBuilder.getWhereStringForFilters(filters, sh)); + } + if (limit != 0 || offset != 0) { + query.append(" LIMIT ? OFFSET ?"); + sh.addParameterValue(limit); + sh.addParameterValue(offset); + } + sh.setQueryString(query.toString()); + return sh; + } + } + +} diff --git a/compatibility-server/src/test/java/com/vaadin/data/util/sqlcontainer/ReadOnlyRowIdTest.java b/compatibility-server/src/test/java/com/vaadin/data/util/sqlcontainer/ReadOnlyRowIdTest.java new file mode 100644 index 0000000000..29968ecf94 --- /dev/null +++ b/compatibility-server/src/test/java/com/vaadin/data/util/sqlcontainer/ReadOnlyRowIdTest.java @@ -0,0 +1,55 @@ +package com.vaadin.data.util.sqlcontainer; + +import org.junit.Assert; +import org.junit.Test; + +public class ReadOnlyRowIdTest { + + @Test + public void getRowNum_shouldReturnRowNumGivenInConstructor() { + int rowNum = 1337; + ReadOnlyRowId rid = new ReadOnlyRowId(rowNum); + Assert.assertEquals(rowNum, rid.getRowNum()); + } + + @Test + public void hashCode_shouldBeEqualToHashCodeOfRowNum() { + int rowNum = 1337; + ReadOnlyRowId rid = new ReadOnlyRowId(rowNum); + Assert.assertEquals(Integer.valueOf(rowNum).hashCode(), rid.hashCode()); + } + + @Test + public void equals_compareWithNull_shouldBeFalse() { + ReadOnlyRowId rid = new ReadOnlyRowId(1337); + Assert.assertFalse(rid.equals(null)); + } + + @Test + public void equals_compareWithSameInstance_shouldBeTrue() { + ReadOnlyRowId rid = new ReadOnlyRowId(1337); + ReadOnlyRowId rid2 = rid; + Assert.assertTrue(rid.equals(rid2)); + } + + @Test + public void equals_compareWithOtherType_shouldBeFalse() { + ReadOnlyRowId rid = new ReadOnlyRowId(1337); + Assert.assertFalse(rid.equals(new Object())); + } + + @Test + public void equals_compareWithOtherRowId_shouldBeFalse() { + ReadOnlyRowId rid = new ReadOnlyRowId(1337); + ReadOnlyRowId rid2 = new ReadOnlyRowId(42); + Assert.assertFalse(rid.equals(rid2)); + } + + @Test + public void toString_rowNumberIsReturned() { + int i = 1; + ReadOnlyRowId rowId = new ReadOnlyRowId(i); + Assert.assertEquals("Unexpected toString value", String.valueOf(i), + rowId.toString()); + } +} diff --git a/compatibility-server/src/test/java/com/vaadin/data/util/sqlcontainer/RowIdTest.java b/compatibility-server/src/test/java/com/vaadin/data/util/sqlcontainer/RowIdTest.java new file mode 100644 index 0000000000..e93048157b --- /dev/null +++ b/compatibility-server/src/test/java/com/vaadin/data/util/sqlcontainer/RowIdTest.java @@ -0,0 +1,60 @@ +package com.vaadin.data.util.sqlcontainer; + +import org.junit.Assert; +import org.junit.Test; + +public class RowIdTest { + + @Test + public void constructor_withArrayOfPrimaryKeyColumns_shouldSucceed() { + RowId id = new RowId(new Object[] { "id", "name" }); + Assert.assertArrayEquals(new Object[] { "id", "name" }, id.getId()); + } + + @Test(expected = IllegalArgumentException.class) + public void constructor_withNullParameter_shouldFail() { + new RowId(null); + } + + @Test + public void hashCode_samePrimaryKeys_sameResult() { + RowId id = new RowId(new Object[] { "id", "name" }); + RowId id2 = new RowId(new Object[] { "id", "name" }); + Assert.assertEquals(id.hashCode(), id2.hashCode()); + } + + @Test + public void hashCode_differentPrimaryKeys_differentResult() { + RowId id = new RowId(new Object[] { "id", "name" }); + RowId id2 = new RowId(new Object[] { "id" }); + Assert.assertFalse(id.hashCode() == id2.hashCode()); + } + + @Test + public void equals_samePrimaryKeys_returnsTrue() { + RowId id = new RowId(new Object[] { "id", "name" }); + RowId id2 = new RowId(new Object[] { "id", "name" }); + Assert.assertEquals(id, id2); + } + + @Test + public void equals_differentPrimaryKeys_returnsFalse() { + RowId id = new RowId(new Object[] { "id", "name" }); + RowId id2 = new RowId(new Object[] { "id" }); + Assert.assertFalse(id.equals(id2.hashCode())); + } + + @Test + public void equals_differentDataType_returnsFalse() { + RowId id = new RowId(new Object[] { "id", "name" }); + Assert.assertFalse(id.equals("Tudiluu")); + Assert.assertFalse(id.equals(new Integer(1337))); + } + + @Test + public void toString_defaultCtor_noException() { + RowId rowId = new RowId(); + Assert.assertTrue("Unexpected to string for empty Row Id", + rowId.toString().isEmpty()); + } +} diff --git a/compatibility-server/src/test/java/com/vaadin/data/util/sqlcontainer/SQLContainerTableQueryTest.java b/compatibility-server/src/test/java/com/vaadin/data/util/sqlcontainer/SQLContainerTableQueryTest.java new file mode 100644 index 0000000000..0bdcc9a3c3 --- /dev/null +++ b/compatibility-server/src/test/java/com/vaadin/data/util/sqlcontainer/SQLContainerTableQueryTest.java @@ -0,0 +1,1353 @@ +package com.vaadin.data.util.sqlcontainer; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.hasItems; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; +import static org.hamcrest.core.IsNull.nullValue; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.math.BigDecimal; +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.easymock.EasyMock; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import com.vaadin.data.Container.ItemSetChangeEvent; +import com.vaadin.data.Container.ItemSetChangeListener; +import com.vaadin.data.Item; +import com.vaadin.data.util.filter.Like; +import com.vaadin.data.util.sqlcontainer.SQLTestsConstants.DB; +import com.vaadin.data.util.sqlcontainer.connection.JDBCConnectionPool; +import com.vaadin.data.util.sqlcontainer.query.OrderBy; +import com.vaadin.data.util.sqlcontainer.query.TableQuery; +import com.vaadin.data.util.sqlcontainer.query.ValidatingSimpleJDBCConnectionPool; + +public class SQLContainerTableQueryTest { + + private static final int offset = SQLTestsConstants.offset; + private final int numberOfRowsInContainer = 4; + private final int numberOfPropertiesInContainer = 3; + private final String NAME = "NAME"; + private final String ID = "ID"; + private final String AGE = "AGE"; + private JDBCConnectionPool connectionPool; + private TableQuery query; + private SQLContainer container; + private final RowId existingItemId = getRowId(1); + private final RowId nonExistingItemId = getRowId(1337); + + @Before + public void setUp() throws SQLException { + + try { + connectionPool = new ValidatingSimpleJDBCConnectionPool( + SQLTestsConstants.dbDriver, SQLTestsConstants.dbURL, + SQLTestsConstants.dbUser, SQLTestsConstants.dbPwd, 2, 2); + } catch (SQLException e) { + e.printStackTrace(); + Assert.fail(e.getMessage()); + } + + DataGenerator.addPeopleToDatabase(connectionPool); + + query = getTableQuery("people"); + container = new SQLContainer(query); + } + + private TableQuery getTableQuery(String tableName) { + return new TableQuery(tableName, connectionPool, + SQLTestsConstants.sqlGen); + } + + private SQLContainer getGarbageContainer() throws SQLException { + DataGenerator.createGarbage(connectionPool); + + return new SQLContainer(getTableQuery("garbage")); + } + + private Item getItem(Object id) { + return container.getItem(id); + } + + private RowId getRowId(int id) { + return new RowId(new Object[] { id + offset }); + } + + @After + public void tearDown() { + if (connectionPool != null) { + connectionPool.destroy(); + } + } + + @Test + public void itemWithExistingVersionColumnIsRemoved() throws SQLException { + container.setAutoCommit(true); + query.setVersionColumn(ID); + + assertTrue(container.removeItem(container.lastItemId())); + } + + @Test(expected = SQLException.class) + public void itemWithNonExistingVersionColumnCannotBeRemoved() + throws SQLException { + query.setVersionColumn("version"); + + container.removeItem(container.lastItemId()); + + container.commit(); + } + + @Test + public void containerContainsId() { + assertTrue(container.containsId(existingItemId)); + } + + @Test + public void containerDoesNotContainId() { + assertFalse(container.containsId(nonExistingItemId)); + } + + @Test + public void idPropertyHasCorrectType() { + if (SQLTestsConstants.db == DB.ORACLE) { + assertEquals(container.getType(ID), BigDecimal.class); + } else { + assertEquals(container.getType(ID), Integer.class); + } + } + + @Test + public void namePropertyHasCorrectType() { + assertEquals(container.getType(NAME), String.class); + } + + @Test + public void nonExistingPropertyDoesNotHaveType() { + assertThat(container.getType("adsf"), is(nullValue())); + } + + @Test + public void sizeIsReturnedCorrectly() { + assertEquals(numberOfRowsInContainer, container.size()); + } + + @Test + public void propertyIsFetchedForExistingItem() { + assertThat(container.getContainerProperty(existingItemId, NAME) + .getValue().toString(), is("Kalle")); + } + + @Test + public void containerDoesNotContainPropertyForExistingItem() { + assertThat(container.getContainerProperty(existingItemId, "asdf"), + is(nullValue())); + } + + @Test + public void containerDoesNotContainExistingPropertyForNonExistingItem() { + assertThat(container.getContainerProperty(nonExistingItemId, NAME), + is(nullValue())); + } + + @Test + public void propertyIdsAreFetched() { + ArrayList<String> propertyIds = new ArrayList<String>( + (Collection<? extends String>) container + .getContainerPropertyIds()); + + assertThat(propertyIds.size(), is(numberOfPropertiesInContainer)); + assertThat(propertyIds, hasItems(ID, NAME, AGE)); + } + + @Test + public void existingItemIsFetched() { + Item item = container.getItem(existingItemId); + + assertThat(item.getItemProperty(NAME).getValue().toString(), + is("Kalle")); + } + + @Test + public void newItemIsAdded() throws SQLException { + Object id = container.addItem(); + getItem(id).getItemProperty(NAME).setValue("foo"); + + container.commit(); + + Item item = getItem(container.lastItemId()); + assertThat(item.getItemProperty(NAME).getValue(), is("foo")); + } + + @Test + public void itemPropertyIsNotRevertedOnRefresh() { + getItem(existingItemId).getItemProperty(NAME).setValue("foo"); + + container.refresh(); + + assertThat(getItem(existingItemId).getItemProperty(NAME).getValue(), + is("foo")); + } + + @Test + public void correctItemIsFetchedFromMultipleRows() throws SQLException { + DataGenerator.addFiveThousandPeople(connectionPool); + + Item item = container.getItem(getRowId(1337)); + + assertThat((Integer) item.getItemProperty(ID).getValue(), + is(equalTo(1337 + offset))); + assertThat(item.getItemProperty(NAME).getValue().toString(), + is("Person 1337")); + } + + @Test + public void getItemIds_table_returnsItemIdsWithKeys0through3() + throws SQLException { + Collection<?> itemIds = container.getItemIds(); + assertEquals(4, itemIds.size()); + RowId zero = new RowId(new Object[] { 0 + offset }); + RowId one = new RowId(new Object[] { 1 + offset }); + RowId two = new RowId(new Object[] { 2 + offset }); + RowId three = new RowId(new Object[] { 3 + offset }); + if (SQLTestsConstants.db == DB.ORACLE) { + String[] correct = new String[] { "1", "2", "3", "4" }; + List<String> oracle = new ArrayList<String>(); + for (Object o : itemIds) { + oracle.add(o.toString()); + } + Assert.assertArrayEquals(correct, oracle.toArray()); + } else { + Assert.assertArrayEquals(new Object[] { zero, one, two, three }, + itemIds.toArray()); + } + } + + @Test + public void size_tableOneAddedItem_returnsFive() throws SQLException { + Connection conn = connectionPool.reserveConnection(); + Statement statement = conn.createStatement(); + if (SQLTestsConstants.db == DB.MSSQL) { + statement.executeUpdate("insert into people values('Bengt', 30)"); + } else { + statement.executeUpdate( + "insert into people values(default, 'Bengt', 30)"); + } + statement.close(); + conn.commit(); + connectionPool.releaseConnection(conn); + + assertEquals(5, container.size()); + } + + @Test + public void indexOfId_tableWithParameterThree_returnsThree() + throws SQLException { + if (SQLTestsConstants.db == DB.ORACLE) { + assertEquals(3, container.indexOfId( + new RowId(new Object[] { new BigDecimal(3 + offset) }))); + } else { + assertEquals(3, container + .indexOfId(new RowId(new Object[] { 3 + offset }))); + } + } + + @Test + public void indexOfId_table5000RowsWithParameter1337_returns1337() + throws SQLException { + DataGenerator.addFiveThousandPeople(connectionPool); + + if (SQLTestsConstants.db == DB.ORACLE) { + container.getItem( + new RowId(new Object[] { new BigDecimal(1337 + offset) })); + assertEquals(1337, container.indexOfId( + new RowId(new Object[] { new BigDecimal(1337 + offset) }))); + } else { + container.getItem(new RowId(new Object[] { 1337 + offset })); + assertEquals(1337, container + .indexOfId(new RowId(new Object[] { 1337 + offset }))); + } + } + + @Test + public void getIdByIndex_table5000rowsIndex1337_returnsRowId1337() + throws SQLException { + DataGenerator.addFiveThousandPeople(connectionPool); + Object itemId = container.getIdByIndex(1337); + if (SQLTestsConstants.db == DB.ORACLE) { + assertEquals(new RowId(new Object[] { 1337 + offset }).toString(), + itemId.toString()); + } else { + assertEquals(new RowId(new Object[] { 1337 + offset }), itemId); + } + } + + @Test + public void getIdByIndex_tableWithPaging5000rowsIndex1337_returnsRowId1337() + throws SQLException { + DataGenerator.addFiveThousandPeople(connectionPool); + + Object itemId = container.getIdByIndex(1337); + if (SQLTestsConstants.db == DB.ORACLE) { + assertEquals(new RowId(new Object[] { 1337 + offset }).toString(), + itemId.toString()); + } else { + assertEquals(new RowId(new Object[] { 1337 + offset }), itemId); + } + } + + @Test + public void nextItemId_tableCurrentItem1337_returnsItem1338() + throws SQLException { + DataGenerator.addFiveThousandPeople(connectionPool); + SQLContainer container = new SQLContainer(new TableQuery("people", + connectionPool, SQLTestsConstants.sqlGen)); + Object itemId = container.getIdByIndex(1337); + if (SQLTestsConstants.db == DB.ORACLE) { + assertEquals(new RowId(new Object[] { 1338 + offset }).toString(), + container.nextItemId(itemId).toString()); + } else { + assertEquals(new RowId(new Object[] { 1338 + offset }), + container.nextItemId(itemId)); + } + } + + @Test + public void prevItemId_tableCurrentItem1337_returns1336() + throws SQLException { + DataGenerator.addFiveThousandPeople(connectionPool); + Object itemId = container.getIdByIndex(1337); + if (SQLTestsConstants.db == DB.ORACLE) { + assertEquals(new RowId(new Object[] { 1336 + offset }).toString(), + container.prevItemId(itemId).toString()); + } else { + assertEquals(new RowId(new Object[] { 1336 + offset }), + container.prevItemId(itemId)); + } + } + + @Test + public void firstItemId_table_returnsItemId0() throws SQLException { + if (SQLTestsConstants.db == DB.ORACLE) { + assertEquals(new RowId(new Object[] { 0 + offset }).toString(), + container.firstItemId().toString()); + } else { + assertEquals(new RowId(new Object[] { 0 + offset }), + container.firstItemId()); + } + } + + @Test + public void lastItemId_table5000Rows_returnsItemId4999() + throws SQLException { + DataGenerator.addFiveThousandPeople(connectionPool); + + if (SQLTestsConstants.db == DB.ORACLE) { + assertEquals(new RowId(new Object[] { 4999 + offset }).toString(), + container.lastItemId().toString()); + } else { + assertEquals(new RowId(new Object[] { 4999 + offset }), + container.lastItemId()); + } + } + + @Test + public void isFirstId_tableActualFirstId_returnsTrue() throws SQLException { + if (SQLTestsConstants.db == DB.ORACLE) { + assertTrue(container.isFirstId( + new RowId(new Object[] { new BigDecimal(0 + offset) }))); + } else { + assertTrue(container + .isFirstId(new RowId(new Object[] { 0 + offset }))); + } + } + + @Test + public void isFirstId_tableSecondId_returnsFalse() throws SQLException { + if (SQLTestsConstants.db == DB.ORACLE) { + Assert.assertFalse(container.isFirstId( + new RowId(new Object[] { new BigDecimal(1 + offset) }))); + } else { + Assert.assertFalse(container + .isFirstId(new RowId(new Object[] { 1 + offset }))); + } + } + + @Test + public void isLastId_tableSecondId_returnsFalse() throws SQLException { + if (SQLTestsConstants.db == DB.ORACLE) { + Assert.assertFalse(container.isLastId( + new RowId(new Object[] { new BigDecimal(1 + offset) }))); + } else { + Assert.assertFalse( + container.isLastId(new RowId(new Object[] { 1 + offset }))); + } + } + + @Test + public void isLastId_tableLastId_returnsTrue() throws SQLException { + if (SQLTestsConstants.db == DB.ORACLE) { + assertTrue(container.isLastId( + new RowId(new Object[] { new BigDecimal(3 + offset) }))); + } else { + assertTrue( + container.isLastId(new RowId(new Object[] { 3 + offset }))); + } + } + + @Test + public void isLastId_table5000RowsLastId_returnsTrue() throws SQLException { + DataGenerator.addFiveThousandPeople(connectionPool); + if (SQLTestsConstants.db == DB.ORACLE) { + assertTrue(container.isLastId( + new RowId(new Object[] { new BigDecimal(4999 + offset) }))); + } else { + assertTrue(container + .isLastId(new RowId(new Object[] { 4999 + offset }))); + } + } + + @Test + public void allIdsFound_table5000RowsLastId_shouldSucceed() + throws SQLException { + DataGenerator.addFiveThousandPeople(connectionPool); + + for (int i = 0; i < 5000; i++) { + assertTrue(container.containsId(container.getIdByIndex(i))); + } + } + + @Test + public void allIdsFound_table5000RowsLastId_autoCommit_shouldSucceed() + throws SQLException { + DataGenerator.addFiveThousandPeople(connectionPool); + + container.setAutoCommit(true); + for (int i = 0; i < 5000; i++) { + assertTrue(container.containsId(container.getIdByIndex(i))); + } + } + + @Test + public void refresh_table_sizeShouldUpdate() throws SQLException { + assertEquals(4, container.size()); + DataGenerator.addFiveThousandPeople(connectionPool); + container.refresh(); + assertEquals(5000, container.size()); + } + + @Test + public void refresh_tableWithoutCallingRefresh_sizeShouldNotUpdate() + throws SQLException { + // Yeah, this is a weird one. We're testing that the size doesn't update + // after adding lots of items unless we call refresh inbetween. This to + // make sure that the refresh method actually refreshes stuff and isn't + // a NOP. + assertEquals(4, container.size()); + DataGenerator.addFiveThousandPeople(connectionPool); + assertEquals(4, container.size()); + } + + @Test + public void setAutoCommit_table_shouldSucceed() throws SQLException { + container.setAutoCommit(true); + assertTrue(container.isAutoCommit()); + container.setAutoCommit(false); + Assert.assertFalse(container.isAutoCommit()); + } + + @Test + public void getPageLength_table_returnsDefault100() throws SQLException { + assertEquals(100, container.getPageLength()); + } + + @Test + public void setPageLength_table_shouldSucceed() throws SQLException { + container.setPageLength(20); + assertEquals(20, container.getPageLength()); + container.setPageLength(200); + assertEquals(200, container.getPageLength()); + } + + @Test(expected = UnsupportedOperationException.class) + public void addContainerProperty_normal_isUnsupported() + throws SQLException { + container.addContainerProperty("asdf", String.class, ""); + } + + @Test(expected = UnsupportedOperationException.class) + public void removeContainerProperty_normal_isUnsupported() + throws SQLException { + container.removeContainerProperty("asdf"); + } + + @Test(expected = UnsupportedOperationException.class) + public void addItemObject_normal_isUnsupported() throws SQLException { + container.addItem("asdf"); + } + + @Test(expected = UnsupportedOperationException.class) + public void addItemAfterObjectObject_normal_isUnsupported() + throws SQLException { + container.addItemAfter("asdf", "foo"); + } + + @Test(expected = UnsupportedOperationException.class) + public void addItemAtIntObject_normal_isUnsupported() throws SQLException { + container.addItemAt(2, "asdf"); + } + + @Test(expected = UnsupportedOperationException.class) + public void addItemAtInt_normal_isUnsupported() throws SQLException { + container.addItemAt(2); + } + + @Test(expected = UnsupportedOperationException.class) + public void addItemAfterObject_normal_isUnsupported() throws SQLException { + container.addItemAfter("asdf"); + } + + @Test + public void addItem_tableAddOneNewItem_returnsItemId() throws SQLException { + Object itemId = container.addItem(); + Assert.assertNotNull(itemId); + } + + @Test + public void addItem_tableAddOneNewItem_autoCommit_returnsFinalItemId() + throws SQLException { + container.setAutoCommit(true); + Object itemId = container.addItem(); + Assert.assertNotNull(itemId); + assertTrue(itemId instanceof RowId); + Assert.assertFalse(itemId instanceof TemporaryRowId); + } + + @Test + public void addItem_tableAddOneNewItem_autoCommit_sizeIsIncreased() + throws SQLException { + container.setAutoCommit(true); + int originalSize = container.size(); + container.addItem(); + assertEquals(originalSize + 1, container.size()); + } + + @Test + public void addItem_tableAddOneNewItem_shouldChangeSize() + throws SQLException { + int size = container.size(); + container.addItem(); + assertEquals(size + 1, container.size()); + } + + @Test + public void addItem_tableAddTwoNewItems_shouldChangeSize() + throws SQLException { + int size = container.size(); + Object id1 = container.addItem(); + Object id2 = container.addItem(); + assertEquals(size + 2, container.size()); + Assert.assertNotSame(id1, id2); + Assert.assertFalse(id1.equals(id2)); + } + + @Test + public void nextItemId_tableNewlyAddedItem_returnsNewlyAdded() + throws SQLException { + Object lastId = container.lastItemId(); + Object id = container.addItem(); + assertEquals(id, container.nextItemId(lastId)); + } + + @Test + public void lastItemId_tableNewlyAddedItem_returnsNewlyAdded() + throws SQLException { + Object lastId = container.lastItemId(); + Object id = container.addItem(); + assertEquals(id, container.lastItemId()); + Assert.assertNotSame(lastId, container.lastItemId()); + } + + @Test + public void indexOfId_tableNewlyAddedItem_returnsFour() + throws SQLException { + Object id = container.addItem(); + assertEquals(4, container.indexOfId(id)); + } + + @Test + public void getItem_tableNewlyAddedItem_returnsNewlyAdded() + throws SQLException { + Object id = container.addItem(); + Assert.assertNotNull(container.getItem(id)); + } + + @Test + public void getItemIds_tableNewlyAddedItem_containsNewlyAdded() + throws SQLException { + Object id = container.addItem(); + assertTrue(container.getItemIds().contains(id)); + } + + @Test + public void getContainerProperty_tableNewlyAddedItem_returnsPropertyOfNewlyAddedItem() + throws SQLException { + Object id = container.addItem(); + Item item = container.getItem(id); + item.getItemProperty(NAME).setValue("asdf"); + assertEquals("asdf", + container.getContainerProperty(id, NAME).getValue()); + } + + @Test + public void containsId_tableNewlyAddedItem_returnsTrue() + throws SQLException { + Object id = container.addItem(); + + assertTrue(container.containsId(id)); + } + + @Test + public void prevItemId_tableTwoNewlyAddedItems_returnsFirstAddedItem() + throws SQLException { + Object id1 = container.addItem(); + Object id2 = container.addItem(); + + assertEquals(id1, container.prevItemId(id2)); + } + + @Test + public void firstItemId_tableEmptyResultSet_returnsFirstAddedItem() + throws SQLException { + SQLContainer garbageContainer = getGarbageContainer(); + + Object id = garbageContainer.addItem(); + + Assert.assertSame(id, garbageContainer.firstItemId()); + } + + @Test + public void isFirstId_tableEmptyResultSet_returnsFirstAddedItem() + throws SQLException { + SQLContainer garbageContainer = getGarbageContainer(); + + Object id = garbageContainer.addItem(); + + assertTrue(garbageContainer.isFirstId(id)); + } + + @Test + public void isLastId_tableOneItemAdded_returnsTrueForAddedItem() + throws SQLException { + Object id = container.addItem(); + + assertTrue(container.isLastId(id)); + } + + @Test + public void isLastId_tableTwoItemsAdded_returnsTrueForLastAddedItem() + throws SQLException { + container.addItem(); + + Object id2 = container.addItem(); + + assertTrue(container.isLastId(id2)); + } + + @Test + public void getIdByIndex_tableOneItemAddedLastIndexInContainer_returnsAddedItem() + throws SQLException { + Object id = container.addItem(); + + assertEquals(id, container.getIdByIndex(container.size() - 1)); + } + + @Test + public void removeItem_tableNoAddedItems_removesItemFromContainer() + throws SQLException { + int originalSize = container.size(); + Object id = container.firstItemId(); + + assertTrue(container.removeItem(id)); + + Assert.assertNotSame(id, container.firstItemId()); + assertEquals(originalSize - 1, container.size()); + } + + @Test + public void containsId_tableRemovedItem_returnsFalse() throws SQLException { + Object id = container.firstItemId(); + assertTrue(container.removeItem(id)); + Assert.assertFalse(container.containsId(id)); + } + + @Test + public void removeItem_tableOneAddedItem_removesTheAddedItem() + throws SQLException { + Object id = container.addItem(); + int size = container.size(); + + assertTrue(container.removeItem(id)); + Assert.assertFalse(container.containsId(id)); + assertEquals(size - 1, container.size()); + } + + @Test + public void getItem_tableItemRemoved_returnsNull() throws SQLException { + Object id = container.firstItemId(); + + assertTrue(container.removeItem(id)); + Assert.assertNull(container.getItem(id)); + } + + @Test + public void getItem_tableAddedItemRemoved_returnsNull() + throws SQLException { + Object id = container.addItem(); + + Assert.assertNotNull(container.getItem(id)); + assertTrue(container.removeItem(id)); + Assert.assertNull(container.getItem(id)); + } + + @Test + public void getItemIds_tableItemRemoved_shouldNotContainRemovedItem() + throws SQLException { + Object id = container.firstItemId(); + + assertTrue(container.getItemIds().contains(id)); + assertTrue(container.removeItem(id)); + Assert.assertFalse(container.getItemIds().contains(id)); + } + + @Test + public void getItemIds_tableAddedItemRemoved_shouldNotContainRemovedItem() + throws SQLException { + Object id = container.addItem(); + + assertTrue(container.getItemIds().contains(id)); + assertTrue(container.removeItem(id)); + Assert.assertFalse(container.getItemIds().contains(id)); + } + + @Test + public void containsId_tableItemRemoved_returnsFalse() throws SQLException { + Object id = container.firstItemId(); + + assertTrue(container.containsId(id)); + assertTrue(container.removeItem(id)); + Assert.assertFalse(container.containsId(id)); + } + + @Test + public void containsId_tableAddedItemRemoved_returnsFalse() + throws SQLException { + Object id = container.addItem(); + + assertTrue(container.containsId(id)); + assertTrue(container.removeItem(id)); + Assert.assertFalse(container.containsId(id)); + } + + @Test + public void nextItemId_tableItemRemoved_skipsRemovedItem() + throws SQLException { + Object first = container.getIdByIndex(0); + Object second = container.getIdByIndex(1); + Object third = container.getIdByIndex(2); + + assertTrue(container.removeItem(second)); + assertEquals(third, container.nextItemId(first)); + } + + @Test + public void nextItemId_tableAddedItemRemoved_skipsRemovedItem() + throws SQLException { + Object first = container.lastItemId(); + Object second = container.addItem(); + Object third = container.addItem(); + + assertTrue(container.removeItem(second)); + assertEquals(third, container.nextItemId(first)); + } + + @Test + public void prevItemId_tableItemRemoved_skipsRemovedItem() + throws SQLException { + Object first = container.getIdByIndex(0); + Object second = container.getIdByIndex(1); + Object third = container.getIdByIndex(2); + + assertTrue(container.removeItem(second)); + assertEquals(first, container.prevItemId(third)); + } + + @Test + public void prevItemId_tableAddedItemRemoved_skipsRemovedItem() + throws SQLException { + Object first = container.lastItemId(); + Object second = container.addItem(); + Object third = container.addItem(); + + assertTrue(container.removeItem(second)); + assertEquals(first, container.prevItemId(third)); + } + + @Test + public void firstItemId_tableFirstItemRemoved_resultChanges() + throws SQLException { + Object first = container.firstItemId(); + + assertTrue(container.removeItem(first)); + Assert.assertNotSame(first, container.firstItemId()); + } + + @Test + public void firstItemId_tableNewlyAddedFirstItemRemoved_resultChanges() + throws SQLException { + SQLContainer garbageContainer = getGarbageContainer(); + + Object first = garbageContainer.addItem(); + Object second = garbageContainer.addItem(); + + Assert.assertSame(first, garbageContainer.firstItemId()); + assertTrue(garbageContainer.removeItem(first)); + Assert.assertSame(second, garbageContainer.firstItemId()); + } + + @Test + public void lastItemId_tableLastItemRemoved_resultChanges() + throws SQLException { + Object last = container.lastItemId(); + + assertTrue(container.removeItem(last)); + Assert.assertNotSame(last, container.lastItemId()); + } + + @Test + public void lastItemId_tableAddedLastItemRemoved_resultChanges() + throws SQLException { + Object last = container.addItem(); + + Assert.assertSame(last, container.lastItemId()); + assertTrue(container.removeItem(last)); + Assert.assertNotSame(last, container.lastItemId()); + } + + @Test + public void isFirstId_tableFirstItemRemoved_returnsFalse() + throws SQLException { + Object first = container.firstItemId(); + + assertTrue(container.removeItem(first)); + Assert.assertFalse(container.isFirstId(first)); + } + + @Test + public void isFirstId_tableAddedFirstItemRemoved_returnsFalse() + throws SQLException { + SQLContainer garbageContainer = getGarbageContainer(); + + Object first = garbageContainer.addItem(); + garbageContainer.addItem(); + + Assert.assertSame(first, garbageContainer.firstItemId()); + assertTrue(garbageContainer.removeItem(first)); + Assert.assertFalse(garbageContainer.isFirstId(first)); + } + + @Test + public void isLastId_tableLastItemRemoved_returnsFalse() + throws SQLException { + Object last = container.lastItemId(); + + assertTrue(container.removeItem(last)); + Assert.assertFalse(container.isLastId(last)); + } + + @Test + public void isLastId_tableAddedLastItemRemoved_returnsFalse() + throws SQLException { + Object last = container.addItem(); + + Assert.assertSame(last, container.lastItemId()); + assertTrue(container.removeItem(last)); + Assert.assertFalse(container.isLastId(last)); + } + + @Test + public void indexOfId_tableItemRemoved_returnsNegOne() throws SQLException { + Object id = container.getIdByIndex(2); + + assertTrue(container.removeItem(id)); + assertEquals(-1, container.indexOfId(id)); + } + + @Test + public void indexOfId_tableAddedItemRemoved_returnsNegOne() + throws SQLException { + Object id = container.addItem(); + + assertTrue(container.indexOfId(id) != -1); + assertTrue(container.removeItem(id)); + assertEquals(-1, container.indexOfId(id)); + } + + @Test + public void getIdByIndex_tableItemRemoved_resultChanges() + throws SQLException { + Object id = container.getIdByIndex(2); + + assertTrue(container.removeItem(id)); + Assert.assertNotSame(id, container.getIdByIndex(2)); + } + + @Test + public void getIdByIndex_tableAddedItemRemoved_resultChanges() + throws SQLException { + Object id = container.addItem(); + container.addItem(); + int index = container.indexOfId(id); + + assertTrue(container.removeItem(id)); + Assert.assertNotSame(id, container.getIdByIndex(index)); + } + + @Test + public void removeAllItems_table_shouldSucceed() throws SQLException { + assertTrue(container.removeAllItems()); + assertEquals(0, container.size()); + } + + @Test + public void removeAllItems_tableAddedItems_shouldSucceed() + throws SQLException { + container.addItem(); + container.addItem(); + + assertTrue(container.removeAllItems()); + assertEquals(0, container.size()); + } + + // Set timeout to ensure there is no infinite looping (#12882) + @Test(timeout = 1000) + public void removeAllItems_manyItems_commit_shouldSucceed() + throws SQLException { + final int itemNumber = (SQLContainer.CACHE_RATIO + 1) + * SQLContainer.DEFAULT_PAGE_LENGTH + 1; + + container.removeAllItems(); + + assertEquals(container.size(), 0); + for (int i = 0; i < itemNumber; ++i) { + container.addItem(); + } + container.commit(); + assertEquals(container.size(), itemNumber); + assertTrue(container.removeAllItems()); + container.commit(); + assertEquals(container.size(), 0); + } + + @Test + public void commit_tableAddedItem_shouldBeWrittenToDB() + throws SQLException { + Object id = container.addItem(); + container.getContainerProperty(id, NAME).setValue("New Name"); + + assertTrue(id instanceof TemporaryRowId); + Assert.assertSame(id, container.lastItemId()); + container.commit(); + Assert.assertFalse(container.lastItemId() instanceof TemporaryRowId); + assertEquals("New Name", container + .getContainerProperty(container.lastItemId(), NAME).getValue()); + } + + @Test + public void commit_tableTwoAddedItems_shouldBeWrittenToDB() + throws SQLException { + Object id = container.addItem(); + Object id2 = container.addItem(); + container.getContainerProperty(id, NAME).setValue("Herbert"); + container.getContainerProperty(id2, NAME).setValue("Larry"); + assertTrue(id2 instanceof TemporaryRowId); + Assert.assertSame(id2, container.lastItemId()); + container.commit(); + Object nextToLast = container.getIdByIndex(container.size() - 2); + + Assert.assertFalse(nextToLast instanceof TemporaryRowId); + assertEquals("Herbert", + container.getContainerProperty(nextToLast, NAME).getValue()); + Assert.assertFalse(container.lastItemId() instanceof TemporaryRowId); + assertEquals("Larry", container + .getContainerProperty(container.lastItemId(), NAME).getValue()); + } + + @Test + public void commit_tableRemovedItem_shouldBeRemovedFromDB() + throws SQLException { + Object last = container.lastItemId(); + container.removeItem(last); + container.commit(); + + Assert.assertFalse(last.equals(container.lastItemId())); + } + + @Test + public void commit_tableLastItemUpdated_shouldUpdateRowInDB() + throws SQLException { + Object last = container.lastItemId(); + container.getContainerProperty(last, NAME).setValue("Donald"); + container.commit(); + + assertEquals("Donald", container + .getContainerProperty(container.lastItemId(), NAME).getValue()); + } + + @Test + public void commit_removeModifiedItem_shouldSucceed() throws SQLException { + int size = container.size(); + Object key = container.firstItemId(); + Item row = container.getItem(key); + row.getItemProperty(NAME).setValue("Pekka"); + + assertTrue(container.removeItem(key)); + container.commit(); + assertEquals(size - 1, container.size()); + } + + @Test + public void rollback_tableItemAdded_discardsAddedItem() + throws SQLException { + int size = container.size(); + Object id = container.addItem(); + container.getContainerProperty(id, NAME).setValue("foo"); + assertEquals(size + 1, container.size()); + container.rollback(); + assertEquals(size, container.size()); + Assert.assertFalse("foo".equals( + container.getContainerProperty(container.lastItemId(), NAME) + .getValue())); + } + + @Test + public void rollback_tableItemRemoved_restoresRemovedItem() + throws SQLException { + int size = container.size(); + Object last = container.lastItemId(); + container.removeItem(last); + assertEquals(size - 1, container.size()); + container.rollback(); + assertEquals(size, container.size()); + assertEquals(last, container.lastItemId()); + } + + @Test + public void rollback_tableItemChanged_discardsChanges() + throws SQLException { + Object last = container.lastItemId(); + container.getContainerProperty(last, NAME).setValue("foo"); + container.rollback(); + Assert.assertFalse("foo".equals( + container.getContainerProperty(container.lastItemId(), NAME) + .getValue())); + } + + @Test + public void itemChangeNotification_table_isModifiedReturnsTrue() + throws SQLException { + Assert.assertFalse(container.isModified()); + RowItem last = (RowItem) container.getItem(container.lastItemId()); + container.itemChangeNotification(last); + assertTrue(container.isModified()); + } + + @Test + public void itemSetChangeListeners_table_shouldFire() throws SQLException { + ItemSetChangeListener listener = EasyMock + .createMock(ItemSetChangeListener.class); + listener.containerItemSetChange(EasyMock.isA(ItemSetChangeEvent.class)); + EasyMock.replay(listener); + + container.addListener(listener); + container.addItem(); + + EasyMock.verify(listener); + } + + @Test + public void itemSetChangeListeners_tableItemRemoved_shouldFire() + throws SQLException { + ItemSetChangeListener listener = EasyMock + .createMock(ItemSetChangeListener.class); + listener.containerItemSetChange(EasyMock.isA(ItemSetChangeEvent.class)); + EasyMock.expectLastCall().anyTimes(); + EasyMock.replay(listener); + + container.addListener(listener); + container.removeItem(container.lastItemId()); + + EasyMock.verify(listener); + } + + @Test + public void removeListener_table_shouldNotFire() throws SQLException { + ItemSetChangeListener listener = EasyMock + .createMock(ItemSetChangeListener.class); + EasyMock.replay(listener); + + container.addListener(listener); + container.removeListener(listener); + container.addItem(); + + EasyMock.verify(listener); + } + + @Test + public void isModified_tableRemovedItem_returnsTrue() throws SQLException { + Assert.assertFalse(container.isModified()); + container.removeItem(container.lastItemId()); + assertTrue(container.isModified()); + } + + @Test + public void isModified_tableAddedItem_returnsTrue() throws SQLException { + Assert.assertFalse(container.isModified()); + container.addItem(); + assertTrue(container.isModified()); + } + + @Test + public void isModified_tableChangedItem_returnsTrue() throws SQLException { + Assert.assertFalse(container.isModified()); + container.getContainerProperty(container.lastItemId(), NAME) + .setValue("foo"); + assertTrue(container.isModified()); + } + + @Test + public void getSortableContainerPropertyIds_table_returnsAllPropertyIds() + throws SQLException { + Collection<?> sortableIds = container.getSortableContainerPropertyIds(); + assertTrue(sortableIds.contains(ID)); + assertTrue(sortableIds.contains(NAME)); + assertTrue(sortableIds.contains("AGE")); + assertEquals(3, sortableIds.size()); + if (SQLTestsConstants.db == DB.MSSQL + || SQLTestsConstants.db == DB.ORACLE) { + Assert.assertFalse(sortableIds.contains("rownum")); + } + } + + @Test + public void addOrderBy_table_shouldReorderResults() throws SQLException { + // Ville, Kalle, Pelle, Börje + assertEquals("Ville", + container.getContainerProperty(container.firstItemId(), NAME) + .getValue()); + assertEquals("Börje", container + .getContainerProperty(container.lastItemId(), NAME).getValue()); + + container.addOrderBy(new OrderBy(NAME, true)); + // Börje, Kalle, Pelle, Ville + assertEquals("Börje", + container.getContainerProperty(container.firstItemId(), NAME) + .getValue()); + assertEquals("Ville", container + .getContainerProperty(container.lastItemId(), NAME).getValue()); + } + + @Test(expected = IllegalArgumentException.class) + public void addOrderBy_tableIllegalColumn_shouldFail() throws SQLException { + container.addOrderBy(new OrderBy("asdf", true)); + } + + @Test + public void sort_table_sortsByName() throws SQLException { + // Ville, Kalle, Pelle, Börje + assertEquals("Ville", + container.getContainerProperty(container.firstItemId(), NAME) + .getValue()); + assertEquals("Börje", container + .getContainerProperty(container.lastItemId(), NAME).getValue()); + + container.sort(new Object[] { NAME }, new boolean[] { true }); + + // Börje, Kalle, Pelle, Ville + assertEquals("Börje", + container.getContainerProperty(container.firstItemId(), NAME) + .getValue()); + assertEquals("Ville", container + .getContainerProperty(container.lastItemId(), NAME).getValue()); + } + + @Test + public void addFilter_table_filtersResults() throws SQLException { + // Ville, Kalle, Pelle, Börje + assertEquals(4, container.size()); + assertEquals("Börje", container + .getContainerProperty(container.lastItemId(), NAME).getValue()); + + container.addContainerFilter(new Like(NAME, "%lle")); + // Ville, Kalle, Pelle + assertEquals(3, container.size()); + assertEquals("Pelle", container + .getContainerProperty(container.lastItemId(), NAME).getValue()); + } + + @Test + public void addContainerFilter_filtersResults() throws SQLException { + // Ville, Kalle, Pelle, Börje + assertEquals(4, container.size()); + + container.addContainerFilter(NAME, "Vi", false, false); + + // Ville + assertEquals(1, container.size()); + assertEquals("Ville", container + .getContainerProperty(container.lastItemId(), NAME).getValue()); + } + + @Test + public void addContainerFilter_ignoreCase_filtersResults() + throws SQLException { + // Ville, Kalle, Pelle, Börje + assertEquals(4, container.size()); + + container.addContainerFilter(NAME, "vi", true, false); + + // Ville + assertEquals(1, container.size()); + assertEquals("Ville", container + .getContainerProperty(container.lastItemId(), NAME).getValue()); + } + + @Test + public void removeAllContainerFilters_table_noFiltering() + throws SQLException { + // Ville, Kalle, Pelle, Börje + assertEquals(4, container.size()); + + container.addContainerFilter(NAME, "Vi", false, false); + + // Ville + assertEquals(1, container.size()); + assertEquals("Ville", container + .getContainerProperty(container.lastItemId(), NAME).getValue()); + + container.removeAllContainerFilters(); + + assertEquals(4, container.size()); + assertEquals("Börje", container + .getContainerProperty(container.lastItemId(), NAME).getValue()); + } + + @Test + public void removeContainerFilters_table_noFiltering() throws SQLException { + // Ville, Kalle, Pelle, Börje + assertEquals(4, container.size()); + + container.addContainerFilter(NAME, "Vi", false, false); + + // Ville + assertEquals(1, container.size()); + assertEquals("Ville", container + .getContainerProperty(container.lastItemId(), NAME).getValue()); + + container.removeContainerFilters(NAME); + + assertEquals(4, container.size()); + assertEquals("Börje", container + .getContainerProperty(container.lastItemId(), NAME).getValue()); + } + + @Test + public void addFilter_tableBufferedItems_alsoFiltersBufferedItems() + throws SQLException { + // Ville, Kalle, Pelle, Börje + assertEquals(4, container.size()); + assertEquals("Börje", container + .getContainerProperty(container.lastItemId(), NAME).getValue()); + + Object id1 = container.addItem(); + container.getContainerProperty(id1, NAME).setValue("Palle"); + Object id2 = container.addItem(); + container.getContainerProperty(id2, NAME).setValue("Bengt"); + + container.addContainerFilter(new Like(NAME, "%lle")); + + // Ville, Kalle, Pelle, Palle + assertEquals(4, container.size()); + assertEquals("Ville", + container.getContainerProperty(container.getIdByIndex(0), NAME) + .getValue()); + assertEquals("Kalle", + container.getContainerProperty(container.getIdByIndex(1), NAME) + .getValue()); + assertEquals("Pelle", + container.getContainerProperty(container.getIdByIndex(2), NAME) + .getValue()); + assertEquals("Palle", + container.getContainerProperty(container.getIdByIndex(3), NAME) + .getValue()); + + try { + container.getIdByIndex(4); + Assert.fail( + "SQLContainer.getIdByIndex() returned a value for an index beyond the end of the container"); + } catch (IndexOutOfBoundsException e) { + // should throw exception - item is filtered out + } + Assert.assertNull(container.nextItemId(container.getIdByIndex(3))); + + Assert.assertFalse(container.containsId(id2)); + Assert.assertFalse(container.getItemIds().contains(id2)); + + Assert.assertNull(container.getItem(id2)); + assertEquals(-1, container.indexOfId(id2)); + + Assert.assertNotSame(id2, container.lastItemId()); + Assert.assertSame(id1, container.lastItemId()); + } + + @Test + public void sort_tableBufferedItems_sortsBufferedItemsLastInOrderAdded() + throws SQLException { + // Ville, Kalle, Pelle, Börje + assertEquals("Ville", + container.getContainerProperty(container.firstItemId(), NAME) + .getValue()); + assertEquals("Börje", container + .getContainerProperty(container.lastItemId(), NAME).getValue()); + + Object id1 = container.addItem(); + container.getContainerProperty(id1, NAME).setValue("Wilbert"); + Object id2 = container.addItem(); + container.getContainerProperty(id2, NAME).setValue("Albert"); + + container.sort(new Object[] { NAME }, new boolean[] { true }); + + // Börje, Kalle, Pelle, Ville, Wilbert, Albert + assertEquals("Börje", + container.getContainerProperty(container.firstItemId(), NAME) + .getValue()); + assertEquals("Wilbert", + container.getContainerProperty( + container.getIdByIndex(container.size() - 2), NAME) + .getValue()); + assertEquals("Albert", container + .getContainerProperty(container.lastItemId(), NAME).getValue()); + } + +} diff --git a/compatibility-server/src/test/java/com/vaadin/data/util/sqlcontainer/SQLContainerTest.java b/compatibility-server/src/test/java/com/vaadin/data/util/sqlcontainer/SQLContainerTest.java new file mode 100644 index 0000000000..d499ceed92 --- /dev/null +++ b/compatibility-server/src/test/java/com/vaadin/data/util/sqlcontainer/SQLContainerTest.java @@ -0,0 +1,2469 @@ +package com.vaadin.data.util.sqlcontainer; + +import java.math.BigDecimal; +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.logging.Handler; +import java.util.logging.LogRecord; +import java.util.logging.Logger; + +import org.easymock.EasyMock; +import org.easymock.IAnswer; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import com.vaadin.data.Container.Filter; +import com.vaadin.data.Container.ItemSetChangeEvent; +import com.vaadin.data.Container.ItemSetChangeListener; +import com.vaadin.data.Item; +import com.vaadin.data.util.filter.Compare.Equal; +import com.vaadin.data.util.filter.Like; +import com.vaadin.data.util.sqlcontainer.SQLTestsConstants.DB; +import com.vaadin.data.util.sqlcontainer.connection.JDBCConnectionPool; +import com.vaadin.data.util.sqlcontainer.query.FreeformQuery; +import com.vaadin.data.util.sqlcontainer.query.FreeformQueryDelegate; +import com.vaadin.data.util.sqlcontainer.query.FreeformStatementDelegate; +import com.vaadin.data.util.sqlcontainer.query.OrderBy; +import com.vaadin.data.util.sqlcontainer.query.ValidatingSimpleJDBCConnectionPool; +import com.vaadin.data.util.sqlcontainer.query.generator.MSSQLGenerator; +import com.vaadin.data.util.sqlcontainer.query.generator.OracleGenerator; +import com.vaadin.data.util.sqlcontainer.query.generator.SQLGenerator; +import com.vaadin.data.util.sqlcontainer.query.generator.StatementHelper; +import com.vaadin.data.util.sqlcontainer.query.generator.filter.QueryBuilder; + +public class SQLContainerTest { + private static final int offset = SQLTestsConstants.offset; + private JDBCConnectionPool connectionPool; + + @Before + public void setUp() throws SQLException { + + try { + connectionPool = new ValidatingSimpleJDBCConnectionPool( + SQLTestsConstants.dbDriver, SQLTestsConstants.dbURL, + SQLTestsConstants.dbUser, SQLTestsConstants.dbPwd, 2, 2); + } catch (SQLException e) { + e.printStackTrace(); + Assert.fail(e.getMessage()); + } + + DataGenerator.addPeopleToDatabase(connectionPool); + } + + @After + public void tearDown() { + if (connectionPool != null) { + connectionPool.destroy(); + } + } + + @Test + public void constructor_withFreeformQuery_shouldSucceed() + throws SQLException { + new SQLContainer(new FreeformQuery("SELECT * FROM people", + connectionPool, "ID")); + } + + @Test(expected = SQLException.class) + public void constructor_withIllegalFreeformQuery_shouldFail() + throws SQLException { + SQLContainer c = new SQLContainer( + new FreeformQuery("SELECT * FROM asdf", connectionPool, "ID")); + c.getItem(c.firstItemId()); + } + + @Test + public void containsId_withFreeformQueryAndExistingId_returnsTrue() + throws SQLException { + SQLContainer container = new SQLContainer(new FreeformQuery( + "SELECT * FROM people", connectionPool, "ID")); + Assert.assertTrue(container.containsId(new RowId(new Object[] { 1 }))); + } + + @Test + public void containsId_withFreeformQueryAndNonexistingId_returnsFalse() + throws SQLException { + SQLContainer container = new SQLContainer(new FreeformQuery( + "SELECT * FROM people", connectionPool, "ID")); + Assert.assertFalse( + container.containsId(new RowId(new Object[] { 1337 }))); + } + + @Test + public void getContainerProperty_freeformExistingItemIdAndPropertyId_returnsProperty() + throws SQLException { + SQLContainer container = new SQLContainer(new FreeformQuery( + "SELECT * FROM people", connectionPool, "ID")); + if (SQLTestsConstants.db == DB.ORACLE) { + Assert.assertEquals("Ville", + container.getContainerProperty(new RowId( + new Object[] { new BigDecimal(0 + offset) }), + "NAME").getValue()); + } else { + Assert.assertEquals("Ville", + container.getContainerProperty( + new RowId(new Object[] { 0 + offset }), "NAME") + .getValue()); + } + } + + @Test + public void getContainerProperty_freeformExistingItemIdAndNonexistingPropertyId_returnsNull() + throws SQLException { + SQLContainer container = new SQLContainer(new FreeformQuery( + "SELECT * FROM people", connectionPool, "ID")); + Assert.assertNull(container.getContainerProperty( + new RowId(new Object[] { 1 + offset }), "asdf")); + } + + @Test + public void getContainerProperty_freeformNonexistingItemId_returnsNull() + throws SQLException { + SQLContainer container = new SQLContainer(new FreeformQuery( + "SELECT * FROM people", connectionPool, "ID")); + Assert.assertNull(container.getContainerProperty( + new RowId(new Object[] { 1337 + offset }), "NAME")); + } + + @Test + public void getContainerPropertyIds_freeform_returnsIDAndNAME() + throws SQLException { + SQLContainer container = new SQLContainer(new FreeformQuery( + "SELECT * FROM people", connectionPool, "ID")); + Collection<?> propertyIds = container.getContainerPropertyIds(); + Assert.assertEquals(3, propertyIds.size()); + Assert.assertArrayEquals(new String[] { "ID", "NAME", "AGE" }, + propertyIds.toArray()); + } + + @Test + public void getItem_freeformExistingItemId_returnsItem() + throws SQLException { + SQLContainer container = new SQLContainer(new FreeformQuery( + "SELECT * FROM people", connectionPool, "ID")); + Item item; + if (SQLTestsConstants.db == DB.ORACLE) { + item = container.getItem( + new RowId(new Object[] { new BigDecimal(0 + offset) })); + } else { + item = container.getItem(new RowId(new Object[] { 0 + offset })); + } + Assert.assertNotNull(item); + Assert.assertEquals("Ville", item.getItemProperty("NAME").getValue()); + } + + @Test + public void nextItemNullAtEnd_freeformExistingItem() throws SQLException { + SQLContainer container = new SQLContainer(new FreeformQuery( + "SELECT * FROM people", connectionPool, "ID")); + Object lastItemId = container.lastItemId(); + Object afterLast = container.nextItemId(lastItemId); + Assert.assertNull(afterLast); + } + + @Test + public void prevItemNullAtStart_freeformExistingItem() throws SQLException { + SQLContainer container = new SQLContainer(new FreeformQuery( + "SELECT * FROM people", connectionPool, "ID")); + Object firstItemId = container.firstItemId(); + Object beforeFirst = container.prevItemId(firstItemId); + Assert.assertNull(beforeFirst); + } + + @Test + public void getItem_freeform5000RowsWithParameter1337_returnsItemWithId1337() + throws SQLException { + DataGenerator.addFiveThousandPeople(connectionPool); + SQLContainer container = new SQLContainer(new FreeformQuery( + "SELECT * FROM people", connectionPool, "ID")); + Item item; + if (SQLTestsConstants.db == DB.ORACLE) { + item = container.getItem( + new RowId(new Object[] { new BigDecimal(1337 + offset) })); + Assert.assertNotNull(item); + Assert.assertEquals(new BigDecimal(1337 + offset), + item.getItemProperty("ID").getValue()); + } else { + item = container.getItem(new RowId(new Object[] { 1337 + offset })); + Assert.assertNotNull(item); + Assert.assertEquals(1337 + offset, + item.getItemProperty("ID").getValue()); + } + Assert.assertEquals("Person 1337", + item.getItemProperty("NAME").getValue()); + } + + @Test + public void getItemIds_freeform_returnsItemIdsWithKeys0through3() + throws SQLException { + SQLContainer container = new SQLContainer(new FreeformQuery( + "SELECT * FROM people", connectionPool, "ID")); + Collection<?> itemIds = container.getItemIds(); + Assert.assertEquals(4, itemIds.size()); + RowId zero = new RowId(new Object[] { 0 + offset }); + RowId one = new RowId(new Object[] { 1 + offset }); + RowId two = new RowId(new Object[] { 2 + offset }); + RowId three = new RowId(new Object[] { 3 + offset }); + if (SQLTestsConstants.db == DB.ORACLE) { + String[] correct = new String[] { "1", "2", "3", "4" }; + List<String> oracle = new ArrayList<String>(); + for (Object o : itemIds) { + oracle.add(o.toString()); + } + Assert.assertArrayEquals(correct, oracle.toArray()); + } else { + Assert.assertArrayEquals(new Object[] { zero, one, two, three }, + itemIds.toArray()); + } + } + + @Test + public void getType_freeformNAMEPropertyId_returnsString() + throws SQLException { + SQLContainer container = new SQLContainer(new FreeformQuery( + "SELECT * FROM people", connectionPool, "ID")); + Assert.assertEquals(String.class, container.getType("NAME")); + } + + @Test + public void getType_freeformIDPropertyId_returnsInteger() + throws SQLException { + SQLContainer container = new SQLContainer(new FreeformQuery( + "SELECT * FROM people", connectionPool, "ID")); + if (SQLTestsConstants.db == DB.ORACLE) { + Assert.assertEquals(BigDecimal.class, container.getType("ID")); + } else { + Assert.assertEquals(Integer.class, container.getType("ID")); + } + } + + @Test + public void getType_freeformNonexistingPropertyId_returnsNull() + throws SQLException { + SQLContainer container = new SQLContainer(new FreeformQuery( + "SELECT * FROM people", connectionPool, "ID")); + Assert.assertNull(container.getType("asdf")); + } + + @Test + public void size_freeform_returnsFour() throws SQLException { + SQLContainer container = new SQLContainer(new FreeformQuery( + "SELECT * FROM people", connectionPool, "ID")); + Assert.assertEquals(4, container.size()); + } + + @Test + public void size_freeformOneAddedItem_returnsFive() throws SQLException { + Connection conn = connectionPool.reserveConnection(); + Statement statement = conn.createStatement(); + if (SQLTestsConstants.db == DB.MSSQL) { + statement.executeUpdate("insert into people values('Bengt', '42')"); + } else { + statement.executeUpdate( + "insert into people values(default, 'Bengt', '42')"); + } + statement.close(); + conn.commit(); + connectionPool.releaseConnection(conn); + + SQLContainer container = new SQLContainer(new FreeformQuery( + "SELECT * FROM people", connectionPool, "ID")); + Assert.assertEquals(5, container.size()); + } + + @Test + public void indexOfId_freeformWithParameterThree_returnsThree() + throws SQLException { + SQLContainer container = new SQLContainer(new FreeformQuery( + "SELECT * FROM people", connectionPool, "ID")); + if (SQLTestsConstants.db == DB.ORACLE) { + Assert.assertEquals(3, container.indexOfId( + new RowId(new Object[] { new BigDecimal(3 + offset) }))); + } else { + Assert.assertEquals(3, container + .indexOfId(new RowId(new Object[] { 3 + offset }))); + } + } + + @Test + public void indexOfId_freeform5000RowsWithParameter1337_returns1337() + throws SQLException { + DataGenerator.addFiveThousandPeople(connectionPool); + SQLContainer container = new SQLContainer( + new FreeformQuery("SELECT * FROM people ORDER BY \"ID\" ASC", + connectionPool, "ID")); + if (SQLTestsConstants.db == DB.ORACLE) { + container.getItem( + new RowId(new Object[] { new BigDecimal(1337 + offset) })); + Assert.assertEquals(1337, container.indexOfId( + new RowId(new Object[] { new BigDecimal(1337 + offset) }))); + } else { + container.getItem(new RowId(new Object[] { 1337 + offset })); + Assert.assertEquals(1337, container + .indexOfId(new RowId(new Object[] { 1337 + offset }))); + } + } + + @Test + public void getIdByIndex_freeform5000rowsIndex1337_returnsRowId1337() + throws SQLException { + DataGenerator.addFiveThousandPeople(connectionPool); + SQLContainer container = new SQLContainer( + new FreeformQuery("SELECT * FROM people ORDER BY \"ID\" ASC", + connectionPool, "ID")); + Object itemId = container.getIdByIndex(1337); + if (SQLTestsConstants.db == DB.ORACLE) { + Assert.assertEquals( + new RowId(new Object[] { new BigDecimal(1337 + offset) }), + itemId); + } else { + Assert.assertEquals(new RowId(new Object[] { 1337 + offset }), + itemId); + } + } + + @SuppressWarnings("unchecked") + @Test + public void getIdByIndex_freeformWithPaging5000rowsIndex1337_returnsRowId1337() + throws SQLException { + DataGenerator.addFiveThousandPeople(connectionPool); + FreeformQuery query = new FreeformQuery("SELECT * FROM people", + connectionPool, "ID"); + FreeformQueryDelegate delegate = EasyMock + .createMock(FreeformQueryDelegate.class); + EasyMock.expect( + delegate.getQueryString(EasyMock.anyInt(), EasyMock.anyInt())) + .andAnswer(new IAnswer<String>() { + @Override + public String answer() throws Throwable { + Object[] args = EasyMock.getCurrentArguments(); + int offset = (Integer) (args[0]); + int limit = (Integer) (args[1]); + if (SQLTestsConstants.db == DB.MSSQL) { + int start = offset + 1; + int end = offset + limit + 1; + String q = "SELECT * FROM (SELECT row_number() OVER" + + " ( ORDER BY \"ID\" ASC) AS rownum, * FROM people)" + + " AS a WHERE a.rownum BETWEEN " + start + + " AND " + end; + return q; + } else if (SQLTestsConstants.db == DB.ORACLE) { + int start = offset + 1; + int end = offset + limit + 1; + String q = "SELECT * FROM (SELECT x.*, ROWNUM AS r FROM" + + " (SELECT * FROM people ORDER BY \"ID\" ASC) x) " + + " WHERE r BETWEEN " + start + " AND " + + end; + return q; + } else { + return "SELECT * FROM people LIMIT " + limit + + " OFFSET " + offset; + } + } + }).anyTimes(); + delegate.setFilters(null); + EasyMock.expectLastCall().anyTimes(); + delegate.setFilters(EasyMock.isA(List.class)); + EasyMock.expectLastCall().anyTimes(); + delegate.setOrderBy(null); + EasyMock.expectLastCall().anyTimes(); + delegate.setOrderBy(EasyMock.isA(List.class)); + EasyMock.expectLastCall().anyTimes(); + EasyMock.expect(delegate.getCountQuery()) + .andThrow(new UnsupportedOperationException()).anyTimes(); + EasyMock.replay(delegate); + query.setDelegate(delegate); + SQLContainer container = new SQLContainer(query); + Object itemId = container.getIdByIndex(1337); + if (SQLTestsConstants.db == DB.ORACLE) { + Assert.assertEquals( + new RowId(new Object[] { 1337 + offset }).toString(), + itemId.toString()); + } else { + Assert.assertEquals(new RowId(new Object[] { 1337 + offset }), + itemId); + } + } + + @Test + public void nextItemId_freeformCurrentItem1337_returnsItem1338() + throws SQLException { + DataGenerator.addFiveThousandPeople(connectionPool); + SQLContainer container = new SQLContainer( + new FreeformQuery("SELECT * FROM people ORDER BY \"ID\" ASC", + connectionPool, "ID")); + Object itemId = container.getIdByIndex(1337); + if (SQLTestsConstants.db == DB.ORACLE) { + Assert.assertEquals( + new RowId(new Object[] { 1338 + offset }).toString(), + container.nextItemId(itemId).toString()); + } else { + Assert.assertEquals(new RowId(new Object[] { 1338 + offset }), + container.nextItemId(itemId)); + } + } + + @Test + public void prevItemId_freeformCurrentItem1337_returns1336() + throws SQLException { + DataGenerator.addFiveThousandPeople(connectionPool); + SQLContainer container = new SQLContainer( + new FreeformQuery("SELECT * FROM people ORDER BY \"ID\" ASC", + connectionPool, "ID")); + Object itemId = container.getIdByIndex(1337); + if (SQLTestsConstants.db == DB.ORACLE) { + Assert.assertEquals( + new RowId(new Object[] { 1336 + offset }).toString(), + container.prevItemId(itemId).toString()); + } else { + Assert.assertEquals(new RowId(new Object[] { 1336 + offset }), + container.prevItemId(itemId)); + } + } + + @Test + public void firstItemId_freeform_returnsItemId0() throws SQLException { + SQLContainer container = new SQLContainer(new FreeformQuery( + "SELECT * FROM people", connectionPool, "ID")); + if (SQLTestsConstants.db == DB.ORACLE) { + Assert.assertEquals( + new RowId(new Object[] { 0 + offset }).toString(), + container.firstItemId().toString()); + } else { + Assert.assertEquals(new RowId(new Object[] { 0 + offset }), + container.firstItemId()); + } + } + + @Test + public void lastItemId_freeform5000Rows_returnsItemId4999() + throws SQLException { + DataGenerator.addFiveThousandPeople(connectionPool); + + SQLContainer container = new SQLContainer( + new FreeformQuery("SELECT * FROM people ORDER BY \"ID\" ASC", + connectionPool, "ID")); + if (SQLTestsConstants.db == DB.ORACLE) { + Assert.assertEquals( + new RowId(new Object[] { 4999 + offset }).toString(), + container.lastItemId().toString()); + } else { + Assert.assertEquals(new RowId(new Object[] { 4999 + offset }), + container.lastItemId()); + } + } + + @Test + public void isFirstId_freeformActualFirstId_returnsTrue() + throws SQLException { + SQLContainer container = new SQLContainer(new FreeformQuery( + "SELECT * FROM people", connectionPool, "ID")); + if (SQLTestsConstants.db == DB.ORACLE) { + Assert.assertTrue(container.isFirstId( + new RowId(new Object[] { new BigDecimal(0 + offset) }))); + } else { + Assert.assertTrue(container + .isFirstId(new RowId(new Object[] { 0 + offset }))); + } + } + + @Test + public void isFirstId_freeformSecondId_returnsFalse() throws SQLException { + SQLContainer container = new SQLContainer(new FreeformQuery( + "SELECT * FROM people", connectionPool, "ID")); + if (SQLTestsConstants.db == DB.ORACLE) { + Assert.assertFalse(container.isFirstId( + new RowId(new Object[] { new BigDecimal(1 + offset) }))); + } else { + Assert.assertFalse(container + .isFirstId(new RowId(new Object[] { 1 + offset }))); + } + } + + @Test + public void isLastId_freeformSecondId_returnsFalse() throws SQLException { + SQLContainer container = new SQLContainer(new FreeformQuery( + "SELECT * FROM people", connectionPool, "ID")); + if (SQLTestsConstants.db == DB.ORACLE) { + Assert.assertFalse(container.isLastId( + new RowId(new Object[] { new BigDecimal(1 + offset) }))); + } else { + Assert.assertFalse( + container.isLastId(new RowId(new Object[] { 1 + offset }))); + } + } + + @Test + public void isLastId_freeformLastId_returnsTrue() throws SQLException { + SQLContainer container = new SQLContainer(new FreeformQuery( + "SELECT * FROM people", connectionPool, "ID")); + if (SQLTestsConstants.db == DB.ORACLE) { + Assert.assertTrue(container.isLastId( + new RowId(new Object[] { new BigDecimal(3 + offset) }))); + } else { + Assert.assertTrue( + container.isLastId(new RowId(new Object[] { 3 + offset }))); + } + } + + @Test + public void isLastId_freeform5000RowsLastId_returnsTrue() + throws SQLException { + DataGenerator.addFiveThousandPeople(connectionPool); + SQLContainer container = new SQLContainer( + new FreeformQuery("SELECT * FROM people ORDER BY \"ID\" ASC", + connectionPool, "ID")); + if (SQLTestsConstants.db == DB.ORACLE) { + Assert.assertTrue(container.isLastId( + new RowId(new Object[] { new BigDecimal(4999 + offset) }))); + } else { + Assert.assertTrue(container + .isLastId(new RowId(new Object[] { 4999 + offset }))); + } + } + + @Test + public void refresh_freeform_sizeShouldUpdate() throws SQLException { + SQLContainer container = new SQLContainer(new FreeformQuery( + "SELECT * FROM people", connectionPool, "ID")); + Assert.assertEquals(4, container.size()); + DataGenerator.addFiveThousandPeople(connectionPool); + container.refresh(); + Assert.assertEquals(5000, container.size()); + } + + @Test + public void refresh_freeformWithoutCallingRefresh_sizeShouldNotUpdate() + throws SQLException { + // Yeah, this is a weird one. We're testing that the size doesn't update + // after adding lots of items unless we call refresh inbetween. This to + // make sure that the refresh method actually refreshes stuff and isn't + // a NOP. + SQLContainer container = new SQLContainer(new FreeformQuery( + "SELECT * FROM people", connectionPool, "ID")); + Assert.assertEquals(4, container.size()); + DataGenerator.addFiveThousandPeople(connectionPool); + Assert.assertEquals(4, container.size()); + } + + @Test + public void setAutoCommit_freeform_shouldSucceed() throws SQLException { + SQLContainer container = new SQLContainer(new FreeformQuery( + "SELECT * FROM people", connectionPool, "ID")); + container.setAutoCommit(true); + Assert.assertTrue(container.isAutoCommit()); + container.setAutoCommit(false); + Assert.assertFalse(container.isAutoCommit()); + } + + @Test + public void getPageLength_freeform_returnsDefault100() throws SQLException { + SQLContainer container = new SQLContainer(new FreeformQuery( + "SELECT * FROM people", connectionPool, "ID")); + Assert.assertEquals(100, container.getPageLength()); + } + + @Test + public void setPageLength_freeform_shouldSucceed() throws SQLException { + SQLContainer container = new SQLContainer(new FreeformQuery( + "SELECT * FROM people", connectionPool, "ID")); + container.setPageLength(20); + Assert.assertEquals(20, container.getPageLength()); + container.setPageLength(200); + Assert.assertEquals(200, container.getPageLength()); + } + + @Test(expected = UnsupportedOperationException.class) + public void addContainerProperty_normal_isUnsupported() + throws SQLException { + SQLContainer container = new SQLContainer(new FreeformQuery( + "SELECT * FROM people", connectionPool, "ID")); + container.addContainerProperty("asdf", String.class, ""); + } + + @Test(expected = UnsupportedOperationException.class) + public void removeContainerProperty_normal_isUnsupported() + throws SQLException { + SQLContainer container = new SQLContainer(new FreeformQuery( + "SELECT * FROM people", connectionPool, "ID")); + container.removeContainerProperty("asdf"); + } + + @Test(expected = UnsupportedOperationException.class) + public void addItemObject_normal_isUnsupported() throws SQLException { + SQLContainer container = new SQLContainer(new FreeformQuery( + "SELECT * FROM people", connectionPool, "ID")); + container.addItem("asdf"); + } + + @Test(expected = UnsupportedOperationException.class) + public void addItemAfterObjectObject_normal_isUnsupported() + throws SQLException { + SQLContainer container = new SQLContainer(new FreeformQuery( + "SELECT * FROM people", connectionPool, "ID")); + container.addItemAfter("asdf", "foo"); + } + + @Test(expected = UnsupportedOperationException.class) + public void addItemAtIntObject_normal_isUnsupported() throws SQLException { + SQLContainer container = new SQLContainer(new FreeformQuery( + "SELECT * FROM people", connectionPool, "ID")); + container.addItemAt(2, "asdf"); + } + + @Test(expected = UnsupportedOperationException.class) + public void addItemAtInt_normal_isUnsupported() throws SQLException { + SQLContainer container = new SQLContainer(new FreeformQuery( + "SELECT * FROM people", connectionPool, "ID")); + container.addItemAt(2); + } + + @Test(expected = UnsupportedOperationException.class) + public void addItemAfterObject_normal_isUnsupported() throws SQLException { + SQLContainer container = new SQLContainer(new FreeformQuery( + "SELECT * FROM people", connectionPool, "ID")); + container.addItemAfter("asdf"); + } + + @Test + public void addItem_freeformAddOneNewItem_returnsItemId() + throws SQLException { + SQLContainer container = new SQLContainer(new FreeformQuery( + "SELECT * FROM people", connectionPool, "ID")); + Object itemId = container.addItem(); + Assert.assertNotNull(itemId); + } + + @Test + public void addItem_freeformAddOneNewItem_shouldChangeSize() + throws SQLException { + SQLContainer container = new SQLContainer(new FreeformQuery( + "SELECT * FROM people", connectionPool, "ID")); + int size = container.size(); + container.addItem(); + Assert.assertEquals(size + 1, container.size()); + } + + @Test + public void addItem_freeformAddTwoNewItems_shouldChangeSize() + throws SQLException { + SQLContainer container = new SQLContainer(new FreeformQuery( + "SELECT * FROM people", connectionPool, "ID")); + int size = container.size(); + Object id1 = container.addItem(); + Object id2 = container.addItem(); + Assert.assertEquals(size + 2, container.size()); + Assert.assertNotSame(id1, id2); + Assert.assertFalse(id1.equals(id2)); + } + + @Test + public void nextItemId_freeformNewlyAddedItem_returnsNewlyAdded() + throws SQLException { + SQLContainer container = new SQLContainer(new FreeformQuery( + "SELECT * FROM people", connectionPool, "ID")); + Object lastId = container.lastItemId(); + Object id = container.addItem(); + Assert.assertEquals(id, container.nextItemId(lastId)); + } + + @Test + public void lastItemId_freeformNewlyAddedItem_returnsNewlyAdded() + throws SQLException { + SQLContainer container = new SQLContainer(new FreeformQuery( + "SELECT * FROM people", connectionPool, "ID")); + Object lastId = container.lastItemId(); + Object id = container.addItem(); + Assert.assertEquals(id, container.lastItemId()); + Assert.assertNotSame(lastId, container.lastItemId()); + } + + @Test + public void indexOfId_freeformNewlyAddedItem_returnsFour() + throws SQLException { + SQLContainer container = new SQLContainer(new FreeformQuery( + "SELECT * FROM people", connectionPool, "ID")); + Object id = container.addItem(); + Assert.assertEquals(4, container.indexOfId(id)); + } + + @Test + public void getItem_freeformNewlyAddedItem_returnsNewlyAdded() + throws SQLException { + SQLContainer container = new SQLContainer(new FreeformQuery( + "SELECT * FROM people", connectionPool, "ID")); + Object id = container.addItem(); + Assert.assertNotNull(container.getItem(id)); + } + + @Test + public void getItem_freeformNewlyAddedItemAndFiltered_returnsNull() + throws SQLException { + SQLContainer container = new SQLContainer(new FreeformQuery( + "SELECT * FROM people", connectionPool, "ID")); + container.addContainerFilter(new Equal("NAME", "asdf")); + Object id = container.addItem(); + Assert.assertNull(container.getItem(id)); + } + + @Test + public void getItemUnfiltered_freeformNewlyAddedItemAndFiltered_returnsNewlyAdded() + throws SQLException { + SQLContainer container = new SQLContainer(new FreeformQuery( + "SELECT * FROM people", connectionPool, "ID")); + container.addContainerFilter(new Equal("NAME", "asdf")); + Object id = container.addItem(); + Assert.assertNotNull(container.getItemUnfiltered(id)); + } + + @Test + public void getItemIds_freeformNewlyAddedItem_containsNewlyAdded() + throws SQLException { + SQLContainer container = new SQLContainer(new FreeformQuery( + "SELECT * FROM people", connectionPool, "ID")); + Object id = container.addItem(); + Assert.assertTrue(container.getItemIds().contains(id)); + } + + @Test + public void getContainerProperty_freeformNewlyAddedItem_returnsPropertyOfNewlyAddedItem() + throws SQLException { + SQLContainer container = new SQLContainer(new FreeformQuery( + "SELECT * FROM people", connectionPool, "ID")); + Object id = container.addItem(); + Item item = container.getItem(id); + item.getItemProperty("NAME").setValue("asdf"); + Assert.assertEquals("asdf", + container.getContainerProperty(id, "NAME").getValue()); + } + + @Test + public void containsId_freeformNewlyAddedItem_returnsTrue() + throws SQLException { + SQLContainer container = new SQLContainer(new FreeformQuery( + "SELECT * FROM people", connectionPool, "ID")); + Object id = container.addItem(); + Assert.assertTrue(container.containsId(id)); + } + + @Test + public void prevItemId_freeformTwoNewlyAddedItems_returnsFirstAddedItem() + throws SQLException { + SQLContainer container = new SQLContainer(new FreeformQuery( + "SELECT * FROM people", connectionPool, "ID")); + Object id1 = container.addItem(); + Object id2 = container.addItem(); + Assert.assertEquals(id1, container.prevItemId(id2)); + } + + @Test + public void firstItemId_freeformEmptyResultSet_returnsFirstAddedItem() + throws SQLException { + DataGenerator.createGarbage(connectionPool); + SQLContainer container = new SQLContainer(new FreeformQuery( + "SELECT * FROM GARBAGE", connectionPool, "ID")); + Object id = container.addItem(); + Assert.assertSame(id, container.firstItemId()); + } + + @Test + public void isFirstId_freeformEmptyResultSet_returnsFirstAddedItem() + throws SQLException { + DataGenerator.createGarbage(connectionPool); + SQLContainer container = new SQLContainer(new FreeformQuery( + "SELECT * FROM GARBAGE", connectionPool, "ID")); + Object id = container.addItem(); + Assert.assertTrue(container.isFirstId(id)); + } + + @Test + public void isLastId_freeformOneItemAdded_returnsTrueForAddedItem() + throws SQLException { + SQLContainer container = new SQLContainer(new FreeformQuery( + "SELECT * FROM people", connectionPool, "ID")); + Object id = container.addItem(); + Assert.assertTrue(container.isLastId(id)); + } + + @Test + public void isLastId_freeformTwoItemsAdded_returnsTrueForLastAddedItem() + throws SQLException { + SQLContainer container = new SQLContainer(new FreeformQuery( + "SELECT * FROM people", connectionPool, "ID")); + container.addItem(); + Object id2 = container.addItem(); + Assert.assertTrue(container.isLastId(id2)); + } + + @Test + public void getIdByIndex_freeformOneItemAddedLastIndexInContainer_returnsAddedItem() + throws SQLException { + SQLContainer container = new SQLContainer(new FreeformQuery( + "SELECT * FROM people", connectionPool, "ID")); + Object id = container.addItem(); + Assert.assertEquals(id, container.getIdByIndex(container.size() - 1)); + } + + @Test + public void removeItem_freeformNoAddedItems_removesItemFromContainer() + throws SQLException { + SQLContainer container = new SQLContainer(new FreeformQuery( + "SELECT * FROM people", connectionPool, "ID")); + int size = container.size(); + Object id = container.firstItemId(); + Assert.assertTrue(container.removeItem(id)); + Assert.assertNotSame(id, container.firstItemId()); + Assert.assertEquals(size - 1, container.size()); + } + + @Test + public void containsId_freeformRemovedItem_returnsFalse() + throws SQLException { + SQLContainer container = new SQLContainer(new FreeformQuery( + "SELECT * FROM people", connectionPool, "ID")); + Object id = container.firstItemId(); + Assert.assertTrue(container.removeItem(id)); + Assert.assertFalse(container.containsId(id)); + } + + @Test + public void containsId_unknownObject() throws SQLException { + + Handler ensureNoLogging = new Handler() { + + @Override + public void publish(LogRecord record) { + Assert.fail("No messages should be logged"); + + } + + @Override + public void flush() { + } + + @Override + public void close() throws SecurityException { + } + }; + + SQLContainer container = new SQLContainer(new FreeformQuery( + "SELECT * FROM people", connectionPool, "ID")); + Logger logger = Logger.getLogger(SQLContainer.class.getName()); + + logger.addHandler(ensureNoLogging); + try { + Assert.assertFalse(container.containsId(new Object())); + } finally { + logger.removeHandler(ensureNoLogging); + } + } + + @Test + public void removeItem_freeformOneAddedItem_removesTheAddedItem() + throws SQLException { + SQLContainer container = new SQLContainer(new FreeformQuery( + "SELECT * FROM people", connectionPool, "ID")); + Object id = container.addItem(); + int size = container.size(); + Assert.assertTrue(container.removeItem(id)); + Assert.assertFalse(container.containsId(id)); + Assert.assertEquals(size - 1, container.size()); + } + + @Test + public void getItem_freeformItemRemoved_returnsNull() throws SQLException { + SQLContainer container = new SQLContainer(new FreeformQuery( + "SELECT * FROM people", connectionPool, "ID")); + Object id = container.firstItemId(); + Assert.assertTrue(container.removeItem(id)); + Assert.assertNull(container.getItem(id)); + } + + @Test + public void getItem_freeformAddedItemRemoved_returnsNull() + throws SQLException { + SQLContainer container = new SQLContainer(new FreeformQuery( + "SELECT * FROM people", connectionPool, "ID")); + Object id = container.addItem(); + Assert.assertNotNull(container.getItem(id)); + Assert.assertTrue(container.removeItem(id)); + Assert.assertNull(container.getItem(id)); + } + + @Test + public void getItemIds_freeformItemRemoved_shouldNotContainRemovedItem() + throws SQLException { + SQLContainer container = new SQLContainer(new FreeformQuery( + "SELECT * FROM people", connectionPool, "ID")); + Object id = container.firstItemId(); + Assert.assertTrue(container.getItemIds().contains(id)); + Assert.assertTrue(container.removeItem(id)); + Assert.assertFalse(container.getItemIds().contains(id)); + } + + @Test + public void getItemIds_freeformAddedItemRemoved_shouldNotContainRemovedItem() + throws SQLException { + SQLContainer container = new SQLContainer(new FreeformQuery( + "SELECT * FROM people", connectionPool, "ID")); + Object id = container.addItem(); + Assert.assertTrue(container.getItemIds().contains(id)); + Assert.assertTrue(container.removeItem(id)); + Assert.assertFalse(container.getItemIds().contains(id)); + } + + @Test + public void containsId_freeformItemRemoved_returnsFalse() + throws SQLException { + SQLContainer container = new SQLContainer(new FreeformQuery( + "SELECT * FROM people", connectionPool, "ID")); + Object id = container.firstItemId(); + Assert.assertTrue(container.containsId(id)); + Assert.assertTrue(container.removeItem(id)); + Assert.assertFalse(container.containsId(id)); + } + + @Test + public void containsId_freeformAddedItemRemoved_returnsFalse() + throws SQLException { + SQLContainer container = new SQLContainer(new FreeformQuery( + "SELECT * FROM people", connectionPool, "ID")); + Object id = container.addItem(); + Assert.assertTrue(container.containsId(id)); + Assert.assertTrue(container.removeItem(id)); + Assert.assertFalse(container.containsId(id)); + } + + @Test + public void nextItemId_freeformItemRemoved_skipsRemovedItem() + throws SQLException { + SQLContainer container = new SQLContainer(new FreeformQuery( + "SELECT * FROM people", connectionPool, "ID")); + Object first = container.getIdByIndex(0); + Object second = container.getIdByIndex(1); + Object third = container.getIdByIndex(2); + Assert.assertTrue(container.removeItem(second)); + Assert.assertEquals(third, container.nextItemId(first)); + } + + @Test + public void nextItemId_freeformAddedItemRemoved_skipsRemovedItem() + throws SQLException { + SQLContainer container = new SQLContainer(new FreeformQuery( + "SELECT * FROM people", connectionPool, "ID")); + Object first = container.lastItemId(); + Object second = container.addItem(); + Object third = container.addItem(); + Assert.assertTrue(container.removeItem(second)); + Assert.assertEquals(third, container.nextItemId(first)); + } + + @Test + public void prevItemId_freeformItemRemoved_skipsRemovedItem() + throws SQLException { + SQLContainer container = new SQLContainer(new FreeformQuery( + "SELECT * FROM people", connectionPool, "ID")); + Object first = container.getIdByIndex(0); + Object second = container.getIdByIndex(1); + Object third = container.getIdByIndex(2); + Assert.assertTrue(container.removeItem(second)); + Assert.assertEquals(first, container.prevItemId(third)); + } + + @Test + public void prevItemId_freeformAddedItemRemoved_skipsRemovedItem() + throws SQLException { + SQLContainer container = new SQLContainer(new FreeformQuery( + "SELECT * FROM people", connectionPool, "ID")); + Object first = container.lastItemId(); + Object second = container.addItem(); + Object third = container.addItem(); + Assert.assertTrue(container.removeItem(second)); + Assert.assertEquals(first, container.prevItemId(third)); + } + + @Test + public void firstItemId_freeformFirstItemRemoved_resultChanges() + throws SQLException { + SQLContainer container = new SQLContainer(new FreeformQuery( + "SELECT * FROM people", connectionPool, "ID")); + Object first = container.firstItemId(); + Assert.assertTrue(container.removeItem(first)); + Assert.assertNotSame(first, container.firstItemId()); + } + + @Test + public void firstItemId_freeformNewlyAddedFirstItemRemoved_resultChanges() + throws SQLException { + DataGenerator.createGarbage(connectionPool); + SQLContainer container = new SQLContainer(new FreeformQuery( + "SELECT * FROM GARBAGE", connectionPool, "ID")); + Object first = container.addItem(); + Object second = container.addItem(); + Assert.assertSame(first, container.firstItemId()); + Assert.assertTrue(container.removeItem(first)); + Assert.assertSame(second, container.firstItemId()); + } + + @Test + public void lastItemId_freeformLastItemRemoved_resultChanges() + throws SQLException { + SQLContainer container = new SQLContainer(new FreeformQuery( + "SELECT * FROM people", connectionPool, "ID")); + Object last = container.lastItemId(); + Assert.assertTrue(container.removeItem(last)); + Assert.assertNotSame(last, container.lastItemId()); + } + + @Test + public void lastItemId_freeformAddedLastItemRemoved_resultChanges() + throws SQLException { + SQLContainer container = new SQLContainer(new FreeformQuery( + "SELECT * FROM people", connectionPool, "ID")); + Object last = container.addItem(); + Assert.assertSame(last, container.lastItemId()); + Assert.assertTrue(container.removeItem(last)); + Assert.assertNotSame(last, container.lastItemId()); + } + + @Test + public void isFirstId_freeformFirstItemRemoved_returnsFalse() + throws SQLException { + SQLContainer container = new SQLContainer(new FreeformQuery( + "SELECT * FROM people", connectionPool, "ID")); + Object first = container.firstItemId(); + Assert.assertTrue(container.removeItem(first)); + Assert.assertFalse(container.isFirstId(first)); + } + + @Test + public void isFirstId_freeformAddedFirstItemRemoved_returnsFalse() + throws SQLException { + DataGenerator.createGarbage(connectionPool); + SQLContainer container = new SQLContainer(new FreeformQuery( + "SELECT * FROM GARBAGE", connectionPool, "ID")); + Object first = container.addItem(); + container.addItem(); + Assert.assertSame(first, container.firstItemId()); + Assert.assertTrue(container.removeItem(first)); + Assert.assertFalse(container.isFirstId(first)); + } + + @Test + public void isLastId_freeformLastItemRemoved_returnsFalse() + throws SQLException { + SQLContainer container = new SQLContainer(new FreeformQuery( + "SELECT * FROM people", connectionPool, "ID")); + Object last = container.lastItemId(); + Assert.assertTrue(container.removeItem(last)); + Assert.assertFalse(container.isLastId(last)); + } + + @Test + public void isLastId_freeformAddedLastItemRemoved_returnsFalse() + throws SQLException { + SQLContainer container = new SQLContainer(new FreeformQuery( + "SELECT * FROM people", connectionPool, "ID")); + Object last = container.addItem(); + Assert.assertSame(last, container.lastItemId()); + Assert.assertTrue(container.removeItem(last)); + Assert.assertFalse(container.isLastId(last)); + } + + @Test + public void indexOfId_freeformItemRemoved_returnsNegOne() + throws SQLException { + SQLContainer container = new SQLContainer(new FreeformQuery( + "SELECT * FROM people", connectionPool, "ID")); + Object id = container.getIdByIndex(2); + Assert.assertTrue(container.removeItem(id)); + Assert.assertEquals(-1, container.indexOfId(id)); + } + + @Test + public void indexOfId_freeformAddedItemRemoved_returnsNegOne() + throws SQLException { + SQLContainer container = new SQLContainer(new FreeformQuery( + "SELECT * FROM people", connectionPool, "ID")); + Object id = container.addItem(); + Assert.assertTrue(container.indexOfId(id) != -1); + Assert.assertTrue(container.removeItem(id)); + Assert.assertEquals(-1, container.indexOfId(id)); + } + + @Test + public void getIdByIndex_freeformItemRemoved_resultChanges() + throws SQLException { + SQLContainer container = new SQLContainer(new FreeformQuery( + "SELECT * FROM people", connectionPool, "ID")); + Object id = container.getIdByIndex(2); + Assert.assertTrue(container.removeItem(id)); + Assert.assertNotSame(id, container.getIdByIndex(2)); + } + + @Test + public void getIdByIndex_freeformAddedItemRemoved_resultChanges() + throws SQLException { + SQLContainer container = new SQLContainer(new FreeformQuery( + "SELECT * FROM people", connectionPool, "ID")); + Object id = container.addItem(); + container.addItem(); + int index = container.indexOfId(id); + Assert.assertTrue(container.removeItem(id)); + Assert.assertNotSame(id, container.getIdByIndex(index)); + } + + @Test + public void removeAllItems_freeform_shouldSucceed() throws SQLException { + SQLContainer container = new SQLContainer(new FreeformQuery( + "SELECT * FROM people", connectionPool, "ID")); + Assert.assertTrue(container.removeAllItems()); + Assert.assertEquals(0, container.size()); + } + + @Test + public void removeAllItems_freeformAddedItems_shouldSucceed() + throws SQLException { + SQLContainer container = new SQLContainer(new FreeformQuery( + "SELECT * FROM people", connectionPool, "ID")); + container.addItem(); + container.addItem(); + Assert.assertTrue(container.removeAllItems()); + Assert.assertEquals(0, container.size()); + } + + @SuppressWarnings("unchecked") + @Test + public void commit_freeformAddedItem_shouldBeWrittenToDB() + throws SQLException { + FreeformQueryDelegate delegate = EasyMock + .createMock(FreeformQueryDelegate.class); + EasyMock.expect(delegate.storeRow(EasyMock.isA(Connection.class), + EasyMock.isA(RowItem.class))).andAnswer(new IAnswer<Integer>() { + @Override + public Integer answer() throws Throwable { + Connection conn = (Connection) EasyMock + .getCurrentArguments()[0]; + RowItem item = (RowItem) EasyMock + .getCurrentArguments()[1]; + Statement statement = conn.createStatement(); + if (SQLTestsConstants.db == DB.MSSQL) { + statement + .executeUpdate("insert into people values('" + + item.getItemProperty("NAME") + .getValue() + + "', '" + + item.getItemProperty("AGE") + .getValue() + + "')"); + } else { + statement.executeUpdate( + "insert into people values(default, '" + + item.getItemProperty("NAME") + .getValue() + + "', '" + + item.getItemProperty("AGE") + .getValue() + + "')"); + } + statement.close(); + conn.commit(); + connectionPool.releaseConnection(conn); + return 1; + } + }).anyTimes(); + EasyMock.expect( + delegate.getQueryString(EasyMock.anyInt(), EasyMock.anyInt())) + .andAnswer(new IAnswer<String>() { + @Override + public String answer() throws Throwable { + Object[] args = EasyMock.getCurrentArguments(); + int offset = (Integer) (args[0]); + int limit = (Integer) (args[1]); + if (SQLTestsConstants.db == DB.MSSQL) { + int start = offset + 1; + int end = offset + limit + 1; + String q = "SELECT * FROM (SELECT row_number() OVER" + + " ( ORDER BY \"ID\" ASC) AS rownum, * FROM people)" + + " AS a WHERE a.rownum BETWEEN " + start + + " AND " + end; + return q; + } else if (SQLTestsConstants.db == DB.ORACLE) { + int start = offset + 1; + int end = offset + limit + 1; + String q = "SELECT * FROM (SELECT x.*, ROWNUM AS r FROM" + + " (SELECT * FROM people ORDER BY \"ID\" ASC) x) " + + " WHERE r BETWEEN " + start + " AND " + + end; + return q; + } else { + return "SELECT * FROM people LIMIT " + limit + + " OFFSET " + offset; + } + } + }).anyTimes(); + delegate.setFilters(null); + EasyMock.expectLastCall().anyTimes(); + delegate.setFilters(EasyMock.isA(List.class)); + EasyMock.expectLastCall().anyTimes(); + delegate.setOrderBy(null); + EasyMock.expectLastCall().anyTimes(); + delegate.setOrderBy(EasyMock.isA(List.class)); + EasyMock.expectLastCall().anyTimes(); + EasyMock.expect(delegate.getCountQuery()) + .andThrow(new UnsupportedOperationException()).anyTimes(); + + FreeformQuery query = new FreeformQuery("SELECT * FROM people", + connectionPool, "ID"); + query.setDelegate(delegate); + EasyMock.replay(delegate); + SQLContainer container = new SQLContainer(query); + Object id = container.addItem(); + container.getContainerProperty(id, "NAME").setValue("New Name"); + container.getContainerProperty(id, "AGE").setValue(30); + Assert.assertTrue(id instanceof TemporaryRowId); + Assert.assertSame(id, container.lastItemId()); + container.commit(); + Assert.assertFalse(container.lastItemId() instanceof TemporaryRowId); + Assert.assertEquals("New Name", + container.getContainerProperty(container.lastItemId(), "NAME") + .getValue()); + EasyMock.verify(delegate); + } + + @SuppressWarnings("unchecked") + @Test + public void commit_freeformTwoAddedItems_shouldBeWrittenToDB() + throws SQLException { + FreeformQueryDelegate delegate = EasyMock + .createMock(FreeformQueryDelegate.class); + EasyMock.expect(delegate.storeRow(EasyMock.isA(Connection.class), + EasyMock.isA(RowItem.class))).andAnswer(new IAnswer<Integer>() { + @Override + public Integer answer() throws Throwable { + Connection conn = (Connection) EasyMock + .getCurrentArguments()[0]; + RowItem item = (RowItem) EasyMock + .getCurrentArguments()[1]; + Statement statement = conn.createStatement(); + if (SQLTestsConstants.db == DB.MSSQL) { + statement + .executeUpdate("insert into people values('" + + item.getItemProperty("NAME") + .getValue() + + "', '" + + item.getItemProperty("AGE") + .getValue() + + "')"); + } else { + statement.executeUpdate( + "insert into people values(default, '" + + item.getItemProperty("NAME") + .getValue() + + "', '" + + item.getItemProperty("AGE") + .getValue() + + "')"); + } + statement.close(); + conn.commit(); + connectionPool.releaseConnection(conn); + return 1; + } + }).anyTimes(); + EasyMock.expect( + delegate.getQueryString(EasyMock.anyInt(), EasyMock.anyInt())) + .andAnswer(new IAnswer<String>() { + @Override + public String answer() throws Throwable { + Object[] args = EasyMock.getCurrentArguments(); + int offset = (Integer) (args[0]); + int limit = (Integer) (args[1]); + if (SQLTestsConstants.db == DB.MSSQL) { + int start = offset + 1; + int end = offset + limit + 1; + String q = "SELECT * FROM (SELECT row_number() OVER" + + " ( ORDER BY \"ID\" ASC) AS rownum, * FROM people)" + + " AS a WHERE a.rownum BETWEEN " + start + + " AND " + end; + return q; + } else if (SQLTestsConstants.db == DB.ORACLE) { + int start = offset + 1; + int end = offset + limit + 1; + String q = "SELECT * FROM (SELECT x.*, ROWNUM AS r FROM" + + " (SELECT * FROM people ORDER BY \"ID\" ASC) x) " + + " WHERE r BETWEEN " + start + " AND " + + end; + return q; + } else { + return "SELECT * FROM people LIMIT " + limit + + " OFFSET " + offset; + } + } + }).anyTimes(); + delegate.setFilters(null); + EasyMock.expectLastCall().anyTimes(); + delegate.setFilters(EasyMock.isA(List.class)); + EasyMock.expectLastCall().anyTimes(); + delegate.setOrderBy(null); + EasyMock.expectLastCall().anyTimes(); + delegate.setOrderBy(EasyMock.isA(List.class)); + EasyMock.expectLastCall().anyTimes(); + EasyMock.expect(delegate.getCountQuery()) + .andThrow(new UnsupportedOperationException()).anyTimes(); + + FreeformQuery query = new FreeformQuery("SELECT * FROM people", + connectionPool, "ID"); + query.setDelegate(delegate); + EasyMock.replay(delegate); + SQLContainer container = new SQLContainer(query); + Object id = container.addItem(); + Object id2 = container.addItem(); + container.getContainerProperty(id, "NAME").setValue("Herbert"); + container.getContainerProperty(id, "AGE").setValue(30); + container.getContainerProperty(id2, "NAME").setValue("Larry"); + container.getContainerProperty(id2, "AGE").setValue(50); + Assert.assertTrue(id2 instanceof TemporaryRowId); + Assert.assertSame(id2, container.lastItemId()); + container.commit(); + Object nextToLast = container.getIdByIndex(container.size() - 2); + Assert.assertFalse(nextToLast instanceof TemporaryRowId); + Assert.assertEquals("Herbert", + container.getContainerProperty(nextToLast, "NAME").getValue()); + Assert.assertFalse(container.lastItemId() instanceof TemporaryRowId); + Assert.assertEquals("Larry", + container.getContainerProperty(container.lastItemId(), "NAME") + .getValue()); + EasyMock.verify(delegate); + } + + @SuppressWarnings("unchecked") + @Test + public void commit_freeformRemovedItem_shouldBeRemovedFromDB() + throws SQLException { + FreeformQueryDelegate delegate = EasyMock + .createMock(FreeformQueryDelegate.class); + EasyMock.expect(delegate.removeRow(EasyMock.isA(Connection.class), + EasyMock.isA(RowItem.class))).andAnswer(new IAnswer<Boolean>() { + @Override + public Boolean answer() throws Throwable { + Connection conn = (Connection) EasyMock + .getCurrentArguments()[0]; + RowItem item = (RowItem) EasyMock + .getCurrentArguments()[1]; + Statement statement = conn.createStatement(); + statement.executeUpdate( + "DELETE FROM people WHERE \"ID\"=" + item + .getItemProperty("ID").getValue()); + statement.close(); + return true; + } + }).anyTimes(); + EasyMock.expect( + delegate.getQueryString(EasyMock.anyInt(), EasyMock.anyInt())) + .andAnswer(new IAnswer<String>() { + @Override + public String answer() throws Throwable { + Object[] args = EasyMock.getCurrentArguments(); + int offset = (Integer) (args[0]); + int limit = (Integer) (args[1]); + if (SQLTestsConstants.db == DB.MSSQL) { + int start = offset + 1; + int end = offset + limit + 1; + String q = "SELECT * FROM (SELECT row_number() OVER" + + " ( ORDER BY \"ID\" ASC) AS rownum, * FROM people)" + + " AS a WHERE a.rownum BETWEEN " + start + + " AND " + end; + return q; + } else if (SQLTestsConstants.db == DB.ORACLE) { + int start = offset + 1; + int end = offset + limit + 1; + String q = "SELECT * FROM (SELECT x.*, ROWNUM AS r FROM" + + " (SELECT * FROM people ORDER BY \"ID\" ASC) x) " + + " WHERE r BETWEEN " + start + " AND " + + end; + return q; + } else { + return "SELECT * FROM people LIMIT " + limit + + " OFFSET " + offset; + } + } + }).anyTimes(); + delegate.setFilters(null); + EasyMock.expectLastCall().anyTimes(); + delegate.setFilters(EasyMock.isA(List.class)); + EasyMock.expectLastCall().anyTimes(); + delegate.setOrderBy(null); + EasyMock.expectLastCall().anyTimes(); + delegate.setOrderBy(EasyMock.isA(List.class)); + EasyMock.expectLastCall().anyTimes(); + EasyMock.expect(delegate.getCountQuery()) + .andThrow(new UnsupportedOperationException()).anyTimes(); + + FreeformQuery query = new FreeformQuery("SELECT * FROM people", + connectionPool, "ID"); + query.setDelegate(delegate); + EasyMock.replay(delegate); + SQLContainer container = new SQLContainer(query); + Object last = container.lastItemId(); + container.removeItem(last); + container.commit(); + Assert.assertFalse(last.equals(container.lastItemId())); + EasyMock.verify(delegate); + } + + @SuppressWarnings("unchecked") + @Test + public void commit_freeformLastItemUpdated_shouldUpdateRowInDB() + throws SQLException { + FreeformQueryDelegate delegate = EasyMock + .createMock(FreeformQueryDelegate.class); + EasyMock.expect(delegate.storeRow(EasyMock.isA(Connection.class), + EasyMock.isA(RowItem.class))).andAnswer(new IAnswer<Integer>() { + @Override + public Integer answer() throws Throwable { + Connection conn = (Connection) EasyMock + .getCurrentArguments()[0]; + RowItem item = (RowItem) EasyMock + .getCurrentArguments()[1]; + Statement statement = conn.createStatement(); + statement.executeUpdate("UPDATE people SET \"NAME\"='" + + item.getItemProperty("NAME").getValue() + + "' WHERE \"ID\"=" + + item.getItemProperty("ID").getValue()); + statement.close(); + conn.commit(); + connectionPool.releaseConnection(conn); + return 1; + } + }).anyTimes(); + EasyMock.expect( + delegate.getQueryString(EasyMock.anyInt(), EasyMock.anyInt())) + .andAnswer(new IAnswer<String>() { + @Override + public String answer() throws Throwable { + Object[] args = EasyMock.getCurrentArguments(); + int offset = (Integer) (args[0]); + int limit = (Integer) (args[1]); + if (SQLTestsConstants.db == DB.MSSQL) { + int start = offset + 1; + int end = offset + limit + 1; + String q = "SELECT * FROM (SELECT row_number() OVER" + + " ( ORDER BY \"ID\" ASC) AS rownum, * FROM people)" + + " AS a WHERE a.rownum BETWEEN " + start + + " AND " + end; + return q; + } else if (SQLTestsConstants.db == DB.ORACLE) { + int start = offset + 1; + int end = offset + limit + 1; + String q = "SELECT * FROM (SELECT x.*, ROWNUM AS r FROM" + + " (SELECT * FROM people ORDER BY \"ID\" ASC) x) " + + " WHERE r BETWEEN " + start + " AND " + + end; + return q; + } else { + return "SELECT * FROM people LIMIT " + limit + + " OFFSET " + offset; + } + } + }).anyTimes(); + delegate.setFilters(null); + EasyMock.expectLastCall().anyTimes(); + delegate.setFilters(EasyMock.isA(List.class)); + EasyMock.expectLastCall().anyTimes(); + delegate.setOrderBy(null); + EasyMock.expectLastCall().anyTimes(); + delegate.setOrderBy(EasyMock.isA(List.class)); + EasyMock.expectLastCall().anyTimes(); + EasyMock.expect(delegate.getCountQuery()) + .andThrow(new UnsupportedOperationException()).anyTimes(); + + FreeformQuery query = new FreeformQuery("SELECT * FROM people", + connectionPool, "ID"); + query.setDelegate(delegate); + EasyMock.replay(delegate); + SQLContainer container = new SQLContainer(query); + Object last = container.lastItemId(); + container.getContainerProperty(last, "NAME").setValue("Donald"); + container.commit(); + Assert.assertEquals("Donald", + container.getContainerProperty(container.lastItemId(), "NAME") + .getValue()); + EasyMock.verify(delegate); + } + + @Test + public void rollback_freeformItemAdded_discardsAddedItem() + throws SQLException { + SQLContainer container = new SQLContainer(new FreeformQuery( + "SELECT * FROM people", connectionPool, "ID")); + int size = container.size(); + Object id = container.addItem(); + container.getContainerProperty(id, "NAME").setValue("foo"); + Assert.assertEquals(size + 1, container.size()); + container.rollback(); + Assert.assertEquals(size, container.size()); + Assert.assertFalse("foo".equals( + container.getContainerProperty(container.lastItemId(), "NAME") + .getValue())); + } + + @Test + public void rollback_freeformItemRemoved_restoresRemovedItem() + throws SQLException { + SQLContainer container = new SQLContainer(new FreeformQuery( + "SELECT * FROM people", connectionPool, "ID")); + int size = container.size(); + Object last = container.lastItemId(); + container.removeItem(last); + Assert.assertEquals(size - 1, container.size()); + container.rollback(); + Assert.assertEquals(size, container.size()); + Assert.assertEquals(last, container.lastItemId()); + } + + @Test + public void rollback_freeformItemChanged_discardsChanges() + throws SQLException { + SQLContainer container = new SQLContainer(new FreeformQuery( + "SELECT * FROM people", connectionPool, "ID")); + Object last = container.lastItemId(); + container.getContainerProperty(last, "NAME").setValue("foo"); + container.rollback(); + Assert.assertFalse("foo".equals( + container.getContainerProperty(container.lastItemId(), "NAME") + .getValue())); + } + + @Test + public void itemChangeNotification_freeform_isModifiedReturnsTrue() + throws SQLException { + SQLContainer container = new SQLContainer(new FreeformQuery( + "SELECT * FROM people", connectionPool, "ID")); + Assert.assertFalse(container.isModified()); + RowItem last = (RowItem) container.getItem(container.lastItemId()); + container.itemChangeNotification(last); + Assert.assertTrue(container.isModified()); + } + + @Test + public void itemSetChangeListeners_freeform_shouldFire() + throws SQLException { + SQLContainer container = new SQLContainer(new FreeformQuery( + "SELECT * FROM people", connectionPool, "ID")); + ItemSetChangeListener listener = EasyMock + .createMock(ItemSetChangeListener.class); + listener.containerItemSetChange(EasyMock.isA(ItemSetChangeEvent.class)); + EasyMock.replay(listener); + + container.addListener(listener); + container.addItem(); + + EasyMock.verify(listener); + } + + @Test + public void itemSetChangeListeners_freeformItemRemoved_shouldFire() + throws SQLException { + SQLContainer container = new SQLContainer(new FreeformQuery( + "SELECT * FROM people", connectionPool, "ID")); + ItemSetChangeListener listener = EasyMock + .createMock(ItemSetChangeListener.class); + listener.containerItemSetChange(EasyMock.isA(ItemSetChangeEvent.class)); + EasyMock.expectLastCall().anyTimes(); + EasyMock.replay(listener); + + container.addListener(listener); + container.removeItem(container.lastItemId()); + + EasyMock.verify(listener); + } + + @Test + public void removeListener_freeform_shouldNotFire() throws SQLException { + SQLContainer container = new SQLContainer(new FreeformQuery( + "SELECT * FROM people", connectionPool, "ID")); + ItemSetChangeListener listener = EasyMock + .createMock(ItemSetChangeListener.class); + EasyMock.replay(listener); + + container.addListener(listener); + container.removeListener(listener); + container.addItem(); + + EasyMock.verify(listener); + } + + @Test + public void isModified_freeformRemovedItem_returnsTrue() + throws SQLException { + SQLContainer container = new SQLContainer(new FreeformQuery( + "SELECT * FROM people", connectionPool, "ID")); + Assert.assertFalse(container.isModified()); + container.removeItem(container.lastItemId()); + Assert.assertTrue(container.isModified()); + } + + @Test + public void isModified_freeformAddedItem_returnsTrue() throws SQLException { + SQLContainer container = new SQLContainer(new FreeformQuery( + "SELECT * FROM people", connectionPool, "ID")); + Assert.assertFalse(container.isModified()); + container.addItem(); + Assert.assertTrue(container.isModified()); + } + + @Test + public void isModified_freeformChangedItem_returnsTrue() + throws SQLException { + SQLContainer container = new SQLContainer(new FreeformQuery( + "SELECT * FROM people", connectionPool, "ID")); + Assert.assertFalse(container.isModified()); + container.getContainerProperty(container.lastItemId(), "NAME") + .setValue("foo"); + Assert.assertTrue(container.isModified()); + } + + @Test + public void getSortableContainerPropertyIds_freeform_returnsAllPropertyIds() + throws SQLException { + SQLContainer container = new SQLContainer(new FreeformQuery( + "SELECT * FROM people", connectionPool, "ID")); + Collection<?> sortableIds = container.getSortableContainerPropertyIds(); + Assert.assertTrue(sortableIds.contains("ID")); + Assert.assertTrue(sortableIds.contains("NAME")); + Assert.assertTrue(sortableIds.contains("AGE")); + Assert.assertEquals(3, sortableIds.size()); + } + + @SuppressWarnings("unchecked") + @Test + public void addOrderBy_freeform_shouldReorderResults() throws SQLException { + FreeformQuery query = new FreeformQuery("SELECT * FROM people", + connectionPool, "ID"); + FreeformQueryDelegate delegate = EasyMock + .createMock(FreeformQueryDelegate.class); + final ArrayList<OrderBy> orderBys = new ArrayList<OrderBy>(); + delegate.setFilters(null); + EasyMock.expectLastCall().anyTimes(); + delegate.setFilters(EasyMock.isA(List.class)); + EasyMock.expectLastCall().anyTimes(); + delegate.setOrderBy(null); + EasyMock.expectLastCall().anyTimes(); + delegate.setOrderBy(EasyMock.isA(List.class)); + EasyMock.expectLastCall().andAnswer(new IAnswer<Object>() { + @Override + public Object answer() throws Throwable { + List<OrderBy> orders = (List<OrderBy>) EasyMock + .getCurrentArguments()[0]; + orderBys.clear(); + orderBys.addAll(orders); + return null; + } + }).anyTimes(); + EasyMock.expect( + delegate.getQueryString(EasyMock.anyInt(), EasyMock.anyInt())) + .andAnswer(new IAnswer<String>() { + @Override + public String answer() throws Throwable { + Object[] args = EasyMock.getCurrentArguments(); + int offset = (Integer) (args[0]); + int limit = (Integer) (args[1]); + if (SQLTestsConstants.db == DB.MSSQL) { + SQLGenerator gen = new MSSQLGenerator(); + if (orderBys == null || orderBys.isEmpty()) { + List<OrderBy> ob = new ArrayList<OrderBy>(); + ob.add(new OrderBy("ID", true)); + return gen + .generateSelectQuery("people", null, ob, + offset, limit, null) + .getQueryString(); + } else { + return gen + .generateSelectQuery("people", null, + orderBys, offset, limit, null) + .getQueryString(); + } + } else if (SQLTestsConstants.db == DB.ORACLE) { + SQLGenerator gen = new OracleGenerator(); + if (orderBys == null || orderBys.isEmpty()) { + List<OrderBy> ob = new ArrayList<OrderBy>(); + ob.add(new OrderBy("ID", true)); + return gen + .generateSelectQuery("people", null, ob, + offset, limit, null) + .getQueryString(); + } else { + return gen + .generateSelectQuery("people", null, + orderBys, offset, limit, null) + .getQueryString(); + } + } else { + StringBuffer query = new StringBuffer( + "SELECT * FROM people"); + if (!orderBys.isEmpty()) { + query.append(" ORDER BY "); + for (OrderBy orderBy : orderBys) { + query.append( + "\"" + orderBy.getColumn() + "\""); + if (orderBy.isAscending()) { + query.append(" ASC"); + } else { + query.append(" DESC"); + } + } + } + query.append(" LIMIT ").append(limit) + .append(" OFFSET ").append(offset); + return query.toString(); + } + } + }).anyTimes(); + EasyMock.expect(delegate.getCountQuery()) + .andThrow(new UnsupportedOperationException()).anyTimes(); + + EasyMock.replay(delegate); + query.setDelegate(delegate); + SQLContainer container = new SQLContainer(query); + // Ville, Kalle, Pelle, Börje + Assert.assertEquals("Ville", + container.getContainerProperty(container.firstItemId(), "NAME") + .getValue()); + Assert.assertEquals("Börje", + container.getContainerProperty(container.lastItemId(), "NAME") + .getValue()); + + container.addOrderBy(new OrderBy("NAME", true)); + // Börje, Kalle, Pelle, Ville + Assert.assertEquals("Börje", + container.getContainerProperty(container.firstItemId(), "NAME") + .getValue()); + Assert.assertEquals("Ville", + container.getContainerProperty(container.lastItemId(), "NAME") + .getValue()); + + EasyMock.verify(delegate); + } + + @Test(expected = IllegalArgumentException.class) + public void addOrderBy_freeformIllegalColumn_shouldFail() + throws SQLException { + SQLContainer container = new SQLContainer(new FreeformQuery( + "SELECT * FROM people", connectionPool, "ID")); + container.addOrderBy(new OrderBy("asdf", true)); + } + + @SuppressWarnings("unchecked") + @Test + public void sort_freeform_sortsByName() throws SQLException { + FreeformQuery query = new FreeformQuery("SELECT * FROM people", + connectionPool, "ID"); + FreeformQueryDelegate delegate = EasyMock + .createMock(FreeformQueryDelegate.class); + final ArrayList<OrderBy> orderBys = new ArrayList<OrderBy>(); + delegate.setFilters(null); + EasyMock.expectLastCall().anyTimes(); + delegate.setFilters(EasyMock.isA(List.class)); + EasyMock.expectLastCall().anyTimes(); + delegate.setOrderBy(null); + EasyMock.expectLastCall().anyTimes(); + delegate.setOrderBy(EasyMock.isA(List.class)); + EasyMock.expectLastCall().andAnswer(new IAnswer<Object>() { + @Override + public Object answer() throws Throwable { + List<OrderBy> orders = (List<OrderBy>) EasyMock + .getCurrentArguments()[0]; + orderBys.clear(); + orderBys.addAll(orders); + return null; + } + }).anyTimes(); + EasyMock.expect( + delegate.getQueryString(EasyMock.anyInt(), EasyMock.anyInt())) + .andAnswer(new IAnswer<String>() { + @Override + public String answer() throws Throwable { + Object[] args = EasyMock.getCurrentArguments(); + int offset = (Integer) (args[0]); + int limit = (Integer) (args[1]); + if (SQLTestsConstants.db == DB.MSSQL) { + SQLGenerator gen = new MSSQLGenerator(); + if (orderBys == null || orderBys.isEmpty()) { + List<OrderBy> ob = new ArrayList<OrderBy>(); + ob.add(new OrderBy("ID", true)); + return gen + .generateSelectQuery("people", null, ob, + offset, limit, null) + .getQueryString(); + } else { + return gen + .generateSelectQuery("people", null, + orderBys, offset, limit, null) + .getQueryString(); + } + } else if (SQLTestsConstants.db == DB.ORACLE) { + SQLGenerator gen = new OracleGenerator(); + if (orderBys == null || orderBys.isEmpty()) { + List<OrderBy> ob = new ArrayList<OrderBy>(); + ob.add(new OrderBy("ID", true)); + return gen + .generateSelectQuery("people", null, ob, + offset, limit, null) + .getQueryString(); + } else { + return gen + .generateSelectQuery("people", null, + orderBys, offset, limit, null) + .getQueryString(); + } + } else { + StringBuffer query = new StringBuffer( + "SELECT * FROM people"); + if (!orderBys.isEmpty()) { + query.append(" ORDER BY "); + for (OrderBy orderBy : orderBys) { + query.append( + "\"" + orderBy.getColumn() + "\""); + if (orderBy.isAscending()) { + query.append(" ASC"); + } else { + query.append(" DESC"); + } + } + } + query.append(" LIMIT ").append(limit) + .append(" OFFSET ").append(offset); + return query.toString(); + } + } + }).anyTimes(); + EasyMock.expect(delegate.getCountQuery()) + .andThrow(new UnsupportedOperationException()).anyTimes(); + EasyMock.replay(delegate); + + query.setDelegate(delegate); + SQLContainer container = new SQLContainer(query); + // Ville, Kalle, Pelle, Börje + Assert.assertEquals("Ville", + container.getContainerProperty(container.firstItemId(), "NAME") + .getValue()); + Assert.assertEquals("Börje", + container.getContainerProperty(container.lastItemId(), "NAME") + .getValue()); + + container.sort(new Object[] { "NAME" }, new boolean[] { true }); + + // Börje, Kalle, Pelle, Ville + Assert.assertEquals("Börje", + container.getContainerProperty(container.firstItemId(), "NAME") + .getValue()); + Assert.assertEquals("Ville", + container.getContainerProperty(container.lastItemId(), "NAME") + .getValue()); + + EasyMock.verify(delegate); + } + + @SuppressWarnings("unchecked") + @Test + public void addFilter_freeform_filtersResults() throws SQLException { + FreeformQuery query = new FreeformQuery("SELECT * FROM people", + connectionPool, "ID"); + FreeformStatementDelegate delegate = EasyMock + .createMock(FreeformStatementDelegate.class); + final ArrayList<Filter> filters = new ArrayList<Filter>(); + delegate.setFilters(null); + EasyMock.expectLastCall().anyTimes(); + delegate.setOrderBy(EasyMock.isA(List.class)); + EasyMock.expectLastCall().anyTimes(); + delegate.setOrderBy(null); + EasyMock.expectLastCall().anyTimes(); + delegate.setFilters(EasyMock.isA(List.class)); + EasyMock.expectLastCall().andAnswer(new IAnswer<Object>() { + @Override + public Object answer() throws Throwable { + List<Filter> orders = (List<Filter>) EasyMock + .getCurrentArguments()[0]; + filters.clear(); + filters.addAll(orders); + return null; + } + }).anyTimes(); + EasyMock.expect(delegate.getQueryStatement(EasyMock.anyInt(), + EasyMock.anyInt())).andAnswer(new IAnswer<StatementHelper>() { + @Override + public StatementHelper answer() throws Throwable { + Object[] args = EasyMock.getCurrentArguments(); + int offset = (Integer) (args[0]); + int limit = (Integer) (args[1]); + return FreeformQueryUtil.getQueryWithFilters(filters, + offset, limit); + } + }).anyTimes(); + EasyMock.expect(delegate.getCountStatement()) + .andAnswer(new IAnswer<StatementHelper>() { + @Override + public StatementHelper answer() throws Throwable { + StatementHelper sh = new StatementHelper(); + StringBuffer query = new StringBuffer( + "SELECT COUNT(*) FROM people"); + if (!filters.isEmpty()) { + query.append(QueryBuilder + .getWhereStringForFilters(filters, sh)); + } + sh.setQueryString(query.toString()); + return sh; + } + }).anyTimes(); + + EasyMock.replay(delegate); + query.setDelegate(delegate); + SQLContainer container = new SQLContainer(query); + // Ville, Kalle, Pelle, Börje + Assert.assertEquals(4, container.size()); + Assert.assertEquals("Börje", + container.getContainerProperty(container.lastItemId(), "NAME") + .getValue()); + + container.addContainerFilter(new Like("NAME", "%lle")); + // Ville, Kalle, Pelle + Assert.assertEquals(3, container.size()); + Assert.assertEquals("Pelle", + container.getContainerProperty(container.lastItemId(), "NAME") + .getValue()); + + EasyMock.verify(delegate); + } + + @SuppressWarnings("unchecked") + @Test + public void addContainerFilter_filtersResults() throws SQLException { + FreeformQuery query = new FreeformQuery("SELECT * FROM people", + connectionPool, "ID"); + FreeformStatementDelegate delegate = EasyMock + .createMock(FreeformStatementDelegate.class); + final ArrayList<Filter> filters = new ArrayList<Filter>(); + delegate.setFilters(null); + EasyMock.expectLastCall().anyTimes(); + delegate.setOrderBy(null); + EasyMock.expectLastCall().anyTimes(); + delegate.setOrderBy(EasyMock.isA(List.class)); + EasyMock.expectLastCall().anyTimes(); + delegate.setFilters(EasyMock.isA(List.class)); + EasyMock.expectLastCall().andAnswer(new IAnswer<Object>() { + @Override + public Object answer() throws Throwable { + List<Filter> orders = (List<Filter>) EasyMock + .getCurrentArguments()[0]; + filters.clear(); + filters.addAll(orders); + return null; + } + }).anyTimes(); + EasyMock.expect(delegate.getQueryStatement(EasyMock.anyInt(), + EasyMock.anyInt())).andAnswer(new IAnswer<StatementHelper>() { + @Override + public StatementHelper answer() throws Throwable { + Object[] args = EasyMock.getCurrentArguments(); + int offset = (Integer) (args[0]); + int limit = (Integer) (args[1]); + return FreeformQueryUtil.getQueryWithFilters(filters, + offset, limit); + } + }).anyTimes(); + EasyMock.expect(delegate.getCountStatement()) + .andAnswer(new IAnswer<StatementHelper>() { + @Override + public StatementHelper answer() throws Throwable { + StatementHelper sh = new StatementHelper(); + StringBuffer query = new StringBuffer( + "SELECT COUNT(*) FROM people"); + if (!filters.isEmpty()) { + query.append(QueryBuilder + .getWhereStringForFilters(filters, sh)); + } + sh.setQueryString(query.toString()); + return sh; + } + }).anyTimes(); + + EasyMock.replay(delegate); + query.setDelegate(delegate); + SQLContainer container = new SQLContainer(query); + // Ville, Kalle, Pelle, Börje + Assert.assertEquals(4, container.size()); + + container.addContainerFilter("NAME", "Vi", false, false); + + // Ville + Assert.assertEquals(1, container.size()); + Assert.assertEquals("Ville", + container.getContainerProperty(container.lastItemId(), "NAME") + .getValue()); + + EasyMock.verify(delegate); + } + + @SuppressWarnings("unchecked") + @Test + public void addContainerFilter_ignoreCase_filtersResults() + throws SQLException { + FreeformQuery query = new FreeformQuery("SELECT * FROM people", + connectionPool, "ID"); + FreeformStatementDelegate delegate = EasyMock + .createMock(FreeformStatementDelegate.class); + final ArrayList<Filter> filters = new ArrayList<Filter>(); + delegate.setFilters(null); + EasyMock.expectLastCall().anyTimes(); + delegate.setOrderBy(EasyMock.isA(List.class)); + EasyMock.expectLastCall().anyTimes(); + delegate.setOrderBy(null); + EasyMock.expectLastCall().anyTimes(); + delegate.setFilters(EasyMock.isA(List.class)); + EasyMock.expectLastCall().andAnswer(new IAnswer<Object>() { + @Override + public Object answer() throws Throwable { + List<Filter> orders = (List<Filter>) EasyMock + .getCurrentArguments()[0]; + filters.clear(); + filters.addAll(orders); + return null; + } + }).anyTimes(); + EasyMock.expect(delegate.getQueryStatement(EasyMock.anyInt(), + EasyMock.anyInt())).andAnswer(new IAnswer<StatementHelper>() { + @Override + public StatementHelper answer() throws Throwable { + Object[] args = EasyMock.getCurrentArguments(); + int offset = (Integer) (args[0]); + int limit = (Integer) (args[1]); + return FreeformQueryUtil.getQueryWithFilters(filters, + offset, limit); + } + }).anyTimes(); + EasyMock.expect(delegate.getCountStatement()) + .andAnswer(new IAnswer<StatementHelper>() { + @Override + public StatementHelper answer() throws Throwable { + StatementHelper sh = new StatementHelper(); + StringBuffer query = new StringBuffer( + "SELECT COUNT(*) FROM people"); + if (!filters.isEmpty()) { + query.append(QueryBuilder + .getWhereStringForFilters(filters, sh)); + } + sh.setQueryString(query.toString()); + return sh; + } + }).anyTimes(); + + EasyMock.replay(delegate); + query.setDelegate(delegate); + SQLContainer container = new SQLContainer(query); + // Ville, Kalle, Pelle, Börje + Assert.assertEquals(4, container.size()); + + // FIXME LIKE %asdf% doesn't match a string that begins with asdf + container.addContainerFilter("NAME", "vi", true, true); + + // Ville + Assert.assertEquals(1, container.size()); + Assert.assertEquals("Ville", + container.getContainerProperty(container.lastItemId(), "NAME") + .getValue()); + + EasyMock.verify(delegate); + } + + @SuppressWarnings("unchecked") + @Test + public void removeAllContainerFilters_freeform_noFiltering() + throws SQLException { + FreeformQuery query = new FreeformQuery("SELECT * FROM people", + connectionPool, "ID"); + FreeformStatementDelegate delegate = EasyMock + .createMock(FreeformStatementDelegate.class); + final ArrayList<Filter> filters = new ArrayList<Filter>(); + delegate.setFilters(null); + EasyMock.expectLastCall().anyTimes(); + delegate.setOrderBy(EasyMock.isA(List.class)); + EasyMock.expectLastCall().anyTimes(); + delegate.setOrderBy(null); + EasyMock.expectLastCall().anyTimes(); + delegate.setFilters(EasyMock.isA(List.class)); + EasyMock.expectLastCall().andAnswer(new IAnswer<Object>() { + @Override + public Object answer() throws Throwable { + List<Filter> orders = (List<Filter>) EasyMock + .getCurrentArguments()[0]; + filters.clear(); + filters.addAll(orders); + return null; + } + }).anyTimes(); + EasyMock.expect(delegate.getQueryStatement(EasyMock.anyInt(), + EasyMock.anyInt())).andAnswer(new IAnswer<StatementHelper>() { + @Override + public StatementHelper answer() throws Throwable { + Object[] args = EasyMock.getCurrentArguments(); + int offset = (Integer) (args[0]); + int limit = (Integer) (args[1]); + return FreeformQueryUtil.getQueryWithFilters(filters, + offset, limit); + } + }).anyTimes(); + EasyMock.expect(delegate.getCountStatement()) + .andAnswer(new IAnswer<StatementHelper>() { + @Override + public StatementHelper answer() throws Throwable { + StatementHelper sh = new StatementHelper(); + StringBuffer query = new StringBuffer( + "SELECT COUNT(*) FROM people"); + if (!filters.isEmpty()) { + query.append(QueryBuilder + .getWhereStringForFilters(filters, sh)); + } + sh.setQueryString(query.toString()); + return sh; + } + }).anyTimes(); + + EasyMock.replay(delegate); + query.setDelegate(delegate); + SQLContainer container = new SQLContainer(query); + // Ville, Kalle, Pelle, Börje + Assert.assertEquals(4, container.size()); + + container.addContainerFilter("NAME", "Vi", false, false); + + // Ville + Assert.assertEquals(1, container.size()); + Assert.assertEquals("Ville", + container.getContainerProperty(container.lastItemId(), "NAME") + .getValue()); + + container.removeAllContainerFilters(); + + Assert.assertEquals(4, container.size()); + Assert.assertEquals("Börje", + container.getContainerProperty(container.lastItemId(), "NAME") + .getValue()); + + EasyMock.verify(delegate); + } + + @SuppressWarnings("unchecked") + @Test + public void removeContainerFilters_freeform_noFiltering() + throws SQLException { + FreeformQuery query = new FreeformQuery("SELECT * FROM people", + connectionPool, "ID"); + FreeformStatementDelegate delegate = EasyMock + .createMock(FreeformStatementDelegate.class); + final ArrayList<Filter> filters = new ArrayList<Filter>(); + delegate.setFilters(null); + EasyMock.expectLastCall().anyTimes(); + delegate.setOrderBy(EasyMock.isA(List.class)); + EasyMock.expectLastCall().anyTimes(); + delegate.setOrderBy(null); + EasyMock.expectLastCall().anyTimes(); + delegate.setFilters(EasyMock.isA(List.class)); + EasyMock.expectLastCall().andAnswer(new IAnswer<Object>() { + @Override + public Object answer() throws Throwable { + List<Filter> orders = (List<Filter>) EasyMock + .getCurrentArguments()[0]; + filters.clear(); + filters.addAll(orders); + return null; + } + }).anyTimes(); + EasyMock.expect(delegate.getQueryStatement(EasyMock.anyInt(), + EasyMock.anyInt())).andAnswer(new IAnswer<StatementHelper>() { + @Override + public StatementHelper answer() throws Throwable { + Object[] args = EasyMock.getCurrentArguments(); + int offset = (Integer) (args[0]); + int limit = (Integer) (args[1]); + return FreeformQueryUtil.getQueryWithFilters(filters, + offset, limit); + } + }).anyTimes(); + EasyMock.expect(delegate.getCountStatement()) + .andAnswer(new IAnswer<StatementHelper>() { + @Override + public StatementHelper answer() throws Throwable { + StatementHelper sh = new StatementHelper(); + StringBuffer query = new StringBuffer( + "SELECT COUNT(*) FROM people"); + if (!filters.isEmpty()) { + query.append(QueryBuilder + .getWhereStringForFilters(filters, sh)); + } + sh.setQueryString(query.toString()); + return sh; + } + }).anyTimes(); + + EasyMock.replay(delegate); + query.setDelegate(delegate); + SQLContainer container = new SQLContainer(query); + // Ville, Kalle, Pelle, Börje + Assert.assertEquals(4, container.size()); + + container.addContainerFilter("NAME", "Vi", false, true); + + // Ville + Assert.assertEquals(1, container.size()); + Assert.assertEquals("Ville", + container.getContainerProperty(container.lastItemId(), "NAME") + .getValue()); + + container.removeContainerFilters("NAME"); + + Assert.assertEquals(4, container.size()); + Assert.assertEquals("Börje", + container.getContainerProperty(container.lastItemId(), "NAME") + .getValue()); + + EasyMock.verify(delegate); + } + + @SuppressWarnings("unchecked") + @Test + public void addFilter_freeformBufferedItems_alsoFiltersBufferedItems() + throws SQLException { + FreeformQuery query = new FreeformQuery("SELECT * FROM people", + connectionPool, "ID"); + FreeformStatementDelegate delegate = EasyMock + .createMock(FreeformStatementDelegate.class); + final ArrayList<Filter> filters = new ArrayList<Filter>(); + delegate.setFilters(null); + EasyMock.expectLastCall().anyTimes(); + delegate.setOrderBy(EasyMock.isA(List.class)); + EasyMock.expectLastCall().anyTimes(); + delegate.setOrderBy(null); + EasyMock.expectLastCall().anyTimes(); + delegate.setFilters(EasyMock.isA(List.class)); + EasyMock.expectLastCall().andAnswer(new IAnswer<Object>() { + @Override + public Object answer() throws Throwable { + List<Filter> orders = (List<Filter>) EasyMock + .getCurrentArguments()[0]; + filters.clear(); + filters.addAll(orders); + return null; + } + }).anyTimes(); + EasyMock.expect(delegate.getQueryStatement(EasyMock.anyInt(), + EasyMock.anyInt())).andAnswer(new IAnswer<StatementHelper>() { + @Override + public StatementHelper answer() throws Throwable { + Object[] args = EasyMock.getCurrentArguments(); + int offset = (Integer) (args[0]); + int limit = (Integer) (args[1]); + return FreeformQueryUtil.getQueryWithFilters(filters, + offset, limit); + } + }).anyTimes(); + EasyMock.expect(delegate.getCountStatement()) + .andAnswer(new IAnswer<StatementHelper>() { + @Override + public StatementHelper answer() throws Throwable { + StatementHelper sh = new StatementHelper(); + StringBuffer query = new StringBuffer( + "SELECT COUNT(*) FROM people"); + if (!filters.isEmpty()) { + query.append(QueryBuilder + .getWhereStringForFilters(filters, sh)); + } + sh.setQueryString(query.toString()); + return sh; + } + }).anyTimes(); + + EasyMock.replay(delegate); + query.setDelegate(delegate); + SQLContainer container = new SQLContainer(query); + // Ville, Kalle, Pelle, Börje + Assert.assertEquals(4, container.size()); + Assert.assertEquals("Börje", + container.getContainerProperty(container.lastItemId(), "NAME") + .getValue()); + + Object id1 = container.addItem(); + container.getContainerProperty(id1, "NAME").setValue("Palle"); + Object id2 = container.addItem(); + container.getContainerProperty(id2, "NAME").setValue("Bengt"); + + container.addContainerFilter(new Like("NAME", "%lle")); + + // Ville, Kalle, Pelle, Palle + Assert.assertEquals(4, container.size()); + Assert.assertEquals("Ville", + container + .getContainerProperty(container.getIdByIndex(0), "NAME") + .getValue()); + Assert.assertEquals("Kalle", + container + .getContainerProperty(container.getIdByIndex(1), "NAME") + .getValue()); + Assert.assertEquals("Pelle", + container + .getContainerProperty(container.getIdByIndex(2), "NAME") + .getValue()); + Assert.assertEquals("Palle", + container + .getContainerProperty(container.getIdByIndex(3), "NAME") + .getValue()); + + try { + container.getIdByIndex(4); + Assert.fail( + "SQLContainer.getIdByIndex() returned a value for an index beyond the end of the container"); + } catch (IndexOutOfBoundsException e) { + // should throw exception - item is filtered out + } + container.nextItemId(container.getIdByIndex(3)); + + Assert.assertFalse(container.containsId(id2)); + Assert.assertFalse(container.getItemIds().contains(id2)); + + Assert.assertNull(container.getItem(id2)); + Assert.assertEquals(-1, container.indexOfId(id2)); + + Assert.assertNotSame(id2, container.lastItemId()); + Assert.assertSame(id1, container.lastItemId()); + + EasyMock.verify(delegate); + } + + @SuppressWarnings("unchecked") + @Test + public void sort_freeformBufferedItems_sortsBufferedItemsLastInOrderAdded() + throws SQLException { + FreeformQuery query = new FreeformQuery("SELECT * FROM people", + connectionPool, "ID"); + FreeformQueryDelegate delegate = EasyMock + .createMock(FreeformQueryDelegate.class); + final ArrayList<OrderBy> orderBys = new ArrayList<OrderBy>(); + delegate.setFilters(null); + EasyMock.expectLastCall().anyTimes(); + delegate.setFilters(EasyMock.isA(List.class)); + EasyMock.expectLastCall().anyTimes(); + delegate.setOrderBy(null); + EasyMock.expectLastCall().anyTimes(); + delegate.setOrderBy(EasyMock.isA(List.class)); + EasyMock.expectLastCall().andAnswer(new IAnswer<Object>() { + @Override + public Object answer() throws Throwable { + List<OrderBy> orders = (List<OrderBy>) EasyMock + .getCurrentArguments()[0]; + orderBys.clear(); + orderBys.addAll(orders); + return null; + } + }).anyTimes(); + EasyMock.expect( + delegate.getQueryString(EasyMock.anyInt(), EasyMock.anyInt())) + .andAnswer(new IAnswer<String>() { + @Override + public String answer() throws Throwable { + Object[] args = EasyMock.getCurrentArguments(); + int offset = (Integer) (args[0]); + int limit = (Integer) (args[1]); + if (SQLTestsConstants.db == DB.MSSQL) { + SQLGenerator gen = new MSSQLGenerator(); + if (orderBys == null || orderBys.isEmpty()) { + List<OrderBy> ob = new ArrayList<OrderBy>(); + ob.add(new OrderBy("ID", true)); + return gen + .generateSelectQuery("people", null, ob, + offset, limit, null) + .getQueryString(); + } else { + return gen + .generateSelectQuery("people", null, + orderBys, offset, limit, null) + .getQueryString(); + } + } else if (SQLTestsConstants.db == DB.ORACLE) { + SQLGenerator gen = new OracleGenerator(); + if (orderBys == null || orderBys.isEmpty()) { + List<OrderBy> ob = new ArrayList<OrderBy>(); + ob.add(new OrderBy("ID", true)); + return gen + .generateSelectQuery("people", null, ob, + offset, limit, null) + .getQueryString(); + } else { + return gen + .generateSelectQuery("people", null, + orderBys, offset, limit, null) + .getQueryString(); + } + } else { + StringBuffer query = new StringBuffer( + "SELECT * FROM people"); + if (!orderBys.isEmpty()) { + query.append(" ORDER BY "); + for (OrderBy orderBy : orderBys) { + query.append( + "\"" + orderBy.getColumn() + "\""); + if (orderBy.isAscending()) { + query.append(" ASC"); + } else { + query.append(" DESC"); + } + } + } + query.append(" LIMIT ").append(limit) + .append(" OFFSET ").append(offset); + return query.toString(); + } + } + }).anyTimes(); + EasyMock.expect(delegate.getCountQuery()) + .andThrow(new UnsupportedOperationException()).anyTimes(); + EasyMock.replay(delegate); + + query.setDelegate(delegate); + SQLContainer container = new SQLContainer(query); + // Ville, Kalle, Pelle, Börje + Assert.assertEquals("Ville", + container.getContainerProperty(container.firstItemId(), "NAME") + .getValue()); + Assert.assertEquals("Börje", + container.getContainerProperty(container.lastItemId(), "NAME") + .getValue()); + + Object id1 = container.addItem(); + container.getContainerProperty(id1, "NAME").setValue("Wilbert"); + Object id2 = container.addItem(); + container.getContainerProperty(id2, "NAME").setValue("Albert"); + + container.sort(new Object[] { "NAME" }, new boolean[] { true }); + + // Börje, Kalle, Pelle, Ville, Wilbert, Albert + Assert.assertEquals("Börje", + container.getContainerProperty(container.firstItemId(), "NAME") + .getValue()); + Assert.assertEquals("Wilbert", + container.getContainerProperty( + container.getIdByIndex(container.size() - 2), "NAME") + .getValue()); + Assert.assertEquals("Albert", + container.getContainerProperty(container.lastItemId(), "NAME") + .getValue()); + + EasyMock.verify(delegate); + } + +} diff --git a/compatibility-server/src/test/java/com/vaadin/data/util/sqlcontainer/SQLTestsConstants.java b/compatibility-server/src/test/java/com/vaadin/data/util/sqlcontainer/SQLTestsConstants.java new file mode 100755 index 0000000000..3718cf756d --- /dev/null +++ b/compatibility-server/src/test/java/com/vaadin/data/util/sqlcontainer/SQLTestsConstants.java @@ -0,0 +1,154 @@ +/* + * Copyright 2000-2016 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.data.util.sqlcontainer; + +import com.vaadin.data.util.sqlcontainer.query.generator.DefaultSQLGenerator; +import com.vaadin.data.util.sqlcontainer.query.generator.MSSQLGenerator; +import com.vaadin.data.util.sqlcontainer.query.generator.OracleGenerator; +import com.vaadin.data.util.sqlcontainer.query.generator.SQLGenerator; + +public class SQLTestsConstants { + + /* Set the DB used for testing here! */ + public enum DB { + HSQLDB, MYSQL, POSTGRESQL, MSSQL, ORACLE; + } + + /* 0 = HSQLDB, 1 = MYSQL, 2 = POSTGRESQL, 3 = MSSQL, 4 = ORACLE */ + public static final DB db = DB.HSQLDB; + + /* Auto-increment column offset (HSQLDB = 0, MYSQL = 1, POSTGRES = 1) */ + public static int offset; + /* Garbage table creation query (=three queries for oracle) */ + public static String createGarbage; + public static String createGarbageSecond; + public static String createGarbageThird; + /* DB Drivers, urls, usernames and passwords */ + public static String dbDriver; + public static String dbURL; + public static String dbUser; + public static String dbPwd; + /* People -test table creation statement(s) */ + public static String peopleFirst; + public static String peopleSecond; + public static String peopleThird; + /* Schema test creation statement(s) */ + public static String createSchema; + public static String createProductTable; + public static String dropSchema; + /* Versioned -test table createion statement(s) */ + public static String[] versionStatements; + /* SQL Generator used during the testing */ + public static SQLGenerator sqlGen; + + /* Set DB-specific settings based on selected DB */ + static { + sqlGen = new DefaultSQLGenerator(); + switch (db) { + case HSQLDB: + offset = 0; + createGarbage = "create table garbage (id integer generated always as identity, type varchar(32), PRIMARY KEY(id))"; + dbDriver = "org.hsqldb.jdbc.JDBCDriver"; + dbURL = "jdbc:hsqldb:mem:sqlcontainer"; + dbUser = "SA"; + dbPwd = ""; + peopleFirst = "create table people (id integer generated always as identity, name varchar(32), AGE INTEGER)"; + peopleSecond = "alter table people add primary key (id)"; + versionStatements = new String[] { + "create table versioned (id integer generated always as identity, text varchar(255), version tinyint default 0)", + "alter table versioned add primary key (id)" }; + // TODO these should ideally exist for all databases + createSchema = "create schema oaas authorization DBA"; + createProductTable = "create table oaas.product (\"ID\" integer generated always as identity primary key, \"NAME\" VARCHAR(32))"; + dropSchema = "drop schema if exists oaas cascade"; + break; + case MYSQL: + offset = 1; + createGarbage = "create table GARBAGE (ID integer auto_increment, type varchar(32), PRIMARY KEY(ID))"; + dbDriver = "com.mysql.jdbc.Driver"; + dbURL = "jdbc:mysql:///sqlcontainer"; + dbUser = "sqlcontainer"; + dbPwd = "sqlcontainer"; + peopleFirst = "create table PEOPLE (ID integer auto_increment not null, NAME varchar(32), AGE INTEGER, primary key(ID))"; + peopleSecond = null; + versionStatements = new String[] { + "create table VERSIONED (ID integer auto_increment not null, TEXT varchar(255), VERSION tinyint default 0, primary key(ID))", + "CREATE TRIGGER upd_version BEFORE UPDATE ON VERSIONED" + + " FOR EACH ROW SET NEW.VERSION = OLD.VERSION+1" }; + break; + case POSTGRESQL: + offset = 1; + createGarbage = "create table GARBAGE (\"ID\" serial PRIMARY KEY, \"TYPE\" varchar(32))"; + dbDriver = "org.postgresql.Driver"; + dbURL = "jdbc:postgresql://localhost:5432/test"; + dbUser = "postgres"; + dbPwd = "postgres"; + peopleFirst = "create table PEOPLE (\"ID\" serial primary key, \"NAME\" VARCHAR(32), \"AGE\" INTEGER)"; + peopleSecond = null; + versionStatements = new String[] { + "create table VERSIONED (\"ID\" serial primary key, \"TEXT\" VARCHAR(255), \"VERSION\" INTEGER DEFAULT 0)", + "CREATE OR REPLACE FUNCTION zz_row_version() RETURNS TRIGGER AS $$" + + "BEGIN" + " IF TG_OP = 'UPDATE'" + + " AND NEW.\"VERSION\" = old.\"VERSION\"" + + " AND ROW(NEW.*) IS DISTINCT FROM ROW (old.*)" + + " THEN" + + " NEW.\"VERSION\" := NEW.\"VERSION\" + 1;" + + " END IF;" + " RETURN NEW;" + "END;" + + "$$ LANGUAGE plpgsql;", + "CREATE TRIGGER \"mytable_modify_dt_tr\" BEFORE UPDATE" + + " ON VERSIONED FOR EACH ROW" + + " EXECUTE PROCEDURE \"public\".\"zz_row_version\"();" }; + createSchema = "create schema oaas"; + createProductTable = "create table oaas.product (\"ID\" serial primary key, \"NAME\" VARCHAR(32))"; + dropSchema = "drop schema oaas cascade"; + break; + case MSSQL: + offset = 1; + createGarbage = "create table GARBAGE (\"ID\" int identity(1,1) primary key, \"TYPE\" varchar(32))"; + dbDriver = "com.microsoft.sqlserver.jdbc.SQLServerDriver"; + dbURL = "jdbc:sqlserver://localhost:1433;databaseName=tempdb;"; + dbUser = "sa"; + dbPwd = "sa"; + peopleFirst = "create table PEOPLE (\"ID\" int identity(1,1) primary key, \"NAME\" VARCHAR(32), \"AGE\" INTEGER)"; + peopleSecond = null; + versionStatements = new String[] { + "create table VERSIONED (\"ID\" int identity(1,1) primary key, \"TEXT\" VARCHAR(255), \"VERSION\" rowversion not null)" }; + sqlGen = new MSSQLGenerator(); + break; + case ORACLE: + offset = 1; + createGarbage = "create table GARBAGE (\"ID\" integer primary key, \"TYPE\" varchar2(32))"; + createGarbageSecond = "create sequence garbage_seq start with 1 increment by 1 nomaxvalue"; + createGarbageThird = "create trigger garbage_trigger before insert on GARBAGE for each row begin select garbage_seq.nextval into :new.ID from dual; end;"; + dbDriver = "oracle.jdbc.OracleDriver"; + dbURL = "jdbc:oracle:thin:test/test@localhost:1521:XE"; + dbUser = "test"; + dbPwd = "test"; + peopleFirst = "create table PEOPLE (\"ID\" integer primary key, \"NAME\" VARCHAR2(32), \"AGE\" INTEGER)"; + peopleSecond = "create sequence people_seq start with 1 increment by 1 nomaxvalue"; + peopleThird = "create trigger people_trigger before insert on PEOPLE for each row begin select people_seq.nextval into :new.ID from dual; end;"; + versionStatements = new String[] { + "create table VERSIONED (\"ID\" integer primary key, \"TEXT\" VARCHAR(255), \"VERSION\" INTEGER DEFAULT 0)", + "create sequence versioned_seq start with 1 increment by 1 nomaxvalue", + "create trigger versioned_trigger before insert on VERSIONED for each row begin select versioned_seq.nextval into :new.ID from dual; end;", + "create sequence versioned_version start with 1 increment by 1 nomaxvalue", + "create trigger versioned_version_trigger before insert or update on VERSIONED for each row begin select versioned_version.nextval into :new.VERSION from dual; end;" }; + sqlGen = new OracleGenerator(); + break; + } + } + +} diff --git a/compatibility-server/src/test/java/com/vaadin/data/util/sqlcontainer/TicketTest.java b/compatibility-server/src/test/java/com/vaadin/data/util/sqlcontainer/TicketTest.java new file mode 100644 index 0000000000..1a19eda542 --- /dev/null +++ b/compatibility-server/src/test/java/com/vaadin/data/util/sqlcontainer/TicketTest.java @@ -0,0 +1,195 @@ +package com.vaadin.data.util.sqlcontainer; + +import java.math.BigDecimal; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.easymock.EasyMock; +import org.easymock.IAnswer; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import com.vaadin.data.Container.Filter; +import com.vaadin.data.Item; +import com.vaadin.data.util.filter.Compare.Equal; +import com.vaadin.data.util.sqlcontainer.SQLTestsConstants.DB; +import com.vaadin.data.util.sqlcontainer.connection.JDBCConnectionPool; +import com.vaadin.data.util.sqlcontainer.query.FreeformQuery; +import com.vaadin.data.util.sqlcontainer.query.FreeformStatementDelegate; +import com.vaadin.data.util.sqlcontainer.query.TableQuery; +import com.vaadin.data.util.sqlcontainer.query.ValidatingSimpleJDBCConnectionPool; +import com.vaadin.data.util.sqlcontainer.query.generator.StatementHelper; +import com.vaadin.data.util.sqlcontainer.query.generator.filter.QueryBuilder; +import com.vaadin.ui.Table; +import com.vaadin.ui.Window; + +public class TicketTest { + + private JDBCConnectionPool connectionPool; + + @Before + public void setUp() throws SQLException { + connectionPool = new ValidatingSimpleJDBCConnectionPool( + SQLTestsConstants.dbDriver, SQLTestsConstants.dbURL, + SQLTestsConstants.dbUser, SQLTestsConstants.dbPwd, 2, 2); + DataGenerator.addPeopleToDatabase(connectionPool); + } + + @Test + public void ticket5867_throwsIllegalState_transactionAlreadyActive() + throws SQLException { + SQLContainer container = new SQLContainer(new FreeformQuery( + "SELECT * FROM people", Arrays.asList("ID"), connectionPool)); + Table table = new Table(); + Window w = new Window(); + w.setContent(table); + table.setContainerDataSource(container); + } + + @SuppressWarnings("unchecked") + @Test + public void ticket6136_freeform_ageIs18() throws SQLException { + FreeformQuery query = new FreeformQuery("SELECT * FROM people", + Arrays.asList("ID"), connectionPool); + FreeformStatementDelegate delegate = EasyMock + .createMock(FreeformStatementDelegate.class); + final ArrayList<Filter> filters = new ArrayList<Filter>(); + delegate.setFilters(null); + EasyMock.expectLastCall().anyTimes(); + delegate.setOrderBy(EasyMock.isA(List.class)); + EasyMock.expectLastCall().anyTimes(); + delegate.setOrderBy(null); + EasyMock.expectLastCall().anyTimes(); + delegate.setFilters(EasyMock.isA(List.class)); + EasyMock.expectLastCall().andAnswer(new IAnswer<Object>() { + @Override + public Object answer() throws Throwable { + List<Filter> orders = (List<Filter>) EasyMock + .getCurrentArguments()[0]; + filters.clear(); + filters.addAll(orders); + return null; + } + }).anyTimes(); + EasyMock.expect(delegate.getQueryStatement(EasyMock.anyInt(), + EasyMock.anyInt())).andAnswer(new IAnswer<StatementHelper>() { + @Override + public StatementHelper answer() throws Throwable { + Object[] args = EasyMock.getCurrentArguments(); + int offset = (Integer) (args[0]); + int limit = (Integer) (args[1]); + return FreeformQueryUtil.getQueryWithFilters(filters, + offset, limit); + } + }).anyTimes(); + EasyMock.expect(delegate.getCountStatement()) + .andAnswer(new IAnswer<StatementHelper>() { + @Override + public StatementHelper answer() throws Throwable { + StatementHelper sh = new StatementHelper(); + StringBuffer query = new StringBuffer( + "SELECT COUNT(*) FROM people"); + if (!filters.isEmpty()) { + query.append(QueryBuilder + .getWhereStringForFilters(filters, sh)); + } + sh.setQueryString(query.toString()); + return sh; + } + }).anyTimes(); + + EasyMock.replay(delegate); + query.setDelegate(delegate); + SQLContainer container = new SQLContainer(query); + // Ville, Kalle, Pelle, Börje + Assert.assertEquals(4, container.size()); + Assert.assertEquals("Börje", + container.getContainerProperty(container.lastItemId(), "NAME") + .getValue()); + + container.addContainerFilter(new Equal("AGE", 18)); + // Pelle + Assert.assertEquals(1, container.size()); + Assert.assertEquals("Pelle", + container.getContainerProperty(container.firstItemId(), "NAME") + .getValue()); + if (SQLTestsConstants.db == DB.ORACLE) { + Assert.assertEquals(new BigDecimal(18), container + .getContainerProperty(container.firstItemId(), "AGE") + .getValue()); + } else { + Assert.assertEquals(18, container + .getContainerProperty(container.firstItemId(), "AGE") + .getValue()); + } + + EasyMock.verify(delegate); + } + + @Test + public void ticket6136_table_ageIs18() throws SQLException { + TableQuery query = new TableQuery("people", connectionPool, + SQLTestsConstants.sqlGen); + SQLContainer container = new SQLContainer(query); + // Ville, Kalle, Pelle, Börje + Assert.assertEquals(4, container.size()); + + container.addContainerFilter(new Equal("AGE", 18)); + + // Pelle + Assert.assertEquals(1, container.size()); + Assert.assertEquals("Pelle", + container.getContainerProperty(container.firstItemId(), "NAME") + .getValue()); + if (SQLTestsConstants.db == DB.ORACLE) { + Assert.assertEquals(new BigDecimal(18), container + .getContainerProperty(container.firstItemId(), "AGE") + .getValue()); + } else { + Assert.assertEquals(18, container + .getContainerProperty(container.firstItemId(), "AGE") + .getValue()); + } + } + + @Test + public void ticket7434_getItem_Modified_Changed_Unchanged() + throws SQLException { + SQLContainer container = new SQLContainer(new TableQuery("people", + connectionPool, SQLTestsConstants.sqlGen)); + + Object id = container.firstItemId(); + Item item = container.getItem(id); + String name = (String) item.getItemProperty("NAME").getValue(); + + // set a different name + item.getItemProperty("NAME").setValue("otherName"); + Assert.assertEquals("otherName", + item.getItemProperty("NAME").getValue()); + + // access the item and reset the name to its old value + Item item2 = container.getItem(id); + item2.getItemProperty("NAME").setValue(name); + Assert.assertEquals(name, item2.getItemProperty("NAME").getValue()); + + Item item3 = container.getItem(id); + String name3 = (String) item3.getItemProperty("NAME").getValue(); + + Assert.assertEquals(name, name3); + } + + @Test + public void ticket10032_empty_set_metadata_correctly_handled() + throws SQLException { + // If problem exists will break when method getPropertyIds() + // is called in constructor SQLContainer(QueryDelegate delegate). + SQLContainer container = new SQLContainer(new FreeformQuery( + "SELECT * FROM people WHERE name='does_not_exist'", + Arrays.asList("ID"), connectionPool)); + Assert.assertTrue("Got items while expected empty set", + container.size() == 0); + } +} diff --git a/compatibility-server/src/test/java/com/vaadin/data/util/sqlcontainer/UtilTest.java b/compatibility-server/src/test/java/com/vaadin/data/util/sqlcontainer/UtilTest.java new file mode 100644 index 0000000000..a575d649f1 --- /dev/null +++ b/compatibility-server/src/test/java/com/vaadin/data/util/sqlcontainer/UtilTest.java @@ -0,0 +1,50 @@ +package com.vaadin.data.util.sqlcontainer; + +import org.junit.Assert; +import org.junit.Test; + +public class UtilTest { + + @Test + public void escapeSQL_noQuotes_returnsSameString() { + Assert.assertEquals("asdf", SQLUtil.escapeSQL("asdf")); + } + + @Test + public void escapeSQL_singleQuotes_returnsEscapedString() { + Assert.assertEquals("O''Brien", SQLUtil.escapeSQL("O'Brien")); + } + + @Test + public void escapeSQL_severalQuotes_returnsEscapedString() { + Assert.assertEquals("asdf''ghjk''qwerty", + SQLUtil.escapeSQL("asdf'ghjk'qwerty")); + } + + @Test + public void escapeSQL_doubleQuotes_returnsEscapedString() { + Assert.assertEquals("asdf\\\"foo", SQLUtil.escapeSQL("asdf\"foo")); + } + + @Test + public void escapeSQL_multipleDoubleQuotes_returnsEscapedString() { + Assert.assertEquals("asdf\\\"foo\\\"bar", + SQLUtil.escapeSQL("asdf\"foo\"bar")); + } + + @Test + public void escapeSQL_backslashes_returnsEscapedString() { + Assert.assertEquals("foo\\\\nbar\\\\r", + SQLUtil.escapeSQL("foo\\nbar\\r")); + } + + @Test + public void escapeSQL_x00_removesX00() { + Assert.assertEquals("foobar", SQLUtil.escapeSQL("foo\\x00bar")); + } + + @Test + public void escapeSQL_x1a_removesX1a() { + Assert.assertEquals("foobar", SQLUtil.escapeSQL("foo\\x1abar")); + } +} diff --git a/compatibility-server/src/test/java/com/vaadin/data/util/sqlcontainer/connection/J2EEConnectionPoolTest.java b/compatibility-server/src/test/java/com/vaadin/data/util/sqlcontainer/connection/J2EEConnectionPoolTest.java new file mode 100644 index 0000000000..1463a27217 --- /dev/null +++ b/compatibility-server/src/test/java/com/vaadin/data/util/sqlcontainer/connection/J2EEConnectionPoolTest.java @@ -0,0 +1,107 @@ +package com.vaadin.data.util.sqlcontainer.connection; + +import java.sql.Connection; +import java.sql.SQLException; + +import javax.naming.Context; +import javax.naming.NamingException; +import javax.sql.DataSource; + +import org.easymock.EasyMock; +import org.junit.Assert; +import org.junit.Test; + +public class J2EEConnectionPoolTest { + + @Test + public void reserveConnection_dataSourceSpecified_shouldReturnValidConnection() + throws SQLException { + Connection connection = EasyMock.createMock(Connection.class); + connection.setAutoCommit(false); + EasyMock.expectLastCall(); + DataSource ds = EasyMock.createMock(DataSource.class); + ds.getConnection(); + EasyMock.expectLastCall().andReturn(connection); + EasyMock.replay(connection, ds); + + J2EEConnectionPool pool = new J2EEConnectionPool(ds); + Connection c = pool.reserveConnection(); + Assert.assertEquals(connection, c); + EasyMock.verify(connection, ds); + } + + @Test + public void releaseConnection_shouldCloseConnection() throws SQLException { + Connection connection = EasyMock.createMock(Connection.class); + connection.setAutoCommit(false); + EasyMock.expectLastCall(); + connection.close(); + EasyMock.expectLastCall(); + DataSource ds = EasyMock.createMock(DataSource.class); + ds.getConnection(); + EasyMock.expectLastCall().andReturn(connection); + EasyMock.replay(connection, ds); + + J2EEConnectionPool pool = new J2EEConnectionPool(ds); + Connection c = pool.reserveConnection(); + Assert.assertEquals(connection, c); + pool.releaseConnection(c); + EasyMock.verify(connection, ds); + } + + @Test + public void reserveConnection_dataSourceLookedUp_shouldReturnValidConnection() + throws SQLException, NamingException { + Connection connection = EasyMock.createMock(Connection.class); + connection.setAutoCommit(false); + EasyMock.expectLastCall(); + connection.close(); + EasyMock.expectLastCall(); + + DataSource ds = EasyMock.createMock(DataSource.class); + ds.getConnection(); + EasyMock.expectLastCall().andReturn(connection); + + System.setProperty("java.naming.factory.initial", + "com.vaadin.data.util.sqlcontainer.connection.MockInitialContextFactory"); + Context context = EasyMock.createMock(Context.class); + context.lookup("testDataSource"); + EasyMock.expectLastCall().andReturn(ds); + MockInitialContextFactory.setMockContext(context); + + EasyMock.replay(context, connection, ds); + + J2EEConnectionPool pool = new J2EEConnectionPool("testDataSource"); + Connection c = pool.reserveConnection(); + Assert.assertEquals(connection, c); + pool.releaseConnection(c); + EasyMock.verify(context, connection, ds); + } + + @Test(expected = SQLException.class) + public void reserveConnection_nonExistantDataSourceLookedUp_shouldFail() + throws SQLException, NamingException { + System.setProperty("java.naming.factory.initial", + "com.vaadin.addon.sqlcontainer.connection.MockInitialContextFactory"); + Context context = EasyMock.createMock(Context.class); + context.lookup("foo"); + EasyMock.expectLastCall().andThrow(new NamingException("fail")); + MockInitialContextFactory.setMockContext(context); + + EasyMock.replay(context); + + J2EEConnectionPool pool = new J2EEConnectionPool("foo"); + pool.reserveConnection(); + EasyMock.verify(context); + } + + @Test + public void releaseConnection_null_shouldSucceed() throws SQLException { + DataSource ds = EasyMock.createMock(DataSource.class); + EasyMock.replay(ds); + + J2EEConnectionPool pool = new J2EEConnectionPool(ds); + pool.releaseConnection(null); + EasyMock.verify(ds); + } +} diff --git a/compatibility-server/src/test/java/com/vaadin/data/util/sqlcontainer/connection/MockInitialContextFactory.java b/compatibility-server/src/test/java/com/vaadin/data/util/sqlcontainer/connection/MockInitialContextFactory.java new file mode 100644 index 0000000000..1c70c8dad7 --- /dev/null +++ b/compatibility-server/src/test/java/com/vaadin/data/util/sqlcontainer/connection/MockInitialContextFactory.java @@ -0,0 +1,27 @@ +package com.vaadin.data.util.sqlcontainer.connection; + +import javax.naming.Context; +import javax.naming.NamingException; +import javax.naming.spi.InitialContextFactory; + +import org.junit.Test; + +/** + * Provides a JNDI initial context factory for the MockContext. + */ +public class MockInitialContextFactory implements InitialContextFactory { + private static Context mockCtx = null; + + public static void setMockContext(Context ctx) { + mockCtx = ctx; + } + + @Override + public Context getInitialContext(java.util.Hashtable<?, ?> environment) + throws NamingException { + if (mockCtx == null) { + throw new IllegalStateException("mock context was not set."); + } + return mockCtx; + } +} diff --git a/compatibility-server/src/test/java/com/vaadin/data/util/sqlcontainer/connection/SimpleJDBCConnectionPoolTest.java b/compatibility-server/src/test/java/com/vaadin/data/util/sqlcontainer/connection/SimpleJDBCConnectionPoolTest.java new file mode 100644 index 0000000000..5a6ae78aeb --- /dev/null +++ b/compatibility-server/src/test/java/com/vaadin/data/util/sqlcontainer/connection/SimpleJDBCConnectionPoolTest.java @@ -0,0 +1,185 @@ +package com.vaadin.data.util.sqlcontainer.connection; + +import java.sql.Connection; +import java.sql.SQLException; + +import org.easymock.EasyMock; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import com.vaadin.data.util.sqlcontainer.SQLTestsConstants; +import com.vaadin.data.util.sqlcontainer.query.ValidatingSimpleJDBCConnectionPool; + +public class SimpleJDBCConnectionPoolTest { + private JDBCConnectionPool connectionPool; + + @Before + public void setUp() throws SQLException { + connectionPool = new ValidatingSimpleJDBCConnectionPool( + SQLTestsConstants.dbDriver, SQLTestsConstants.dbURL, + SQLTestsConstants.dbUser, SQLTestsConstants.dbPwd, 2, 2); + } + + @Test + public void reserveConnection_reserveNewConnection_returnsConnection() + throws SQLException { + Connection conn = connectionPool.reserveConnection(); + Assert.assertNotNull(conn); + } + + @Test + public void releaseConnection_releaseUnused_shouldNotThrowException() + throws SQLException { + Connection conn = connectionPool.reserveConnection(); + connectionPool.releaseConnection(conn); + Assert.assertFalse(conn.isClosed()); + } + + @Test(expected = SQLException.class) + public void reserveConnection_noConnectionsLeft_shouldFail() + throws SQLException { + try { + connectionPool.reserveConnection(); + connectionPool.reserveConnection(); + } catch (SQLException e) { + e.printStackTrace(); + Assert.fail( + "Exception before all connections used! " + e.getMessage()); + } + + connectionPool.reserveConnection(); + Assert.fail( + "Reserving connection didn't fail even though no connections are available!"); + } + + @Test + public void reserveConnection_oneConnectionLeft_returnsConnection() + throws SQLException { + try { + connectionPool.reserveConnection(); + } catch (SQLException e) { + e.printStackTrace(); + Assert.fail( + "Exception before all connections used! " + e.getMessage()); + } + + Connection conn = connectionPool.reserveConnection(); + Assert.assertNotNull(conn); + } + + @Test + public void reserveConnection_oneConnectionJustReleased_returnsConnection() + throws SQLException { + Connection conn2 = null; + try { + connectionPool.reserveConnection(); + conn2 = connectionPool.reserveConnection(); + } catch (SQLException e) { + e.printStackTrace(); + Assert.fail( + "Exception before all connections used! " + e.getMessage()); + } + + connectionPool.releaseConnection(conn2); + + connectionPool.reserveConnection(); + } + + @Test(expected = IllegalArgumentException.class) + public void construct_allParametersNull_shouldFail() throws SQLException { + SimpleJDBCConnectionPool cp = new SimpleJDBCConnectionPool(null, null, + null, null); + } + + @Test(expected = IllegalArgumentException.class) + public void construct_onlyDriverNameGiven_shouldFail() throws SQLException { + SimpleJDBCConnectionPool cp = new SimpleJDBCConnectionPool( + SQLTestsConstants.dbDriver, null, null, null); + } + + @Test(expected = IllegalArgumentException.class) + public void construct_onlyDriverNameAndUrlGiven_shouldFail() + throws SQLException { + SimpleJDBCConnectionPool cp = new SimpleJDBCConnectionPool( + SQLTestsConstants.dbDriver, SQLTestsConstants.dbURL, null, + null); + } + + @Test(expected = IllegalArgumentException.class) + public void construct_onlyDriverNameAndUrlAndUserGiven_shouldFail() + throws SQLException { + SimpleJDBCConnectionPool cp = new SimpleJDBCConnectionPool( + SQLTestsConstants.dbDriver, SQLTestsConstants.dbURL, + SQLTestsConstants.dbUser, null); + } + + @Test(expected = RuntimeException.class) + public void construct_nonExistingDriver_shouldFail() throws SQLException { + SimpleJDBCConnectionPool cp = new SimpleJDBCConnectionPool("foo", + SQLTestsConstants.dbURL, SQLTestsConstants.dbUser, + SQLTestsConstants.dbPwd); + } + + @Test + public void reserveConnection_newConnectionOpened_shouldSucceed() + throws SQLException { + connectionPool = new SimpleJDBCConnectionPool( + SQLTestsConstants.dbDriver, SQLTestsConstants.dbURL, + SQLTestsConstants.dbUser, SQLTestsConstants.dbPwd, 0, 2); + Connection c = connectionPool.reserveConnection(); + Assert.assertNotNull(c); + } + + @Test + public void releaseConnection_nullConnection_shouldDoNothing() { + connectionPool.releaseConnection(null); + } + + @Test + public void releaseConnection_failingRollback_shouldCallClose() + throws SQLException { + Connection c = EasyMock.createMock(Connection.class); + c.getAutoCommit(); + EasyMock.expectLastCall().andReturn(false); + c.rollback(); + EasyMock.expectLastCall().andThrow(new SQLException("Rollback failed")); + c.close(); + EasyMock.expectLastCall().atLeastOnce(); + EasyMock.replay(c); + // make sure the connection pool is initialized + // Bypass validation + JDBCConnectionPool realPool = ((ValidatingSimpleJDBCConnectionPool) connectionPool) + .getRealPool(); + realPool.reserveConnection(); + realPool.releaseConnection(c); + EasyMock.verify(c); + } + + @Test + public void destroy_shouldCloseAllConnections() throws SQLException { + Connection c1 = connectionPool.reserveConnection(); + Connection c2 = connectionPool.reserveConnection(); + try { + connectionPool.destroy(); + } catch (RuntimeException e) { + // The test connection pool throws an exception when the pool was + // not empty but only after cleanup of the real pool has been done + } + + Assert.assertTrue(c1.isClosed()); + Assert.assertTrue(c2.isClosed()); + } + + @Test + public void destroy_shouldCloseAllConnections2() throws SQLException { + Connection c1 = connectionPool.reserveConnection(); + Connection c2 = connectionPool.reserveConnection(); + connectionPool.releaseConnection(c1); + connectionPool.releaseConnection(c2); + connectionPool.destroy(); + Assert.assertTrue(c1.isClosed()); + Assert.assertTrue(c2.isClosed()); + } + +} diff --git a/compatibility-server/src/test/java/com/vaadin/data/util/sqlcontainer/filters/BetweenTest.java b/compatibility-server/src/test/java/com/vaadin/data/util/sqlcontainer/filters/BetweenTest.java new file mode 100644 index 0000000000..41db88b881 --- /dev/null +++ b/compatibility-server/src/test/java/com/vaadin/data/util/sqlcontainer/filters/BetweenTest.java @@ -0,0 +1,182 @@ +package com.vaadin.data.util.sqlcontainer.filters; + +import org.easymock.EasyMock; +import org.junit.Assert; +import org.junit.Test; + +import com.vaadin.data.Item; +import com.vaadin.data.Property; +import com.vaadin.data.util.filter.Between; + +public class BetweenTest { + + private Item itemWithPropertyValue(Object propertyId, Object value) { + Property<?> property = EasyMock.createMock(Property.class); + property.getValue(); + EasyMock.expectLastCall().andReturn(value).anyTimes(); + EasyMock.replay(property); + + Item item = EasyMock.createMock(Item.class); + item.getItemProperty(propertyId); + EasyMock.expectLastCall().andReturn(property).anyTimes(); + EasyMock.replay(item); + return item; + } + + @Test + public void passesFilter_valueIsInRange_shouldBeTrue() { + Item item = itemWithPropertyValue("foo", 15); + Between between = new Between("foo", 1, 30); + Assert.assertTrue(between.passesFilter("foo", item)); + } + + @Test + public void passesFilter_valueIsOutOfRange_shouldBeFalse() { + Item item = itemWithPropertyValue("foo", 15); + Between between = new Between("foo", 0, 2); + Assert.assertFalse(between.passesFilter("foo", item)); + } + + @Test + public void passesFilter_valueNotComparable_shouldBeFalse() { + Item item = itemWithPropertyValue("foo", new Object()); + Between between = new Between("foo", 0, 2); + Assert.assertFalse(between.passesFilter("foo", item)); + } + + @Test + public void appliesToProperty_differentProperties_shoudlBeFalse() { + Between between = new Between("foo", 0, 2); + Assert.assertFalse(between.appliesToProperty("bar")); + } + + @Test + public void appliesToProperty_sameProperties_shouldBeTrue() { + Between between = new Between("foo", 0, 2); + Assert.assertTrue(between.appliesToProperty("foo")); + } + + @Test + public void hashCode_equalInstances_shouldBeEqual() { + Between b1 = new Between("foo", 0, 2); + Between b2 = new Between("foo", 0, 2); + Assert.assertEquals(b1.hashCode(), b2.hashCode()); + } + + @Test + public void equals_differentObjects_shouldBeFalse() { + Between b1 = new Between("foo", 0, 2); + Object obj = new Object(); + Assert.assertFalse(b1.equals(obj)); + } + + @Test + public void equals_sameInstance_shouldBeTrue() { + Between b1 = new Between("foo", 0, 2); + Between b2 = b1; + Assert.assertTrue(b1.equals(b2)); + } + + @Test + public void equals_equalInstances_shouldBeTrue() { + Between b1 = new Between("foo", 0, 2); + Between b2 = new Between("foo", 0, 2); + Assert.assertTrue(b1.equals(b2)); + } + + @Test + public void equals_equalInstances2_shouldBeTrue() { + Between b1 = new Between(null, null, null); + Between b2 = new Between(null, null, null); + Assert.assertTrue(b1.equals(b2)); + } + + @Test + public void equals_secondValueDiffers_shouldBeFalse() { + Between b1 = new Between("foo", 0, 1); + Between b2 = new Between("foo", 0, 2); + Assert.assertFalse(b1.equals(b2)); + } + + @Test + public void equals_firstAndSecondValueDiffers_shouldBeFalse() { + Between b1 = new Between("foo", 0, null); + Between b2 = new Between("foo", 1, 2); + Assert.assertFalse(b1.equals(b2)); + } + + @Test + public void equals_propertyAndFirstAndSecondValueDiffers_shouldBeFalse() { + Between b1 = new Between("foo", null, 1); + Between b2 = new Between("bar", 1, 2); + Assert.assertFalse(b1.equals(b2)); + } + + @Test + public void equals_propertiesDiffer_shouldBeFalse() { + Between b1 = new Between(null, 0, 1); + Between b2 = new Between("bar", 0, 1); + Assert.assertFalse(b1.equals(b2)); + } + + @Test + public void hashCode_nullStartValue_shouldBeEqual() { + Between b1 = new Between("foo", null, 2); + Between b2 = new Between("foo", null, 2); + Assert.assertEquals(b1.hashCode(), b2.hashCode()); + } + + @Test + public void hashCode_nullEndValue_shouldBeEqual() { + Between b1 = new Between("foo", 0, null); + Between b2 = new Between("foo", 0, null); + Assert.assertEquals(b1.hashCode(), b2.hashCode()); + } + + @Test + public void hashCode_nullPropertyId_shouldBeEqual() { + Between b1 = new Between(null, 0, 2); + Between b2 = new Between(null, 0, 2); + Assert.assertEquals(b1.hashCode(), b2.hashCode()); + } + + @Test + public void passesFilter_nullValue_filterIsPassed() { + String id = "id"; + Between between = new Between(id, null, null); + Assert.assertTrue( + between.passesFilter(id, itemWithPropertyValue(id, null))); + } + + @Test + public void passesFilter_nullStartValue_filterIsPassed() { + String id = "id"; + Between between = new Between(id, null, 2); + Assert.assertTrue( + between.passesFilter(id, itemWithPropertyValue(id, 1))); + } + + @Test + public void passesFilter_nullEndValue_filterIsPassed() { + String id = "id"; + Between between = new Between(id, 0, null); + Assert.assertTrue( + between.passesFilter(id, itemWithPropertyValue(id, 1))); + } + + @Test + public void passesFilter_nullStartValueAndEndValue_filterIsPassed() { + String id = "id"; + Between between = new Between(id, null, null); + Assert.assertTrue( + between.passesFilter(id, itemWithPropertyValue(id, 1))); + } + + @Test + public void passesFilter_nullStartValueAndEndValueAndValueIsNotComparable_filterIsNotPassed() { + String id = "id"; + Between between = new Between(id, null, null); + Assert.assertFalse(between.passesFilter(id, + itemWithPropertyValue(id, new Object()))); + } +} diff --git a/compatibility-server/src/test/java/com/vaadin/data/util/sqlcontainer/filters/CompareTest.java b/compatibility-server/src/test/java/com/vaadin/data/util/sqlcontainer/filters/CompareTest.java new file mode 100644 index 0000000000..a6b6f4b55c --- /dev/null +++ b/compatibility-server/src/test/java/com/vaadin/data/util/sqlcontainer/filters/CompareTest.java @@ -0,0 +1,44 @@ +/* + * Copyright 2000-2016 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.data.util.sqlcontainer.filters; + +import org.junit.Assert; +import org.junit.Test; + +import com.vaadin.data.util.filter.Compare; + +public class CompareTest { + + @Test + public void testEquals() { + Compare c1 = new Compare.Equal("prop1", "val1"); + Compare c2 = new Compare.Equal("prop1", "val1"); + Assert.assertTrue(c1.equals(c2)); + } + + @Test + public void testDifferentTypeEquals() { + Compare c1 = new Compare.Equal("prop1", "val1"); + Compare c2 = new Compare.Greater("prop1", "val1"); + Assert.assertFalse(c1.equals(c2)); + } + + @Test + public void testEqualsNull() { + Compare c1 = new Compare.Equal("prop1", "val1"); + Assert.assertFalse(c1.equals(null)); + } +} diff --git a/compatibility-server/src/test/java/com/vaadin/data/util/sqlcontainer/filters/LikeTest.java b/compatibility-server/src/test/java/com/vaadin/data/util/sqlcontainer/filters/LikeTest.java new file mode 100644 index 0000000000..f1130aad80 --- /dev/null +++ b/compatibility-server/src/test/java/com/vaadin/data/util/sqlcontainer/filters/LikeTest.java @@ -0,0 +1,229 @@ +package com.vaadin.data.util.sqlcontainer.filters; + +import org.junit.Assert; +import org.junit.Test; + +import com.vaadin.data.Item; +import com.vaadin.data.util.ObjectProperty; +import com.vaadin.data.util.PropertysetItem; +import com.vaadin.data.util.filter.Like; + +public class LikeTest { + + @Test + public void passesFilter_valueIsNotStringType_shouldFail() { + Like like = new Like("test", "%foo%"); + + Item item = new PropertysetItem(); + item.addItemProperty("test", new ObjectProperty<Integer>(5)); + + Assert.assertFalse(like.passesFilter("id", item)); + } + + @Test + public void passesFilter_containsLikeQueryOnStringContainingValue_shouldSucceed() { + Like like = new Like("test", "%foo%"); + + Item item = new PropertysetItem(); + item.addItemProperty("test", new ObjectProperty<String>("asdfooghij")); + + Assert.assertTrue(like.passesFilter("id", item)); + } + + @Test + public void passesFilter_containsLikeQueryOnStringContainingValueCaseInsensitive_shouldSucceed() { + Like like = new Like("test", "%foo%"); + like.setCaseSensitive(false); + + Item item = new PropertysetItem(); + item.addItemProperty("test", new ObjectProperty<String>("asdfOOghij")); + + Assert.assertTrue(like.passesFilter("id", item)); + } + + @Test + public void passesFilter_containsLikeQueryOnStringContainingValueConstructedCaseInsensitive_shouldSucceed() { + Like like = new Like("test", "%foo%", false); + + Item item = new PropertysetItem(); + item.addItemProperty("test", new ObjectProperty<String>("asdfOOghij")); + + Assert.assertTrue(like.passesFilter("id", item)); + } + + @Test + public void passesFilter_containsLikeQueryOnStringNotContainingValue_shouldFail() { + Like like = new Like("test", "%foo%"); + + Item item = new PropertysetItem(); + item.addItemProperty("test", new ObjectProperty<String>("asdbarghij")); + + Assert.assertFalse(like.passesFilter("id", item)); + } + + @Test + public void passesFilter_containsLikeQueryOnStringExactlyEqualToValue_shouldSucceed() { + Like like = new Like("test", "%foo%"); + + Item item = new PropertysetItem(); + item.addItemProperty("test", new ObjectProperty<String>("foo")); + + Assert.assertTrue(like.passesFilter("id", item)); + } + + @Test + public void passesFilter_containsLikeQueryOnStringEqualToValueMinusOneCharAtTheEnd_shouldFail() { + Like like = new Like("test", "%foo%"); + + Item item = new PropertysetItem(); + item.addItemProperty("test", new ObjectProperty<String>("fo")); + + Assert.assertFalse(like.passesFilter("id", item)); + } + + @Test + public void passesFilter_beginsWithLikeQueryOnStringBeginningWithValue_shouldSucceed() { + Like like = new Like("test", "foo%"); + + Item item = new PropertysetItem(); + item.addItemProperty("test", new ObjectProperty<String>("foobar")); + + Assert.assertTrue(like.passesFilter("id", item)); + } + + @Test + public void passesFilter_beginsWithLikeQueryOnStringNotBeginningWithValue_shouldFail() { + Like like = new Like("test", "foo%"); + + Item item = new PropertysetItem(); + item.addItemProperty("test", new ObjectProperty<String>("barfoo")); + + Assert.assertFalse(like.passesFilter("id", item)); + } + + @Test + public void passesFilter_endsWithLikeQueryOnStringEndingWithValue_shouldSucceed() { + Like like = new Like("test", "%foo"); + + Item item = new PropertysetItem(); + item.addItemProperty("test", new ObjectProperty<String>("barfoo")); + + Assert.assertTrue(like.passesFilter("id", item)); + } + + @Test + public void passesFilter_endsWithLikeQueryOnStringNotEndingWithValue_shouldFail() { + Like like = new Like("test", "%foo"); + + Item item = new PropertysetItem(); + item.addItemProperty("test", new ObjectProperty<String>("foobar")); + + Assert.assertFalse(like.passesFilter("id", item)); + } + + @Test + public void passesFilter_startsWithAndEndsWithOnMatchingValue_shouldSucceed() { + Like like = new Like("test", "foo%bar"); + + Item item = new PropertysetItem(); + item.addItemProperty("test", new ObjectProperty<String>("fooASDFbar")); + + Assert.assertTrue(like.passesFilter("id", item)); + } + + @Test + public void appliesToProperty_valueIsProperty_shouldBeTrue() { + Like like = new Like("test", "%foo"); + Assert.assertTrue(like.appliesToProperty("test")); + } + + @Test + public void appliesToProperty_valueIsNotProperty_shouldBeFalse() { + Like like = new Like("test", "%foo"); + Assert.assertFalse(like.appliesToProperty("bar")); + } + + @Test + public void equals_sameInstances_shouldBeTrue() { + Like like1 = new Like("test", "%foo"); + Like like2 = like1; + Assert.assertTrue(like1.equals(like2)); + } + + @Test + public void equals_twoEqualInstances_shouldBeTrue() { + Like like1 = new Like("test", "foo"); + Like like2 = new Like("test", "foo"); + Assert.assertTrue(like1.equals(like2)); + } + + @Test + public void equals_differentValues_shouldBeFalse() { + Like like1 = new Like("test", "foo"); + Like like2 = new Like("test", "bar"); + Assert.assertFalse(like1.equals(like2)); + } + + @Test + public void equals_differentProperties_shouldBeFalse() { + Like like1 = new Like("foo", "test"); + Like like2 = new Like("bar", "test"); + Assert.assertFalse(like1.equals(like2)); + } + + @Test + public void equals_differentPropertiesAndValues_shouldBeFalse() { + Like like1 = new Like("foo", "bar"); + Like like2 = new Like("baz", "zomg"); + Assert.assertFalse(like1.equals(like2)); + } + + @Test + public void equals_differentClasses_shouldBeFalse() { + Like like1 = new Like("foo", "bar"); + Object obj = new Object(); + Assert.assertFalse(like1.equals(obj)); + } + + @Test + public void equals_bothHaveNullProperties_shouldBeTrue() { + Like like1 = new Like(null, "foo"); + Like like2 = new Like(null, "foo"); + Assert.assertTrue(like1.equals(like2)); + } + + @Test + public void equals_bothHaveNullValues_shouldBeTrue() { + Like like1 = new Like("foo", null); + Like like2 = new Like("foo", null); + Assert.assertTrue(like1.equals(like2)); + } + + @Test + public void equals_onePropertyIsNull_shouldBeFalse() { + Like like1 = new Like(null, "bar"); + Like like2 = new Like("foo", "baz"); + Assert.assertFalse(like1.equals(like2)); + } + + @Test + public void equals_oneValueIsNull_shouldBeFalse() { + Like like1 = new Like("foo", null); + Like like2 = new Like("baz", "bar"); + Assert.assertFalse(like1.equals(like2)); + } + + @Test + public void hashCode_equalInstances_shouldBeEqual() { + Like like1 = new Like("test", "foo"); + Like like2 = new Like("test", "foo"); + Assert.assertEquals(like1.hashCode(), like2.hashCode()); + } + + @Test + public void hashCode_differentPropertiesAndValues_shouldNotEqual() { + Like like1 = new Like("foo", "bar"); + Like like2 = new Like("baz", "zomg"); + Assert.assertTrue(like1.hashCode() != like2.hashCode()); + } +} diff --git a/compatibility-server/src/test/java/com/vaadin/data/util/sqlcontainer/generator/SQLGeneratorsTest.java b/compatibility-server/src/test/java/com/vaadin/data/util/sqlcontainer/generator/SQLGeneratorsTest.java new file mode 100644 index 0000000000..dd1db30b72 --- /dev/null +++ b/compatibility-server/src/test/java/com/vaadin/data/util/sqlcontainer/generator/SQLGeneratorsTest.java @@ -0,0 +1,239 @@ +package com.vaadin.data.util.sqlcontainer.generator; + +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import com.vaadin.data.Container.Filter; +import com.vaadin.data.util.filter.Like; +import com.vaadin.data.util.filter.Or; +import com.vaadin.data.util.sqlcontainer.DataGenerator; +import com.vaadin.data.util.sqlcontainer.RowItem; +import com.vaadin.data.util.sqlcontainer.SQLContainer; +import com.vaadin.data.util.sqlcontainer.SQLTestsConstants; +import com.vaadin.data.util.sqlcontainer.connection.JDBCConnectionPool; +import com.vaadin.data.util.sqlcontainer.query.OrderBy; +import com.vaadin.data.util.sqlcontainer.query.TableQuery; +import com.vaadin.data.util.sqlcontainer.query.ValidatingSimpleJDBCConnectionPool; +import com.vaadin.data.util.sqlcontainer.query.generator.DefaultSQLGenerator; +import com.vaadin.data.util.sqlcontainer.query.generator.MSSQLGenerator; +import com.vaadin.data.util.sqlcontainer.query.generator.OracleGenerator; +import com.vaadin.data.util.sqlcontainer.query.generator.SQLGenerator; +import com.vaadin.data.util.sqlcontainer.query.generator.StatementHelper; + +public class SQLGeneratorsTest { + private JDBCConnectionPool connectionPool; + + @Before + public void setUp() throws SQLException { + + try { + connectionPool = new ValidatingSimpleJDBCConnectionPool( + SQLTestsConstants.dbDriver, SQLTestsConstants.dbURL, + SQLTestsConstants.dbUser, SQLTestsConstants.dbPwd, 2, 2); + } catch (SQLException e) { + e.printStackTrace(); + Assert.fail(e.getMessage()); + } + + DataGenerator.addPeopleToDatabase(connectionPool); + } + + @After + public void tearDown() { + if (connectionPool != null) { + connectionPool.destroy(); + } + } + + @Test + public void generateSelectQuery_basicQuery_shouldSucceed() { + SQLGenerator sg = new DefaultSQLGenerator(); + StatementHelper sh = sg.generateSelectQuery("TABLE", null, null, 0, 0, + null); + Assert.assertEquals(sh.getQueryString(), "SELECT * FROM TABLE"); + } + + @Test + public void generateSelectQuery_pagingAndColumnsSet_shouldSucceed() { + SQLGenerator sg = new DefaultSQLGenerator(); + StatementHelper sh = sg.generateSelectQuery("TABLE", null, null, 4, 8, + "COL1, COL2, COL3"); + Assert.assertEquals(sh.getQueryString(), + "SELECT COL1, COL2, COL3 FROM TABLE LIMIT 8 OFFSET 4"); + } + + /** + * Note: Only tests one kind of filter and ordering. + */ + @Test + public void generateSelectQuery_filtersAndOrderingSet_shouldSucceed() { + SQLGenerator sg = new DefaultSQLGenerator(); + List<com.vaadin.data.Container.Filter> f = new ArrayList<Filter>(); + f.add(new Like("name", "%lle")); + List<OrderBy> ob = Arrays.asList(new OrderBy("name", true)); + StatementHelper sh = sg.generateSelectQuery("TABLE", f, ob, 0, 0, null); + Assert.assertEquals(sh.getQueryString(), + "SELECT * FROM TABLE WHERE \"name\" LIKE ? ORDER BY \"name\" ASC"); + } + + @Test + public void generateSelectQuery_filtersAndOrderingSet_exclusiveFilteringMode_shouldSucceed() { + SQLGenerator sg = new DefaultSQLGenerator(); + List<Filter> f = new ArrayList<Filter>(); + f.add(new Or(new Like("name", "%lle"), new Like("name", "vi%"))); + List<OrderBy> ob = Arrays.asList(new OrderBy("name", true)); + StatementHelper sh = sg.generateSelectQuery("TABLE", f, ob, 0, 0, null); + // TODO + Assert.assertEquals(sh.getQueryString(), + "SELECT * FROM TABLE WHERE (\"name\" LIKE ? " + + "OR \"name\" LIKE ?) ORDER BY \"name\" ASC"); + } + + @Test + public void generateDeleteQuery_basicQuery_shouldSucceed() + throws SQLException { + /* + * No need to run this for Oracle/MSSQL generators since the + * DefaultSQLGenerator method would be called anyway. + */ + if (SQLTestsConstants.sqlGen instanceof MSSQLGenerator + || SQLTestsConstants.sqlGen instanceof OracleGenerator) { + return; + } + SQLGenerator sg = SQLTestsConstants.sqlGen; + TableQuery query = new TableQuery("people", connectionPool, + SQLTestsConstants.sqlGen); + SQLContainer container = new SQLContainer(query); + + StatementHelper sh = sg.generateDeleteQuery("people", + query.getPrimaryKeyColumns(), null, (RowItem) container + .getItem(container.getItemIds().iterator().next())); + Assert.assertEquals("DELETE FROM people WHERE \"ID\" = ?", + sh.getQueryString()); + } + + @Test + public void generateUpdateQuery_basicQuery_shouldSucceed() + throws SQLException { + /* + * No need to run this for Oracle/MSSQL generators since the + * DefaultSQLGenerator method would be called anyway. + */ + if (SQLTestsConstants.sqlGen instanceof MSSQLGenerator + || SQLTestsConstants.sqlGen instanceof OracleGenerator) { + return; + } + SQLGenerator sg = new DefaultSQLGenerator(); + TableQuery query = new TableQuery("people", connectionPool); + SQLContainer container = new SQLContainer(query); + + RowItem ri = (RowItem) container + .getItem(container.getItemIds().iterator().next()); + ri.getItemProperty("NAME").setValue("Viljami"); + + StatementHelper sh = sg.generateUpdateQuery("people", ri); + Assert.assertTrue( + "UPDATE people SET \"NAME\" = ?, \"AGE\" = ? WHERE \"ID\" = ?" + .equals(sh.getQueryString()) + || "UPDATE people SET \"AGE\" = ?, \"NAME\" = ? WHERE \"ID\" = ?" + .equals(sh.getQueryString())); + } + + @Test + public void generateInsertQuery_basicQuery_shouldSucceed() + throws SQLException { + /* + * No need to run this for Oracle/MSSQL generators since the + * DefaultSQLGenerator method would be called anyway. + */ + if (SQLTestsConstants.sqlGen instanceof MSSQLGenerator + || SQLTestsConstants.sqlGen instanceof OracleGenerator) { + return; + } + SQLGenerator sg = new DefaultSQLGenerator(); + TableQuery query = new TableQuery("people", connectionPool); + SQLContainer container = new SQLContainer(query); + + RowItem ri = (RowItem) container.getItem(container.addItem()); + ri.getItemProperty("NAME").setValue("Viljami"); + + StatementHelper sh = sg.generateInsertQuery("people", ri); + + Assert.assertTrue("INSERT INTO people (\"NAME\", \"AGE\") VALUES (?, ?)" + .equals(sh.getQueryString()) + || "INSERT INTO people (\"AGE\", \"NAME\") VALUES (?, ?)" + .equals(sh.getQueryString())); + } + + @Test + public void generateComplexSelectQuery_forOracle_shouldSucceed() + throws SQLException { + SQLGenerator sg = new OracleGenerator(); + List<Filter> f = new ArrayList<Filter>(); + f.add(new Like("name", "%lle")); + List<OrderBy> ob = Arrays.asList(new OrderBy("name", true)); + StatementHelper sh = sg.generateSelectQuery("TABLE", f, ob, 4, 8, + "NAME, ID"); + Assert.assertEquals( + "SELECT * FROM (SELECT x.*, ROWNUM AS \"rownum\" FROM" + + " (SELECT NAME, ID FROM TABLE WHERE \"name\" LIKE ?" + + " ORDER BY \"name\" ASC) x) WHERE \"rownum\" BETWEEN 5 AND 12", + sh.getQueryString()); + } + + @Test + public void generateComplexSelectQuery_forMSSQL_shouldSucceed() + throws SQLException { + SQLGenerator sg = new MSSQLGenerator(); + List<Filter> f = new ArrayList<Filter>(); + f.add(new Like("name", "%lle")); + List<OrderBy> ob = Arrays.asList(new OrderBy("name", true)); + StatementHelper sh = sg.generateSelectQuery("TABLE", f, ob, 4, 8, + "NAME, ID"); + Assert.assertEquals(sh.getQueryString(), + "SELECT * FROM (SELECT row_number() OVER " + + "( ORDER BY \"name\" ASC) AS rownum, NAME, ID " + + "FROM TABLE WHERE \"name\" LIKE ?) " + + "AS a WHERE a.rownum BETWEEN 5 AND 12"); + } + + @Test + public void generateComplexSelectQuery_forOracle_exclusiveFilteringMode_shouldSucceed() + throws SQLException { + SQLGenerator sg = new OracleGenerator(); + List<Filter> f = new ArrayList<Filter>(); + f.add(new Or(new Like("name", "%lle"), new Like("name", "vi%"))); + List<OrderBy> ob = Arrays.asList(new OrderBy("name", true)); + StatementHelper sh = sg.generateSelectQuery("TABLE", f, ob, 4, 8, + "NAME, ID"); + Assert.assertEquals(sh.getQueryString(), + "SELECT * FROM (SELECT x.*, ROWNUM AS \"rownum\" FROM" + + " (SELECT NAME, ID FROM TABLE WHERE (\"name\" LIKE ?" + + " OR \"name\" LIKE ?) " + + "ORDER BY \"name\" ASC) x) WHERE \"rownum\" BETWEEN 5 AND 12"); + } + + @Test + public void generateComplexSelectQuery_forMSSQL_exclusiveFilteringMode_shouldSucceed() + throws SQLException { + SQLGenerator sg = new MSSQLGenerator(); + List<Filter> f = new ArrayList<Filter>(); + f.add(new Or(new Like("name", "%lle"), new Like("name", "vi%"))); + List<OrderBy> ob = Arrays.asList(new OrderBy("name", true)); + StatementHelper sh = sg.generateSelectQuery("TABLE", f, ob, 4, 8, + "NAME, ID"); + Assert.assertEquals(sh.getQueryString(), + "SELECT * FROM (SELECT row_number() OVER " + + "( ORDER BY \"name\" ASC) AS rownum, NAME, ID " + + "FROM TABLE WHERE (\"name\" LIKE ? " + + "OR \"name\" LIKE ?)) " + + "AS a WHERE a.rownum BETWEEN 5 AND 12"); + } +} diff --git a/compatibility-server/src/test/java/com/vaadin/data/util/sqlcontainer/generator/StatementHelperTest.java b/compatibility-server/src/test/java/com/vaadin/data/util/sqlcontainer/generator/StatementHelperTest.java new file mode 100644 index 0000000000..7201ec7ad8 --- /dev/null +++ b/compatibility-server/src/test/java/com/vaadin/data/util/sqlcontainer/generator/StatementHelperTest.java @@ -0,0 +1,64 @@ +/* + * Copyright 2000-2016 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.data.util.sqlcontainer.generator; + +import java.sql.PreparedStatement; +import java.sql.SQLException; + +import org.easymock.EasyMock; +import org.junit.Assert; +import org.junit.Test; + +import com.vaadin.data.util.sqlcontainer.query.generator.StatementHelper; + +/** + * + * @author Vaadin Ltd + */ +public class StatementHelperTest { + + @Test + public void testSetValueNullParameter() throws SQLException { + StatementHelper helper = new StatementHelper(); + helper.addParameterValue(null, StatementHelper.class); + PreparedStatement statement = EasyMock + .createMock(PreparedStatement.class); + // should throw SQLException, not NPE + try { + helper.setParameterValuesToStatement(statement); + Assert.fail("Expected SQLExecption for unsupported type"); + } catch (SQLException e) { + // Exception should contain info about which parameter and the type + // which was unsupported + Assert.assertTrue(e.getMessage().contains("parameter 0")); + Assert.assertTrue( + e.getMessage().contains(StatementHelper.class.getName())); + } + } + + @Test + public void testSetByteArrayValue() throws SQLException { + StatementHelper helper = new StatementHelper(); + helper.addParameterValue(null, byte[].class); + PreparedStatement statement = EasyMock + .createMock(PreparedStatement.class); + // should not throw SQLException + helper.setParameterValuesToStatement(statement); + + EasyMock.replay(statement); + statement.setBytes(1, null); + } +} diff --git a/compatibility-server/src/test/java/com/vaadin/data/util/sqlcontainer/query/FreeformQueryTest.java b/compatibility-server/src/test/java/com/vaadin/data/util/sqlcontainer/query/FreeformQueryTest.java new file mode 100644 index 0000000000..1b9e14b0d8 --- /dev/null +++ b/compatibility-server/src/test/java/com/vaadin/data/util/sqlcontainer/query/FreeformQueryTest.java @@ -0,0 +1,1005 @@ +package com.vaadin.data.util.sqlcontainer.query; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.easymock.EasyMock; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import com.vaadin.data.Container.Filter; +import com.vaadin.data.util.filter.Like; +import com.vaadin.data.util.sqlcontainer.DataGenerator; +import com.vaadin.data.util.sqlcontainer.RowId; +import com.vaadin.data.util.sqlcontainer.RowItem; +import com.vaadin.data.util.sqlcontainer.SQLContainer; +import com.vaadin.data.util.sqlcontainer.SQLTestsConstants; +import com.vaadin.data.util.sqlcontainer.SQLTestsConstants.DB; +import com.vaadin.data.util.sqlcontainer.connection.JDBCConnectionPool; +import com.vaadin.data.util.sqlcontainer.query.generator.StatementHelper; + +public class FreeformQueryTest { + + private static final int offset = SQLTestsConstants.offset; + private JDBCConnectionPool connectionPool; + + @Before + public void setUp() throws SQLException { + + try { + connectionPool = new ValidatingSimpleJDBCConnectionPool( + SQLTestsConstants.dbDriver, SQLTestsConstants.dbURL, + SQLTestsConstants.dbUser, SQLTestsConstants.dbPwd, 2, 2); + } catch (SQLException e) { + e.printStackTrace(); + Assert.fail(e.getMessage()); + } + + DataGenerator.addPeopleToDatabase(connectionPool); + } + + @After + public void tearDown() { + if (connectionPool != null) { + connectionPool.destroy(); + } + } + + @Test + public void construction_legalParameters_shouldSucceed() { + FreeformQuery ffQuery = new FreeformQuery("SELECT * FROM foo", + Arrays.asList("ID"), connectionPool); + Assert.assertArrayEquals(new Object[] { "ID" }, + ffQuery.getPrimaryKeyColumns().toArray()); + + Assert.assertEquals("SELECT * FROM foo", ffQuery.getQueryString()); + } + + @Test(expected = IllegalArgumentException.class) + public void construction_emptyQueryString_shouldFail() { + new FreeformQuery("", Arrays.asList("ID"), connectionPool); + } + + @Test + public void construction_nullPrimaryKeys_shouldSucceed() { + new FreeformQuery("SELECT * FROM foo", null, connectionPool); + } + + @Test + public void construction_nullPrimaryKeys2_shouldSucceed() { + new FreeformQuery("SELECT * FROM foo", connectionPool); + } + + @Test + public void construction_emptyPrimaryKeys_shouldSucceed() { + new FreeformQuery("SELECT * FROM foo", connectionPool); + } + + @Test(expected = IllegalArgumentException.class) + public void construction_emptyStringsInPrimaryKeys_shouldFail() { + new FreeformQuery("SELECT * FROM foo", Arrays.asList(""), + connectionPool); + } + + @Test(expected = IllegalArgumentException.class) + public void construction_nullConnectionPool_shouldFail() { + new FreeformQuery("SELECT * FROM foo", Arrays.asList("ID"), null); + } + + @Test + public void getCount_simpleQuery_returnsFour() throws SQLException { + FreeformQuery query = new FreeformQuery("SELECT * FROM people", + Arrays.asList("ID"), connectionPool); + Assert.assertEquals(4, query.getCount()); + } + + @Test(expected = SQLException.class) + public void getCount_illegalQuery_shouldThrowSQLException() + throws SQLException { + FreeformQuery query = new FreeformQuery("SELECT * FROM asdf", + Arrays.asList("ID"), connectionPool); + query.getResults(0, 50); + } + + @Test + public void getCount_simpleQueryTwoMorePeopleAdded_returnsSix() + throws SQLException { + // Add some people + Connection conn = connectionPool.reserveConnection(); + Statement statement = conn.createStatement(); + if (SQLTestsConstants.db == DB.MSSQL) { + statement.executeUpdate("insert into people values('Bengt', 30)"); + statement.executeUpdate("insert into people values('Ingvar', 50)"); + } else { + statement.executeUpdate( + "insert into people values(default, 'Bengt', 30)"); + statement.executeUpdate( + "insert into people values(default, 'Ingvar', 50)"); + } + statement.close(); + conn.commit(); + connectionPool.releaseConnection(conn); + + FreeformQuery query = new FreeformQuery("SELECT * FROM people", + Arrays.asList("ID"), connectionPool); + + Assert.assertEquals(6, query.getCount()); + } + + @Test + public void getCount_moreComplexQuery_returnsThree() throws SQLException { + FreeformQuery query = new FreeformQuery( + "SELECT * FROM people WHERE \"NAME\" LIKE '%lle'", + connectionPool, new String[] { "ID" }); + Assert.assertEquals(3, query.getCount()); + } + + @Test + public void getCount_normalState_releasesConnection() throws SQLException { + FreeformQuery query = new FreeformQuery( + "SELECT * FROM people WHERE \"NAME\" LIKE '%lle'", + connectionPool, "ID"); + query.getCount(); + query.getCount(); + Connection c = connectionPool.reserveConnection(); + Assert.assertNotNull(c); + // Cleanup to make test connection pool happy + connectionPool.releaseConnection(c); + } + + @Test + public void getCount_delegateRegistered_shouldUseDelegate() + throws SQLException { + FreeformQuery query = new FreeformQuery("SELECT * FROM people", + Arrays.asList("ID"), connectionPool); + FreeformQueryDelegate delegate = EasyMock + .createMock(FreeformQueryDelegate.class); + EasyMock.expect(delegate.getCountQuery()).andReturn( + "SELECT COUNT(*) FROM people WHERE \"NAME\" LIKE '%lle'"); + EasyMock.replay(delegate); + query.setDelegate(delegate); + Assert.assertEquals(3, query.getCount()); + EasyMock.verify(delegate); + } + + @Test + public void getCount_delegateRegisteredZeroRows_returnsZero() + throws SQLException { + DataGenerator.createGarbage(connectionPool); + FreeformQuery query = new FreeformQuery("SELECT * FROM GARBAGE", + Arrays.asList("ID"), connectionPool); + FreeformQueryDelegate delegate = EasyMock + .createMock(FreeformQueryDelegate.class); + EasyMock.expect(delegate.getCountQuery()) + .andReturn("SELECT COUNT(*) FROM GARBAGE"); + EasyMock.replay(delegate); + query.setDelegate(delegate); + Assert.assertEquals(0, query.getCount()); + EasyMock.verify(delegate); + } + + @Test + public void getResults_simpleQuery_returnsFourRecords() + throws SQLException { + FreeformQuery query = new FreeformQuery( + "SELECT \"ID\",\"NAME\" FROM people", Arrays.asList("ID"), + connectionPool); + query.beginTransaction(); + ResultSet rs = query.getResults(0, 0); + + Assert.assertTrue(rs.next()); + Assert.assertEquals(0 + offset, rs.getInt(1)); + Assert.assertEquals("Ville", rs.getString(2)); + + Assert.assertTrue(rs.next()); + Assert.assertEquals(1 + offset, rs.getInt(1)); + Assert.assertEquals("Kalle", rs.getString(2)); + + Assert.assertTrue(rs.next()); + Assert.assertEquals(2 + offset, rs.getInt(1)); + Assert.assertEquals("Pelle", rs.getString(2)); + + Assert.assertTrue(rs.next()); + Assert.assertEquals(3 + offset, rs.getInt(1)); + Assert.assertEquals("Börje", rs.getString(2)); + + Assert.assertFalse(rs.next()); + query.commit(); + } + + @Test + public void getResults_moreComplexQuery_returnsThreeRecords() + throws SQLException { + FreeformQuery query = new FreeformQuery( + "SELECT * FROM people WHERE \"NAME\" LIKE '%lle'", + Arrays.asList("ID"), connectionPool); + query.beginTransaction(); + ResultSet rs = query.getResults(0, 0); + + Assert.assertTrue(rs.next()); + Assert.assertEquals(0 + offset, rs.getInt(1)); + Assert.assertEquals("Ville", rs.getString(2)); + + Assert.assertTrue(rs.next()); + Assert.assertEquals(1 + offset, rs.getInt(1)); + Assert.assertEquals("Kalle", rs.getString(2)); + + Assert.assertTrue(rs.next()); + Assert.assertEquals(2 + offset, rs.getInt(1)); + Assert.assertEquals("Pelle", rs.getString(2)); + + Assert.assertFalse(rs.next()); + query.commit(); + } + + @Test + public void getResults_noDelegate5000Rows_returns5000rows() + throws SQLException { + DataGenerator.addFiveThousandPeople(connectionPool); + + FreeformQuery query = new FreeformQuery("SELECT * FROM people", + Arrays.asList("ID"), connectionPool); + query.beginTransaction(); + ResultSet rs = query.getResults(0, 0); + for (int i = 0; i < 5000; i++) { + Assert.assertTrue(rs.next()); + } + Assert.assertFalse(rs.next()); + query.commit(); + } + + @Test(expected = UnsupportedOperationException.class) + public void setFilters_noDelegate_shouldFail() { + FreeformQuery query = new FreeformQuery("SELECT * FROM people", + Arrays.asList("ID"), connectionPool); + ArrayList<Filter> filters = new ArrayList<Filter>(); + filters.add(new Like("name", "%lle")); + query.setFilters(filters); + } + + @Test(expected = UnsupportedOperationException.class) + public void setOrderBy_noDelegate_shouldFail() { + FreeformQuery query = new FreeformQuery("SELECT * FROM people", + Arrays.asList("ID"), connectionPool); + query.setOrderBy(Arrays.asList(new OrderBy("name", true))); + } + + @Test(expected = IllegalStateException.class) + public void storeRow_noDelegateNoTransactionActive_shouldFail() + throws SQLException { + FreeformQuery query = new FreeformQuery("SELECT * FROM people", + Arrays.asList("ID"), connectionPool); + query.storeRow(new RowItem(new SQLContainer(query), + new RowId(new Object[] { 1 }), null)); + } + + @Test + public void storeRow_noDelegate_shouldFail() throws SQLException { + FreeformQuery query = new FreeformQuery("SELECT * FROM people", + Arrays.asList("ID"), connectionPool); + SQLContainer container = EasyMock.createNiceMock(SQLContainer.class); + EasyMock.replay(container); + query.beginTransaction(); + try { + query.storeRow(new RowItem(container, new RowId(new Object[] { 1 }), + null)); + Assert.fail("storeRow should fail when there is no delegate"); + } catch (UnsupportedOperationException e) { + // Cleanup to make test connection pool happy + query.rollback(); + } + } + + @Test + public void removeRow_noDelegate_shouldFail() throws SQLException { + FreeformQuery query = new FreeformQuery("SELECT * FROM people", + Arrays.asList("ID"), connectionPool); + SQLContainer container = EasyMock.createNiceMock(SQLContainer.class); + EasyMock.replay(container); + query.beginTransaction(); + try { + query.removeRow(new RowItem(container, + new RowId(new Object[] { 1 }), null)); + Assert.fail("removeRow should fail when there is no delgate"); + } catch (UnsupportedOperationException e) { + // Cleanup to make test connection pool happy + query.rollback(); + } + } + + @Test + public void commit_readOnly_shouldSucceed() throws SQLException { + FreeformQuery query = new FreeformQuery("SELECT * FROM people", + Arrays.asList("ID"), connectionPool); + query.beginTransaction(); + query.commit(); + } + + @Test + public void rollback_readOnly_shouldSucceed() throws SQLException { + FreeformQuery query = new FreeformQuery("SELECT * FROM people", + Arrays.asList("ID"), connectionPool); + query.beginTransaction(); + query.rollback(); + } + + @Test(expected = SQLException.class) + public void commit_noActiveTransaction_shouldFail() throws SQLException { + FreeformQuery query = new FreeformQuery("SELECT * FROM people", + Arrays.asList("ID"), connectionPool); + query.commit(); + } + + @Test(expected = SQLException.class) + public void rollback_noActiveTransaction_shouldFail() throws SQLException { + FreeformQuery query = new FreeformQuery("SELECT * FROM people", + Arrays.asList("ID"), connectionPool); + query.rollback(); + } + + @Test + public void containsRowWithKeys_simpleQueryWithExistingKeys_returnsTrue() + throws SQLException { + FreeformQuery query = new FreeformQuery("SELECT * FROM people", + Arrays.asList("ID"), connectionPool); + Assert.assertTrue(query.containsRowWithKey(1)); + } + + @Test + public void containsRowWithKeys_simpleQueryWithNonexistingKeys_returnsTrue() + throws SQLException { + FreeformQuery query = new FreeformQuery("SELECT * FROM people", + Arrays.asList("ID"), connectionPool); + Assert.assertFalse(query.containsRowWithKey(1337)); + } + + // (expected = SQLException.class) + @Test + public void containsRowWithKeys_simpleQueryWithInvalidKeys_shouldFail() + throws SQLException { + FreeformQuery query = new FreeformQuery("SELECT * FROM people", + Arrays.asList("ID"), connectionPool); + Assert.assertFalse(query.containsRowWithKey(38796)); + } + + @Test + public void containsRowWithKeys_queryContainingWhereClauseAndExistingKeys_returnsTrue() + throws SQLException { + FreeformQuery query = new FreeformQuery( + "SELECT * FROM people WHERE \"NAME\" LIKE '%lle'", + Arrays.asList("ID"), connectionPool); + Assert.assertTrue(query.containsRowWithKey(1)); + } + + @Test + public void containsRowWithKeys_queryContainingLowercaseWhereClauseAndExistingKeys_returnsTrue() + throws SQLException { + FreeformQuery query = new FreeformQuery( + "select * from people where \"NAME\" like '%lle'", + Arrays.asList("ID"), connectionPool); + Assert.assertTrue(query.containsRowWithKey(1)); + } + + @Test + public void containsRowWithKeys_nullKeys_shouldFailAndReleaseConnections() + throws SQLException { + FreeformQuery query = new FreeformQuery( + "select * from people where \"NAME\" like '%lle'", + Arrays.asList("ID"), connectionPool); + try { + query.containsRowWithKey(new Object[] { null }); + } catch (SQLException e) { + // We should now be able to reserve two connections + connectionPool.reserveConnection(); + connectionPool.reserveConnection(); + } + } + + /* + * -------- Tests with a delegate --------- + */ + + @Test + public void setDelegate_noExistingDelegate_shouldRegisterNewDelegate() { + FreeformQuery query = new FreeformQuery("SELECT * FROM people", + Arrays.asList("ID"), connectionPool); + FreeformQueryDelegate delegate = EasyMock + .createMock(FreeformQueryDelegate.class); + query.setDelegate(delegate); + Assert.assertEquals(delegate, query.getDelegate()); + } + + @Test + public void getResults_hasDelegate_shouldCallDelegate() + throws SQLException { + FreeformQuery query = new FreeformQuery("SELECT * FROM people", + Arrays.asList("ID"), connectionPool); + FreeformQueryDelegate delegate = EasyMock + .createMock(FreeformQueryDelegate.class); + if (SQLTestsConstants.db == DB.MSSQL) { + EasyMock.expect(delegate.getQueryString(0, 2)) + .andReturn("SELECT * FROM (SELECT row_number()" + + "OVER (ORDER BY id ASC) AS rownum, * FROM people)" + + " AS a WHERE a.rownum BETWEEN 0 AND 2"); + } else if (SQLTestsConstants.db == DB.ORACLE) { + EasyMock.expect(delegate.getQueryString(0, 2)) + .andReturn("SELECT * FROM (SELECT x.*, ROWNUM AS r FROM" + + " (SELECT * FROM people) x) WHERE r BETWEEN 1 AND 2"); + } else { + EasyMock.expect(delegate.getQueryString(0, 2)) + .andReturn("SELECT * FROM people LIMIT 2 OFFSET 0"); + } + EasyMock.replay(delegate); + + query.setDelegate(delegate); + query.beginTransaction(); + query.getResults(0, 2); + EasyMock.verify(delegate); + query.commit(); + } + + @Test + public void getResults_delegateImplementsGetQueryString_shouldHonorOffsetAndPagelength() + throws SQLException { + FreeformQuery query = new FreeformQuery("SELECT * FROM people", + Arrays.asList("ID"), connectionPool); + FreeformQueryDelegate delegate = EasyMock + .createMock(FreeformQueryDelegate.class); + if (SQLTestsConstants.db == DB.MSSQL) { + EasyMock.expect(delegate.getQueryString(0, 2)) + .andReturn("SELECT * FROM (SELECT row_number()" + + "OVER (ORDER BY id ASC) AS rownum, * FROM people)" + + " AS a WHERE a.rownum BETWEEN 0 AND 2"); + } else if (SQLTestsConstants.db == DB.ORACLE) { + EasyMock.expect(delegate.getQueryString(0, 2)) + .andReturn("SELECT * FROM (SELECT x.*, ROWNUM AS r FROM" + + " (SELECT * FROM people) x) WHERE r BETWEEN 1 AND 2"); + } else { + EasyMock.expect(delegate.getQueryString(0, 2)) + .andReturn("SELECT * FROM people LIMIT 2 OFFSET 0"); + } + EasyMock.replay(delegate); + query.setDelegate(delegate); + + query.beginTransaction(); + ResultSet rs = query.getResults(0, 2); + int rsoffset = 0; + if (SQLTestsConstants.db == DB.MSSQL) { + rsoffset++; + } + Assert.assertTrue(rs.next()); + Assert.assertEquals(0 + offset, rs.getInt(1 + rsoffset)); + Assert.assertEquals("Ville", rs.getString(2 + rsoffset)); + + Assert.assertTrue(rs.next()); + Assert.assertEquals(1 + offset, rs.getInt(1 + rsoffset)); + Assert.assertEquals("Kalle", rs.getString(2 + rsoffset)); + + Assert.assertFalse(rs.next()); + + EasyMock.verify(delegate); + query.commit(); + } + + @Test + public void getResults_delegateRegistered5000Rows_returns100rows() + throws SQLException { + DataGenerator.addFiveThousandPeople(connectionPool); + FreeformQuery query = new FreeformQuery("SELECT * FROM people", + Arrays.asList("ID"), connectionPool); + FreeformQueryDelegate delegate = EasyMock + .createMock(FreeformQueryDelegate.class); + if (SQLTestsConstants.db == DB.MSSQL) { + EasyMock.expect(delegate.getQueryString(200, 100)) + .andReturn("SELECT * FROM (SELECT row_number()" + + "OVER (ORDER BY id ASC) AS rownum, * FROM people)" + + " AS a WHERE a.rownum BETWEEN 201 AND 300"); + } else if (SQLTestsConstants.db == DB.ORACLE) { + EasyMock.expect(delegate.getQueryString(200, 100)) + .andReturn("SELECT * FROM (SELECT x.*, ROWNUM AS r FROM" + + " (SELECT * FROM people ORDER BY ID ASC) x) WHERE r BETWEEN 201 AND 300"); + } else { + EasyMock.expect(delegate.getQueryString(200, 100)) + .andReturn("SELECT * FROM people LIMIT 100 OFFSET 200"); + } + EasyMock.replay(delegate); + query.setDelegate(delegate); + + query.beginTransaction(); + ResultSet rs = query.getResults(200, 100); + for (int i = 0; i < 100; i++) { + Assert.assertTrue(rs.next()); + Assert.assertEquals(200 + i + offset, rs.getInt("ID")); + } + Assert.assertFalse(rs.next()); + query.commit(); + } + + @Test + public void setFilters_delegateImplementsSetFilters_shouldPassFiltersToDelegate() { + FreeformQuery query = new FreeformQuery("SELECT * FROM people", + Arrays.asList("ID"), connectionPool); + FreeformQueryDelegate delegate = EasyMock + .createMock(FreeformQueryDelegate.class); + List<Filter> filters = new ArrayList<Filter>(); + filters.add(new Like("name", "%lle")); + delegate.setFilters(filters); + + EasyMock.replay(delegate); + query.setDelegate(delegate); + + query.setFilters(filters); + + EasyMock.verify(delegate); + } + + @Test(expected = UnsupportedOperationException.class) + public void setFilters_delegateDoesNotImplementSetFilters_shouldFail() { + FreeformQuery query = new FreeformQuery("SELECT * FROM people", + Arrays.asList("ID"), connectionPool); + FreeformQueryDelegate delegate = EasyMock + .createMock(FreeformQueryDelegate.class); + List<Filter> filters = new ArrayList<Filter>(); + filters.add(new Like("name", "%lle")); + delegate.setFilters(filters); + EasyMock.expectLastCall().andThrow(new UnsupportedOperationException()); + EasyMock.replay(delegate); + query.setDelegate(delegate); + + query.setFilters(filters); + + EasyMock.verify(delegate); + } + + @Test + public void setOrderBy_delegateImplementsSetOrderBy_shouldPassArgumentsToDelegate() { + FreeformQuery query = new FreeformQuery("SELECT * FROM people", + Arrays.asList("ID"), connectionPool); + FreeformQueryDelegate delegate = EasyMock + .createMock(FreeformQueryDelegate.class); + List<OrderBy> orderBys = Arrays.asList(new OrderBy("name", false)); + delegate.setOrderBy(orderBys); + EasyMock.replay(delegate); + query.setDelegate(delegate); + + query.setOrderBy(orderBys); + + EasyMock.verify(delegate); + } + + @Test(expected = UnsupportedOperationException.class) + public void setOrderBy_delegateDoesNotImplementSetOrderBy_shouldFail() { + FreeformQuery query = new FreeformQuery("SELECT * FROM people", + Arrays.asList("ID"), connectionPool); + FreeformQueryDelegate delegate = EasyMock + .createMock(FreeformQueryDelegate.class); + List<OrderBy> orderBys = Arrays.asList(new OrderBy("name", false)); + delegate.setOrderBy(orderBys); + EasyMock.expectLastCall().andThrow(new UnsupportedOperationException()); + EasyMock.replay(delegate); + query.setDelegate(delegate); + + query.setOrderBy(orderBys); + + EasyMock.verify(delegate); + } + + @Test + public void setFilters_noDelegateAndNullParameter_shouldSucceed() { + FreeformQuery query = new FreeformQuery("SELECT * FROM people", + Arrays.asList("ID"), connectionPool); + query.setFilters(null); + } + + @Test + public void setOrderBy_noDelegateAndNullParameter_shouldSucceed() { + FreeformQuery query = new FreeformQuery("SELECT * FROM people", + Arrays.asList("ID"), connectionPool); + query.setOrderBy(null); + } + + @Test + public void storeRow_delegateImplementsStoreRow_shouldPassToDelegate() + throws SQLException { + FreeformQuery query = new FreeformQuery("SELECT * FROM people", + Arrays.asList("ID"), connectionPool); + FreeformQueryDelegate delegate = EasyMock + .createMock(FreeformQueryDelegate.class); + EasyMock.expect(delegate.storeRow(EasyMock.isA(Connection.class), + EasyMock.isA(RowItem.class))).andReturn(1); + SQLContainer container = EasyMock.createNiceMock(SQLContainer.class); + EasyMock.replay(delegate, container); + query.setDelegate(delegate); + + query.beginTransaction(); + RowItem row = new RowItem(container, new RowId(new Object[] { 1 }), + null); + query.storeRow(row); + query.commit(); + + EasyMock.verify(delegate, container); + } + + @Test + public void storeRow_delegateDoesNotImplementStoreRow_shouldFail() + throws SQLException { + FreeformQuery query = new FreeformQuery("SELECT * FROM people", + Arrays.asList("ID"), connectionPool); + FreeformQueryDelegate delegate = EasyMock + .createMock(FreeformQueryDelegate.class); + EasyMock.expect(delegate.storeRow(EasyMock.isA(Connection.class), + EasyMock.isA(RowItem.class))) + .andThrow(new UnsupportedOperationException()); + SQLContainer container = EasyMock.createNiceMock(SQLContainer.class); + EasyMock.replay(delegate, container); + query.setDelegate(delegate); + + query.beginTransaction(); + RowItem row = new RowItem(container, new RowId(new Object[] { 1 }), + null); + try { + query.storeRow(row); + Assert.fail( + "storeRow should fail when delgate does not implement storeRow"); + } catch (UnsupportedOperationException e) { + // Cleanup to make test connection pool happy + query.rollback(); + } + } + + @Test + public void removeRow_delegateImplementsRemoveRow_shouldPassToDelegate() + throws SQLException { + FreeformQuery query = new FreeformQuery("SELECT * FROM people", + Arrays.asList("ID"), connectionPool); + FreeformQueryDelegate delegate = EasyMock + .createMock(FreeformQueryDelegate.class); + EasyMock.expect(delegate.removeRow(EasyMock.isA(Connection.class), + EasyMock.isA(RowItem.class))).andReturn(true); + SQLContainer container = EasyMock.createNiceMock(SQLContainer.class); + EasyMock.replay(delegate, container); + query.setDelegate(delegate); + + query.beginTransaction(); + RowItem row = new RowItem(container, new RowId(new Object[] { 1 }), + null); + query.removeRow(row); + query.commit(); + + EasyMock.verify(delegate, container); + } + + @Test + public void removeRow_delegateDoesNotImplementRemoveRow_shouldFail() + throws SQLException { + FreeformQuery query = new FreeformQuery("SELECT * FROM people", + Arrays.asList("ID"), connectionPool); + FreeformQueryDelegate delegate = EasyMock + .createMock(FreeformQueryDelegate.class); + EasyMock.expect(delegate.removeRow(EasyMock.isA(Connection.class), + EasyMock.isA(RowItem.class))) + .andThrow(new UnsupportedOperationException()); + SQLContainer container = EasyMock.createNiceMock(SQLContainer.class); + EasyMock.replay(delegate, container); + query.setDelegate(delegate); + + query.beginTransaction(); + RowItem row = new RowItem(container, new RowId(new Object[] { 1 }), + null); + try { + query.removeRow(row); + Assert.fail( + "removeRow should fail when delegate does not implement removeRow"); + } catch (UnsupportedOperationException e) { + // Cleanup to make test connection pool happy + query.rollback(); + } + } + + @Test + public void beginTransaction_delegateRegistered_shouldSucceed() + throws UnsupportedOperationException, SQLException { + FreeformQuery query = new FreeformQuery("SELECT * FROM people", + Arrays.asList("ID"), connectionPool); + FreeformQueryDelegate delegate = EasyMock + .createMock(FreeformQueryDelegate.class); + EasyMock.replay(delegate); + query.setDelegate(delegate); + + query.beginTransaction(); + // Cleanup to make test connection pool happy + query.rollback(); + } + + @Test + public void beginTransaction_transactionAlreadyActive_shouldFail() + throws SQLException { + FreeformQuery query = new FreeformQuery("SELECT * FROM people", + Arrays.asList("ID"), connectionPool); + + query.beginTransaction(); + try { + query.beginTransaction(); + Assert.fail( + "Should throw exception when starting a transaction while already in a transaction"); + } catch (IllegalStateException e) { + // Cleanup to make test connection pool happy + query.rollback(); + } + } + + @Test(expected = SQLException.class) + public void commit_delegateRegisteredNoActiveTransaction_shouldFail() + throws UnsupportedOperationException, SQLException { + FreeformQuery query = new FreeformQuery("SELECT * FROM people", + Arrays.asList("ID"), connectionPool); + FreeformQueryDelegate delegate = EasyMock + .createMock(FreeformQueryDelegate.class); + EasyMock.replay(delegate); + query.setDelegate(delegate); + + query.commit(); + } + + @Test + public void commit_delegateRegisteredActiveTransaction_shouldSucceed() + throws UnsupportedOperationException, SQLException { + FreeformQuery query = new FreeformQuery("SELECT * FROM people", + Arrays.asList("ID"), connectionPool); + FreeformQueryDelegate delegate = EasyMock + .createMock(FreeformQueryDelegate.class); + EasyMock.replay(delegate); + query.setDelegate(delegate); + + query.beginTransaction(); + query.commit(); + } + + @Test(expected = SQLException.class) + public void commit_delegateRegisteredActiveTransactionDoubleCommit_shouldFail() + throws UnsupportedOperationException, SQLException { + FreeformQuery query = new FreeformQuery("SELECT * FROM people", + Arrays.asList("ID"), connectionPool); + FreeformQueryDelegate delegate = EasyMock + .createMock(FreeformQueryDelegate.class); + EasyMock.replay(delegate); + query.setDelegate(delegate); + + query.beginTransaction(); + query.commit(); + query.commit(); + } + + @Test(expected = SQLException.class) + public void rollback_delegateRegisteredNoActiveTransaction_shouldFail() + throws UnsupportedOperationException, SQLException { + FreeformQuery query = new FreeformQuery("SELECT * FROM people", + Arrays.asList("ID"), connectionPool); + FreeformQueryDelegate delegate = EasyMock + .createMock(FreeformQueryDelegate.class); + EasyMock.replay(delegate); + query.setDelegate(delegate); + + query.rollback(); + } + + @Test + public void rollback_delegateRegisteredActiveTransaction_shouldSucceed() + throws UnsupportedOperationException, SQLException { + FreeformQuery query = new FreeformQuery("SELECT * FROM people", + Arrays.asList("ID"), connectionPool); + FreeformQueryDelegate delegate = EasyMock + .createMock(FreeformQueryDelegate.class); + EasyMock.replay(delegate); + query.setDelegate(delegate); + + query.beginTransaction(); + query.rollback(); + } + + @Test(expected = SQLException.class) + public void rollback_delegateRegisteredActiveTransactionDoubleRollback_shouldFail() + throws UnsupportedOperationException, SQLException { + FreeformQuery query = new FreeformQuery("SELECT * FROM people", + Arrays.asList("ID"), connectionPool); + FreeformQueryDelegate delegate = EasyMock + .createMock(FreeformQueryDelegate.class); + EasyMock.replay(delegate); + query.setDelegate(delegate); + + query.beginTransaction(); + query.rollback(); + query.rollback(); + } + + @Test(expected = SQLException.class) + public void rollback_delegateRegisteredCommittedTransaction_shouldFail() + throws UnsupportedOperationException, SQLException { + FreeformQuery query = new FreeformQuery("SELECT * FROM people", + Arrays.asList("ID"), connectionPool); + FreeformQueryDelegate delegate = EasyMock + .createMock(FreeformQueryDelegate.class); + EasyMock.replay(delegate); + query.setDelegate(delegate); + + query.beginTransaction(); + query.commit(); + query.rollback(); + } + + @Test(expected = SQLException.class) + public void commit_delegateRegisteredRollbackedTransaction_shouldFail() + throws UnsupportedOperationException, SQLException { + FreeformQuery query = new FreeformQuery("SELECT * FROM people", + Arrays.asList("ID"), connectionPool); + FreeformQueryDelegate delegate = EasyMock + .createMock(FreeformQueryDelegate.class); + EasyMock.replay(delegate); + query.setDelegate(delegate); + + query.beginTransaction(); + query.rollback(); + query.commit(); + } + + @Test(expected = SQLException.class) + public void containsRowWithKeys_delegateRegistered_shouldCallGetContainsRowQueryString() + throws SQLException { + FreeformQuery query = new FreeformQuery( + "SELECT * FROM people WHERE name LIKE '%lle'", + Arrays.asList("ID"), connectionPool); + FreeformQueryDelegate delegate = EasyMock + .createMock(FreeformQueryDelegate.class); + EasyMock.expect(delegate.getContainsRowQueryString(1)).andReturn(""); + EasyMock.replay(delegate); + query.setDelegate(delegate); + + query.containsRowWithKey(1); + + EasyMock.verify(delegate); + } + + @Test + public void containsRowWithKeys_delegateRegistered_shouldUseResultFromGetContainsRowQueryString() + throws SQLException { + FreeformQuery query = new FreeformQuery( + "SELECT * FROM people WHERE \"NAME\" LIKE '%lle'", + Arrays.asList("ID"), connectionPool); + FreeformQueryDelegate delegate = EasyMock + .createMock(FreeformQueryDelegate.class); + // In order to test that this is the query that is actually used, we use + // a non-existing id in place of the existing one. + EasyMock.expect(delegate.getContainsRowQueryString(1)).andReturn( + "SELECT * FROM people WHERE \"NAME\" LIKE '%lle' AND \"ID\" = 1337"); + EasyMock.replay(delegate); + query.setDelegate(delegate); + + // The id (key) used should be 1337 as above, for the call with key = 1 + Assert.assertFalse(query.containsRowWithKey(1)); + + EasyMock.verify(delegate); + } + + public static class NonMatchingDelegateWithGroupBy + implements FreeformQueryDelegate { + + private String fromWhere = "FROM people p1 LEFT JOIN people p2 ON p2.id = p1.id WHERE p1.\"NAME\" LIKE 'notfound' GROUP BY p1.ID"; + + @Override + public int storeRow(Connection conn, RowItem row) + throws UnsupportedOperationException, SQLException { + // Not used in this test + return 0; + } + + @Override + public void setOrderBy(List<OrderBy> orderBys) + throws UnsupportedOperationException { + // Not used in this test + } + + @Override + public void setFilters(List<Filter> filters) + throws UnsupportedOperationException { + // Not used in this test + } + + @Override + public boolean removeRow(Connection conn, RowItem row) + throws UnsupportedOperationException, SQLException { + // Not used in this test + return false; + } + + @Override + public String getQueryString(int offset, int limit) + throws UnsupportedOperationException { + return "SELECT * " + fromWhere; + } + + @Override + public String getCountQuery() throws UnsupportedOperationException { + return "SELECT COUNT(*) " + fromWhere; + } + + @Override + public String getContainsRowQueryString(Object... keys) + throws UnsupportedOperationException { + // Not used in this test + return null; + } + } + + public static class NonMatchingStatementDelegateWithGroupBy + extends NonMatchingDelegateWithGroupBy + implements FreeformStatementDelegate { + + @Override + public StatementHelper getQueryStatement(int offset, int limit) + throws UnsupportedOperationException { + StatementHelper sh = new StatementHelper(); + sh.setQueryString(getQueryString(offset, limit)); + return sh; + } + + @Override + public StatementHelper getCountStatement() + throws UnsupportedOperationException { + StatementHelper sh = new StatementHelper(); + sh.setQueryString(getCountQuery()); + return sh; + } + + @Override + public StatementHelper getContainsRowQueryStatement(Object... keys) + throws UnsupportedOperationException { + // Not used in this test + return null; + } + } + + @Test + public void containsRowWithKeys_delegateRegisteredGetContainsRowQueryStringNotImplemented_shouldBuildQueryString() + throws SQLException { + FreeformQuery query = new FreeformQuery( + "SELECT * FROM people WHERE \"NAME\" LIKE '%lle'", + Arrays.asList("ID"), connectionPool); + FreeformQueryDelegate delegate = EasyMock + .createMock(FreeformQueryDelegate.class); + EasyMock.expect(delegate.getContainsRowQueryString(1)) + .andThrow(new UnsupportedOperationException()); + EasyMock.replay(delegate); + query.setDelegate(delegate); + + Assert.assertTrue(query.containsRowWithKey(1)); + + EasyMock.verify(delegate); + } + + @Test + public void delegateStatementCountWithGroupBy() throws SQLException { + String dummyNotUsed = "foo"; + FreeformQuery query = new FreeformQuery(dummyNotUsed, connectionPool, + "p1.ID"); + query.setDelegate(new NonMatchingStatementDelegateWithGroupBy()); + + Assert.assertEquals(0, query.getCount()); + } + + @Test + public void delegateCountWithGroupBy() throws SQLException { + String dummyNotUsed = "foo"; + FreeformQuery query = new FreeformQuery(dummyNotUsed, connectionPool, + "p1.ID"); + query.setDelegate(new NonMatchingDelegateWithGroupBy()); + + Assert.assertEquals(0, query.getCount()); + } +} diff --git a/compatibility-server/src/test/java/com/vaadin/data/util/sqlcontainer/query/QueryBuilderTest.java b/compatibility-server/src/test/java/com/vaadin/data/util/sqlcontainer/query/QueryBuilderTest.java new file mode 100644 index 0000000000..7974582147 --- /dev/null +++ b/compatibility-server/src/test/java/com/vaadin/data/util/sqlcontainer/query/QueryBuilderTest.java @@ -0,0 +1,315 @@ +package com.vaadin.data.util.sqlcontainer.query; + +import java.util.ArrayList; + +import org.easymock.EasyMock; +import org.junit.Assert; +import org.junit.Test; + +import com.vaadin.data.Container.Filter; +import com.vaadin.data.util.filter.And; +import com.vaadin.data.util.filter.Between; +import com.vaadin.data.util.filter.Compare.Equal; +import com.vaadin.data.util.filter.Compare.Greater; +import com.vaadin.data.util.filter.Compare.GreaterOrEqual; +import com.vaadin.data.util.filter.Compare.Less; +import com.vaadin.data.util.filter.Compare.LessOrEqual; +import com.vaadin.data.util.filter.IsNull; +import com.vaadin.data.util.filter.Like; +import com.vaadin.data.util.filter.Not; +import com.vaadin.data.util.filter.Or; +import com.vaadin.data.util.filter.SimpleStringFilter; +import com.vaadin.data.util.sqlcontainer.query.generator.StatementHelper; +import com.vaadin.data.util.sqlcontainer.query.generator.filter.QueryBuilder; +import com.vaadin.data.util.sqlcontainer.query.generator.filter.StringDecorator; + +public class QueryBuilderTest { + + private StatementHelper mockedStatementHelper(Object... values) { + StatementHelper sh = EasyMock.createMock(StatementHelper.class); + for (Object val : values) { + sh.addParameterValue(val); + EasyMock.expectLastCall(); + } + EasyMock.replay(sh); + return sh; + } + + // escape bad characters and wildcards + + @Test + public void getWhereStringForFilter_equals() { + StatementHelper sh = mockedStatementHelper("Fido"); + Equal f = new Equal("NAME", "Fido"); + Assert.assertEquals("\"NAME\" = ?", + QueryBuilder.getWhereStringForFilter(f, sh)); + EasyMock.verify(sh); + } + + @Test + public void getWhereStringForFilter_greater() { + StatementHelper sh = mockedStatementHelper(18); + Greater f = new Greater("AGE", 18); + Assert.assertEquals("\"AGE\" > ?", + QueryBuilder.getWhereStringForFilter(f, sh)); + EasyMock.verify(sh); + } + + @Test + public void getWhereStringForFilter_less() { + StatementHelper sh = mockedStatementHelper(65); + Less f = new Less("AGE", 65); + Assert.assertEquals("\"AGE\" < ?", + QueryBuilder.getWhereStringForFilter(f, sh)); + EasyMock.verify(sh); + } + + @Test + public void getWhereStringForFilter_greaterOrEqual() { + StatementHelper sh = mockedStatementHelper(18); + GreaterOrEqual f = new GreaterOrEqual("AGE", 18); + Assert.assertEquals("\"AGE\" >= ?", + QueryBuilder.getWhereStringForFilter(f, sh)); + EasyMock.verify(sh); + } + + @Test + public void getWhereStringForFilter_lessOrEqual() { + StatementHelper sh = mockedStatementHelper(65); + LessOrEqual f = new LessOrEqual("AGE", 65); + Assert.assertEquals("\"AGE\" <= ?", + QueryBuilder.getWhereStringForFilter(f, sh)); + EasyMock.verify(sh); + } + + @Test + public void getWhereStringForFilter_simpleStringFilter() { + StatementHelper sh = mockedStatementHelper("Vi%"); + SimpleStringFilter f = new SimpleStringFilter("NAME", "Vi", false, + true); + Assert.assertEquals("\"NAME\" LIKE ?", + QueryBuilder.getWhereStringForFilter(f, sh)); + EasyMock.verify(sh); + } + + @Test + public void getWhereStringForFilter_simpleStringFilterMatchAnywhere() { + StatementHelper sh = mockedStatementHelper("%Vi%"); + SimpleStringFilter f = new SimpleStringFilter("NAME", "Vi", false, + false); + Assert.assertEquals("\"NAME\" LIKE ?", + QueryBuilder.getWhereStringForFilter(f, sh)); + EasyMock.verify(sh); + } + + @Test + public void getWhereStringForFilter_simpleStringFilterMatchAnywhereIgnoreCase() { + StatementHelper sh = mockedStatementHelper("%VI%"); + SimpleStringFilter f = new SimpleStringFilter("NAME", "Vi", true, + false); + Assert.assertEquals("UPPER(\"NAME\") LIKE ?", + QueryBuilder.getWhereStringForFilter(f, sh)); + EasyMock.verify(sh); + } + + @Test + public void getWhereStringForFilter_startsWith() { + StatementHelper sh = mockedStatementHelper("Vi%"); + Like f = new Like("NAME", "Vi%"); + Assert.assertEquals("\"NAME\" LIKE ?", + QueryBuilder.getWhereStringForFilter(f, sh)); + EasyMock.verify(sh); + } + + @Test + public void getWhereStringForFilter_startsWithNumber() { + StatementHelper sh = mockedStatementHelper("1%"); + Like f = new Like("AGE", "1%"); + Assert.assertEquals("\"AGE\" LIKE ?", + QueryBuilder.getWhereStringForFilter(f, sh)); + EasyMock.verify(sh); + } + + @Test + public void getWhereStringForFilter_endsWith() { + StatementHelper sh = mockedStatementHelper("%lle"); + Like f = new Like("NAME", "%lle"); + Assert.assertEquals("\"NAME\" LIKE ?", + QueryBuilder.getWhereStringForFilter(f, sh)); + EasyMock.verify(sh); + } + + @Test + public void getWhereStringForFilter_contains() { + StatementHelper sh = mockedStatementHelper("%ill%"); + Like f = new Like("NAME", "%ill%"); + Assert.assertEquals("\"NAME\" LIKE ?", + QueryBuilder.getWhereStringForFilter(f, sh)); + EasyMock.verify(sh); + } + + @Test + public void getWhereStringForFilter_between() { + StatementHelper sh = mockedStatementHelper(18, 65); + Between f = new Between("AGE", 18, 65); + Assert.assertEquals("\"AGE\" BETWEEN ? AND ?", + QueryBuilder.getWhereStringForFilter(f, sh)); + EasyMock.verify(sh); + } + + @Test + public void getWhereStringForFilter_caseInsensitive_equals() { + StatementHelper sh = mockedStatementHelper("FIDO"); + Like f = new Like("NAME", "Fido"); + f.setCaseSensitive(false); + Assert.assertEquals("UPPER(\"NAME\") LIKE ?", + QueryBuilder.getWhereStringForFilter(f, sh)); + EasyMock.verify(sh); + } + + @Test + public void getWhereStringForFilter_caseInsensitive_startsWith() { + StatementHelper sh = mockedStatementHelper("VI%"); + Like f = new Like("NAME", "Vi%"); + f.setCaseSensitive(false); + Assert.assertEquals("UPPER(\"NAME\") LIKE ?", + QueryBuilder.getWhereStringForFilter(f, sh)); + EasyMock.verify(sh); + } + + @Test + public void getWhereStringForFilter_caseInsensitive_endsWith() { + StatementHelper sh = mockedStatementHelper("%LLE"); + Like f = new Like("NAME", "%lle"); + f.setCaseSensitive(false); + Assert.assertEquals("UPPER(\"NAME\") LIKE ?", + QueryBuilder.getWhereStringForFilter(f, sh)); + EasyMock.verify(sh); + } + + @Test + public void getWhereStringForFilter_caseInsensitive_contains() { + StatementHelper sh = mockedStatementHelper("%ILL%"); + Like f = new Like("NAME", "%ill%"); + f.setCaseSensitive(false); + Assert.assertEquals("UPPER(\"NAME\") LIKE ?", + QueryBuilder.getWhereStringForFilter(f, sh)); + EasyMock.verify(sh); + } + + @Test + public void getWhereStringForFilters_listOfFilters() { + StatementHelper sh = mockedStatementHelper("%lle", 18); + ArrayList<Filter> filters = new ArrayList<Filter>(); + filters.add(new Like("NAME", "%lle")); + filters.add(new Greater("AGE", 18)); + Assert.assertEquals(" WHERE \"NAME\" LIKE ? AND \"AGE\" > ?", + QueryBuilder.getWhereStringForFilters(filters, sh)); + EasyMock.verify(sh); + } + + @Test + public void getWhereStringForFilters_oneAndFilter() { + StatementHelper sh = mockedStatementHelper("%lle", 18); + ArrayList<Filter> filters = new ArrayList<Filter>(); + filters.add(new And(new Like("NAME", "%lle"), new Greater("AGE", 18))); + Assert.assertEquals(" WHERE (\"NAME\" LIKE ? AND \"AGE\" > ?)", + QueryBuilder.getWhereStringForFilters(filters, sh)); + EasyMock.verify(sh); + } + + @Test + public void getWhereStringForFilters_oneOrFilter() { + StatementHelper sh = mockedStatementHelper("%lle", 18); + ArrayList<Filter> filters = new ArrayList<Filter>(); + filters.add(new Or(new Like("NAME", "%lle"), new Greater("AGE", 18))); + Assert.assertEquals(" WHERE (\"NAME\" LIKE ? OR \"AGE\" > ?)", + QueryBuilder.getWhereStringForFilters(filters, sh)); + EasyMock.verify(sh); + } + + @Test + public void getWhereStringForFilters_complexCompoundFilters() { + StatementHelper sh = mockedStatementHelper("%lle", 18, 65, "Pelle"); + ArrayList<Filter> filters = new ArrayList<Filter>(); + filters.add(new Or( + new And(new Like("NAME", "%lle"), + new Or(new Less("AGE", 18), new Greater("AGE", 65))), + new Equal("NAME", "Pelle"))); + Assert.assertEquals( + " WHERE ((\"NAME\" LIKE ? AND (\"AGE\" < ? OR \"AGE\" > ?)) OR \"NAME\" = ?)", + QueryBuilder.getWhereStringForFilters(filters, sh)); + EasyMock.verify(sh); + } + + @Test + public void getWhereStringForFilters_complexCompoundFiltersAndSingleFilter() { + StatementHelper sh = mockedStatementHelper("%lle", 18, 65, "Pelle", + "Virtanen"); + ArrayList<Filter> filters = new ArrayList<Filter>(); + filters.add(new Or( + new And(new Like("NAME", "%lle"), + new Or(new Less("AGE", 18), new Greater("AGE", 65))), + new Equal("NAME", "Pelle"))); + filters.add(new Equal("LASTNAME", "Virtanen")); + Assert.assertEquals( + " WHERE ((\"NAME\" LIKE ? AND (\"AGE\" < ? OR \"AGE\" > ?)) OR \"NAME\" = ?) AND \"LASTNAME\" = ?", + QueryBuilder.getWhereStringForFilters(filters, sh)); + EasyMock.verify(sh); + } + + @Test + public void getWhereStringForFilters_emptyList_shouldReturnEmptyString() { + ArrayList<Filter> filters = new ArrayList<Filter>(); + Assert.assertEquals("", QueryBuilder.getWhereStringForFilters(filters, + new StatementHelper())); + } + + @Test + public void getWhereStringForFilters_NotFilter() { + StatementHelper sh = mockedStatementHelper(18); + ArrayList<Filter> filters = new ArrayList<Filter>(); + filters.add(new Not(new Equal("AGE", 18))); + Assert.assertEquals(" WHERE NOT \"AGE\" = ?", + QueryBuilder.getWhereStringForFilters(filters, sh)); + EasyMock.verify(sh); + } + + @Test + public void getWhereStringForFilters_complexNegatedFilter() { + StatementHelper sh = mockedStatementHelper(65, 18); + ArrayList<Filter> filters = new ArrayList<Filter>(); + filters.add( + new Not(new Or(new Equal("AGE", 65), new Equal("AGE", 18)))); + Assert.assertEquals(" WHERE NOT (\"AGE\" = ? OR \"AGE\" = ?)", + QueryBuilder.getWhereStringForFilters(filters, sh)); + EasyMock.verify(sh); + } + + @Test + public void getWhereStringForFilters_isNull() { + ArrayList<Filter> filters = new ArrayList<Filter>(); + filters.add(new IsNull("NAME")); + Assert.assertEquals(" WHERE \"NAME\" IS NULL", QueryBuilder + .getWhereStringForFilters(filters, new StatementHelper())); + } + + @Test + public void getWhereStringForFilters_isNotNull() { + ArrayList<Filter> filters = new ArrayList<Filter>(); + filters.add(new Not(new IsNull("NAME"))); + Assert.assertEquals(" WHERE \"NAME\" IS NOT NULL", QueryBuilder + .getWhereStringForFilters(filters, new StatementHelper())); + } + + @Test + public void getWhereStringForFilters_customStringDecorator() { + QueryBuilder.setStringDecorator(new StringDecorator("[", "]")); + ArrayList<Filter> filters = new ArrayList<Filter>(); + filters.add(new Not(new IsNull("NAME"))); + Assert.assertEquals(" WHERE [NAME] IS NOT NULL", QueryBuilder + .getWhereStringForFilters(filters, new StatementHelper())); + // Reset the default string decorator + QueryBuilder.setStringDecorator(new StringDecorator("\"", "\"")); + } +} diff --git a/compatibility-server/src/test/java/com/vaadin/data/util/sqlcontainer/query/TableQueryTest.java b/compatibility-server/src/test/java/com/vaadin/data/util/sqlcontainer/query/TableQueryTest.java new file mode 100644 index 0000000000..d2067a85b4 --- /dev/null +++ b/compatibility-server/src/test/java/com/vaadin/data/util/sqlcontainer/query/TableQueryTest.java @@ -0,0 +1,746 @@ +package com.vaadin.data.util.sqlcontainer.query; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import com.vaadin.data.Container.Filter; +import com.vaadin.data.util.filter.Compare.Equal; +import com.vaadin.data.util.filter.Like; +import com.vaadin.data.util.sqlcontainer.DataGenerator; +import com.vaadin.data.util.sqlcontainer.OptimisticLockException; +import com.vaadin.data.util.sqlcontainer.RowItem; +import com.vaadin.data.util.sqlcontainer.SQLContainer; +import com.vaadin.data.util.sqlcontainer.SQLTestsConstants; +import com.vaadin.data.util.sqlcontainer.SQLTestsConstants.DB; +import com.vaadin.data.util.sqlcontainer.connection.JDBCConnectionPool; +import com.vaadin.data.util.sqlcontainer.query.generator.DefaultSQLGenerator; + +public class TableQueryTest { + private static final int offset = SQLTestsConstants.offset; + private JDBCConnectionPool connectionPool; + + @Before + public void setUp() throws SQLException { + try { + connectionPool = new ValidatingSimpleJDBCConnectionPool( + SQLTestsConstants.dbDriver, SQLTestsConstants.dbURL, + SQLTestsConstants.dbUser, SQLTestsConstants.dbPwd, 2, 2); + } catch (SQLException e) { + e.printStackTrace(); + Assert.fail(e.getMessage()); + } + DataGenerator.addPeopleToDatabase(connectionPool); + } + + @After + public void tearDown() { + if (connectionPool != null) { + connectionPool.destroy(); + } + } + + /********************************************************************** + * TableQuery construction tests + **********************************************************************/ + @Test + public void construction_legalParameters_shouldSucceed() { + TableQuery tQuery = new TableQuery("people", connectionPool, + new DefaultSQLGenerator()); + Assert.assertArrayEquals(new Object[] { "ID" }, + tQuery.getPrimaryKeyColumns().toArray()); + boolean correctTableName = "people" + .equalsIgnoreCase(tQuery.getTableName()); + Assert.assertTrue(correctTableName); + } + + @Test + public void construction_legalParameters_defaultGenerator_shouldSucceed() { + TableQuery tQuery = new TableQuery("people", connectionPool, + SQLTestsConstants.sqlGen); + Assert.assertArrayEquals(new Object[] { "ID" }, + tQuery.getPrimaryKeyColumns().toArray()); + boolean correctTableName = "people" + .equalsIgnoreCase(tQuery.getTableName()); + Assert.assertTrue(correctTableName); + } + + @Test(expected = IllegalArgumentException.class) + public void construction_nonExistingTableName_shouldFail() { + new TableQuery("skgwaguhsd", connectionPool, new DefaultSQLGenerator()); + } + + @Test(expected = IllegalArgumentException.class) + public void construction_emptyTableName_shouldFail() { + new TableQuery("", connectionPool, new DefaultSQLGenerator()); + } + + @Test(expected = IllegalArgumentException.class) + public void construction_nullSqlGenerator_shouldFail() { + new TableQuery("people", connectionPool, null); + } + + @Test(expected = IllegalArgumentException.class) + public void construction_nullConnectionPool_shouldFail() { + new TableQuery("people", null, new DefaultSQLGenerator()); + } + + /********************************************************************** + * TableQuery row count tests + **********************************************************************/ + @Test + public void getCount_simpleQuery_returnsFour() throws SQLException { + TableQuery tQuery = new TableQuery("people", connectionPool, + SQLTestsConstants.sqlGen); + Assert.assertEquals(4, tQuery.getCount()); + } + + @Test + public void getCount_simpleQueryTwoMorePeopleAdded_returnsSix() + throws SQLException { + // Add some people + Connection conn = connectionPool.reserveConnection(); + Statement statement = conn.createStatement(); + if (SQLTestsConstants.db == DB.MSSQL) { + statement.executeUpdate("insert into people values('Bengt', 30)"); + statement.executeUpdate("insert into people values('Ingvar', 50)"); + } else { + statement.executeUpdate( + "insert into people values(default, 'Bengt', 30)"); + statement.executeUpdate( + "insert into people values(default, 'Ingvar', 50)"); + } + statement.close(); + conn.commit(); + connectionPool.releaseConnection(conn); + + TableQuery tQuery = new TableQuery("people", connectionPool, + SQLTestsConstants.sqlGen); + + Assert.assertEquals(6, tQuery.getCount()); + } + + @Test + public void getCount_normalState_releasesConnection() throws SQLException { + TableQuery tQuery = new TableQuery("people", connectionPool, + SQLTestsConstants.sqlGen); + tQuery.getCount(); + tQuery.getCount(); + Connection c = connectionPool.reserveConnection(); + Assert.assertNotNull(c); + connectionPool.releaseConnection(c); + } + + /********************************************************************** + * TableQuery get results tests + **********************************************************************/ + @Test + public void getResults_simpleQuery_returnsFourRecords() + throws SQLException { + TableQuery tQuery = new TableQuery("people", connectionPool, + SQLTestsConstants.sqlGen); + tQuery.beginTransaction(); + ResultSet rs = tQuery.getResults(0, 0); + + Assert.assertTrue(rs.next()); + Assert.assertEquals(0 + offset, rs.getInt(1)); + Assert.assertEquals("Ville", rs.getString(2)); + + Assert.assertTrue(rs.next()); + Assert.assertEquals(1 + offset, rs.getInt(1)); + Assert.assertEquals("Kalle", rs.getString(2)); + + Assert.assertTrue(rs.next()); + Assert.assertEquals(2 + offset, rs.getInt(1)); + Assert.assertEquals("Pelle", rs.getString(2)); + + Assert.assertTrue(rs.next()); + Assert.assertEquals(3 + offset, rs.getInt(1)); + Assert.assertEquals("Börje", rs.getString(2)); + + Assert.assertFalse(rs.next()); + tQuery.commit(); + } + + @Test + public void getResults_noDelegate5000Rows_returns5000rows() + throws SQLException { + DataGenerator.addFiveThousandPeople(connectionPool); + + TableQuery tQuery = new TableQuery("people", connectionPool, + SQLTestsConstants.sqlGen); + + tQuery.beginTransaction(); + ResultSet rs = tQuery.getResults(0, 0); + for (int i = 0; i < 5000; i++) { + Assert.assertTrue(rs.next()); + } + Assert.assertFalse(rs.next()); + tQuery.commit(); + } + + /********************************************************************** + * TableQuery transaction management tests + **********************************************************************/ + @Test + public void beginTransaction_transactionAlreadyActive_shouldFail() + throws SQLException { + TableQuery tQuery = new TableQuery("people", connectionPool, + SQLTestsConstants.sqlGen); + + tQuery.beginTransaction(); + try { + tQuery.beginTransaction(); + Assert.fail( + "Should throw exception when starting a transaction while already in a transaction"); + } catch (IllegalStateException e) { + // Cleanup to make test connection pool happy + tQuery.rollback(); + } + } + + @Test + public void commit_readOnly_shouldSucceed() throws SQLException { + TableQuery tQuery = new TableQuery("people", connectionPool, + SQLTestsConstants.sqlGen); + tQuery.beginTransaction(); + tQuery.commit(); + } + + @Test + public void rollback_readOnly_shouldSucceed() throws SQLException { + TableQuery tQuery = new TableQuery("people", connectionPool, + SQLTestsConstants.sqlGen); + tQuery.beginTransaction(); + tQuery.rollback(); + } + + @Test(expected = SQLException.class) + public void commit_noActiveTransaction_shouldFail() throws SQLException { + TableQuery tQuery = new TableQuery("people", connectionPool, + SQLTestsConstants.sqlGen); + tQuery.commit(); + } + + @Test(expected = SQLException.class) + public void rollback_noActiveTransaction_shouldFail() throws SQLException { + TableQuery tQuery = new TableQuery("people", connectionPool, + SQLTestsConstants.sqlGen); + tQuery.rollback(); + } + + /********************************************************************** + * TableQuery row query with given keys tests + **********************************************************************/ + @Test + public void containsRowWithKeys_existingKeys_returnsTrue() + throws SQLException { + TableQuery tQuery = new TableQuery("people", connectionPool, + SQLTestsConstants.sqlGen); + Assert.assertTrue(tQuery.containsRowWithKey(1)); + } + + @Test + public void containsRowWithKeys_nonexistingKeys_returnsTrue() + throws SQLException { + TableQuery tQuery = new TableQuery("people", connectionPool, + SQLTestsConstants.sqlGen); + + Assert.assertFalse(tQuery.containsRowWithKey(1337)); + } + + @Test + public void containsRowWithKeys_invalidKeys_shouldFail() + throws SQLException { + TableQuery tQuery = new TableQuery("people", connectionPool, + SQLTestsConstants.sqlGen); + boolean b = true; + try { + b = tQuery.containsRowWithKey("foo"); + } catch (SQLException se) { + return; + } + Assert.assertFalse(b); + } + + @Test + public void containsRowWithKeys_nullKeys_shouldFailAndReleaseConnections() + throws SQLException { + TableQuery tQuery = new TableQuery("people", connectionPool, + SQLTestsConstants.sqlGen); + try { + tQuery.containsRowWithKey(new Object[] { null }); + org.junit.Assert.fail( + "null should throw an IllegalArgumentException from StatementHelper"); + } catch (IllegalArgumentException e) { + // We should now be able to reserve two connections + Connection c1 = connectionPool.reserveConnection(); + Connection c2 = connectionPool.reserveConnection(); + + // Cleanup to make test connection pool happy + connectionPool.releaseConnection(c1); + connectionPool.releaseConnection(c2); + + } + } + + /********************************************************************** + * TableQuery filtering and ordering tests + **********************************************************************/ + @Test + public void setFilters_shouldReturnCorrectCount() throws SQLException { + TableQuery tQuery = new TableQuery("people", connectionPool, + SQLTestsConstants.sqlGen); + List<Filter> filters = new ArrayList<Filter>(); + filters.add(new Like("NAME", "%lle")); + tQuery.setFilters(filters); + Assert.assertEquals(3, tQuery.getCount()); + } + + @Test + public void setOrderByNameAscending_shouldReturnCorrectOrder() + throws SQLException { + TableQuery tQuery = new TableQuery("people", connectionPool, + SQLTestsConstants.sqlGen); + + List<OrderBy> orderBys = Arrays.asList(new OrderBy("NAME", true)); + tQuery.setOrderBy(orderBys); + + tQuery.beginTransaction(); + ResultSet rs; + rs = tQuery.getResults(0, 0); + + Assert.assertTrue(rs.next()); + Assert.assertEquals(3 + offset, rs.getInt(1)); + Assert.assertEquals("Börje", rs.getString(2)); + + Assert.assertTrue(rs.next()); + Assert.assertEquals(1 + offset, rs.getInt(1)); + Assert.assertEquals("Kalle", rs.getString(2)); + + Assert.assertTrue(rs.next()); + Assert.assertEquals(2 + offset, rs.getInt(1)); + Assert.assertEquals("Pelle", rs.getString(2)); + + Assert.assertTrue(rs.next()); + Assert.assertEquals(0 + offset, rs.getInt(1)); + Assert.assertEquals("Ville", rs.getString(2)); + + Assert.assertFalse(rs.next()); + tQuery.commit(); + } + + @Test + public void setOrderByNameDescending_shouldReturnCorrectOrder() + throws SQLException { + TableQuery tQuery = new TableQuery("people", connectionPool, + SQLTestsConstants.sqlGen); + + List<OrderBy> orderBys = Arrays.asList(new OrderBy("NAME", false)); + tQuery.setOrderBy(orderBys); + + tQuery.beginTransaction(); + ResultSet rs; + rs = tQuery.getResults(0, 0); + + Assert.assertTrue(rs.next()); + Assert.assertEquals(0 + offset, rs.getInt(1)); + Assert.assertEquals("Ville", rs.getString(2)); + + Assert.assertTrue(rs.next()); + Assert.assertEquals(2 + offset, rs.getInt(1)); + Assert.assertEquals("Pelle", rs.getString(2)); + + Assert.assertTrue(rs.next()); + Assert.assertEquals(1 + offset, rs.getInt(1)); + Assert.assertEquals("Kalle", rs.getString(2)); + + Assert.assertTrue(rs.next()); + Assert.assertEquals(3 + offset, rs.getInt(1)); + Assert.assertEquals("Börje", rs.getString(2)); + + Assert.assertFalse(rs.next()); + tQuery.commit(); + } + + @Test + public void setFilters_nullParameter_shouldSucceed() { + TableQuery tQuery = new TableQuery("people", connectionPool, + SQLTestsConstants.sqlGen); + tQuery.setFilters(null); + } + + @Test + public void setOrderBy_nullParameter_shouldSucceed() { + TableQuery tQuery = new TableQuery("people", connectionPool, + SQLTestsConstants.sqlGen); + tQuery.setOrderBy(null); + } + + /********************************************************************** + * TableQuery row removal tests + **********************************************************************/ + @Test + public void removeRowThroughContainer_legalRowItem_shouldSucceed() + throws SQLException { + TableQuery tQuery = new TableQuery("people", connectionPool, + SQLTestsConstants.sqlGen); + SQLContainer container = new SQLContainer(tQuery); + container.setAutoCommit(false); + Assert.assertTrue( + container.removeItem(container.getItemIds().iterator().next())); + + Assert.assertEquals(4, tQuery.getCount()); + Assert.assertEquals(3, container.size()); + container.commit(); + + Assert.assertEquals(3, tQuery.getCount()); + Assert.assertEquals(3, container.size()); + } + + @Test + public void removeRowThroughContainer_nonexistingRowId_shouldFail() + throws SQLException { + TableQuery tQuery = new TableQuery("people", connectionPool, + SQLTestsConstants.sqlGen); + + SQLContainer container = new SQLContainer(tQuery); + container.setAutoCommit(true); + Assert.assertFalse(container.removeItem("foo")); + } + + /********************************************************************** + * TableQuery row adding / modification tests + **********************************************************************/ + @Test + public void insertRowThroughContainer_shouldSucceed() throws SQLException { + TableQuery tQuery = new TableQuery("people", connectionPool, + SQLTestsConstants.sqlGen); + tQuery.setVersionColumn("ID"); + + SQLContainer container = new SQLContainer(tQuery); + container.setAutoCommit(false); + + Object item = container.addItem(); + Assert.assertNotNull(item); + + Assert.assertEquals(4, tQuery.getCount()); + Assert.assertEquals(5, container.size()); + container.commit(); + + Assert.assertEquals(5, tQuery.getCount()); + Assert.assertEquals(5, container.size()); + } + + @Test + public void modifyRowThroughContainer_shouldSucceed() throws SQLException { + TableQuery tQuery = new TableQuery("people", connectionPool, + SQLTestsConstants.sqlGen); + + // In this test the primary key is used as a version column + tQuery.setVersionColumn("ID"); + SQLContainer container = new SQLContainer(tQuery); + container.setAutoCommit(false); + + /* Check that the container size is correct and there is no 'Viljami' */ + Assert.assertEquals(4, container.size()); + List<Filter> filters = new ArrayList<Filter>(); + filters.add(new Equal("NAME", "Viljami")); + tQuery.setFilters(filters); + Assert.assertEquals(0, tQuery.getCount()); + tQuery.setFilters(null); + + /* Fetch first item, modify and commit */ + Object item = container + .getItem(container.getItemIds().iterator().next()); + Assert.assertNotNull(item); + + RowItem ri = (RowItem) item; + Assert.assertNotNull(ri.getItemProperty("NAME")); + ri.getItemProperty("NAME").setValue("Viljami"); + + container.commit(); + + // Check that the size is still correct and only 1 'Viljami' is found + Assert.assertEquals(4, tQuery.getCount()); + Assert.assertEquals(4, container.size()); + tQuery.setFilters(filters); + Assert.assertEquals(1, tQuery.getCount()); + } + + @Test + public void storeRow_noVersionColumn_shouldSucceed() + throws UnsupportedOperationException, SQLException { + TableQuery tQuery = new TableQuery("people", connectionPool, + SQLTestsConstants.sqlGen); + SQLContainer container = new SQLContainer(tQuery); + Object id = container.addItem(); + RowItem row = (RowItem) container.getItem(id); + row.getItemProperty("NAME").setValue("R2D2"); + row.getItemProperty("AGE").setValue(123); + tQuery.beginTransaction(); + tQuery.storeRow(row); + tQuery.commit(); + + Connection conn = connectionPool.reserveConnection(); + PreparedStatement stmt = conn + .prepareStatement("SELECT * FROM PEOPLE WHERE \"NAME\" = ?"); + stmt.setString(1, "R2D2"); + ResultSet rs = stmt.executeQuery(); + Assert.assertTrue(rs.next()); + rs.close(); + stmt.close(); + connectionPool.releaseConnection(conn); + } + + @Test + public void storeRow_versionSetAndEqualToDBValue_shouldSucceed() + throws SQLException { + DataGenerator.addVersionedData(connectionPool); + + TableQuery tQuery = new TableQuery("versioned", connectionPool, + SQLTestsConstants.sqlGen); + tQuery.setVersionColumn("VERSION"); + SQLContainer container = new SQLContainer(tQuery); + RowItem row = (RowItem) container.getItem(container.firstItemId()); + Assert.assertEquals("Junk", row.getItemProperty("TEXT").getValue()); + + row.getItemProperty("TEXT").setValue("asdf"); + container.commit(); + + Connection conn = connectionPool.reserveConnection(); + PreparedStatement stmt = conn + .prepareStatement("SELECT * FROM VERSIONED WHERE \"TEXT\" = ?"); + stmt.setString(1, "asdf"); + ResultSet rs = stmt.executeQuery(); + Assert.assertTrue(rs.next()); + rs.close(); + stmt.close(); + conn.commit(); + connectionPool.releaseConnection(conn); + } + + @Test(expected = OptimisticLockException.class) + public void storeRow_versionSetAndLessThanDBValue_shouldThrowException() + throws SQLException { + if (SQLTestsConstants.db == DB.HSQLDB) { + throw new OptimisticLockException( + "HSQLDB doesn't support row versioning for optimistic locking - don't run this test.", + null); + } + DataGenerator.addVersionedData(connectionPool); + + TableQuery tQuery = new TableQuery("versioned", connectionPool, + SQLTestsConstants.sqlGen); + tQuery.setVersionColumn("VERSION"); + SQLContainer container = new SQLContainer(tQuery); + RowItem row = (RowItem) container.getItem(container.firstItemId()); + Assert.assertEquals("Junk", row.getItemProperty("TEXT").getValue()); + + row.getItemProperty("TEXT").setValue("asdf"); + + // Update the version using another connection. + Connection conn = connectionPool.reserveConnection(); + PreparedStatement stmt = conn.prepareStatement( + "UPDATE VERSIONED SET \"TEXT\" = ? WHERE \"ID\" = ?"); + stmt.setString(1, "foo"); + stmt.setObject(2, row.getItemProperty("ID").getValue()); + stmt.executeUpdate(); + stmt.close(); + conn.commit(); + connectionPool.releaseConnection(conn); + + container.commit(); + } + + @Test + public void removeRow_versionSetAndEqualToDBValue_shouldSucceed() + throws SQLException { + DataGenerator.addVersionedData(connectionPool); + + TableQuery tQuery = new TableQuery("versioned", connectionPool, + SQLTestsConstants.sqlGen); + tQuery.setVersionColumn("VERSION"); + SQLContainer container = new SQLContainer(tQuery); + RowItem row = (RowItem) container.getItem(container.firstItemId()); + Assert.assertEquals("Junk", row.getItemProperty("TEXT").getValue()); + + container.removeItem(container.firstItemId()); + container.commit(); + + Connection conn = connectionPool.reserveConnection(); + PreparedStatement stmt = conn + .prepareStatement("SELECT * FROM VERSIONED WHERE \"TEXT\" = ?"); + stmt.setString(1, "Junk"); + ResultSet rs = stmt.executeQuery(); + Assert.assertFalse(rs.next()); + rs.close(); + stmt.close(); + conn.commit(); + connectionPool.releaseConnection(conn); + } + + @Test(expected = OptimisticLockException.class) + public void removeRow_versionSetAndLessThanDBValue_shouldThrowException() + throws SQLException { + if (SQLTestsConstants.db == SQLTestsConstants.DB.HSQLDB) { + // HSQLDB doesn't support versioning, so this is to make the test + // green. + throw new OptimisticLockException(null); + } + DataGenerator.addVersionedData(connectionPool); + + TableQuery tQuery = new TableQuery("versioned", connectionPool, + SQLTestsConstants.sqlGen); + tQuery.setVersionColumn("VERSION"); + SQLContainer container = new SQLContainer(tQuery); + RowItem row = (RowItem) container.getItem(container.firstItemId()); + Assert.assertEquals("Junk", row.getItemProperty("TEXT").getValue()); + + // Update the version using another connection. + Connection conn = connectionPool.reserveConnection(); + PreparedStatement stmt = conn.prepareStatement( + "UPDATE VERSIONED SET \"TEXT\" = ? WHERE \"ID\" = ?"); + stmt.setString(1, "asdf"); + stmt.setObject(2, row.getItemProperty("ID").getValue()); + stmt.executeUpdate(); + stmt.close(); + conn.commit(); + connectionPool.releaseConnection(conn); + + container.removeItem(container.firstItemId()); + container.commit(); + } + + @Test + public void removeRow_throwsOptimisticLockException_shouldStillWork() + throws SQLException { + if (SQLTestsConstants.db == SQLTestsConstants.DB.HSQLDB) { + // HSQLDB doesn't support versioning, so this is to make the test + // green. + return; + } + DataGenerator.addVersionedData(connectionPool); + + TableQuery tQuery = new TableQuery("versioned", connectionPool, + SQLTestsConstants.sqlGen); + tQuery.setVersionColumn("VERSION"); + SQLContainer container = new SQLContainer(tQuery); + RowItem row = (RowItem) container.getItem(container.firstItemId()); + Assert.assertEquals("Junk", row.getItemProperty("TEXT").getValue()); + + // Update the version using another connection. + Connection conn = connectionPool.reserveConnection(); + PreparedStatement stmt = conn.prepareStatement( + "UPDATE VERSIONED SET \"TEXT\" = ? WHERE \"ID\" = ?"); + stmt.setString(1, "asdf"); + stmt.setObject(2, row.getItemProperty("ID").getValue()); + stmt.executeUpdate(); + stmt.close(); + conn.commit(); + connectionPool.releaseConnection(conn); + + Object itemToRemove = container.firstItemId(); + try { + container.removeItem(itemToRemove); + container.commit(); + } catch (OptimisticLockException e) { + // This is expected, refresh and try again. + container.rollback(); + container.removeItem(itemToRemove); + container.commit(); + } + Object id = container.addItem(); + RowItem item = (RowItem) container.getItem(id); + item.getItemProperty("TEXT").setValue("foo"); + container.commit(); + } + + @Test + public void construction_explicitSchema_shouldSucceed() + throws SQLException { + if (SQLTestsConstants.createSchema == null + || SQLTestsConstants.createProductTable == null + || SQLTestsConstants.dropSchema == null) { + // only perform the test on the databases for which the setup and + // cleanup statements are available + return; + } + + // create schema "oaas" and table "product" in it + Connection conn = connectionPool.reserveConnection(); + Statement statement = conn.createStatement(); + try { + statement.execute(SQLTestsConstants.dropSchema); + } catch (SQLException e) { + // May fail if schema doesn't exist, which is OK. + conn.rollback(); + } + statement.execute(SQLTestsConstants.createSchema); + statement.execute(SQLTestsConstants.createProductTable); + conn.commit(); + + try { + // metadata scanning at query creation time should not fail + TableQuery tq1 = new TableQuery(null, "oaas", "product", + connectionPool, SQLTestsConstants.sqlGen); + Assert.assertNotNull(tq1); + } finally { + // cleanup - might not be an in-memory DB + statement.execute(SQLTestsConstants.dropSchema); + } + + // Cleanup to make test connection pool happy + connectionPool.releaseConnection(conn); + } + + @Test + public void construction_explicitCatalogAndSchema_shouldSucceed() + throws SQLException { + // not all databases support explicit catalogs, test with PostgreSQL + // using database name as catalog + if (SQLTestsConstants.db != SQLTestsConstants.DB.POSTGRESQL + || SQLTestsConstants.createSchema == null + || SQLTestsConstants.createProductTable == null + || SQLTestsConstants.dropSchema == null) { + // only perform the test on the databases for which the setup and + // cleanup statements are available + return; + } + + // create schema "oaas" and table "product" in it + Connection conn = connectionPool.reserveConnection(); + Statement statement = conn.createStatement(); + try { + statement.execute(SQLTestsConstants.dropSchema); + } catch (SQLException e) { + // May fail if schema doesn't exist, which is OK. + conn.rollback(); + } + statement.execute(SQLTestsConstants.createSchema); + statement.execute(SQLTestsConstants.createProductTable); + conn.commit(); + + try { + // metadata scanning at query creation time should not fail + // note that for most DBMS, catalog is just an optional database + // name + TableQuery tq1 = new TableQuery("sqlcontainer", "oaas", "product", + connectionPool, SQLTestsConstants.sqlGen); + Assert.assertNotNull(tq1); + } finally { + // cleanup - might not be an in-memory DB + statement.execute(SQLTestsConstants.dropSchema); + } + } +} diff --git a/compatibility-server/src/test/java/com/vaadin/data/util/sqlcontainer/query/ValidatingSimpleJDBCConnectionPool.java b/compatibility-server/src/test/java/com/vaadin/data/util/sqlcontainer/query/ValidatingSimpleJDBCConnectionPool.java new file mode 100644 index 0000000000..8a6ee0aa45 --- /dev/null +++ b/compatibility-server/src/test/java/com/vaadin/data/util/sqlcontainer/query/ValidatingSimpleJDBCConnectionPool.java @@ -0,0 +1,88 @@ +/* + * Copyright 2000-2016 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.data.util.sqlcontainer.query; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.HashSet; +import java.util.Set; +import java.util.logging.Logger; + +import com.vaadin.data.util.sqlcontainer.connection.JDBCConnectionPool; +import com.vaadin.data.util.sqlcontainer.connection.SimpleJDBCConnectionPool; + +/** + * Connection pool for testing SQLContainer. Ensures that only reserved + * connections are released and that all connections are released before the + * pool is destroyed. + * + * @author Vaadin Ltd + */ +public class ValidatingSimpleJDBCConnectionPool implements JDBCConnectionPool { + + private JDBCConnectionPool realPool; + private Set<Connection> reserved = new HashSet<Connection>(); + private Set<Connection> alreadyReleased = new HashSet<Connection>(); + + public ValidatingSimpleJDBCConnectionPool(String driverName, + String connectionUri, String userName, String password, + int initialConnections, int maxConnections) throws SQLException { + realPool = new SimpleJDBCConnectionPool(driverName, connectionUri, + userName, password, initialConnections, maxConnections); + } + + @Deprecated + public JDBCConnectionPool getRealPool() { + return realPool; + } + + @Override + public Connection reserveConnection() throws SQLException { + Connection c = realPool.reserveConnection(); + reserved.add(c); + return c; + } + + @Override + public void releaseConnection(Connection conn) { + if (conn != null && !reserved.remove(conn)) { + if (alreadyReleased.contains(conn)) { + getLogger().severe("Tried to release connection (" + conn + + ") which has already been released"); + } else { + throw new RuntimeException("Tried to release connection (" + + conn + ") not reserved using reserveConnection"); + } + } + realPool.releaseConnection(conn); + alreadyReleased.add(conn); + + } + + @Override + public void destroy() { + realPool.destroy(); + if (!reserved.isEmpty()) { + throw new RuntimeException( + reserved.size() + " connections never released"); + } + } + + private static Logger getLogger() { + return Logger + .getLogger(ValidatingSimpleJDBCConnectionPool.class.getName()); + } +}
\ No newline at end of file diff --git a/compatibility-server/src/test/java/com/vaadin/tests/server/component/abstractfield/LegacyAbstractFieldListenersTest.java b/compatibility-server/src/test/java/com/vaadin/tests/server/component/abstractfield/LegacyAbstractFieldListenersTest.java new file mode 100644 index 0000000000..5843bd901f --- /dev/null +++ b/compatibility-server/src/test/java/com/vaadin/tests/server/component/abstractfield/LegacyAbstractFieldListenersTest.java @@ -0,0 +1,27 @@ +package com.vaadin.tests.server.component.abstractfield; + +import org.junit.Test; + +import com.vaadin.data.Property.ReadOnlyStatusChangeEvent; +import com.vaadin.data.Property.ReadOnlyStatusChangeListener; +import com.vaadin.data.Property.ValueChangeEvent; +import com.vaadin.data.Property.ValueChangeListener; +import com.vaadin.tests.server.component.AbstractListenerMethodsTestBase; +import com.vaadin.ui.Table; + +public class LegacyAbstractFieldListenersTest + extends AbstractListenerMethodsTestBase { + + @Test + public void testReadOnlyStatusChangeListenerAddGetRemove() + throws Exception { + testListenerAddGetRemove(Table.class, ReadOnlyStatusChangeEvent.class, + ReadOnlyStatusChangeListener.class); + } + + @Test + public void testValueChangeListenerAddGetRemove() throws Exception { + testListenerAddGetRemove(Table.class, ValueChangeEvent.class, + ValueChangeListener.class); + } +} diff --git a/compatibility-server/src/test/java/com/vaadin/tests/server/component/table/CacheUpdateExceptionCausesTest.java b/compatibility-server/src/test/java/com/vaadin/tests/server/component/table/CacheUpdateExceptionCausesTest.java new file mode 100644 index 0000000000..86bb34145f --- /dev/null +++ b/compatibility-server/src/test/java/com/vaadin/tests/server/component/table/CacheUpdateExceptionCausesTest.java @@ -0,0 +1,55 @@ +/* + * Copyright 2012 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.vaadin.tests.server.component.table; + +import org.junit.Assert; +import org.junit.Test; + +import com.vaadin.ui.Table; +import com.vaadin.ui.Table.CacheUpdateException; + +public class CacheUpdateExceptionCausesTest { + @Test + public void testSingleCauseException() { + Table table = new Table(); + Throwable[] causes = new Throwable[] { + new RuntimeException("Broken in one way.") }; + + CacheUpdateException exception = new CacheUpdateException(table, + "Error during Table cache update.", causes); + + Assert.assertSame(causes[0], exception.getCause()); + Assert.assertEquals("Error during Table cache update.", + exception.getMessage()); + } + + @Test + public void testMultipleCauseException() { + Table table = new Table(); + Throwable[] causes = new Throwable[] { + new RuntimeException("Broken in the first way."), + new RuntimeException("Broken in the second way.") }; + + CacheUpdateException exception = new CacheUpdateException(table, + "Error during Table cache update.", causes); + + Assert.assertSame(causes[0], exception.getCause()); + Assert.assertEquals( + "Error during Table cache update. Additional causes not shown.", + exception.getMessage()); + } +} diff --git a/compatibility-server/src/test/java/com/vaadin/tests/server/component/table/FooterTest.java b/compatibility-server/src/test/java/com/vaadin/tests/server/component/table/FooterTest.java new file mode 100644 index 0000000000..3df2b8fbcb --- /dev/null +++ b/compatibility-server/src/test/java/com/vaadin/tests/server/component/table/FooterTest.java @@ -0,0 +1,103 @@ +package com.vaadin.tests.server.component.table; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +import com.vaadin.data.Container; +import com.vaadin.data.Item; +import com.vaadin.data.util.IndexedContainer; +import com.vaadin.ui.Table; + +/** + * Test case for testing the footer API + * + */ +public class FooterTest { + + /** + * Tests if setting the footer visibility works properly + */ + @Test + public void testFooterVisibility() { + Table table = new Table("Test table", createContainer()); + + // The footer should by default be hidden + assertFalse(table.isFooterVisible()); + + // Set footer visibility to tru should be reflected in the + // isFooterVisible() method + table.setFooterVisible(true); + assertTrue(table.isFooterVisible()); + } + + /** + * Tests adding footers to the columns + */ + @Test + public void testAddingFooters() { + Table table = new Table("Test table", createContainer()); + + // Table should not contain any footers at initialization + assertNull(table.getColumnFooter("col1")); + assertNull(table.getColumnFooter("col2")); + assertNull(table.getColumnFooter("col3")); + + // Adding column footer + table.setColumnFooter("col1", "Footer1"); + assertEquals("Footer1", table.getColumnFooter("col1")); + + // Add another footer + table.setColumnFooter("col2", "Footer2"); + assertEquals("Footer2", table.getColumnFooter("col2")); + + // Add footer for a non-existing column + table.setColumnFooter("fail", "FooterFail"); + } + + /** + * Test removing footers + */ + @Test + public void testRemovingFooters() { + Table table = new Table("Test table", createContainer()); + table.setColumnFooter("col1", "Footer1"); + table.setColumnFooter("col2", "Footer2"); + + // Test removing footer + assertNotNull(table.getColumnFooter("col1")); + table.setColumnFooter("col1", null); + assertNull(table.getColumnFooter("col1")); + + // The other footer should still be there + assertNotNull(table.getColumnFooter("col2")); + + // Remove non-existing footer + table.setColumnFooter("fail", null); + } + + /** + * Creates a container with three properties "col1,col2,col3" with 100 items + * + * @return Returns the created table + */ + private static Container createContainer() { + IndexedContainer container = new IndexedContainer(); + container.addContainerProperty("col1", String.class, ""); + container.addContainerProperty("col2", String.class, ""); + container.addContainerProperty("col3", String.class, ""); + + for (int i = 0; i < 100; i++) { + Item item = container.addItem("item " + i); + item.getItemProperty("col1").setValue("first" + i); + item.getItemProperty("col2").setValue("middle" + i); + item.getItemProperty("col3").setValue("last" + i); + } + + return container; + } +} diff --git a/compatibility-server/src/test/java/com/vaadin/tests/server/component/table/MultipleSelectionTest.java b/compatibility-server/src/test/java/com/vaadin/tests/server/component/table/MultipleSelectionTest.java new file mode 100644 index 0000000000..b29357ea6a --- /dev/null +++ b/compatibility-server/src/test/java/com/vaadin/tests/server/component/table/MultipleSelectionTest.java @@ -0,0 +1,62 @@ +package com.vaadin.tests.server.component.table; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.util.Arrays; +import java.util.Set; + +import org.junit.Test; + +import com.vaadin.data.Container; +import com.vaadin.data.util.IndexedContainer; +import com.vaadin.shared.ui.MultiSelectMode; +import com.vaadin.ui.Table; + +public class MultipleSelectionTest { + + /** + * Tests weather the multiple select mode is set when using Table.set + */ + @Test + @SuppressWarnings("unchecked") + public void testSetMultipleItems() { + Table table = new Table("", createTestContainer()); + + // Tests if multiple selection is set + table.setMultiSelect(true); + assertTrue(table.isMultiSelect()); + + // Test multiselect by setting several items at once + + table.setValue(Arrays.asList("1", new String[] { "3" })); + assertEquals(2, ((Set<String>) table.getValue()).size()); + } + + /** + * Tests setting the multiselect mode of the Table. The multiselect mode + * affects how mouse selection is made in the table by the user. + */ + @Test + public void testSetMultiSelectMode() { + Table table = new Table("", createTestContainer()); + + // Default multiselect mode should be MultiSelectMode.DEFAULT + assertEquals(MultiSelectMode.DEFAULT, table.getMultiSelectMode()); + + // Tests if multiselectmode is set + table.setMultiSelectMode(MultiSelectMode.SIMPLE); + assertEquals(MultiSelectMode.SIMPLE, table.getMultiSelectMode()); + } + + /** + * Creates a testing container for the tests + * + * @return A new container with test items + */ + private Container createTestContainer() { + IndexedContainer container = new IndexedContainer( + Arrays.asList("1", new String[] { "2", "3", "4" })); + return container; + } +} diff --git a/compatibility-server/src/test/java/com/vaadin/tests/server/component/table/TableColumnAlignmentsTest.java b/compatibility-server/src/test/java/com/vaadin/tests/server/component/table/TableColumnAlignmentsTest.java new file mode 100644 index 0000000000..ff4c8336fb --- /dev/null +++ b/compatibility-server/src/test/java/com/vaadin/tests/server/component/table/TableColumnAlignmentsTest.java @@ -0,0 +1,143 @@ +package com.vaadin.tests.server.component.table; + +import static org.junit.Assert.assertArrayEquals; + +import org.junit.Test; + +import com.vaadin.ui.Table; +import com.vaadin.ui.Table.Align; + +public class TableColumnAlignmentsTest { + + @Test + public void defaultColumnAlignments() { + for (int properties = 0; properties < 10; properties++) { + Table t = TableGeneratorTest + .createTableWithDefaultContainer(properties, 10); + Object[] expected = new Object[properties]; + for (int i = 0; i < properties; i++) { + expected[i] = Align.LEFT; + } + org.junit.Assert.assertArrayEquals("getColumnAlignments", expected, + t.getColumnAlignments()); + } + } + + @Test + public void explicitColumnAlignments() { + int properties = 5; + Table t = TableGeneratorTest.createTableWithDefaultContainer(properties, + 10); + Align[] explicitAlignments = new Align[] { Align.CENTER, Align.LEFT, + Align.RIGHT, Align.RIGHT, Align.LEFT }; + + t.setColumnAlignments(explicitAlignments); + + assertArrayEquals("Explicit visible columns, 5 properties", + explicitAlignments, t.getColumnAlignments()); + } + + @Test + public void invalidColumnAlignmentStrings() { + Table t = TableGeneratorTest.createTableWithDefaultContainer(3, 7); + Align[] defaultAlignments = new Align[] { Align.LEFT, Align.LEFT, + Align.LEFT }; + try { + t.setColumnAlignments(new Align[] { Align.RIGHT, Align.RIGHT }); + junit.framework.Assert + .fail("No exception thrown for invalid array length"); + } catch (IllegalArgumentException e) { + // Ok, expected + } + + assertArrayEquals("Invalid change affected alignments", + defaultAlignments, t.getColumnAlignments()); + + } + + @Test + public void columnAlignmentForPropertyNotInContainer() { + Table t = TableGeneratorTest.createTableWithDefaultContainer(3, 7); + Align[] defaultAlignments = new Align[] { Align.LEFT, Align.LEFT, + Align.LEFT }; + try { + t.setColumnAlignment("Property 1200", Align.LEFT); + // FIXME: Uncomment as there should be an exception (#6475) + // junit.framework.Assert + // .fail("No exception thrown for property not in container"); + } catch (IllegalArgumentException e) { + // Ok, expected + } + + assertArrayEquals("Invalid change affected alignments", + defaultAlignments, t.getColumnAlignments()); + + // FIXME: Uncomment as null should be returned (#6474) + // junit.framework.Assert.assertEquals( + // "Column alignment for property not in container returned", + // null, t.getColumnAlignment("Property 1200")); + + } + + @Test + public void invalidColumnAlignmentsLength() { + Table t = TableGeneratorTest.createTableWithDefaultContainer(7, 7); + Align[] defaultAlignments = new Align[] { Align.LEFT, Align.LEFT, + Align.LEFT, Align.LEFT, Align.LEFT, Align.LEFT, Align.LEFT }; + + try { + t.setColumnAlignments(new Align[] { Align.LEFT }); + junit.framework.Assert + .fail("No exception thrown for invalid array length"); + } catch (IllegalArgumentException e) { + // Ok, expected + } + assertArrayEquals("Invalid change affected alignments", + defaultAlignments, t.getColumnAlignments()); + + try { + t.setColumnAlignments(new Align[] {}); + junit.framework.Assert + .fail("No exception thrown for invalid array length"); + } catch (IllegalArgumentException e) { + // Ok, expected + } + assertArrayEquals("Invalid change affected alignments", + defaultAlignments, t.getColumnAlignments()); + + try { + t.setColumnAlignments(new Align[] { Align.LEFT, Align.LEFT, + Align.LEFT, Align.LEFT, Align.LEFT, Align.LEFT, Align.LEFT, + Align.LEFT }); + junit.framework.Assert + .fail("No exception thrown for invalid array length"); + } catch (IllegalArgumentException e) { + // Ok, expected + } + assertArrayEquals("Invalid change affected alignments", + defaultAlignments, t.getColumnAlignments()); + + } + + @Test + public void explicitColumnAlignmentOneByOne() { + int properties = 5; + Table t = TableGeneratorTest.createTableWithDefaultContainer(properties, + 10); + Align[] explicitAlignments = new Align[] { Align.CENTER, Align.LEFT, + Align.RIGHT, Align.RIGHT, Align.LEFT }; + + Align[] currentAlignments = new Align[] { Align.LEFT, Align.LEFT, + Align.LEFT, Align.LEFT, Align.LEFT }; + + for (int i = 0; i < properties; i++) { + t.setColumnAlignment("Property " + i, explicitAlignments[i]); + currentAlignments[i] = explicitAlignments[i]; + + assertArrayEquals( + "Explicit visible columns, " + i + " alignments set", + currentAlignments, t.getColumnAlignments()); + } + + } +} diff --git a/compatibility-server/src/test/java/com/vaadin/tests/server/component/table/TableContextClickTest.java b/compatibility-server/src/test/java/com/vaadin/tests/server/component/table/TableContextClickTest.java new file mode 100644 index 0000000000..3ee8b76d76 --- /dev/null +++ b/compatibility-server/src/test/java/com/vaadin/tests/server/component/table/TableContextClickTest.java @@ -0,0 +1,57 @@ +/* + * Copyright 2000-2016 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.tests.server.component.table; + +import org.junit.Assert; +import org.junit.Test; + +import com.vaadin.event.ContextClickEvent; +import com.vaadin.event.ContextClickEvent.ContextClickListener; +import com.vaadin.shared.ui.table.TableConstants.Section; +import com.vaadin.ui.Table; + +public class TableContextClickTest extends Table { + + private String error = null; + private boolean handled = false; + + @Test + public void testContextClickListenerWithTableEvent() { + addContextClickListener(new ContextClickListener() { + + @Override + public void contextClick(ContextClickEvent event) { + if (!(event instanceof TableContextClickEvent)) { + return; + } + + TableContextClickEvent e = (TableContextClickEvent) event; + if (e.getSection() != Section.BODY) { + error = "Event section was not BODY."; + } + handled = true; + } + }); + fireEvent(new TableContextClickEvent(this, null, null, null, + Section.BODY)); + + if (error != null) { + Assert.fail(error); + } else if (!handled) { + Assert.fail("Event was not handled by the ContextClickListener"); + } + } +} diff --git a/compatibility-server/src/test/java/com/vaadin/tests/server/component/table/TableDeclarativeTest.java b/compatibility-server/src/test/java/com/vaadin/tests/server/component/table/TableDeclarativeTest.java new file mode 100644 index 0000000000..f3b9b1cece --- /dev/null +++ b/compatibility-server/src/test/java/com/vaadin/tests/server/component/table/TableDeclarativeTest.java @@ -0,0 +1,180 @@ +/* + * Copyright 2000-2016 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.tests.server.component.table; + +import org.junit.Assert; +import org.junit.Test; + +import com.vaadin.server.ExternalResource; +import com.vaadin.shared.ui.MultiSelectMode; +import com.vaadin.ui.Table; +import com.vaadin.ui.Table.Align; +import com.vaadin.ui.Table.ColumnHeaderMode; +import com.vaadin.ui.Table.RowHeaderMode; +import com.vaadin.ui.Table.TableDragMode; +import com.vaadin.ui.declarative.Design; + +/** + * Test declarative support for {@link Table}. + * + * @since + * @author Vaadin Ltd + */ +public class TableDeclarativeTest extends TableDeclarativeTestBase { + + @Test + public void testBasicAttributes() { + + String design = "<" + getTag() + + " page-length=30 cache-rate=3 selectable editable " + + "sortable=false sort-ascending=false sort-container-property-id=foo " + + "drag-mode=row multi-select-mode=simple column-header-mode=id row-header-mode=id " + + "column-reordering-allowed column-collapsing-allowed />"; + + Table table = getTable(); + table.setPageLength(30); + table.setCacheRate(3); + table.setSelectable(true); + table.setEditable(true); + + table.setSortEnabled(false); + table.setSortAscending(false); + table.setSortContainerPropertyId("foo"); + + table.setDragMode(TableDragMode.ROW); + table.setMultiSelectMode(MultiSelectMode.SIMPLE); + table.setColumnHeaderMode(ColumnHeaderMode.ID); + table.setRowHeaderMode(RowHeaderMode.ID); + + table.setColumnReorderingAllowed(true); + table.setColumnCollapsingAllowed(true); + + testRead(design, table); + testWrite(design, table); + } + + @Test + public void testColumns() { + String design = "<" + getTag() + " column-collapsing-allowed>" // + + " <table>" // + + " <colgroup>" + " <col property-id='foo' width=300>" + + " <col property-id='bar' center expand=1 collapsible=false>" + + " <col property-id='baz' right expand=2 collapsed>" + + " </colgroup>" // + + " </table>"; + + Table table = getTable(); + table.setColumnCollapsingAllowed(true); + + table.addContainerProperty("foo", String.class, null); + table.setColumnAlignment("foo", Align.LEFT); + table.setColumnWidth("foo", 300); + + table.addContainerProperty("bar", String.class, null); + table.setColumnAlignment("bar", Align.CENTER); + table.setColumnExpandRatio("bar", 1); + table.setColumnCollapsible("bar", false); + + table.addContainerProperty("baz", String.class, null); + table.setColumnAlignment("baz", Align.RIGHT); + table.setColumnExpandRatio("baz", 2); + table.setColumnCollapsed("baz", true); + + testRead(design, table); + testWrite(design, table); + } + + @Test + public void testHeadersFooters() { + String design = "<" + getTag() + ">" // + + " <table>" // + + " <colgroup><col property-id=foo><col property-id=bar></colgroup>" // + + " <thead>" // + + " <tr><th icon='http://example.com/icon.png'>FOO<th>BAR" // + + " </thead>" // + + " <tfoot>" // + + " <tr><td>foo<td>bar" // + + " </tfoot>" // + + " </table>"; + + Table table = getTable(); + table.setFooterVisible(true); + + table.addContainerProperty("foo", String.class, null); + table.setColumnHeader("foo", "FOO"); + table.setColumnIcon("foo", + new ExternalResource("http://example.com/icon.png")); + table.setColumnFooter("foo", "foo"); + + table.addContainerProperty("bar", String.class, null); + table.setColumnHeader("bar", "BAR"); + table.setColumnFooter("bar", "bar"); + + testRead(design, table); + testWrite(design, table); + } + + @Test + public void testInlineData() { + String design = "<" + getTag() + ">" // + + " <table>" // + + " <colgroup>" + " <col property-id='foo' />" + + " <col property-id='bar' />" + + " <col property-id='baz' />" // + + " </colgroup>" + " <thead>" + + " <tr><th>Description<th>Milestone<th>Status</tr>" + + " </thead>" + " <tbody>" + + " <tr item-id=1><td>r1c1</td><td>r1c2</td><td>r1c3</td>" // + + " <tr item-id=2><td>r2c1</td><td>r2c2</td><td>r2c3</td>" // + + " </tbody>" // + + " <tfoot>" // + + " <tr><td>F1<td>F2<td>F3</tr>" // + + " </tfoot>" // + + " </table>"; + + Table table = getTable(); + table.addContainerProperty("foo", String.class, null); + table.addContainerProperty("bar", String.class, null); + table.addContainerProperty("baz", String.class, null); + table.setColumnHeaders("Description", "Milestone", "Status"); + table.setColumnFooter("foo", "F1"); + table.setColumnFooter("bar", "F2"); + table.setColumnFooter("baz", "F3"); + table.addItem(new Object[] { "r1c1", "r1c2", "r1c3" }, "1"); + table.addItem(new Object[] { "r2c1", "r2c2", "r2c3" }, "2"); + table.setFooterVisible(true); + + testRead(design, table); + testWrite(design, table, true); + } + + @Test + public void testHtmlEntities() { + String design = "<v-table>" + "<table>" + " <colgroup>" + + " <col property-id=\"test\"" + " </colgroup>" + + " <thead>" + " <tr><th>& Test</th></tr>" + + " </thead>" + " <tbody>" + + " <tr item-id=\"test\"><td>& Test</tr>" + " </tbody>" + + " <tfoot>" + " <tr><td>& Test</td></tr>" + + " </tfoot>" + "</table>" + "</v-table>"; + Table read = read(design); + + Assert.assertEquals("& Test", + read.getContainerProperty("test", "test").getValue()); + Assert.assertEquals("& Test", read.getColumnHeader("test")); + Assert.assertEquals("& Test", read.getColumnFooter("test")); + } +} diff --git a/compatibility-server/src/test/java/com/vaadin/tests/server/component/table/TableDeclarativeTestBase.java b/compatibility-server/src/test/java/com/vaadin/tests/server/component/table/TableDeclarativeTestBase.java new file mode 100644 index 0000000000..7a7b13e933 --- /dev/null +++ b/compatibility-server/src/test/java/com/vaadin/tests/server/component/table/TableDeclarativeTestBase.java @@ -0,0 +1,74 @@ +/* + * Copyright 2000-2016 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.tests.server.component.table; + +import static org.junit.Assert.assertTrue; + +import com.vaadin.tests.design.DeclarativeTestBase; +import com.vaadin.ui.Table; + +public abstract class TableDeclarativeTestBase + extends DeclarativeTestBase<Table> { + + @Override + public Table testRead(String design, Table expected) { + Table read = super.testRead(design, expected); + compareColumns(read, expected); + compareBody(read, expected); + return read; + } + + protected Table getTable() { + return new Table(); + } + + protected String getTag() { + return "vaadin-table"; + } + + protected void compareBody(Table read, Table expected) { + assertEquals("number of items", expected.getItemIds().size(), + read.getItemIds().size()); + for (Object rowId : expected.getItemIds()) { + assertTrue(read.containsId(rowId)); + for (Object propertyId : read.getVisibleColumns()) { + Object expectedItem = expected.getContainerProperty(rowId, + propertyId); + Object readItem = read.getContainerProperty(rowId, propertyId); + assertEquals("property '" + propertyId + "'", expectedItem, + readItem); + } + } + } + + protected void compareColumns(Table read, Table expected) { + for (Object pid : expected.getVisibleColumns()) { + String col = "column '" + pid + "'"; + assertEquals(col + " width", expected.getColumnWidth(pid), + read.getColumnWidth(pid)); + assertEquals(col + " expand ratio", + expected.getColumnExpandRatio(pid), + read.getColumnExpandRatio(pid)); + assertEquals(col + " collapsible", + expected.isColumnCollapsible(pid), + read.isColumnCollapsible(pid)); + assertEquals(col + " collapsed", expected.isColumnCollapsed(pid), + read.isColumnCollapsed(pid)); + assertEquals(col + " footer", expected.getColumnFooter(pid), + read.getColumnFooter(pid)); + } + } +} diff --git a/compatibility-server/src/test/java/com/vaadin/tests/server/component/table/TableGeneratorTest.java b/compatibility-server/src/test/java/com/vaadin/tests/server/component/table/TableGeneratorTest.java new file mode 100644 index 0000000000..6fbe5557f8 --- /dev/null +++ b/compatibility-server/src/test/java/com/vaadin/tests/server/component/table/TableGeneratorTest.java @@ -0,0 +1,42 @@ +package com.vaadin.tests.server.component.table; + +import org.junit.Test; + +import com.vaadin.data.Item; +import com.vaadin.ui.Table; + +public class TableGeneratorTest { + public static Table createTableWithDefaultContainer(int properties, + int items) { + Table t = new Table(); + + for (int i = 0; i < properties; i++) { + t.addContainerProperty("Property " + i, String.class, null); + } + + for (int j = 0; j < items; j++) { + Item item = t.addItem("Item " + j); + for (int i = 0; i < properties; i++) { + item.getItemProperty("Property " + i) + .setValue("Item " + j + "/Property " + i); + } + } + + return t; + } + + @Test + public void testTableGenerator() { + Table t = createTableWithDefaultContainer(1, 1); + junit.framework.Assert.assertEquals(t.size(), 1); + junit.framework.Assert.assertEquals(t.getContainerPropertyIds().size(), + 1); + + t = createTableWithDefaultContainer(100, 50); + junit.framework.Assert.assertEquals(t.size(), 50); + junit.framework.Assert.assertEquals(t.getContainerPropertyIds().size(), + 100); + + } + +} diff --git a/compatibility-server/src/test/java/com/vaadin/tests/server/component/table/TableListenersTest.java b/compatibility-server/src/test/java/com/vaadin/tests/server/component/table/TableListenersTest.java new file mode 100644 index 0000000000..ac6fb171e3 --- /dev/null +++ b/compatibility-server/src/test/java/com/vaadin/tests/server/component/table/TableListenersTest.java @@ -0,0 +1,49 @@ +package com.vaadin.tests.server.component.table; + +import org.junit.Test; + +import com.vaadin.event.ItemClickEvent; +import com.vaadin.event.ItemClickEvent.ItemClickListener; +import com.vaadin.tests.server.component.AbstractListenerMethodsTestBase; +import com.vaadin.ui.Table; +import com.vaadin.ui.Table.ColumnReorderEvent; +import com.vaadin.ui.Table.ColumnReorderListener; +import com.vaadin.ui.Table.ColumnResizeEvent; +import com.vaadin.ui.Table.ColumnResizeListener; +import com.vaadin.ui.Table.FooterClickEvent; +import com.vaadin.ui.Table.FooterClickListener; +import com.vaadin.ui.Table.HeaderClickEvent; +import com.vaadin.ui.Table.HeaderClickListener; + +public class TableListenersTest extends AbstractListenerMethodsTestBase { + + @Test + public void testColumnResizeListenerAddGetRemove() throws Exception { + testListenerAddGetRemove(Table.class, ColumnResizeEvent.class, + ColumnResizeListener.class); + } + + @Test + public void testItemClickListenerAddGetRemove() throws Exception { + testListenerAddGetRemove(Table.class, ItemClickEvent.class, + ItemClickListener.class); + } + + @Test + public void testFooterClickListenerAddGetRemove() throws Exception { + testListenerAddGetRemove(Table.class, FooterClickEvent.class, + FooterClickListener.class); + } + + @Test + public void testHeaderClickListenerAddGetRemove() throws Exception { + testListenerAddGetRemove(Table.class, HeaderClickEvent.class, + HeaderClickListener.class); + } + + @Test + public void testColumnReorderListenerAddGetRemove() throws Exception { + testListenerAddGetRemove(Table.class, ColumnReorderEvent.class, + ColumnReorderListener.class); + } +} diff --git a/compatibility-server/src/test/java/com/vaadin/tests/server/component/table/TablePropertyValueConverterTest.java b/compatibility-server/src/test/java/com/vaadin/tests/server/component/table/TablePropertyValueConverterTest.java new file mode 100644 index 0000000000..5a9297014c --- /dev/null +++ b/compatibility-server/src/test/java/com/vaadin/tests/server/component/table/TablePropertyValueConverterTest.java @@ -0,0 +1,384 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.tests.server.component.table; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.lang.reflect.Field; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Locale; +import java.util.Map.Entry; +import java.util.Set; + +import org.junit.Before; +import org.junit.Test; + +import com.vaadin.data.Container; +import com.vaadin.data.Item; +import com.vaadin.data.Property; +import com.vaadin.data.util.IndexedContainer; +import com.vaadin.ui.Table; +import com.vaadin.v7.data.util.converter.LegacyConverter; + +public class TablePropertyValueConverterTest { + protected TestableTable table; + protected Collection<?> initialProperties; + + @Test + public void testRemovePropertyId() { + Collection<Object> converters = table.getCurrentConverters(); + assertTrue("Set of converters was empty at the start.", + converters.size() > 0); + + Object firstId = converters.iterator().next(); + + table.removeContainerProperty(firstId); + + Collection<Object> converters2 = table.getCurrentConverters(); + assertTrue("FirstId was not removed", !converters2.contains(firstId)); + + assertTrue("The number of removed converters was not one.", + converters.size() - converters2.size() == 1); + + for (Object originalId : converters) { + if (!originalId.equals(firstId)) { + assertTrue("The wrong converter was removed.", + converters2.contains(originalId)); + } + } + + } + + @Test + public void testSetContainer() { + table.setContainerDataSource(createContainer( + new String[] { "col1", "col3", "col4", "col5" })); + Collection<Object> converters = table.getCurrentConverters(); + assertTrue("There should only have been one converter left.", + converters.size() == 1); + Object onlyKey = converters.iterator().next(); + assertTrue("The incorrect key was left.", onlyKey.equals("col1")); + + } + + @Test + public void testSetContainerWithInexactButCompatibleTypes() { + TestableTable customTable = new TestableTable("Test table", + createContainer(new String[] { "col1", "col2", "col3" }, + new Class[] { String.class, BaseClass.class, + DerivedClass.class })); + customTable.setConverter("col1", new LegacyConverter<String, String>() { + private static final long serialVersionUID = 1L; + + @Override + public String convertToModel(String value, + Class<? extends String> targetType, Locale locale) + throws com.vaadin.v7.data.util.converter.LegacyConverter.ConversionException { + return "model"; + } + + @Override + public String convertToPresentation(String value, + Class<? extends String> targetType, Locale locale) + throws com.vaadin.v7.data.util.converter.LegacyConverter.ConversionException { + return "presentation"; + } + + @Override + public Class<String> getModelType() { + return String.class; + } + + @Override + public Class<String> getPresentationType() { + return String.class; + } + + }); + customTable.setConverter("col2", + new LegacyConverter<String, BaseClass>() { + private static final long serialVersionUID = 1L; + + @Override + public BaseClass convertToModel(String value, + Class<? extends BaseClass> targetType, + Locale locale) + throws com.vaadin.v7.data.util.converter.LegacyConverter.ConversionException { + return new BaseClass("model"); + } + + @Override + public Class<BaseClass> getModelType() { + return BaseClass.class; + } + + @Override + public Class<String> getPresentationType() { + return String.class; + } + + @Override + public String convertToPresentation(BaseClass value, + Class<? extends String> targetType, Locale locale) + throws com.vaadin.v7.data.util.converter.LegacyConverter.ConversionException { + return null; + } + }); + customTable.setConverter("col3", + new LegacyConverter<String, DerivedClass>() { + private static final long serialVersionUID = 1L; + + @Override + public DerivedClass convertToModel(String value, + Class<? extends DerivedClass> targetType, + Locale locale) + throws com.vaadin.v7.data.util.converter.LegacyConverter.ConversionException { + return new DerivedClass("derived" + 1001); + } + + @Override + public Class<DerivedClass> getModelType() { + return DerivedClass.class; + } + + @Override + public Class<String> getPresentationType() { + return String.class; + } + + @Override + public String convertToPresentation(DerivedClass value, + Class<? extends String> targetType, Locale locale) + throws com.vaadin.v7.data.util.converter.LegacyConverter.ConversionException { + return null; + } + }); + customTable.setContainerDataSource( + createContainer(new String[] { "col1", "col2", "col3" }, + new Class[] { DerivedClass.class, DerivedClass.class, + BaseClass.class })); + Set<Object> converters = customTable.getCurrentConverters(); + // TODO Test temporarily disabled as this feature + // is not yet implemented in Table + /* + * assertTrue("Incompatible types were not removed.", converters.size() + * <= 1); assertTrue("Even compatible types were removed", + * converters.size() == 1); assertTrue("Compatible type was missing.", + * converters.contains("col2")); + */ + } + + @Test + public void testPrimitiveTypeConverters() { + TestableTable customTable = new TestableTable("Test table", + createContainer(new String[] { "col1", "col2", "col3" }, + new Class[] { int.class, BaseClass.class, + DerivedClass.class })); + customTable.setConverter("col1", + new LegacyConverter<String, Integer>() { + private static final long serialVersionUID = 1L; + + @Override + public Integer convertToModel(String value, + Class<? extends Integer> targetType, Locale locale) + throws com.vaadin.v7.data.util.converter.LegacyConverter.ConversionException { + return 11; + } + + @Override + public String convertToPresentation(Integer value, + Class<? extends String> targetType, Locale locale) + throws com.vaadin.v7.data.util.converter.LegacyConverter.ConversionException { + return "presentation"; + } + + @Override + public Class<Integer> getModelType() { + return Integer.class; + } + + @Override + public Class<String> getPresentationType() { + return String.class; + } + }); + Set<Object> converters = customTable.getCurrentConverters(); + assertTrue("Converter was not set.", converters.size() > 0); + } + + @Test + public void testInheritance() { + assertTrue("BaseClass isn't assignable from DerivedClass", + BaseClass.class.isAssignableFrom(DerivedClass.class)); + assertFalse("DerivedClass is assignable from BaseClass", + DerivedClass.class.isAssignableFrom(BaseClass.class)); + } + + @Before + public void setUp() { + table = new TestableTable("Test table", + createContainer(new String[] { "col1", "col2", "col3" })); + table.setConverter("col1", new LegacyConverter<String, String>() { + private static final long serialVersionUID = 1L; + + @Override + public String convertToModel(String value, + Class<? extends String> targetType, Locale locale) + throws com.vaadin.v7.data.util.converter.LegacyConverter.ConversionException { + return "model"; + } + + @Override + public String convertToPresentation(String value, + Class<? extends String> targetType, Locale locale) + throws com.vaadin.v7.data.util.converter.LegacyConverter.ConversionException { + return "presentation"; + } + + @Override + public Class<String> getModelType() { + return String.class; + } + + @Override + public Class<String> getPresentationType() { + return String.class; + } + + }); + + table.setConverter("col2", new LegacyConverter<String, String>() { + private static final long serialVersionUID = 1L; + + @Override + public String convertToModel(String value, + Class<? extends String> targetType, Locale locale) + throws com.vaadin.v7.data.util.converter.LegacyConverter.ConversionException { + return "model2"; + } + + @Override + public String convertToPresentation(String value, + Class<? extends String> targetType, Locale locale) + throws com.vaadin.v7.data.util.converter.LegacyConverter.ConversionException { + return "presentation2"; + } + + @Override + public Class<String> getModelType() { + return String.class; + } + + @Override + public Class<String> getPresentationType() { + return String.class; + } + + }); + + initialProperties = table.getContainerPropertyIds(); + } + + private static Container createContainer(Object[] ids) { + Class[] types = new Class[ids.length]; + for (int i = 0; i < types.length; ++i) { + types[i] = String.class; + } + return createContainer(ids, types); + } + + private static Container createContainer(Object[] ids, Class[] types) { + IndexedContainer container = new IndexedContainer(); + if (ids.length > types.length) { + throw new IllegalArgumentException("Too few defined types"); + } + for (int i = 0; i < ids.length; ++i) { + container.addContainerProperty(ids[i], types[i], ""); + } + + for (int i = 0; i < 100; i++) { + Item item = container.addItem("item " + i); + for (int j = 0; j < ids.length; ++j) { + Property itemProperty = item.getItemProperty(ids[j]); + if (types[j] == String.class) { + itemProperty.setValue(ids[j].toString() + i); + } else if (types[j] == BaseClass.class) { + itemProperty.setValue(new BaseClass("base" + i)); + } else if (types[j] == DerivedClass.class) { + itemProperty.setValue(new DerivedClass("derived" + i)); + } else if (types[j] == int.class) { + // FIXME can't set values because the int is autoboxed into + // an Integer and not unboxed prior to set + + // itemProperty.setValue(i); + } else { + throw new IllegalArgumentException( + "Unhandled type in createContainer: " + types[j]); + } + } + } + + return container; + } + + private class TestableTable extends Table { + /** + * @param string + * @param createContainer + */ + public TestableTable(String string, Container container) { + super(string, container); + } + + Set<Object> getCurrentConverters() { + try { + Field f = Table.class + .getDeclaredField("propertyValueConverters"); + f.setAccessible(true); + HashMap<Object, LegacyConverter<String, Object>> pvc = (HashMap<Object, LegacyConverter<String, Object>>) f + .get(this); + Set<Object> currentConverters = new HashSet<Object>(); + for (Entry<Object, LegacyConverter<String, Object>> entry : pvc + .entrySet()) { + currentConverters.add(entry.getKey()); + } + return currentConverters; + + } catch (Exception e) { + fail("Unable to retrieve propertyValueConverters"); + return null; + } + } + } + + private static class BaseClass { + private String title; + + public BaseClass(String title) { + this.title = title; + } + } + + private static class DerivedClass extends BaseClass { + public DerivedClass(String title) { + super(title); + } + } +} diff --git a/compatibility-server/src/test/java/com/vaadin/tests/server/component/table/TableSelectableTest.java b/compatibility-server/src/test/java/com/vaadin/tests/server/component/table/TableSelectableTest.java new file mode 100644 index 0000000000..906827958f --- /dev/null +++ b/compatibility-server/src/test/java/com/vaadin/tests/server/component/table/TableSelectableTest.java @@ -0,0 +1,75 @@ +/* + * Copyright 2000-2016 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.tests.server.component.table; + +import org.easymock.EasyMock; +import org.junit.Assert; +import org.junit.Test; + +import com.vaadin.data.Property.ValueChangeListener; +import com.vaadin.ui.Table; + +/** + * Tests for 'selectable' property of {@link Table} class. + * + * @author Vaadin Ltd + */ +public class TableSelectableTest { + + @Test + public void setSelectable_explicitSelectable_tableIsSelectable() { + Table table = new Table(); + table.setSelectable(true); + + Assert.assertTrue(table.isSelectable()); + } + + @Test + public void addValueChangeListener_explicitSelectable_tableIsSelectable() { + TestTable table = new TestTable(); + table.addValueChangeListener( + EasyMock.createMock(ValueChangeListener.class)); + + Assert.assertTrue(table.isSelectable()); + Assert.assertTrue(table.markAsDirtyCalled); + } + + @Test + public void tableIsNotSelectableByDefult() { + Table table = new Table(); + + Assert.assertFalse(table.isSelectable()); + } + + @Test + public void setSelectable_explicitNotSelectable_tableIsNotSelectable() { + Table table = new Table(); + table.setSelectable(false); + table.addValueChangeListener( + EasyMock.createMock(ValueChangeListener.class)); + + Assert.assertFalse(table.isSelectable()); + } + + private static final class TestTable extends Table { + @Override + public void markAsDirty() { + markAsDirtyCalled = true; + } + + private boolean markAsDirtyCalled; + } +} diff --git a/compatibility-server/src/test/java/com/vaadin/tests/server/component/table/TableSerializationTest.java b/compatibility-server/src/test/java/com/vaadin/tests/server/component/table/TableSerializationTest.java new file mode 100644 index 0000000000..22f2381980 --- /dev/null +++ b/compatibility-server/src/test/java/com/vaadin/tests/server/component/table/TableSerializationTest.java @@ -0,0 +1,26 @@ +package com.vaadin.tests.server.component.table; + +import org.apache.commons.lang.SerializationUtils; +import org.junit.Test; + +import com.vaadin.ui.Table; + +public class TableSerializationTest { + + @Test + public void testSerialization() { + Table t = new Table(); + byte[] ser = SerializationUtils.serialize(t); + Table t2 = (Table) SerializationUtils.deserialize(ser); + + } + + @Test + public void testSerializationWithRowHeaders() { + Table t = new Table(); + t.setRowHeaderMode(Table.ROW_HEADER_MODE_EXPLICIT); + t.setColumnWidth(null, 100); + byte[] ser = SerializationUtils.serialize(t); + Table t2 = (Table) SerializationUtils.deserialize(ser); + } +} diff --git a/compatibility-server/src/test/java/com/vaadin/tests/server/component/table/TableStateTest.java b/compatibility-server/src/test/java/com/vaadin/tests/server/component/table/TableStateTest.java new file mode 100644 index 0000000000..43b03c41e8 --- /dev/null +++ b/compatibility-server/src/test/java/com/vaadin/tests/server/component/table/TableStateTest.java @@ -0,0 +1,60 @@ +/* + * Copyright 2000-2016 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.tests.server.component.table; + +import org.junit.Assert; +import org.junit.Test; + +import com.vaadin.shared.ui.table.TableState; +import com.vaadin.ui.Table; + +/** + * Tests for Table State. + * + */ +public class TableStateTest { + + @Test + public void getState_tableHasCustomState() { + TestTable table = new TestTable(); + TableState state = table.getState(); + Assert.assertEquals("Unexpected state class", TableState.class, + state.getClass()); + } + + @Test + public void getPrimaryStyleName_tableHasCustomPrimaryStyleName() { + Table table = new Table(); + TableState state = new TableState(); + Assert.assertEquals("Unexpected primary style name", + state.primaryStyleName, table.getPrimaryStyleName()); + } + + @Test + public void tableStateHasCustomPrimaryStyleName() { + TableState state = new TableState(); + Assert.assertEquals("Unexpected primary style name", "v-table", + state.primaryStyleName); + } + + private static class TestTable extends Table { + + @Override + public TableState getState() { + return super.getState(); + } + } +} diff --git a/compatibility-server/src/test/java/com/vaadin/tests/server/component/table/TableVisibleColumnsTest.java b/compatibility-server/src/test/java/com/vaadin/tests/server/component/table/TableVisibleColumnsTest.java new file mode 100644 index 0000000000..f9f6e7f979 --- /dev/null +++ b/compatibility-server/src/test/java/com/vaadin/tests/server/component/table/TableVisibleColumnsTest.java @@ -0,0 +1,72 @@ +package com.vaadin.tests.server.component.table; + +import static org.junit.Assert.assertArrayEquals; + +import org.junit.Test; + +import com.vaadin.ui.Table; + +public class TableVisibleColumnsTest { + + String[] defaultColumns3 = new String[] { "Property 0", "Property 1", + "Property 2" }; + + @Test + public void defaultVisibleColumns() { + for (int properties = 0; properties < 10; properties++) { + Table t = TableGeneratorTest + .createTableWithDefaultContainer(properties, 10); + Object[] expected = new Object[properties]; + for (int i = 0; i < properties; i++) { + expected[i] = "Property " + i; + } + org.junit.Assert.assertArrayEquals("getVisibleColumns", expected, + t.getVisibleColumns()); + } + } + + @Test + public void explicitVisibleColumns() { + Table t = TableGeneratorTest.createTableWithDefaultContainer(5, 10); + Object[] newVisibleColumns = new Object[] { "Property 1", + "Property 2" }; + t.setVisibleColumns(newVisibleColumns); + assertArrayEquals("Explicit visible columns, 5 properties", + newVisibleColumns, t.getVisibleColumns()); + + } + + @Test + public void invalidVisibleColumnIds() { + Table t = TableGeneratorTest.createTableWithDefaultContainer(3, 10); + + try { + t.setVisibleColumns( + new Object[] { "a", "Property 2", "Property 3" }); + junit.framework.Assert.fail("IllegalArgumentException expected"); + } catch (IllegalArgumentException e) { + // OK, expected + } + assertArrayEquals(defaultColumns3, t.getVisibleColumns()); + } + + @Test + public void duplicateVisibleColumnIds() { + Table t = TableGeneratorTest.createTableWithDefaultContainer(3, 10); + try { + t.setVisibleColumns(new Object[] { "Property 0", "Property 1", + "Property 2", "Property 1" }); + } catch (IllegalArgumentException e) { + // OK, expected + } + assertArrayEquals(defaultColumns3, t.getVisibleColumns()); + } + + @Test + public void noVisibleColumns() { + Table t = TableGeneratorTest.createTableWithDefaultContainer(3, 10); + t.setVisibleColumns(new Object[] {}); + assertArrayEquals(new Object[] {}, t.getVisibleColumns()); + + } +} diff --git a/compatibility-server/src/test/java/com/vaadin/tests/server/component/treetable/EmptyTreeTableTest.java b/compatibility-server/src/test/java/com/vaadin/tests/server/component/treetable/EmptyTreeTableTest.java new file mode 100644 index 0000000000..2a667bea72 --- /dev/null +++ b/compatibility-server/src/test/java/com/vaadin/tests/server/component/treetable/EmptyTreeTableTest.java @@ -0,0 +1,17 @@ +package com.vaadin.tests.server.component.treetable; + +import static org.junit.Assert.assertFalse; + +import org.junit.Test; + +import com.vaadin.ui.TreeTable; + +public class EmptyTreeTableTest { + + @Test + public void testLastId() { + TreeTable treeTable = new TreeTable(); + + assertFalse(treeTable.isLastId(treeTable.getValue())); + } +} diff --git a/compatibility-server/src/test/java/com/vaadin/tests/server/component/treetable/TreeTableDeclarativeTest.java b/compatibility-server/src/test/java/com/vaadin/tests/server/component/treetable/TreeTableDeclarativeTest.java new file mode 100644 index 0000000000..eb25bcb0aa --- /dev/null +++ b/compatibility-server/src/test/java/com/vaadin/tests/server/component/treetable/TreeTableDeclarativeTest.java @@ -0,0 +1,156 @@ +/* + * Copyright 2000-2016 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.tests.server.component.treetable; + +import org.junit.Assert; +import org.junit.Test; + +import com.vaadin.tests.server.component.table.TableDeclarativeTest; +import com.vaadin.ui.Table; +import com.vaadin.ui.TreeTable; +import com.vaadin.ui.declarative.DesignException; + +/** + * Test declarative support for {@link TreeTable}. + * + * @since + * @author Vaadin Ltd + */ +public class TreeTableDeclarativeTest extends TableDeclarativeTest { + + @Test + public void testAttributes() { + String design = "<vaadin-tree-table animations-enabled>"; + TreeTable table = getTable(); + table.setAnimationsEnabled(true); + + testRead(design, table); + testWrite(design, table); + } + + @Test + public void testHierarchy() { + String design = "<vaadin-tree-table>" // + + "<table>" // + + "<colgroup><col property-id=''></colgroup>" // + + "<tbody>" // + + " <tr item-id='1'><td></tr>" // + + " <tr depth=1 item-id='1.1'><td></tr>" // + + " <tr depth=1 item-id='1.2'><td></tr>" // + + " <tr depth=2 item-id='1.2.1'><td></tr>" // + + " <tr depth=3 item-id='1.2.1.1'><td></tr>" // + + " <tr depth=2 item-id='1.2.2'><td></tr>" // + + " <tr item-id='2'><td></tr>" // + + " <tr depth=1 item-id='2.1'><td></tr>" // + + "</tbody>" // + + "</table>" // + + "</vaadin-tree-table>"; + + TreeTable table = getTable(); + table.addContainerProperty("", String.class, ""); + + table.addItem("1"); + table.addItem("1.1"); + table.setParent("1.1", "1"); + table.addItem("1.2"); + table.setParent("1.2", "1"); + table.addItem("1.2.1"); + table.setParent("1.2.1", "1.2"); + table.addItem("1.2.1.1"); + table.setParent("1.2.1.1", "1.2.1"); + table.addItem("1.2.2"); + table.setParent("1.2.2", "1.2"); + table.addItem("2"); + table.addItem("2.1"); + table.setParent("2.1", "2"); + + testRead(design, table); + testWrite(design, table, true); + } + + @Test + public void testCollapsed() { + String design = "<vaadin-tree-table>" // + + " <table>" // + + " <colgroup><col property-id=''></colgroup>" // + + " <tbody>" // + + " <tr item-id='1' collapsed=false><td></tr>" // + + " <tr depth=1 item-id='1.1'><td></tr>" // + + " <tr depth=2 item-id='1.1.1'><td></tr>" // + + " </tbody>" // + + " </table>" // + + "</vaadin-tree-table>"; + + TreeTable table = getTable(); + table.addContainerProperty("", String.class, ""); + + table.addItem("1"); + table.setCollapsed("1", false); + table.addItem("1.1"); + table.setParent("1.1", "1"); + table.addItem("1.1.1"); + table.setParent("1.1.1", "1.1"); + + testRead(design, table); + testWrite(design, table, true); + } + + @Test + public void testMalformedHierarchy() { + assertMalformed("<tr depth=-4><td>"); + assertMalformed("<tr depth=1><td>"); + assertMalformed("<tr><td><tr depth=3><td>"); + } + + protected void assertMalformed(String hierarchy) { + String design = "<vaadin-tree-table>" // + + " <table>" // + + " <colgroup><col property-id=''></colgroup>" // + + " <tbody>" + hierarchy + "</tbody>" // + + " </table>" // + + "</vaadin-tree-table>"; + + try { + read(design); + Assert.fail("Malformed hierarchy should fail: " + hierarchy); + } catch (DesignException expected) { + } + } + + @Override + protected void compareBody(Table read, Table expected) { + super.compareBody(read, expected); + + for (Object itemId : read.getItemIds()) { + Assert.assertEquals("parent of item " + itemId, + ((TreeTable) expected).getParent(itemId), + ((TreeTable) read).getParent(itemId)); + Assert.assertEquals("collapsed status of item " + itemId, + ((TreeTable) expected).isCollapsed(itemId), + ((TreeTable) read).isCollapsed(itemId)); + } + } + + @Override + protected TreeTable getTable() { + return new TreeTable(); + } + + @Override + protected String getTag() { + return "vaadin-tree-table"; + } +} diff --git a/compatibility-server/src/test/java/com/vaadin/tests/server/component/treetable/TreeTableSetContainerNullTest.java b/compatibility-server/src/test/java/com/vaadin/tests/server/component/treetable/TreeTableSetContainerNullTest.java new file mode 100644 index 0000000000..9718d24f8c --- /dev/null +++ b/compatibility-server/src/test/java/com/vaadin/tests/server/component/treetable/TreeTableSetContainerNullTest.java @@ -0,0 +1,16 @@ +package com.vaadin.tests.server.component.treetable; + +import org.junit.Test; + +import com.vaadin.ui.TreeTable; + +public class TreeTableSetContainerNullTest { + + @Test + public void testNullContainer() { + TreeTable treeTable = new TreeTable(); + + // should not cause an exception + treeTable.setContainerDataSource(null); + } +} diff --git a/compatibility-server/src/test/java/com/vaadin/tests/server/component/treetable/TreeTableTest.java b/compatibility-server/src/test/java/com/vaadin/tests/server/component/treetable/TreeTableTest.java new file mode 100644 index 0000000000..4706ec1413 --- /dev/null +++ b/compatibility-server/src/test/java/com/vaadin/tests/server/component/treetable/TreeTableTest.java @@ -0,0 +1,102 @@ +/* + * Copyright 2000-2016 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.tests.server.component.treetable; + +import java.util.EnumSet; + +import org.junit.Assert; +import org.junit.Test; + +import com.vaadin.shared.ui.treetable.TreeTableState; +import com.vaadin.ui.Table.RowHeaderMode; +import com.vaadin.ui.TreeTable; + +/** + * Tests for {@link TreeTable} + * + * @author Vaadin Ltd + */ +public class TreeTableTest { + + @Test + public void rowHeadersAreEnabled_iconRowHeaderMode_rowHeadersAreDisabled() { + TestTreeTable tree = new TestTreeTable(); + tree.setRowHeaderMode(RowHeaderMode.ICON_ONLY); + + Assert.assertFalse("Row headers are enabled for Icon header mode", + tree.rowHeadersAreEnabled()); + } + + @Test + public void rowHeadersAreEnabled_hiddenRowHeaderMode_rowHeadersAreDisabled() { + TestTreeTable tree = new TestTreeTable(); + tree.setRowHeaderMode(RowHeaderMode.HIDDEN); + + Assert.assertFalse("Row headers are enabled for Hidden header mode", + tree.rowHeadersAreEnabled()); + } + + @Test + public void rowHeadersAreEnabled_otherRowHeaderModes_rowHeadersAreEnabled() { + TestTreeTable tree = new TestTreeTable(); + EnumSet<RowHeaderMode> modes = EnumSet.allOf(RowHeaderMode.class); + modes.remove(RowHeaderMode.ICON_ONLY); + modes.remove(RowHeaderMode.HIDDEN); + + for (RowHeaderMode mode : modes) { + tree.setRowHeaderMode(mode); + Assert.assertTrue( + "Row headers are disabled for " + mode + " header mode", + tree.rowHeadersAreEnabled()); + } + } + + @Test + public void getState_treeTableHasCustomState() { + TestTreeTable table = new TestTreeTable(); + TreeTableState state = table.getState(); + Assert.assertEquals("Unexpected state class", TreeTableState.class, + state.getClass()); + } + + @Test + public void getPrimaryStyleName_treeTableHasCustomPrimaryStyleName() { + TreeTable table = new TreeTable(); + TreeTableState state = new TreeTableState(); + Assert.assertEquals("Unexpected primary style name", + state.primaryStyleName, table.getPrimaryStyleName()); + } + + @Test + public void treeTableStateHasCustomPrimaryStyleName() { + TreeTableState state = new TreeTableState(); + Assert.assertEquals("Unexpected primary style name", "v-table", + state.primaryStyleName); + } + + private static class TestTreeTable extends TreeTable { + + @Override + protected boolean rowHeadersAreEnabled() { + return super.rowHeadersAreEnabled(); + } + + @Override + public TreeTableState getState() { + return super.getState(); + } + } +} |