diff options
Diffstat (limited to 'server')
36 files changed, 10031 insertions, 66 deletions
diff --git a/server/src/com/vaadin/data/Container.java b/server/src/com/vaadin/data/Container.java index 8e99bac541..c58d37d5fe 100644 --- a/server/src/com/vaadin/data/Container.java +++ b/server/src/com/vaadin/data/Container.java @@ -582,6 +582,60 @@ public interface Container extends Serializable { public Item addItemAt(int index, Object newItemId) throws UnsupportedOperationException; + /** + * An <code>Event</code> object specifying information about the added + * items. + */ + public interface ItemAddEvent extends ItemSetChangeEvent { + + /** + * Gets the item id of the first added item. + * + * @return item id of the first added item + */ + public Object getFirstItemId(); + + /** + * Gets the index of the first added item. + * + * @return index of the first added item + */ + public int getFirstIndex(); + + /** + * Gets the number of the added items. + * + * @return the number of added items. + */ + public int getAddedItemsCount(); + } + + /** + * An <code>Event</code> object specifying information about the removed + * items. + */ + public interface ItemRemoveEvent extends ItemSetChangeEvent { + /** + * Gets the item id of the first removed item. + * + * @return item id of the first removed item + */ + public Object getFirstItemId(); + + /** + * Gets the index of the first removed item. + * + * @return index of the first removed item + */ + public int getFirstIndex(); + + /** + * Gets the number of the removed items. + * + * @return the number of removed items + */ + public int getRemovedItemsCount(); + } } /** diff --git a/server/src/com/vaadin/data/RpcDataProviderExtension.java b/server/src/com/vaadin/data/RpcDataProviderExtension.java new file mode 100644 index 0000000000..f28d95f610 --- /dev/null +++ b/server/src/com/vaadin/data/RpcDataProviderExtension.java @@ -0,0 +1,991 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.vaadin.data; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; + +import com.google.gwt.thirdparty.guava.common.collect.BiMap; +import com.google.gwt.thirdparty.guava.common.collect.HashBiMap; +import com.vaadin.data.Container.Indexed; +import com.vaadin.data.Container.Indexed.ItemAddEvent; +import com.vaadin.data.Container.Indexed.ItemRemoveEvent; +import com.vaadin.data.Container.ItemSetChangeEvent; +import com.vaadin.data.Container.ItemSetChangeListener; +import com.vaadin.data.Container.ItemSetChangeNotifier; +import com.vaadin.data.Property.ValueChangeEvent; +import com.vaadin.data.Property.ValueChangeListener; +import com.vaadin.data.Property.ValueChangeNotifier; +import com.vaadin.data.util.converter.Converter; +import com.vaadin.data.util.converter.Converter.ConversionException; +import com.vaadin.server.AbstractExtension; +import com.vaadin.server.ClientConnector; +import com.vaadin.server.KeyMapper; +import com.vaadin.shared.data.DataProviderRpc; +import com.vaadin.shared.data.DataRequestRpc; +import com.vaadin.shared.ui.grid.GridState; +import com.vaadin.shared.ui.grid.Range; +import com.vaadin.ui.Grid; +import com.vaadin.ui.Grid.CellStyleGenerator; +import com.vaadin.ui.Grid.Column; +import com.vaadin.ui.renderer.Renderer; + +import elemental.json.Json; +import elemental.json.JsonArray; +import elemental.json.JsonObject; +import elemental.json.JsonValue; + +/** + * Provides Vaadin server-side container data source to a + * {@link com.vaadin.client.ui.grid.GridConnector}. This is currently + * implemented as an Extension hardcoded to support a specific connector type. + * This will be changed once framework support for something more flexible has + * been implemented. + * + * @since + * @author Vaadin Ltd + */ +public class RpcDataProviderExtension extends AbstractExtension { + + /** + * ItemId to Key to ItemId mapper. + * <p> + * This class is used when transmitting information about items in container + * related to Grid. It introduces a consistent way of mapping ItemIds and + * its container to a String that can be mapped back to ItemId. + * <p> + * <em>Technical note:</em> This class also keeps tabs on which indices are + * being shown/selected, and is able to clean up after itself once the + * itemId ⇆ key mapping is not needed anymore. In other words, this + * doesn't leak memory. + */ + public class DataProviderKeyMapper implements Serializable { + private final BiMap<Integer, Object> indexToItemId = HashBiMap.create(); + private final BiMap<Object, String> itemIdToKey = HashBiMap.create(); + private Set<Object> pinnedItemIds = new HashSet<Object>(); + private Range activeRange = Range.withLength(0, 0); + private long rollingIndex = 0; + + private DataProviderKeyMapper() { + // private implementation + } + + void preActiveRowsChange(Range newActiveRange, int firstNewIndex, + List<?> itemIds) { + final Range[] removed = activeRange.partitionWith(newActiveRange); + final Range[] added = newActiveRange.partitionWith(activeRange); + + removeActiveRows(removed[0]); + removeActiveRows(removed[2]); + addActiveRows(added[0], firstNewIndex, itemIds); + addActiveRows(added[2], firstNewIndex, itemIds); + + activeRange = newActiveRange; + } + + private void removeActiveRows(final Range deprecated) { + for (int i = deprecated.getStart(); i < deprecated.getEnd(); i++) { + final Integer ii = Integer.valueOf(i); + final Object itemId = indexToItemId.get(ii); + + if (!isPinned(itemId)) { + itemIdToKey.remove(itemId); + indexToItemId.remove(ii); + } + } + } + + private void addActiveRows(final Range added, int firstNewIndex, + List<?> newItemIds) { + + for (int i = added.getStart(); i < added.getEnd(); i++) { + + /* + * We might be in a situation we have an index <-> itemId entry + * already. This happens when something was selected, scrolled + * out of view and now we're scrolling it back into view. It's + * unnecessary to overwrite it in that case. + * + * Fun thought: considering branch prediction, it _might_ even + * be a bit faster to simply always run the code beyond this + * if-state. But it sounds too stupid (and most often too + * insignificant) to try out. + */ + final Integer ii = Integer.valueOf(i); + if (indexToItemId.containsKey(ii)) { + continue; + } + + /* + * We might be in a situation where we have an itemId <-> key + * entry already, but no index for it. This happens when + * something that is out of view is selected programmatically. + * In that case, we only want to add an index for that entry, + * and not overwrite the key. + */ + final Object itemId = newItemIds.get(i - firstNewIndex); + if (!itemIdToKey.containsKey(itemId)) { + itemIdToKey.put(itemId, nextKey()); + } + indexToItemId.forcePut(ii, itemId); + } + } + + private String nextKey() { + return String.valueOf(rollingIndex++); + } + + String getKey(Object itemId) { + String key = itemIdToKey.get(itemId); + if (key == null) { + key = nextKey(); + itemIdToKey.put(itemId, key); + } + return key; + } + + /** + * Gets keys for a collection of item ids. + * <p> + * If the itemIds are currently cached, the existing keys will be used. + * Otherwise new ones will be created. + * + * @param itemIds + * the item ids for which to get keys + * @return keys for the {@code itemIds} + */ + public List<String> getKeys(Collection<Object> itemIds) { + if (itemIds == null) { + throw new IllegalArgumentException("itemIds can't be null"); + } + + ArrayList<String> keys = new ArrayList<String>(itemIds.size()); + for (Object itemId : itemIds) { + keys.add(getKey(itemId)); + } + return keys; + } + + /** + * Gets the registered item id based on its key. + * <p> + * A key is used to identify a particular row on both a server and a + * client. This method can be used to get the item id for the row key + * that the client has sent. + * + * @param key + * the row key for which to retrieve an item id + * @return the item id corresponding to {@code key} + * @throws IllegalStateException + * if the key mapper does not have a record of {@code key} . + */ + public Object getItemId(String key) throws IllegalStateException { + Object itemId = itemIdToKey.inverse().get(key); + if (itemId != null) { + return itemId; + } else { + throw new IllegalStateException("No item id for key " + key + + " found."); + } + } + + /** + * Gets corresponding item ids for each of the keys in a collection. + * + * @param keys + * the keys for which to retrieve item ids + * @return a collection of item ids for the {@code keys} + * @throws IllegalStateException + * if one or more of keys don't have a corresponding item id + * in the cache + */ + public Collection<Object> getItemIds(Collection<String> keys) + throws IllegalStateException { + if (keys == null) { + throw new IllegalArgumentException("keys may not be null"); + } + + ArrayList<Object> itemIds = new ArrayList<Object>(keys.size()); + for (String key : keys) { + itemIds.add(getItemId(key)); + } + return itemIds; + } + + /** + * Pin an item id to be cached indefinitely. + * <p> + * Normally when an itemId is not an active row, it is discarded from + * the cache. Pinning an item id will make sure that it is kept in the + * cache. + * <p> + * In effect, while an item id is pinned, it always has the same key. + * + * @param itemId + * the item id to pin + * @throws IllegalStateException + * if {@code itemId} was already pinned + * @see #unpin(Object) + * @see #isPinned(Object) + * @see #getItemIds(Collection) + */ + public void pin(Object itemId) throws IllegalStateException { + if (isPinned(itemId)) { + throw new IllegalStateException("Item id " + itemId + + " was pinned already"); + } + pinnedItemIds.add(itemId); + } + + /** + * Unpin an item id. + * <p> + * This cancels the effect of pinning an item id. If the item id is + * currently inactive, it will be immediately removed from the cache. + * + * @param itemId + * the item id to unpin + * @throws IllegalStateException + * if {@code itemId} was not pinned + * @see #pin(Object) + * @see #isPinned(Object) + * @see #getItemIds(Collection) + */ + public void unpin(Object itemId) throws IllegalStateException { + if (!isPinned(itemId)) { + throw new IllegalStateException("Item id " + itemId + + " was not pinned"); + } + + pinnedItemIds.remove(itemId); + final Integer index = indexToItemId.inverse().get(itemId); + if (index == null || !activeRange.contains(index.intValue())) { + itemIdToKey.remove(itemId); + indexToItemId.remove(index); + } + } + + /** + * Checks whether an item id is pinned or not. + * + * @param itemId + * the item id to check for pin status + * @return {@code true} iff the item id is currently pinned + */ + public boolean isPinned(Object itemId) { + return pinnedItemIds.contains(itemId); + } + + Object itemIdAtIndex(int index) { + return indexToItemId.inverse().get(Integer.valueOf(index)); + } + } + + /** + * A helper class that handles the client-side Escalator logic relating to + * making sure that whatever is currently visible to the user, is properly + * initialized and otherwise handled on the server side (as far as + * required). + * <p> + * This bookeeping includes, but is not limited to: + * <ul> + * <li>listening to the currently visible {@link com.vaadin.data.Property + * Properties'} value changes on the server side and sending those back to + * the client; and + * <li>attaching and detaching {@link com.vaadin.ui.Component Components} + * from the Vaadin Component hierarchy. + * </ul> + */ + private class ActiveRowHandler implements Serializable { + /** + * A map from itemId to the value change listener used for all of its + * properties + */ + private final Map<Object, GridValueChangeListener> valueChangeListeners = new HashMap<Object, GridValueChangeListener>(); + + /** + * The currently active range. Practically, it's the range of row + * indices being cached currently. + */ + private Range activeRange = Range.withLength(0, 0); + + /** + * A hook for making sure that appropriate data is "active". All other + * rows should be "inactive". + * <p> + * "Active" can mean different things in different contexts. For + * example, only the Properties in the active range need + * ValueChangeListeners. Also, whenever a row with a Component becomes + * active, it needs to be attached (and conversely, when inactive, it + * needs to be detached). + * + * @param firstActiveRow + * the first active row + * @param activeRowCount + * the number of active rows + */ + public void setActiveRows(int firstActiveRow, int activeRowCount) { + + final Range newActiveRange = Range.withLength(firstActiveRow, + activeRowCount); + + // TODO [[Components]] attach and detach components + + /*- + * Example + * + * New Range: [3, 4, 5, 6, 7] + * Old Range: [1, 2, 3, 4, 5] + * Result: [1, 2][3, 4, 5] [] + */ + final Range[] depractionPartition = activeRange + .partitionWith(newActiveRange); + removeValueChangeListeners(depractionPartition[0]); + removeValueChangeListeners(depractionPartition[2]); + + /*- + * Example + * + * Old Range: [1, 2, 3, 4, 5] + * New Range: [3, 4, 5, 6, 7] + * Result: [] [3, 4, 5][6, 7] + */ + final Range[] activationPartition = newActiveRange + .partitionWith(activeRange); + addValueChangeListeners(activationPartition[0]); + addValueChangeListeners(activationPartition[2]); + + activeRange = newActiveRange; + } + + private void addValueChangeListeners(Range range) { + for (int i = range.getStart(); i < range.getEnd(); i++) { + + final Object itemId = container.getIdByIndex(i); + final Item item = container.getItem(itemId); + + if (valueChangeListeners.containsKey(itemId)) { + /* + * This might occur when items are removed from above the + * viewport, the escalator scrolls up to compensate, but the + * same items remain in the view: It looks as if one row was + * scrolled, when in fact the whole viewport was shifted up. + */ + continue; + } + + GridValueChangeListener listener = new GridValueChangeListener( + itemId); + valueChangeListeners.put(itemId, listener); + + for (final Object propertyId : item.getItemPropertyIds()) { + final Property<?> property = item + .getItemProperty(propertyId); + if (property instanceof ValueChangeNotifier) { + ((ValueChangeNotifier) property) + .addValueChangeListener(listener); + } + } + } + } + + private void removeValueChangeListeners(Range range) { + for (int i = range.getStart(); i < range.getEnd(); i++) { + final Object itemId = container.getIdByIndex(i); + final Item item = container.getItem(itemId); + final GridValueChangeListener listener = valueChangeListeners + .remove(itemId); + + if (listener != null) { + for (final Object propertyId : item.getItemPropertyIds()) { + final Property<?> property = item + .getItemProperty(propertyId); + if (property instanceof ValueChangeNotifier) { + ((ValueChangeNotifier) property) + .removeValueChangeListener(listener); + } + } + } + } + } + + /** + * Manages removed properties in active rows. + * + * @param removedPropertyIds + * the property ids that have been removed from the container + */ + public void propertiesRemoved(@SuppressWarnings("unused") + Collection<Object> removedPropertyIds) { + /* + * no-op, for now. + * + * The Container should be responsible for cleaning out any + * ValueChangeListeners from removed Properties. Components will + * benefit from this, however. + */ + } + + /** + * Manages added properties in active rows. + * + * @param addedPropertyIds + * the property ids that have been added to the container + */ + public void propertiesAdded(Collection<Object> addedPropertyIds) { + if (addedPropertyIds.isEmpty()) { + return; + } + + for (int i = activeRange.getStart(); i < activeRange.getEnd(); i++) { + final Object itemId = container.getIdByIndex(i); + final Item item = container.getItem(itemId); + final GridValueChangeListener listener = valueChangeListeners + .get(itemId); + assert (listener != null) : "a listener should've been pre-made by addValueChangeListeners"; + + for (final Object propertyId : addedPropertyIds) { + final Property<?> property = item + .getItemProperty(propertyId); + if (property instanceof ValueChangeNotifier) { + ((ValueChangeNotifier) property) + .addValueChangeListener(listener); + } + } + + updateRowData(i); + } + } + + /** + * Handles the insertion of rows. + * <p> + * This method's responsibilities are to: + * <ul> + * <li>shift the internal bookkeeping by <code>count</code> if the + * insertion happens above currently active range + * <li>ignore rows inserted below the currently active range + * <li>shift (and deactivate) rows pushed out of view + * <li>activate rows that are inserted in the current viewport + * </ul> + * + * @param firstIndex + * the index of the first inserted rows + * @param count + * the number of rows inserted at <code>firstIndex</code> + */ + public void insertRows(int firstIndex, int count) { + if (firstIndex < activeRange.getStart()) { + activeRange = activeRange.offsetBy(count); + } else if (firstIndex < activeRange.getEnd()) { + final Range deprecatedRange = Range.withLength( + activeRange.getEnd(), count); + removeValueChangeListeners(deprecatedRange); + + final Range freshRange = Range.between(firstIndex, count); + addValueChangeListeners(freshRange); + } else { + // out of view, noop + } + } + + /** + * Removes a single item by its id. + * + * @param itemId + * the id of the removed id. <em>Note:</em> this item does + * not exist anymore in the datasource + */ + public void removeItemId(Object itemId) { + final GridValueChangeListener removedListener = valueChangeListeners + .remove(itemId); + if (removedListener != null) { + /* + * We removed an item from somewhere in the visible range, so we + * make the active range shorter. The empty hole will be filled + * by the client-side code when it asks for more information. + */ + activeRange = Range.withLength(activeRange.getStart(), + activeRange.length() - 1); + } + } + } + + /** + * A class to listen to changes in property values in the Container added + * with {@link Grid#setContainerDatasource(Container.Indexed)}, and notifies + * the data source to update the client-side representation of the modified + * item. + * <p> + * One instance of this class can (and should) be reused for all the + * properties in an item, since this class will inform that the entire row + * needs to be re-evaluated (in contrast to a property-based change + * management) + * <p> + * Since there's no Container-wide possibility to listen to any kind of + * value changes, an instance of this class needs to be attached to each and + * every Item's Property in the container. + * + * @see Grid#addValueChangeListener(Container, Object, Object) + * @see Grid#valueChangeListeners + */ + private class GridValueChangeListener implements ValueChangeListener { + private final Object itemId; + + public GridValueChangeListener(Object itemId) { + /* + * Using an assert instead of an exception throw, just to optimize + * prematurely + */ + assert itemId != null : "null itemId not accepted"; + this.itemId = itemId; + } + + @Override + public void valueChange(ValueChangeEvent event) { + updateRowData(container.indexOfId(itemId)); + } + } + + private final Indexed container; + + private final ActiveRowHandler activeRowHandler = new ActiveRowHandler(); + + private DataProviderRpc rpc; + + private final ItemSetChangeListener itemListener = new ItemSetChangeListener() { + @Override + public void containerItemSetChange(ItemSetChangeEvent event) { + + if (event instanceof ItemAddEvent) { + ItemAddEvent addEvent = (ItemAddEvent) event; + int firstIndex = addEvent.getFirstIndex(); + int count = addEvent.getAddedItemsCount(); + insertRowData(firstIndex, count); + } + + else if (event instanceof ItemRemoveEvent) { + ItemRemoveEvent removeEvent = (ItemRemoveEvent) event; + int firstIndex = removeEvent.getFirstIndex(); + int count = removeEvent.getRemovedItemsCount(); + removeRowData(firstIndex, count); + } + + else { + + /* + * Clear everything we have in view, and let the client + * re-request for whatever it needs. + * + * Why this shortcut? Well, since anything could've happened, we + * don't know what has happened. There are a lot of use-cases we + * can cover at once with this carte blanche operation: + * + * 1) Grid is scrolled somewhere in the middle and all the + * rows-inview are removed. We need a new pageful. + * + * 2) Grid is scrolled somewhere in the middle and none of the + * visible rows are removed. We need no new rows. + * + * 3) Grid is scrolled all the way to the bottom, and the last + * rows are being removed. Grid needs to scroll up and request + * for more rows at the top. + * + * 4) Grid is scrolled pretty much to the bottom, and the last + * rows are being removed. Grid needs to be aware that some + * scrolling is needed, but not to compensate for all the + * removed rows. And it also needs to request for some more rows + * to the top. + * + * 5) Some ranges of rows are removed from view. We need to + * collapse the gaps with existing rows and load the missing + * rows. + * + * 6) The ultimate use case! Grid has 1.5 pages of rows and + * scrolled a bit down. One page of rows is removed. We need to + * make sure that new rows are loaded, but not all old slots are + * occupied, since the page can't be filled with new row data. + * It also needs to be scrolled to the top. + * + * So, it's easier (and safer) to do the simple thing instead of + * taking all the corner cases into account. + */ + + activeRowHandler.activeRange = Range.withLength(0, 0); + activeRowHandler.valueChangeListeners.clear(); + rpc.resetDataAndSize(event.getContainer().size()); + } + } + }; + + private final DataProviderKeyMapper keyMapper = new DataProviderKeyMapper(); + + private KeyMapper<Object> columnKeys; + + /* Has client been initialized */ + private boolean clientInitialized = false; + + /** + * Creates a new data provider using the given container. + * + * @param container + * the container to make available + */ + public RpcDataProviderExtension(Indexed container) { + this.container = container; + rpc = getRpcProxy(DataProviderRpc.class); + + registerRpc(new DataRequestRpc() { + @Override + public void requestRows(int firstRow, int numberOfRows, + int firstCachedRowIndex, int cacheSize) { + + pushRowData(firstRow, numberOfRows, firstCachedRowIndex, + cacheSize); + } + + @Override + public void setPinned(String key, boolean isPinned) { + Object itemId = keyMapper.getItemId(key); + if (isPinned) { + // Row might already be pinned if it was selected from the + // server + if (!keyMapper.isPinned(itemId)) { + keyMapper.pin(itemId); + } + } else { + keyMapper.unpin(itemId); + } + } + }); + + if (container instanceof ItemSetChangeNotifier) { + ((ItemSetChangeNotifier) container) + .addItemSetChangeListener(itemListener); + } + + } + + @Override + public void beforeClientResponse(boolean initial) { + super.beforeClientResponse(initial); + if (!clientInitialized) { + clientInitialized = true; + + /* + * Push initial set of rows, assuming Grid will initially be + * rendered scrolled to the top and with a decent amount of rows + * visible. If this guess is right, initial data can be shown + * without a round-trip and if it's wrong, the data will simply be + * discarded. + */ + int size = container.size(); + rpc.resetDataAndSize(size); + + int numberOfRows = Math.min(40, size); + pushRowData(0, numberOfRows, 0, 0); + } + } + + private void pushRowData(int firstRowToPush, int numberOfRows, + int firstCachedRowIndex, int cacheSize) { + Range active = Range.withLength(firstRowToPush, numberOfRows); + if (cacheSize != 0) { + Range cached = Range.withLength(firstCachedRowIndex, cacheSize); + active = active.combineWith(cached); + } + + List<?> itemIds = RpcDataProviderExtension.this.container.getItemIds( + firstRowToPush, numberOfRows); + keyMapper.preActiveRowsChange(active, firstRowToPush, itemIds); + + JsonArray rows = Json.createArray(); + for (int i = 0; i < itemIds.size(); ++i) { + rows.set(i, getRowData(getGrid().getColumns(), itemIds.get(i))); + } + rpc.setRowData(firstRowToPush, rows.toJson()); + + activeRowHandler.setActiveRows(active.getStart(), active.length()); + } + + private JsonValue getRowData(Collection<Column> columns, Object itemId) { + Item item = container.getItem(itemId); + + JsonObject rowData = Json.createObject(); + + Grid grid = getGrid(); + + for (Column column : columns) { + Object propertyId = column.getColumnProperty(); + + Object propertyValue = item.getItemProperty(propertyId).getValue(); + JsonValue encodedValue = encodeValue(propertyValue, + column.getRenderer(), column.getConverter(), + grid.getLocale()); + + rowData.put(columnKeys.key(propertyId), encodedValue); + } + + final JsonObject rowObject = Json.createObject(); + rowObject.put(GridState.JSONKEY_DATA, rowData); + rowObject.put(GridState.JSONKEY_ROWKEY, keyMapper.getKey(itemId)); + + CellStyleGenerator cellStyleGenerator = grid.getCellStyleGenerator(); + if (cellStyleGenerator != null) { + setGeneratedStyles(cellStyleGenerator, rowObject, columns, itemId); + } + + return rowObject; + } + + private void setGeneratedStyles(CellStyleGenerator generator, + JsonObject rowObject, Collection<Column> columns, Object itemId) { + Grid grid = getGrid(); + + JsonObject cellStyles = null; + for (Column column : columns) { + Object propertyId = column.getColumnProperty(); + String style = generator.getStyle(grid, itemId, propertyId); + if (style != null) { + if (cellStyles == null) { + cellStyles = Json.createObject(); + } + + String columnKey = columnKeys.key(propertyId); + cellStyles.put(columnKey, style); + } + } + if (cellStyles != null) { + rowObject.put(GridState.JSONKEY_CELLSTYLES, cellStyles); + } + + String rowStyle = generator.getStyle(grid, itemId, null); + if (rowStyle != null) { + rowObject.put(GridState.JSONKEY_ROWSTYLE, rowStyle); + } + } + + /** + * Makes the data source available to the given {@link Grid} component. + * + * @param component + * the remote data grid component to extend + */ + public void extend(Grid component, KeyMapper<Object> columnKeys) { + this.columnKeys = columnKeys; + super.extend(component); + } + + /** + * Informs the client side that new rows have been inserted into the data + * source. + * + * @param index + * the index at which new rows have been inserted + * @param count + * the number of rows inserted at <code>index</code> + */ + private void insertRowData(int index, int count) { + if (clientInitialized) { + rpc.insertRowData(index, count); + } + + activeRowHandler.insertRows(index, count); + } + + /** + * Informs the client side that rows have been removed from the data source. + * + * @param firstIndex + * the index of the first row removed + * @param count + * the number of rows removed + * @param firstItemId + * the item id of the first removed item + */ + private void removeRowData(int firstIndex, int count) { + if (clientInitialized) { + rpc.removeRowData(firstIndex, count); + } + + for (int i = 0; i < count; i++) { + Object itemId = keyMapper.itemIdAtIndex(firstIndex + i); + if (itemId != null) { + activeRowHandler.removeItemId(itemId); + } + } + } + + /** + * Informs the client side that data of a row has been modified in the data + * source. + * + * @param index + * the index of the row that was updated + */ + public void updateRowData(int index) { + /* + * TODO: ignore duplicate requests for the same index during the same + * roundtrip. + */ + Object itemId = container.getIdByIndex(index); + JsonValue row = getRowData(getGrid().getColumns(), itemId); + JsonArray rowArray = Json.createArray(); + rowArray.set(0, row); + rpc.setRowData(index, rowArray.toJson()); + } + + /** + * Pushes a new version of all the rows in the active cache range. + */ + public void refreshCache() { + if (!clientInitialized) { + return; + } + + int firstRow = activeRowHandler.activeRange.getStart(); + int numberOfRows = activeRowHandler.activeRange.length(); + + pushRowData(firstRow, numberOfRows, firstRow, numberOfRows); + } + + @Override + public void setParent(ClientConnector parent) { + super.setParent(parent); + if (parent == null) { + // We're detached, release various listeners + + activeRowHandler + .removeValueChangeListeners(activeRowHandler.activeRange); + + if (container instanceof ItemSetChangeNotifier) { + ((ItemSetChangeNotifier) container) + .removeItemSetChangeListener(itemListener); + } + + } + } + + /** + * Informs this data provider that some of the properties have been removed + * from the container. + * <p> + * Please note that we could add our own + * {@link com.vaadin.data.Container.PropertySetChangeListener + * PropertySetChangeListener} to the container, but then we'd need to + * implement the same bookeeping for finding what's added and removed that + * Grid already does in its own listener. + * + * @param removedColumns + * a list of property ids for the removed columns + */ + public void propertiesRemoved(List<Object> removedColumns) { + activeRowHandler.propertiesRemoved(removedColumns); + } + + /** + * Informs this data provider that some of the properties have been added to + * the container. + * <p> + * Please note that we could add our own + * {@link com.vaadin.data.Container.PropertySetChangeListener + * PropertySetChangeListener} to the container, but then we'd need to + * implement the same bookeeping for finding what's added and removed that + * Grid already does in its own listener. + * + * @param addedPropertyIds + * a list of property ids for the added columns + */ + public void propertiesAdded(HashSet<Object> addedPropertyIds) { + activeRowHandler.propertiesAdded(addedPropertyIds); + } + + public DataProviderKeyMapper getKeyMapper() { + return keyMapper; + } + + protected Grid getGrid() { + return (Grid) getParent(); + } + + /** + * Converts and encodes the given data model property value using the given + * converter and renderer. This method is public only for testing purposes. + * + * @param renderer + * the renderer to use + * @param converter + * the converter to use + * @param modelValue + * the value to convert and encode + * @param locale + * the locale to use in conversion + * @return an encoded value ready to be sent to the client + */ + public static <T> JsonValue encodeValue(Object modelValue, + Renderer<T> renderer, Converter<?, ?> converter, Locale locale) { + Class<T> presentationType = renderer.getPresentationType(); + T presentationValue; + + if (converter == null) { + try { + presentationValue = presentationType.cast(modelValue); + } catch (ClassCastException e) { + ConversionException ee = new Converter.ConversionException( + "Unable to convert value of type " + + modelValue.getClass().getName() + + " to presentation type " + + presentationType.getName() + + ". No converter is set and the types are not compatible."); + if (presentationType == String.class) { + // We don't want to throw an exception for the default cause + // when one column can't be rendered. Just log the exception + // and let the column be empty + presentationValue = (T) ""; + getLogger().log(Level.SEVERE, ee.getMessage(), ee); + } else { + throw ee; + } + } + } else { + assert presentationType.isAssignableFrom(converter + .getPresentationType()); + @SuppressWarnings("unchecked") + Converter<T, Object> safeConverter = (Converter<T, Object>) converter; + presentationValue = safeConverter.convertToPresentation(modelValue, + safeConverter.getPresentationType(), locale); + } + + JsonValue encodedValue = renderer.encode(presentationValue); + + return encodedValue; + } + + private static Logger getLogger() { + return Logger.getLogger(RpcDataProviderExtension.class.getName()); + } + +} diff --git a/server/src/com/vaadin/data/sort/Sort.java b/server/src/com/vaadin/data/sort/Sort.java new file mode 100644 index 0000000000..914a92f249 --- /dev/null +++ b/server/src/com/vaadin/data/sort/Sort.java @@ -0,0 +1,153 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.data.sort; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +import com.vaadin.shared.ui.grid.SortDirection; + +/** + * Fluid Sort API. Provides a convenient, human-readable way of specifying + * multi-column sort order. + * + * @since 7.4 + * @author Vaadin Ltd + */ +public class Sort implements Serializable { + + private final Sort previous; + private final SortOrder order; + + /** + * Initial constructor, called by the static by() methods. + * + * @param propertyId + * a property ID, corresponding to a property in the data source + * @param direction + * a sort direction value + */ + private Sort(Object propertyId, SortDirection direction) { + previous = null; + order = new SortOrder(propertyId, direction); + } + + /** + * Chaining constructor, called by the non-static then() methods. This + * constructor links to the previous Sort object. + * + * @param previous + * the sort marker that comes before this one + * @param propertyId + * a property ID, corresponding to a property in the data source + * @param direction + * a sort direction value + */ + private Sort(Sort previous, Object propertyId, SortDirection direction) { + this.previous = previous; + order = new SortOrder(propertyId, direction); + + Sort s = previous; + while (s != null) { + if (s.order.getPropertyId() == propertyId) { + throw new IllegalStateException( + "Can not sort along the same property (" + propertyId + + ") twice!"); + } + s = s.previous; + } + + } + + /** + * Start building a Sort order by sorting a provided column in ascending + * order. + * + * @param propertyId + * a property id, corresponding to a data source property + * @return a sort object + */ + public static Sort by(Object propertyId) { + return by(propertyId, SortDirection.ASCENDING); + } + + /** + * Start building a Sort order by sorting a provided column. + * + * @param propertyId + * a property id, corresponding to a data source property + * @param direction + * a sort direction value + * @return a sort object + */ + public static Sort by(Object propertyId, SortDirection direction) { + return new Sort(propertyId, direction); + } + + /** + * Continue building a Sort order. The provided property is sorted in + * ascending order if the previously added properties have been evaluated as + * equals. + * + * @param propertyId + * a property id, corresponding to a data source property + * @return a sort object + */ + public Sort then(Object propertyId) { + return then(propertyId, SortDirection.ASCENDING); + } + + /** + * Continue building a Sort order. The provided property is sorted in + * specified order if the previously added properties have been evaluated as + * equals. + * + * @param propertyId + * a property id, corresponding to a data source property + * @param direction + * a sort direction value + * @return a sort object + */ + public Sort then(Object propertyId, SortDirection direction) { + return new Sort(this, propertyId, direction); + } + + /** + * Build a sort order list, ready to be passed to Grid + * + * @return a sort order list. + */ + public List<SortOrder> build() { + + int count = 1; + Sort s = this; + while (s.previous != null) { + s = s.previous; + ++count; + } + + List<SortOrder> order = new ArrayList<SortOrder>(count); + + s = this; + do { + order.add(0, s.order); + s = s.previous; + } while (s != null); + + return order; + } +} diff --git a/server/src/com/vaadin/data/sort/SortOrder.java b/server/src/com/vaadin/data/sort/SortOrder.java new file mode 100644 index 0000000000..55cae8c51d --- /dev/null +++ b/server/src/com/vaadin/data/sort/SortOrder.java @@ -0,0 +1,106 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.data.sort; + +import java.io.Serializable; + +import com.vaadin.shared.ui.grid.SortDirection; + +/** + * Sort order descriptor. Links together a {@link SortDirection} value and a + * Vaadin container property ID. + * + * @since 7.4 + * @author Vaadin Ltd + */ +public class SortOrder implements Serializable { + + private final Object propertyId; + private final SortDirection direction; + + /** + * Create a SortOrder object. Both arguments must be non-null. + * + * @param propertyId + * id of the data source property to sort by + * @param direction + * value indicating whether the property id should be sorted in + * ascending or descending order + */ + public SortOrder(Object propertyId, SortDirection direction) { + if (propertyId == null) { + throw new IllegalArgumentException("Property ID can not be null!"); + } + if (direction == null) { + throw new IllegalArgumentException( + "Direction value can not be null!"); + } + this.propertyId = propertyId; + this.direction = direction; + } + + /** + * Returns the property ID. + * + * @return a property ID + */ + public Object getPropertyId() { + return propertyId; + } + + /** + * Returns the {@link SortDirection} value. + * + * @return a sort direction value + */ + public SortDirection getDirection() { + return direction; + } + + @Override + public String toString() { + return propertyId + " " + direction; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + direction.hashCode(); + result = prime * result + propertyId.hashCode(); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } else if (obj == null) { + return false; + } else if (getClass() != obj.getClass()) { + return false; + } + + SortOrder other = (SortOrder) obj; + if (direction != other.direction) { + return false; + } else if (!propertyId.equals(other.propertyId)) { + return false; + } + return true; + } + +} diff --git a/server/src/com/vaadin/data/util/AbstractBeanContainer.java b/server/src/com/vaadin/data/util/AbstractBeanContainer.java index fad0934e53..6dcfbb2b84 100644 --- a/server/src/com/vaadin/data/util/AbstractBeanContainer.java +++ b/server/src/com/vaadin/data/util/AbstractBeanContainer.java @@ -226,6 +226,7 @@ public abstract class AbstractBeanContainer<IDTYPE, BEANTYPE> extends @Override public boolean removeAllItems() { int origSize = size(); + IDTYPE firstItem = getFirstVisibleItem(); internalRemoveAllItems(); @@ -238,7 +239,7 @@ public abstract class AbstractBeanContainer<IDTYPE, BEANTYPE> extends // fire event only if the visible view changed, regardless of whether // filtered out items were removed or not if (origSize != 0) { - fireItemSetChange(); + fireItemsRemoved(0, firstItem, origSize); } return true; @@ -683,6 +684,8 @@ public abstract class AbstractBeanContainer<IDTYPE, BEANTYPE> extends protected void addAll(Collection<? extends BEANTYPE> collection) throws IllegalStateException, IllegalArgumentException { boolean modified = false; + int origSize = size(); + for (BEANTYPE bean : collection) { // TODO skipping invalid beans - should not allow them in javadoc? if (bean == null @@ -703,13 +706,22 @@ public abstract class AbstractBeanContainer<IDTYPE, BEANTYPE> extends if (modified) { // Filter the contents when all items have been added if (isFiltered()) { - filterAll(); - } else { - fireItemSetChange(); + doFilterContainer(!getFilters().isEmpty()); + } + if (visibleNewItemsWasAdded(origSize)) { + // fire event about added items + int firstPosition = origSize; + IDTYPE firstItemId = getVisibleItemIds().get(firstPosition); + int affectedItems = size() - origSize; + fireItemsAdded(firstPosition, firstItemId, affectedItems); } } } + private boolean visibleNewItemsWasAdded(int origSize) { + return size() > origSize; + } + /** * Use the bean resolver to get the identifier for a bean. * diff --git a/server/src/com/vaadin/data/util/AbstractInMemoryContainer.java b/server/src/com/vaadin/data/util/AbstractInMemoryContainer.java index b19cddb021..5ddc11ec6f 100644 --- a/server/src/com/vaadin/data/util/AbstractInMemoryContainer.java +++ b/server/src/com/vaadin/data/util/AbstractInMemoryContainer.java @@ -15,8 +15,10 @@ */ package com.vaadin.data.util; +import java.io.Serializable; import java.util.Collection; import java.util.Collections; +import java.util.EventObject; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; @@ -146,6 +148,85 @@ public abstract class AbstractInMemoryContainer<ITEMIDTYPE, PROPERTYIDCLASS, ITE } } + private static abstract class BaseItemAddOrRemoveEvent extends + EventObject implements Serializable { + protected Object itemId; + protected int index; + protected int count; + + public BaseItemAddOrRemoveEvent(Container source, Object itemId, + int index, int count) { + super(source); + this.itemId = itemId; + this.index = index; + this.count = count; + } + + public Container getContainer() { + return (Container) getSource(); + } + + public Object getFirstItemId() { + return itemId; + } + + public int getFirstIndex() { + return index; + } + + public int getAffectedItemsCount() { + return count; + } + } + + /** + * An <code>Event</code> object specifying information about the added + * items. + * + * <p> + * This class provides information about the first added item and the number + * of added items. + * </p> + */ + protected static class BaseItemAddEvent extends + BaseItemAddOrRemoveEvent implements + Container.Indexed.ItemAddEvent { + + public BaseItemAddEvent(Container source, Object itemId, int index, + int count) { + super(source, itemId, index, count); + } + + @Override + public int getAddedItemsCount() { + return getAffectedItemsCount(); + } + } + + /** + * An <code>Event</code> object specifying information about the removed + * items. + * + * <p> + * This class provides information about the first removed item and the + * number of removed items. + * </p> + */ + protected static class BaseItemRemoveEvent extends + BaseItemAddOrRemoveEvent implements + Container.Indexed.ItemRemoveEvent { + + public BaseItemRemoveEvent(Container source, Object itemId, + int index, int count) { + super(source, itemId, index, count); + } + + @Override + public int getRemovedItemsCount() { + return getAffectedItemsCount(); + } + } + /** * Get an item even if filtered out. * @@ -898,36 +979,69 @@ public abstract class AbstractInMemoryContainer<ITEMIDTYPE, PROPERTYIDCLASS, ITE * Notify item set change listeners that an item has been added to the * container. * - * Unless subclasses specify otherwise, the default notification indicates a - * full refresh. - * * @param postion - * position of the added item in the view (if visible) + * position of the added item in the view * @param itemId * id of the added item * @param item * the added item */ protected void fireItemAdded(int position, ITEMIDTYPE itemId, ITEMCLASS item) { - fireItemSetChange(); + fireItemsAdded(position, itemId, 1); + } + + /** + * Notify item set change listeners that items has been added to the + * container. + * + * @param firstPosition + * position of the first visible added item in the view + * @param firstItemId + * id of the first visible added item + * @param numberOfItems + * the number of visible added items + */ + protected void fireItemsAdded(int firstPosition, ITEMIDTYPE firstItemId, + int numberOfItems) { + BaseItemAddEvent addEvent = new BaseItemAddEvent(this, + firstItemId, firstPosition, numberOfItems); + fireItemSetChange(addEvent); } /** * Notify item set change listeners that an item has been removed from the * container. * - * Unless subclasses specify otherwise, the default notification indicates a - * full refresh. + * @param position + * position of the removed item in the view prior to removal * - * @param postion - * position of the removed item in the view prior to removal (if - * was visible) * @param itemId * id of the removed item, of type {@link Object} to satisfy * {@link Container#removeItem(Object)} API */ protected void fireItemRemoved(int position, Object itemId) { - fireItemSetChange(); + fireItemsRemoved(position, itemId, 1); + } + + /** + * Notify item set change listeners that items has been removed from the + * container. + * + * @param firstPosition + * position of the first visible removed item in the view prior + * to removal + * @param firstItemId + * id of the first visible removed item, of type {@link Object} + * to satisfy {@link Container#removeItem(Object)} API + * @param numberOfItems + * the number of removed visible items + * + */ + protected void fireItemsRemoved(int firstPosition, Object firstItemId, + int numberOfItems) { + BaseItemRemoveEvent removeEvent = new BaseItemRemoveEvent(this, + firstItemId, firstPosition, numberOfItems); + fireItemSetChange(removeEvent); } // visible and filtered item identifier lists @@ -946,6 +1060,21 @@ public abstract class AbstractInMemoryContainer<ITEMIDTYPE, PROPERTYIDCLASS, ITE } /** + * Returns the item id of the first visible item after filtering. 'Null' is + * returned if there is no visible items. + * + * For internal use only. + * + * @return item id of the first visible item + */ + protected ITEMIDTYPE getFirstVisibleItem() { + if (!getVisibleItemIds().isEmpty()) { + return getVisibleItemIds().get(0); + } + return null; + } + + /** * Returns true is the container has active filters. * * @return true if the container is currently filtered diff --git a/server/src/com/vaadin/data/util/GeneratedPropertyContainer.java b/server/src/com/vaadin/data/util/GeneratedPropertyContainer.java new file mode 100644 index 0000000000..17472807b5 --- /dev/null +++ b/server/src/com/vaadin/data/util/GeneratedPropertyContainer.java @@ -0,0 +1,715 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.data.util; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +import com.google.gwt.thirdparty.guava.common.collect.Sets; +import com.vaadin.data.Container; +import com.vaadin.data.Item; +import com.vaadin.data.Property; +import com.vaadin.data.sort.SortOrder; +import com.vaadin.data.util.filter.UnsupportedFilterException; +import com.vaadin.shared.ui.grid.SortDirection; + +/** + * Container wrapper that adds support for generated properties. This container + * only supports adding new generated properties. Adding new normal properties + * should be done for the wrapped container. + * + * <p> + * Removing properties from this container does not remove anything from the + * wrapped container but instead only hides them from the results. These + * properties can be returned to this container by calling + * {@link #addContainerProperty(Object, Class, Object)} with same property id + * which was removed. + * + * <p> + * If wrapped container is Filterable and/or Sortable it should only be handled + * through this container as generated properties need to be handled in a + * specific way when sorting/filtering. + * + * <p> + * Items returned by this container do not support adding or removing + * properties. Generated properties are always read-only. Trying to make them + * editable throws an exception. + * + * @since + * @author Vaadin Ltd + */ +public class GeneratedPropertyContainer extends AbstractContainer implements + Container.Indexed, Container.Sortable, Container.Filterable, + Container.PropertySetChangeNotifier, Container.ItemSetChangeNotifier { + + private final Container.Indexed wrappedContainer; + private final Map<Object, PropertyValueGenerator<?>> propertyGenerators; + private final Map<Filter, List<Filter>> activeFilters; + private Sortable sortableContainer = null; + private Filterable filterableContainer = null; + + /* Removed properties which are hidden but not actually removed */ + private final Set<Object> removedProperties = new HashSet<Object>(); + + /** + * Property implementation for generated properties + */ + protected static class GeneratedProperty<T> implements Property<T> { + + private Item item; + private Object itemId; + private Object propertyId; + private PropertyValueGenerator<T> generator; + + public GeneratedProperty(Item item, Object propertyId, Object itemId, + PropertyValueGenerator<T> generator) { + this.item = item; + this.itemId = itemId; + this.propertyId = propertyId; + this.generator = generator; + } + + @Override + public T getValue() { + return generator.getValue(item, itemId, propertyId); + } + + @Override + public void setValue(T newValue) throws ReadOnlyException { + throw new ReadOnlyException("Generated properties are read only"); + } + + @Override + public Class<? extends T> getType() { + return generator.getType(); + } + + @Override + public boolean isReadOnly() { + return true; + } + + @Override + public void setReadOnly(boolean newStatus) { + if (newStatus) { + // No-op + return; + } + throw new UnsupportedOperationException( + "Generated properties are read only"); + } + } + + /** + * Item implementation for generated properties. + */ + protected class GeneratedPropertyItem implements Item { + + private Item wrappedItem; + private Object itemId; + + protected GeneratedPropertyItem(Object itemId, Item item) { + this.itemId = itemId; + wrappedItem = item; + } + + @Override + public Property getItemProperty(Object id) { + if (propertyGenerators.containsKey(id)) { + return createProperty(wrappedItem, id, itemId, + propertyGenerators.get(id)); + } + return wrappedItem.getItemProperty(id); + } + + @Override + public Collection<?> getItemPropertyIds() { + Set<?> wrappedProperties = asSet(wrappedItem.getItemPropertyIds()); + return Sets.union( + Sets.difference(wrappedProperties, removedProperties), + propertyGenerators.keySet()); + } + + @Override + public boolean addItemProperty(Object id, Property property) + throws UnsupportedOperationException { + throw new UnsupportedOperationException( + "GeneratedPropertyItem does not support adding properties"); + } + + @Override + public boolean removeItemProperty(Object id) + throws UnsupportedOperationException { + throw new UnsupportedOperationException( + "GeneratedPropertyItem does not support removing properties"); + } + }; + + /** + * Base implementation for item add or remove events. This is used when an + * event is fired from wrapped container and needs to be reconstructed to + * act like it actually came from this container. + */ + protected abstract class GeneratedItemAddOrRemoveEvent implements + Serializable { + + private Object firstItemId; + private int firstIndex; + private int count; + + protected GeneratedItemAddOrRemoveEvent(Object itemId, int first, + int count) { + firstItemId = itemId; + firstIndex = first; + this.count = count; + } + + public Container getContainer() { + return GeneratedPropertyContainer.this; + } + + public Object getFirstItemId() { + return firstItemId; + } + + public int getFirstIndex() { + return firstIndex; + } + + public int getAffectedItemsCount() { + return count; + } + }; + + protected class GeneratedItemRemoveEvent extends + GeneratedItemAddOrRemoveEvent implements ItemRemoveEvent { + + protected GeneratedItemRemoveEvent(ItemRemoveEvent event) { + super(event.getFirstItemId(), event.getFirstIndex(), event + .getRemovedItemsCount()); + } + + @Override + public int getRemovedItemsCount() { + return super.getAffectedItemsCount(); + } + } + + protected class GeneratedItemAddEvent extends GeneratedItemAddOrRemoveEvent + implements ItemAddEvent { + + protected GeneratedItemAddEvent(ItemAddEvent event) { + super(event.getFirstItemId(), event.getFirstIndex(), event + .getAddedItemsCount()); + } + + @Override + public int getAddedItemsCount() { + return super.getAffectedItemsCount(); + } + + } + + /** + * Constructor for GeneratedPropertyContainer. + * + * @param container + * underlying indexed container + */ + public GeneratedPropertyContainer(Container.Indexed container) { + wrappedContainer = container; + propertyGenerators = new HashMap<Object, PropertyValueGenerator<?>>(); + + if (wrappedContainer instanceof Sortable) { + sortableContainer = (Sortable) wrappedContainer; + } + + if (wrappedContainer instanceof Filterable) { + activeFilters = new HashMap<Filter, List<Filter>>(); + filterableContainer = (Filterable) wrappedContainer; + } else { + activeFilters = null; + } + + // ItemSetChangeEvents + if (wrappedContainer instanceof ItemSetChangeNotifier) { + ((ItemSetChangeNotifier) wrappedContainer) + .addItemSetChangeListener(new ItemSetChangeListener() { + + @Override + public void containerItemSetChange( + ItemSetChangeEvent event) { + if (event instanceof ItemAddEvent) { + final ItemAddEvent addEvent = (ItemAddEvent) event; + fireItemSetChange(new GeneratedItemAddEvent( + addEvent)); + } else if (event instanceof ItemRemoveEvent) { + final ItemRemoveEvent removeEvent = (ItemRemoveEvent) event; + fireItemSetChange(new GeneratedItemRemoveEvent( + removeEvent)); + } else { + fireItemSetChange(); + } + } + }); + } + + // PropertySetChangeEvents + if (wrappedContainer instanceof PropertySetChangeNotifier) { + ((PropertySetChangeNotifier) wrappedContainer) + .addPropertySetChangeListener(new PropertySetChangeListener() { + + @Override + public void containerPropertySetChange( + PropertySetChangeEvent event) { + fireContainerPropertySetChange(); + } + }); + } + } + + /* Functions related to generated properties */ + + /** + * Add a new PropertyValueGenerator with given property id. This will + * override any existing properties with the same property id. Fires a + * PropertySetChangeEvent. + * + * @param propertyId + * property id + * @param generator + * a property value generator + */ + public void addGeneratedProperty(Object propertyId, + PropertyValueGenerator<?> generator) { + propertyGenerators.put(propertyId, generator); + fireContainerPropertySetChange(); + } + + /** + * Removes any possible PropertyValueGenerator with given property id. Fires + * a PropertySetChangeEvent. + * + * @param propertyId + * property id + */ + public void removeGeneratedProperty(Object propertyId) { + if (propertyGenerators.containsKey(propertyId)) { + propertyGenerators.remove(propertyId); + fireContainerPropertySetChange(); + } + } + + private Item createGeneratedPropertyItem(final Object itemId, + final Item item) { + return new GeneratedPropertyItem(itemId, item); + } + + private <T> Property<T> createProperty(final Item item, + final Object propertyId, final Object itemId, + final PropertyValueGenerator<T> generator) { + return new GeneratedProperty<T>(item, propertyId, itemId, generator); + } + + private static <T> LinkedHashSet<T> asSet(Collection<T> collection) { + if (collection instanceof LinkedHashSet) { + return (LinkedHashSet<T>) collection; + } else { + return new LinkedHashSet<T>(collection); + } + } + + /* Listener functionality */ + + @Override + public void addItemSetChangeListener(ItemSetChangeListener listener) { + super.addItemSetChangeListener(listener); + } + + @Override + public void addListener(ItemSetChangeListener listener) { + super.addListener(listener); + } + + @Override + public void removeItemSetChangeListener(ItemSetChangeListener listener) { + super.removeItemSetChangeListener(listener); + } + + @Override + public void removeListener(ItemSetChangeListener listener) { + super.removeListener(listener); + } + + @Override + public void addPropertySetChangeListener(PropertySetChangeListener listener) { + super.addPropertySetChangeListener(listener); + } + + @Override + public void addListener(PropertySetChangeListener listener) { + super.addListener(listener); + } + + @Override + public void removePropertySetChangeListener( + PropertySetChangeListener listener) { + super.removePropertySetChangeListener(listener); + } + + @Override + public void removeListener(PropertySetChangeListener listener) { + super.removeListener(listener); + } + + /* Filtering functionality */ + + @Override + public void addContainerFilter(Filter filter) + throws UnsupportedFilterException { + if (filterableContainer == null) { + throw new UnsupportedOperationException( + "Wrapped container is not filterable"); + } + + List<Filter> addedFilters = new ArrayList<Filter>(); + for (Entry<?, PropertyValueGenerator<?>> entry : propertyGenerators + .entrySet()) { + Object property = entry.getKey(); + if (filter.appliesToProperty(property)) { + // Have generated property modify filter to fit the original + // data in the container. + Filter modifiedFilter = entry.getValue().modifyFilter(filter); + filterableContainer.addContainerFilter(modifiedFilter); + // Keep track of added filters + addedFilters.add(modifiedFilter); + } + } + + if (addedFilters.isEmpty()) { + // No generated property modified this filter, use it as is + addedFilters.add(filter); + filterableContainer.addContainerFilter(filter); + } + // Map filter to actually added filters + activeFilters.put(filter, addedFilters); + } + + @Override + public void removeContainerFilter(Filter filter) { + if (filterableContainer == null) { + throw new UnsupportedOperationException( + "Wrapped container is not filterable"); + } + + if (activeFilters.containsKey(filter)) { + for (Filter f : activeFilters.get(filter)) { + filterableContainer.removeContainerFilter(f); + } + activeFilters.remove(filter); + } + } + + @Override + public void removeAllContainerFilters() { + if (filterableContainer == null) { + throw new UnsupportedOperationException( + "Wrapped container is not filterable"); + } + filterableContainer.removeAllContainerFilters(); + activeFilters.clear(); + } + + @Override + public Collection<Filter> getContainerFilters() { + if (filterableContainer == null) { + throw new UnsupportedOperationException( + "Wrapped container is not filterable"); + } + return Collections.unmodifiableSet(activeFilters.keySet()); + } + + /* Sorting functionality */ + + @Override + public void sort(Object[] propertyId, boolean[] ascending) { + if (sortableContainer == null) { + new UnsupportedOperationException( + "Wrapped container is not Sortable"); + } + + if (propertyId.length == 0) { + sortableContainer.sort(propertyId, ascending); + return; + } + + List<Object> actualSortProperties = new ArrayList<Object>(); + List<Boolean> actualSortDirections = new ArrayList<Boolean>(); + + for (int i = 0; i < propertyId.length; ++i) { + Object property = propertyId[i]; + SortDirection direction; + boolean isAscending = i < ascending.length ? ascending[i] : true; + if (isAscending) { + direction = SortDirection.ASCENDING; + } else { + direction = SortDirection.DESCENDING; + } + + if (propertyGenerators.containsKey(property)) { + // Sorting by a generated property. Generated property should + // modify sort orders to work with original properties in the + // container. + for (SortOrder s : propertyGenerators.get(property) + .getSortProperties(new SortOrder(property, direction))) { + actualSortProperties.add(s.getPropertyId()); + actualSortDirections + .add(s.getDirection() == SortDirection.ASCENDING); + } + } else { + actualSortProperties.add(property); + actualSortDirections.add(isAscending); + } + } + + boolean[] actualAscending = new boolean[actualSortDirections.size()]; + for (int i = 0; i < actualAscending.length; ++i) { + actualAscending[i] = actualSortDirections.get(i); + } + + sortableContainer.sort(actualSortProperties.toArray(), actualAscending); + } + + @Override + public Collection<?> getSortableContainerPropertyIds() { + if (sortableContainer == null) { + new UnsupportedOperationException( + "Wrapped container is not Sortable"); + } + + Set<Object> sortablePropertySet = new HashSet<Object>( + sortableContainer.getSortableContainerPropertyIds()); + for (Entry<?, PropertyValueGenerator<?>> entry : propertyGenerators + .entrySet()) { + Object property = entry.getKey(); + SortOrder order = new SortOrder(property, SortDirection.ASCENDING); + if (entry.getValue().getSortProperties(order).length > 0) { + sortablePropertySet.add(property); + } else { + sortablePropertySet.remove(property); + } + } + + return sortablePropertySet; + } + + /* Item related overrides */ + + @Override + public Item addItemAfter(Object previousItemId, Object newItemId) + throws UnsupportedOperationException { + Item item = wrappedContainer.addItemAfter(previousItemId, newItemId); + return createGeneratedPropertyItem(newItemId, item); + } + + @Override + public Item addItem(Object itemId) throws UnsupportedOperationException { + Item item = wrappedContainer.addItem(itemId); + return createGeneratedPropertyItem(itemId, item); + } + + @Override + public Item addItemAt(int index, Object newItemId) + throws UnsupportedOperationException { + Item item = wrappedContainer.addItemAt(index, newItemId); + return createGeneratedPropertyItem(newItemId, item); + } + + @Override + public Item getItem(Object itemId) { + Item item = wrappedContainer.getItem(itemId); + return createGeneratedPropertyItem(itemId, item); + } + + /* Property related overrides */ + + @Override + public Property<?> getContainerProperty(Object itemId, Object propertyId) { + if (propertyGenerators.keySet().contains(propertyId)) { + return getItem(itemId).getItemProperty(propertyId); + } else if (!removedProperties.contains(propertyId)) { + return wrappedContainer.getContainerProperty(itemId, propertyId); + } + return null; + } + + /** + * Returns a list of propety ids available in this container. This + * collection will contain properties for generated properties. Removed + * properties will not show unless there is a generated property overriding + * those. + */ + @Override + public Collection<?> getContainerPropertyIds() { + Set<?> wrappedProperties = asSet(wrappedContainer + .getContainerPropertyIds()); + return Sets.union( + Sets.difference(wrappedProperties, removedProperties), + propertyGenerators.keySet()); + } + + /** + * Adds a previously removed property back to GeneratedPropertyContainer. + * Adding a property that is not previously removed causes an + * UnsupportedOperationException. + */ + @Override + public boolean addContainerProperty(Object propertyId, Class<?> type, + Object defaultValue) throws UnsupportedOperationException { + if (!removedProperties.contains(propertyId)) { + throw new UnsupportedOperationException( + "GeneratedPropertyContainer does not support adding properties."); + } + removedProperties.remove(propertyId); + fireContainerPropertySetChange(); + return true; + } + + /** + * Marks the given property as hidden. This property from wrapped container + * will be removed from {@link #getContainerPropertyIds()} and is no longer + * be available in Items retrieved from this container. + */ + @Override + public boolean removeContainerProperty(Object propertyId) + throws UnsupportedOperationException { + if (wrappedContainer.getContainerPropertyIds().contains(propertyId) + && removedProperties.add(propertyId)) { + fireContainerPropertySetChange(); + return true; + } + return false; + } + + /* Type related overrides */ + + @Override + public Class<?> getType(Object propertyId) { + if (propertyGenerators.containsKey(propertyId)) { + return propertyGenerators.get(propertyId).getType(); + } else { + return wrappedContainer.getType(propertyId); + } + } + + /* Unmodified functions */ + + @Override + public Object nextItemId(Object itemId) { + return wrappedContainer.nextItemId(itemId); + } + + @Override + public Object prevItemId(Object itemId) { + return wrappedContainer.prevItemId(itemId); + } + + @Override + public Object firstItemId() { + return wrappedContainer.firstItemId(); + } + + @Override + public Object lastItemId() { + return wrappedContainer.lastItemId(); + } + + @Override + public boolean isFirstId(Object itemId) { + return wrappedContainer.isFirstId(itemId); + } + + @Override + public boolean isLastId(Object itemId) { + return wrappedContainer.isLastId(itemId); + } + + @Override + public Object addItemAfter(Object previousItemId) + throws UnsupportedOperationException { + return wrappedContainer.addItemAfter(previousItemId); + } + + @Override + public Collection<?> getItemIds() { + return wrappedContainer.getItemIds(); + } + + @Override + public int size() { + return wrappedContainer.size(); + } + + @Override + public boolean containsId(Object itemId) { + return wrappedContainer.containsId(itemId); + } + + @Override + public Object addItem() throws UnsupportedOperationException { + return wrappedContainer.addItem(); + } + + @Override + public boolean removeItem(Object itemId) + throws UnsupportedOperationException { + return wrappedContainer.removeItem(itemId); + } + + @Override + public boolean removeAllItems() throws UnsupportedOperationException { + return wrappedContainer.removeAllItems(); + } + + @Override + public int indexOfId(Object itemId) { + return wrappedContainer.indexOfId(itemId); + } + + @Override + public Object getIdByIndex(int index) { + return wrappedContainer.getIdByIndex(index); + } + + @Override + public List<?> getItemIds(int startIndex, int numberOfItems) { + return wrappedContainer.getItemIds(startIndex, numberOfItems); + } + + @Override + public Object addItemAt(int index) throws UnsupportedOperationException { + return wrappedContainer.addItemAt(index); + } +} diff --git a/server/src/com/vaadin/data/util/IndexedContainer.java b/server/src/com/vaadin/data/util/IndexedContainer.java index 68960335d7..b851baf674 100644 --- a/server/src/com/vaadin/data/util/IndexedContainer.java +++ b/server/src/com/vaadin/data/util/IndexedContainer.java @@ -226,6 +226,7 @@ public class IndexedContainer extends @Override public boolean removeAllItems() { int origSize = size(); + Object firstItem = getFirstVisibleItem(); internalRemoveAllItems(); @@ -235,16 +236,17 @@ public class IndexedContainer extends // filtered out items were removed or not if (origSize != 0) { // Sends a change event - fireItemSetChange(); + fireItemsRemoved(0, firstItem, origSize); } return true; } - /* - * (non-Javadoc) - * - * @see com.vaadin.data.Container#addItem() + /** + * {@inheritDoc} + * <p> + * The item ID is generated from a sequence of Integers. The id of the first + * added item is 1. */ @Override public Object addItem() { @@ -362,10 +364,11 @@ public class IndexedContainer extends new IndexedContainerItem(newItemId), true); } - /* - * (non-Javadoc) - * - * @see com.vaadin.data.Container.Ordered#addItemAfter(java.lang.Object) + /** + * {@inheritDoc} + * <p> + * The item ID is generated from a sequence of Integers. The id of the first + * added item is 1. */ @Override public Object addItemAfter(Object previousItemId) { @@ -391,10 +394,11 @@ public class IndexedContainer extends newItemId), true); } - /* - * (non-Javadoc) - * - * @see com.vaadin.data.Container.Indexed#addItemAt(int) + /** + * {@inheritDoc} + * <p> + * The item ID is generated from a sequence of Integers. The id of the first + * added item is 1. */ @Override public Object addItemAt(int index) { @@ -620,8 +624,7 @@ public class IndexedContainer extends @Override protected void fireItemAdded(int position, Object itemId, Item item) { if (position >= 0) { - fireItemSetChange(new IndexedContainer.ItemSetChangeEvent(this, - position)); + super.fireItemAdded(position, itemId, item); } } @@ -1211,4 +1214,5 @@ public class IndexedContainer extends public Collection<Filter> getContainerFilters() { return super.getContainerFilters(); } + } diff --git a/server/src/com/vaadin/data/util/PropertyValueGenerator.java b/server/src/com/vaadin/data/util/PropertyValueGenerator.java new file mode 100644 index 0000000000..c535803ee1 --- /dev/null +++ b/server/src/com/vaadin/data/util/PropertyValueGenerator.java @@ -0,0 +1,100 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.data.util; + +import java.io.Serializable; + +import com.vaadin.data.Container.Filter; +import com.vaadin.data.Item; +import com.vaadin.data.Property; +import com.vaadin.data.sort.SortOrder; +import com.vaadin.data.util.filter.UnsupportedFilterException; + +/** + * PropertyValueGenerator for GeneratedPropertyContainer. + * + * @param <T> + * Property data type + * @since + * @author Vaadin Ltd + */ +public abstract class PropertyValueGenerator<T> implements Serializable { + + /** + * Returns value for given Item. Used by GeneratedPropertyContainer when + * generating new properties. + * + * @param item + * currently handled item + * @param itemId + * item id for currently handled item + * @param propertyId + * id for this property + * @return generated value + */ + public abstract T getValue(Item item, Object itemId, Object propertyId); + + /** + * Return Property type for this generator. This function is called when + * {@link Property#getType()} is called for generated property. + * + * @return type of generated property + */ + public abstract Class<T> getType(); + + /** + * Translates sorting of the generated property in a specific direction to a + * set of property ids and directions in the underlying container. + * + * SortOrder is similar to (or the same as) the SortOrder already defined + * for Grid. + * + * The default implementation of this method returns an empty array, which + * means that the property will not be included in + * getSortableContainerPropertyIds(). Attempting to sort by that column + * throws UnsupportedOperationException. + * + * Returning null is not allowed. + * + * @param order + * a sort order for this property + * @return an array of sort orders describing how this property is sorted + */ + public SortOrder[] getSortProperties(SortOrder order) { + return new SortOrder[] {}; + } + + /** + * Return an updated filter that should be compatible with the underlying + * container. + * + * This function is called when setting a filter for this generated + * property. Returning null from this function causes + * GeneratedPropertyContainer to discard the filter and not use it. + * + * By default this function throws UnsupportedFilterException. + * + * @param filter + * original filter for this property + * @return modified filter that is compatible with the underlying container + * @throws UnsupportedFilterException + */ + public Filter modifyFilter(Filter filter) throws UnsupportedFilterException { + throw new UnsupportedFilterException("Filter" + filter + + " is not supported"); + } + +} diff --git a/server/src/com/vaadin/event/SelectionChangeEvent.java b/server/src/com/vaadin/event/SelectionChangeEvent.java new file mode 100644 index 0000000000..adc7b13d49 --- /dev/null +++ b/server/src/com/vaadin/event/SelectionChangeEvent.java @@ -0,0 +1,110 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.event; + +import java.io.Serializable; +import java.util.Collection; +import java.util.EventObject; +import java.util.LinkedHashSet; +import java.util.Set; + +import com.google.gwt.thirdparty.guava.common.collect.Sets; + +/** + * An event that specifies what in a selection has changed, and where the + * selection took place. + * + * @since + * @author Vaadin Ltd + */ +public class SelectionChangeEvent extends EventObject { + + private LinkedHashSet<Object> oldSelection; + private LinkedHashSet<Object> newSelection; + + public SelectionChangeEvent(Object source, Collection<Object> oldSelection, + Collection<Object> newSelection) { + super(source); + this.oldSelection = new LinkedHashSet<Object>(oldSelection); + this.newSelection = new LinkedHashSet<Object>(newSelection); + } + + /** + * A {@link Collection} of all the itemIds that became selected. + * <p> + * <em>Note:</em> this excludes all itemIds that might have been previously + * selected. + * + * @return a Collection of the itemIds that became selected + */ + public Set<Object> getAdded() { + return Sets.difference(newSelection, oldSelection); + } + + /** + * A {@link Collection} of all the itemIds that became deselected. + * <p> + * <em>Note:</em> this excludes all itemIds that might have been previously + * deselected. + * + * @return a Collection of the itemIds that became deselected + */ + public Set<Object> getRemoved() { + return Sets.difference(oldSelection, newSelection); + } + + /** + * The listener interface for receiving {@link SelectionChangeEvent + * SelectionChangeEvents}. + * + * @since + * @author Vaadin Ltd + */ + public interface SelectionChangeListener extends Serializable { + /** + * Notifies the listener that the selection state has changed. + * + * @param event + * the selection change event + */ + void selectionChange(SelectionChangeEvent event); + } + + /** + * The interface for adding and removing listeners for + * {@link SelectionChangeEvent SelectionChangeEvents}. + * + * @since + * @author Vaadin Ltd + */ + public interface SelectionChangeNotifier extends Serializable { + /** + * Registers a new selection change listener + * + * @param listener + * the listener to register + */ + void addSelectionChangeListener(SelectionChangeListener listener); + + /** + * Removes a previously registered selection change listener + * + * @param listener + * the listener to remove + */ + void removeSelectionChangeListener(SelectionChangeListener listener); + } +} diff --git a/server/src/com/vaadin/event/SortOrderChangeEvent.java b/server/src/com/vaadin/event/SortOrderChangeEvent.java new file mode 100644 index 0000000000..e38097e3ba --- /dev/null +++ b/server/src/com/vaadin/event/SortOrderChangeEvent.java @@ -0,0 +1,111 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.event; + +import java.io.Serializable; +import java.util.List; + +import com.vaadin.data.sort.SortOrder; +import com.vaadin.shared.ui.grid.SortEventOriginator; +import com.vaadin.ui.Component; + +/** + * Event describing a change in sorting of a {@link Container}. Fired by + * {@link SortOrderChangeNotifier SortOrderChangeNotifiers}. + * + * @see SortOrderChangeListener + * + * @since + * @author Vaadin Ltd + */ +public class SortOrderChangeEvent extends Component.Event { + + private final List<SortOrder> sortOrder; + private final SortEventOriginator originator; + + /** + * Creates a new sort order change event with a sort order list. + * + * @param source + * the component from which the event originates + * @param sortOrder + * the new sort order list + * @param originator + * an enumeration describing what triggered the sorting + */ + public SortOrderChangeEvent(Component source, List<SortOrder> sortOrder, + SortEventOriginator originator) { + super(source); + this.sortOrder = sortOrder; + this.originator = originator; + } + + /** + * Gets the sort order list. + * + * @return the sort order list + */ + public List<SortOrder> getSortOrder() { + return sortOrder; + } + + /** + * Returns whether this event originated from actions done by the user. + * + * @return true if sort event originated from user interaction + */ + public boolean isUserOriginated() { + return originator == SortEventOriginator.USER; + } + + /** + * Listener for sort order change events. + */ + public interface SortOrderChangeListener extends Serializable { + /** + * Called when the sort order has changed. + * + * @param event + * the sort order change event + */ + public void sortOrderChange(SortOrderChangeEvent event); + } + + /** + * The interface for adding and removing listeners for + * {@link SortOrderChangeEvent SortOrderChangeEvents}. + */ + public interface SortOrderChangeNotifier extends Serializable { + /** + * Adds a sort order change listener that gets notified when the sort + * order changes. + * + * @param listener + * the sort order change listener to add + */ + public void addSortOrderChangeListener(SortOrderChangeListener listener); + + /** + * Removes a sort order change listener previously added using + * {@link #addSortOrderChangeListener(SortOrderChangeListener)}. + * + * @param listener + * the sort order change listener to remove + */ + public void removeSortOrderChangeListener( + SortOrderChangeListener listener); + } +} diff --git a/server/src/com/vaadin/ui/DefaultFieldFactory.java b/server/src/com/vaadin/ui/DefaultFieldFactory.java index ad6461686c..535943bcd5 100644 --- a/server/src/com/vaadin/ui/DefaultFieldFactory.java +++ b/server/src/com/vaadin/ui/DefaultFieldFactory.java @@ -20,6 +20,7 @@ import java.util.Date; import com.vaadin.data.Container; import com.vaadin.data.Item; import com.vaadin.data.Property; +import com.vaadin.shared.util.SharedUtil; /** * This class contains a basic implementation for both {@link FormFieldFactory} @@ -75,42 +76,7 @@ public class DefaultFieldFactory implements FormFieldFactory, TableFieldFactory * @return the formatted caption string */ public static String createCaptionByPropertyId(Object propertyId) { - String name = propertyId.toString(); - if (name.length() > 0) { - - int dotLocation = name.lastIndexOf('.'); - if (dotLocation > 0 && dotLocation < name.length() - 1) { - name = name.substring(dotLocation + 1); - } - if (name.indexOf(' ') < 0 - && name.charAt(0) == Character.toLowerCase(name.charAt(0)) - && name.charAt(0) != Character.toUpperCase(name.charAt(0))) { - StringBuffer out = new StringBuffer(); - out.append(Character.toUpperCase(name.charAt(0))); - int i = 1; - - while (i < name.length()) { - int j = i; - for (; j < name.length(); j++) { - char c = name.charAt(j); - if (Character.toLowerCase(c) != c - && Character.toUpperCase(c) == c) { - break; - } - } - if (j == name.length()) { - out.append(name.substring(i)); - } else { - out.append(name.substring(i, j)); - out.append(" " + name.charAt(j)); - } - i = j + 1; - } - - name = out.toString(); - } - } - return name; + return SharedUtil.propertyIdToHumanFriendly(propertyId); } /** diff --git a/server/src/com/vaadin/ui/Grid.java b/server/src/com/vaadin/ui/Grid.java new file mode 100644 index 0000000000..d0485c3d68 --- /dev/null +++ b/server/src/com/vaadin/ui/Grid.java @@ -0,0 +1,4152 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.vaadin.ui; + +import java.io.Serializable; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; + +import com.google.gwt.thirdparty.guava.common.collect.Sets; +import com.google.gwt.thirdparty.guava.common.collect.Sets.SetView; +import com.vaadin.data.Container; +import com.vaadin.data.Container.Indexed; +import com.vaadin.data.Container.PropertySetChangeEvent; +import com.vaadin.data.Container.PropertySetChangeListener; +import com.vaadin.data.Container.PropertySetChangeNotifier; +import com.vaadin.data.Container.Sortable; +import com.vaadin.data.Item; +import com.vaadin.data.Property; +import com.vaadin.data.RpcDataProviderExtension; +import com.vaadin.data.RpcDataProviderExtension.DataProviderKeyMapper; +import com.vaadin.data.fieldgroup.FieldGroup; +import com.vaadin.data.fieldgroup.FieldGroup.BindException; +import com.vaadin.data.fieldgroup.FieldGroup.CommitException; +import com.vaadin.data.fieldgroup.FieldGroupFieldFactory; +import com.vaadin.data.sort.Sort; +import com.vaadin.data.sort.SortOrder; +import com.vaadin.data.util.IndexedContainer; +import com.vaadin.data.util.converter.Converter; +import com.vaadin.data.util.converter.ConverterUtil; +import com.vaadin.event.SelectionChangeEvent; +import com.vaadin.event.SelectionChangeEvent.SelectionChangeListener; +import com.vaadin.event.SelectionChangeEvent.SelectionChangeNotifier; +import com.vaadin.event.SortOrderChangeEvent; +import com.vaadin.event.SortOrderChangeEvent.SortOrderChangeListener; +import com.vaadin.event.SortOrderChangeEvent.SortOrderChangeNotifier; +import com.vaadin.server.AbstractClientConnector; +import com.vaadin.server.AbstractExtension; +import com.vaadin.server.ErrorHandler; +import com.vaadin.server.ErrorMessage; +import com.vaadin.server.JsonCodec; +import com.vaadin.server.KeyMapper; +import com.vaadin.server.VaadinSession; +import com.vaadin.shared.ui.grid.EditorRowClientRpc; +import com.vaadin.shared.ui.grid.EditorRowServerRpc; +import com.vaadin.shared.ui.grid.GridClientRpc; +import com.vaadin.shared.ui.grid.GridColumnState; +import com.vaadin.shared.ui.grid.GridServerRpc; +import com.vaadin.shared.ui.grid.GridState; +import com.vaadin.shared.ui.grid.GridState.SharedSelectionMode; +import com.vaadin.shared.ui.grid.GridStaticCellType; +import com.vaadin.shared.ui.grid.GridStaticSectionState; +import com.vaadin.shared.ui.grid.GridStaticSectionState.CellState; +import com.vaadin.shared.ui.grid.GridStaticSectionState.RowState; +import com.vaadin.shared.ui.grid.HeightMode; +import com.vaadin.shared.ui.grid.ScrollDestination; +import com.vaadin.shared.ui.grid.SortDirection; +import com.vaadin.shared.ui.grid.SortEventOriginator; +import com.vaadin.shared.util.SharedUtil; +import com.vaadin.ui.renderer.Renderer; +import com.vaadin.ui.renderer.TextRenderer; +import com.vaadin.util.ReflectTools; + +import elemental.json.Json; +import elemental.json.JsonArray; +import elemental.json.JsonObject; +import elemental.json.JsonValue; + +/** + * A grid component for displaying tabular data. + * <p> + * Grid is always bound to a {@link Container.Indexed}, but is not a + * {@code Container} of any kind in of itself. The contents of the given + * Container is displayed with the help of {@link Renderer Renderers}. + * + * <h3 id="grid-headers-and-footers">Headers and Footers</h3> + * <p> + * + * + * <h3 id="grid-converters-and-renderers">Converters and Renderers</h3> + * <p> + * Each column has its own {@link Renderer} that displays data into something + * that can be displayed in the browser. That data is first converted with a + * {@link com.vaadin.data.util.converter.Converter Converter} into something + * that the Renderer can process. This can also be an implicit step - if a + * column has a simple data type, like a String, no explicit assignment is + * needed. + * <p> + * Usually a renderer takes some kind of object, and converts it into a + * HTML-formatted string. + * <p> + * <code><pre> + * Grid grid = new Grid(myContainer); + * Column column = grid.getColumn(STRING_DATE_PROPERTY); + * column.setConverter(new StringToDateConverter()); + * column.setRenderer(new MyColorfulDateRenderer()); + * </pre></code> + * + * <h3 id="grid-lazyloading">Lazy Loading</h3> + * <p> + * The data is accessed as it is needed by Grid and not any sooner. In other + * words, if the given Container is huge, but only the first few rows are + * displayed to the user, only those (and a few more, for caching purposes) are + * accessed. + * + * <h3 id="grid-selection-modes-and-models">Selection Modes and Models</h3> + * <p> + * Grid supports three selection <em>{@link SelectionMode modes}</em> (single, + * multi, none), and comes bundled with one + * <em>{@link SelectionModel model}</em> for each of the modes. The distinction + * between a selection mode and selection model is as follows: a <em>mode</em> + * essentially says whether you can have one, many or no rows selected. The + * model, however, has the behavioral details of each. A single selection model + * may require that the user deselects one row before selecting another one. A + * variant of a multiselect might have a configurable maximum of rows that may + * be selected. And so on. + * <p> + * <code><pre> + * Grid grid = new Grid(myContainer); + * + * // uses the bundled SingleSelectionModel class + * grid.setSelectionMode(SelectionMode.SINGLE); + * + * // changes the behavior to a custom selection model + * grid.setSelectionModel(new MyTwoSelectionModel()); + * </pre></code> + * + * @since + * @author Vaadin Ltd + */ +public class Grid extends AbstractComponent implements SelectionChangeNotifier, + SortOrderChangeNotifier, SelectiveRenderer { + + /** + * Selection modes representing built-in {@link SelectionModel + * SelectionModels} that come bundled with {@link Grid}. + * <p> + * Passing one of these enums into + * {@link Grid#setSelectionMode(SelectionMode)} is equivalent to calling + * {@link Grid#setSelectionModel(SelectionModel)} with one of the built-in + * implementations of {@link SelectionModel}. + * + * @see Grid#setSelectionMode(SelectionMode) + * @see Grid#setSelectionModel(SelectionModel) + */ + public enum SelectionMode { + /** A SelectionMode that maps to {@link SingleSelectionModel} */ + SINGLE { + @Override + protected SelectionModel createModel() { + return new SingleSelectionModel(); + } + + }, + + /** A SelectionMode that maps to {@link MultiSelectionModel} */ + MULTI { + @Override + protected SelectionModel createModel() { + return new MultiSelectionModel(); + } + }, + + /** A SelectionMode that maps to {@link NoSelectionModel} */ + NONE { + @Override + protected SelectionModel createModel() { + return new NoSelectionModel(); + } + }; + + protected abstract SelectionModel createModel(); + } + + /** + * The server-side interface that controls Grid's selection state. + * + * @since + * @author Vaadin Ltd + */ + public interface SelectionModel extends Serializable { + /** + * Checks whether an item is selected or not. + * + * @param itemId + * the item id to check for + * @return <code>true</code> iff the item is selected + */ + boolean isSelected(Object itemId); + + /** + * Returns a collection of all the currently selected itemIds. + * + * @return a collection of all the currently selected itemIds + */ + Collection<Object> getSelectedRows(); + + /** + * Injects the current {@link Grid} instance into the SelectionModel. + * <p> + * <em>Note:</em> This method should not be called manually. + * + * @param grid + * the Grid in which the SelectionModel currently is, or + * <code>null</code> when a selection model is being detached + * from a Grid. + */ + void setGrid(Grid grid); + + /** + * Resets the SelectiomModel to an initial state. + * <p> + * Most often this means that the selection state is cleared, but + * implementations are free to interpret the "initial state" as they + * wish. Some, for example, may want to keep the first selected item as + * selected. + */ + void reset(); + + /** + * A SelectionModel that supports multiple selections to be made. + * <p> + * This interface has a contract of having the same behavior, no matter + * how the selection model is interacted with. In other words, if + * something is forbidden to do in e.g. the user interface, it must also + * be forbidden to do in the server-side and client-side APIs. + */ + public interface Multi extends SelectionModel { + + /** + * Marks items as selected. + * <p> + * This method does not clear any previous selection state, only + * adds to it. + * + * @param itemIds + * the itemId(s) to mark as selected + * @return <code>true</code> if the selection state changed. + * <code>false</code> if all the given itemIds already were + * selected + * @throws IllegalArgumentException + * if the <code>itemIds</code> varargs array is + * <code>null</code> or given itemIds don't exist in the + * container of Grid + * @see #deselect(Object...) + */ + boolean select(Object... itemIds) throws IllegalArgumentException; + + /** + * Marks items as selected. + * <p> + * This method does not clear any previous selection state, only + * adds to it. + * + * @param itemIds + * the itemIds to mark as selected + * @return <code>true</code> if the selection state changed. + * <code>false</code> if all the given itemIds already were + * selected + * @throws IllegalArgumentException + * if <code>itemIds</code> is <code>null</code> or given + * itemIds don't exist in the container of Grid + * @see #deselect(Collection) + */ + boolean select(Collection<?> itemIds) + throws IllegalArgumentException; + + /** + * Marks items as deselected. + * + * @param itemIds + * the itemId(s) to remove from being selected + * @return <code>true</code> if the selection state changed. + * <code>false</code> if none the given itemIds were + * selected previously + * @throws IllegalArgumentException + * if the <code>itemIds</code> varargs array is + * <code>null</code> + * @see #select(Object...) + */ + boolean deselect(Object... itemIds) throws IllegalArgumentException; + + /** + * Marks items as deselected. + * + * @param itemIds + * the itemId(s) to remove from being selected + * @return <code>true</code> if the selection state changed. + * <code>false</code> if none the given itemIds were + * selected previously + * @throws IllegalArgumentException + * if <code>itemIds</code> is <code>null</code> + * @see #select(Collection) + */ + boolean deselect(Collection<?> itemIds) + throws IllegalArgumentException; + + /** + * Marks all the items in the current Container as selected + * + * @return <code>true</code> iff some items were previously not + * selected + * @see #deselectAll() + */ + boolean selectAll(); + + /** + * Marks all the items in the current Container as deselected + * + * @return <code>true</code> iff some items were previously selected + * @see #selectAll() + */ + boolean deselectAll(); + } + + /** + * A SelectionModel that supports for only single rows to be selected at + * a time. + * <p> + * This interface has a contract of having the same behavior, no matter + * how the selection model is interacted with. In other words, if + * something is forbidden to do in e.g. the user interface, it must also + * be forbidden to do in the server-side and client-side APIs. + */ + public interface Single extends SelectionModel { + + /** + * Marks an item as selected. + * + * @param itemIds + * the itemId to mark as selected; <code>null</code> for + * deselect + * @return <code>true</code> if the selection state changed. + * <code>false</code> if the itemId already was selected + * @throws IllegalStateException + * if the selection was illegal. One such reason might + * be that the given id was null, indicating a deselect, + * but implementation doesn't allow deselecting. + * re-selecting something + * @throws IllegalArgumentException + * if given itemId does not exist in the container of + * Grid + */ + boolean select(Object itemId) throws IllegalStateException, + IllegalArgumentException; + + /** + * Gets the item id of the currently selected item. + * + * @return the item id of the currently selected item, or + * <code>null</code> if nothing is selected + */ + Object getSelectedRow(); + } + + /** + * A SelectionModel that does not allow for rows to be selected. + * <p> + * This interface has a contract of having the same behavior, no matter + * how the selection model is interacted with. In other words, if the + * developer is unable to select something programmatically, it is not + * allowed for the end-user to select anything, either. + */ + public interface None extends SelectionModel { + + /** + * {@inheritDoc} + * + * @return always <code>false</code>. + */ + @Override + public boolean isSelected(Object itemId); + + /** + * {@inheritDoc} + * + * @return always an empty collection. + */ + @Override + public Collection<Object> getSelectedRows(); + } + } + + /** + * A base class for SelectionModels that contains some of the logic that is + * reusable. + * + * @since + * @author Vaadin Ltd + */ + public static abstract class AbstractSelectionModel implements + SelectionModel { + protected final LinkedHashSet<Object> selection = new LinkedHashSet<Object>(); + protected Grid grid = null; + + @Override + public boolean isSelected(final Object itemId) { + return selection.contains(itemId); + } + + @Override + public Collection<Object> getSelectedRows() { + return new ArrayList<Object>(selection); + } + + @Override + public void setGrid(final Grid grid) { + this.grid = grid; + } + + /** + * Sanity check for existence of item id. + * + * @param itemId + * item id to be selected / deselected + * + * @throws IllegalArgumentException + * if item Id doesn't exist in the container of Grid + */ + protected void checkItemIdExists(Object itemId) + throws IllegalArgumentException { + if (!grid.getContainerDataSource().containsId(itemId)) { + throw new IllegalArgumentException("Given item id (" + itemId + + ") does not exist in the container"); + } + } + + /** + * Sanity check for existence of item ids in given collection. + * + * @param itemIds + * item id collection to be selected / deselected + * + * @throws IllegalArgumentException + * if at least one item id doesn't exist in the container of + * Grid + */ + protected void checkItemIdsExist(Collection<?> itemIds) + throws IllegalArgumentException { + for (Object itemId : itemIds) { + checkItemIdExists(itemId); + } + } + + /** + * Fires a {@link SelectionChangeEvent} to all the + * {@link SelectionChangeListener SelectionChangeListeners} currently + * added to the Grid in which this SelectionModel is. + * <p> + * Note that this is only a helper method, and routes the call all the + * way to Grid. A {@link SelectionModel} is not a + * {@link SelectionChangeNotifier} + * + * @param oldSelection + * the complete {@link Collection} of the itemIds that were + * selected <em>before</em> this event happened + * @param newSelection + * the complete {@link Collection} of the itemIds that are + * selected <em>after</em> this event happened + */ + protected void fireSelectionChangeEvent( + final Collection<Object> oldSelection, + final Collection<Object> newSelection) { + grid.fireSelectionChangeEvent(oldSelection, newSelection); + } + } + + /** + * A default implementation of a {@link SelectionModel.Single} + * + * @since + * @author Vaadin Ltd + */ + public static class SingleSelectionModel extends AbstractSelectionModel + implements SelectionModel.Single { + @Override + public boolean select(final Object itemId) { + if (itemId == null) { + return deselect(getSelectedRow()); + } + + checkItemIdExists(itemId); + + final Object selectedRow = getSelectedRow(); + final boolean modified = selection.add(itemId); + if (modified) { + final Collection<Object> deselected; + if (selectedRow != null) { + deselectInternal(selectedRow, false); + deselected = Collections.singleton(selectedRow); + } else { + deselected = Collections.emptySet(); + } + + fireSelectionChangeEvent(deselected, selection); + } + + return modified; + } + + private boolean deselect(final Object itemId) { + return deselectInternal(itemId, true); + } + + private boolean deselectInternal(final Object itemId, + boolean fireEventIfNeeded) { + final boolean modified = selection.remove(itemId); + if (fireEventIfNeeded && modified) { + fireSelectionChangeEvent(Collections.singleton(itemId), + Collections.emptySet()); + } + return modified; + } + + @Override + public Object getSelectedRow() { + if (selection.isEmpty()) { + return null; + } else { + return selection.iterator().next(); + } + } + + /** + * Resets the selection state. + * <p> + * If an item is selected, it will become deselected. + */ + @Override + public void reset() { + deselect(getSelectedRow()); + } + } + + /** + * A default implementation for a {@link SelectionModel.None} + * + * @since + * @author Vaadin Ltd + */ + public static class NoSelectionModel implements SelectionModel.None { + @Override + public void setGrid(final Grid grid) { + // NOOP, not needed for anything + } + + @Override + public boolean isSelected(final Object itemId) { + return false; + } + + @Override + public Collection<Object> getSelectedRows() { + return Collections.emptyList(); + } + + /** + * Semantically resets the selection model. + * <p> + * Effectively a no-op. + */ + @Override + public void reset() { + // NOOP + } + } + + /** + * A default implementation of a {@link SelectionModel.Multi} + * + * @since + * @author Vaadin Ltd + */ + public static class MultiSelectionModel extends AbstractSelectionModel + implements SelectionModel.Multi { + + /** + * The default selection size limit. + * + * @see #setSelectionLimit(int) + */ + public static final int DEFAULT_MAX_SELECTIONS = 1000; + + private int selectionLimit = DEFAULT_MAX_SELECTIONS; + + @Override + public boolean select(final Object... itemIds) + throws IllegalArgumentException { + if (itemIds != null) { + // select will fire the event + return select(Arrays.asList(itemIds)); + } else { + throw new IllegalArgumentException( + "Vararg array of itemIds may not be null"); + } + } + + /** + * {@inheritDoc} + * <p> + * All items might not be selected if the limit set using + * {@link #setSelectionLimit(int)} is exceeded. + */ + @Override + public boolean select(final Collection<?> itemIds) + throws IllegalArgumentException { + if (itemIds == null) { + throw new IllegalArgumentException("itemIds may not be null"); + } + + // Sanity check + checkItemIdsExist(itemIds); + + final boolean selectionWillChange = !selection.containsAll(itemIds) + && selection.size() < selectionLimit; + if (selectionWillChange) { + final HashSet<Object> oldSelection = new HashSet<Object>( + selection); + if (selection.size() + itemIds.size() >= selectionLimit) { + // Add one at a time if there's a risk of overflow + Iterator<?> iterator = itemIds.iterator(); + while (iterator.hasNext() + && selection.size() < selectionLimit) { + selection.add(iterator.next()); + } + } else { + selection.addAll(itemIds); + } + fireSelectionChangeEvent(oldSelection, selection); + } + return selectionWillChange; + } + + /** + * Sets the maximum number of rows that can be selected at once. This is + * a mechanism to prevent exhausting server memory in situations where + * users select lots of rows. If the limit is reached, newly selected + * rows will not become recorded. + * <p> + * Old selections are not discarded if the current number of selected + * row exceeds the new limit. + * <p> + * The default limit is {@value #DEFAULT_MAX_SELECTIONS} rows. + * + * @param selectionLimit + * the non-negative selection limit to set + * @throws IllegalArgumentException + * if the limit is negative + */ + public void setSelectionLimit(int selectionLimit) { + if (selectionLimit < 0) { + throw new IllegalArgumentException( + "The selection limit must be non-negative"); + } + this.selectionLimit = selectionLimit; + } + + /** + * Gets the selection limit. + * + * @see #setSelectionLimit(int) + * + * @return the selection limit + */ + public int getSelectionLimit() { + return selectionLimit; + } + + @Override + public boolean deselect(final Object... itemIds) + throws IllegalArgumentException { + if (itemIds != null) { + // deselect will fire the event + return deselect(Arrays.asList(itemIds)); + } else { + throw new IllegalArgumentException( + "Vararg array of itemIds may not be null"); + } + } + + @Override + public boolean deselect(final Collection<?> itemIds) + throws IllegalArgumentException { + if (itemIds == null) { + throw new IllegalArgumentException("itemIds may not be null"); + } + + final boolean hasCommonElements = !Collections.disjoint(itemIds, + selection); + if (hasCommonElements) { + final HashSet<Object> oldSelection = new HashSet<Object>( + selection); + selection.removeAll(itemIds); + fireSelectionChangeEvent(oldSelection, selection); + } + return hasCommonElements; + } + + @Override + public boolean selectAll() { + // select will fire the event + final Indexed container = grid.getContainerDataSource(); + if (container != null) { + return select(container.getItemIds()); + } else if (selection.isEmpty()) { + return false; + } else { + /* + * this should never happen (no container but has a selection), + * but I guess the only theoretically correct course of + * action... + */ + return deselectAll(); + } + } + + @Override + public boolean deselectAll() { + // deselect will fire the event + return deselect(getSelectedRows()); + } + + /** + * {@inheritDoc} + * <p> + * The returned Collection is in <strong>order of selection</strong> + * – the item that was first selected will be first in the + * collection, and so on. Should an item have been selected twice + * without being deselected in between, it will have remained in its + * original position. + */ + @Override + public Collection<Object> getSelectedRows() { + // overridden only for JavaDoc + return super.getSelectedRows(); + } + + /** + * Resets the selection model. + * <p> + * Equivalent to calling {@link #deselectAll()} + */ + @Override + public void reset() { + deselectAll(); + } + } + + /** + * Callback interface for generating custom style names for data rows and + * cells. + * + * @see Grid#setCellStyleGenerator(CellStyleGenerator) + */ + public interface CellStyleGenerator extends Serializable { + + /** + * Called by Grid to generate a style name for a row or cell element. + * Row styles are generated when the column parameter is + * <code>null</code>, otherwise a cell style is generated. + * + * @param grid + * the source grid + * @param itemId + * the itemId of the target row + * @param propertyId + * the propertyId of the target cell, <code>null</code> when + * getting row style + * @return the style name to add to this cell or row element, or + * <code>null</code> to not set any style + */ + public abstract String getStyle(Grid grid, Object itemId, + Object propertyId); + } + + /** + * Abstract base class for Grid header and footer sections. + * + * @param <ROWTYPE> + * the type of the rows in the section + */ + protected static abstract class StaticSection<ROWTYPE extends StaticSection.StaticRow<?>> + implements Serializable { + + /** + * Abstract base class for Grid header and footer rows. + * + * @param <CELLTYPE> + * the type of the cells in the row + */ + abstract static class StaticRow<CELLTYPE extends StaticCell> implements + Serializable { + + private RowState rowState = new RowState(); + protected StaticSection<?> section; + private Map<Object, CELLTYPE> cells = new LinkedHashMap<Object, CELLTYPE>(); + private Map<Set<CELLTYPE>, CELLTYPE> cellGroups = new HashMap<Set<CELLTYPE>, CELLTYPE>(); + + protected StaticRow(StaticSection<?> section) { + this.section = section; + } + + protected void addCell(Object propertyId) { + CELLTYPE cell = createCell(); + cell.setColumnId(section.grid.getColumn(propertyId).getState().id); + cells.put(propertyId, cell); + rowState.cells.add(cell.getCellState()); + } + + protected void removeCell(Object propertyId) { + CELLTYPE cell = cells.remove(propertyId); + if (cell != null) { + Set<CELLTYPE> cellGroupForCell = getCellGroupForCell(cell); + if (cellGroupForCell != null) { + removeCellFromGroup(cell, cellGroupForCell); + } + rowState.cells.remove(cell.getCellState()); + } + } + + private void removeCellFromGroup(CELLTYPE cell, + Set<CELLTYPE> cellGroup) { + String columnId = cell.getColumnId(); + for (Set<String> group : rowState.cellGroups.keySet()) { + if (group.contains(columnId)) { + if (group.size() > 2) { + // Update map key correctly + CELLTYPE mergedCell = cellGroups.remove(cellGroup); + cellGroup.remove(cell); + cellGroups.put(cellGroup, mergedCell); + + group.remove(columnId); + } else { + rowState.cellGroups.remove(group); + cellGroups.remove(cellGroup); + } + return; + } + } + } + + /** + * Creates and returns a new instance of the cell type. + * + * @return the created cell + */ + protected abstract CELLTYPE createCell(); + + protected RowState getRowState() { + return rowState; + } + + /** + * Returns the cell for the given property id on this row. If the + * column is merged returned cell is the cell for the whole group. + * + * @param propertyId + * the property id of the column + * @return the cell for the given property, merged cell for merged + * properties, null if not found + */ + public CELLTYPE getCell(Object propertyId) { + CELLTYPE cell = cells.get(propertyId); + Set<CELLTYPE> cellGroup = getCellGroupForCell(cell); + if (cellGroup != null) { + cell = cellGroups.get(cellGroup); + } + return cell; + } + + /** + * Merges columns cells in a row + * + * @param propertyIds + * The property ids of columns to merge + * @return The remaining visible cell after the merge + */ + public CELLTYPE join(Object... propertyIds) { + assert propertyIds.length > 1 : "You need to merge at least 2 properties"; + + Set<CELLTYPE> cells = new HashSet<CELLTYPE>(); + for (int i = 0; i < propertyIds.length; ++i) { + cells.add(getCell(propertyIds[i])); + } + + return join(cells); + } + + /** + * Merges columns cells in a row + * + * @param cells + * The cells to merge. Must be from the same row. + * @return The remaining visible cell after the merge + */ + public CELLTYPE join(CELLTYPE... cells) { + assert cells.length > 1 : "You need to merge at least 2 cells"; + + return join(new HashSet<CELLTYPE>(Arrays.asList(cells))); + } + + protected CELLTYPE join(Set<CELLTYPE> cells) { + for (CELLTYPE cell : cells) { + if (getCellGroupForCell(cell) != null) { + throw new IllegalArgumentException( + "Cell already merged"); + } else if (!this.cells.containsValue(cell)) { + throw new IllegalArgumentException( + "Cell does not exist on this row"); + } + } + + // Create new cell data for the group + CELLTYPE newCell = createCell(); + + Set<String> columnGroup = new HashSet<String>(); + for (CELLTYPE cell : cells) { + columnGroup.add(cell.getColumnId()); + } + rowState.cellGroups.put(columnGroup, newCell.getCellState()); + cellGroups.put(cells, newCell); + return newCell; + } + + private Set<CELLTYPE> getCellGroupForCell(CELLTYPE cell) { + for (Set<CELLTYPE> group : cellGroups.keySet()) { + if (group.contains(cell)) { + return group; + } + } + return null; + } + + /** + * Returns the custom style name for this row. + * + * @return the style name or null if no style name has been set + */ + public String getStyleName() { + return getRowState().styleName; + } + + /** + * Sets a custom style name for this row. + * + * @param styleName + * the style name to set or null to not use any style + * name + */ + public void setStyleName(String styleName) { + getRowState().styleName = styleName; + } + + } + + /** + * A header or footer cell. Has a simple textual caption. + */ + abstract static class StaticCell implements Serializable { + + private CellState cellState = new CellState(); + private StaticRow<?> row; + + protected StaticCell(StaticRow<?> row) { + this.row = row; + } + + void setColumnId(String id) { + cellState.columnId = id; + } + + String getColumnId() { + return cellState.columnId; + } + + /** + * Gets the row where this cell is. + * + * @return row for this cell + */ + public StaticRow<?> getRow() { + return row; + } + + protected CellState getCellState() { + return cellState; + } + + /** + * Sets the text displayed in this cell. + * + * @param text + * a plain text caption + */ + public void setText(String text) { + cellState.text = text; + cellState.type = GridStaticCellType.TEXT; + row.section.markAsDirty(); + } + + /** + * Returns the text displayed in this cell. + * + * @return the plain text caption + */ + public String getText() { + if (cellState.type != GridStaticCellType.TEXT) { + throw new IllegalStateException( + "Cannot fetch Text from a cell with type " + + cellState.type); + } + return cellState.text; + } + + /** + * Returns the HTML content displayed in this cell. + * + * @return the html + * + */ + public String getHtml() { + if (cellState.type != GridStaticCellType.HTML) { + throw new IllegalStateException( + "Cannot fetch HTML from a cell with type " + + cellState.type); + } + return cellState.html; + } + + /** + * Sets the HTML content displayed in this cell. + * + * @param html + * the html to set + */ + public void setHtml(String html) { + cellState.html = html; + cellState.type = GridStaticCellType.HTML; + row.section.markAsDirty(); + } + + /** + * Returns the component displayed in this cell. + * + * @return the component + */ + public Component getComponent() { + if (cellState.type != GridStaticCellType.WIDGET) { + throw new IllegalStateException( + "Cannot fetch Component from a cell with type " + + cellState.type); + } + return (Component) cellState.connector; + } + + /** + * Sets the component displayed in this cell. + * + * @param component + * the component to set + */ + public void setComponent(Component component) { + component.setParent(row.section.grid); + cellState.connector = component; + cellState.type = GridStaticCellType.WIDGET; + row.section.markAsDirty(); + } + + /** + * Returns the custom style name for this cell. + * + * @return the style name or null if no style name has been set + */ + public String getStyleName() { + return cellState.styleName; + } + + /** + * Sets a custom style name for this cell. + * + * @param styleName + * the style name to set or null to not use any style + * name + */ + public void setStyleName(String styleName) { + cellState.styleName = styleName; + row.section.markAsDirty(); + } + + } + + protected Grid grid; + protected List<ROWTYPE> rows = new ArrayList<ROWTYPE>(); + + /** + * Sets the visibility of the whole section. + * + * @param visible + * true to show this section, false to hide + */ + public void setVisible(boolean visible) { + if (getSectionState().visible != visible) { + getSectionState().visible = visible; + markAsDirty(); + } + } + + /** + * Returns the visibility of this section. + * + * @return true if visible, false otherwise. + */ + public boolean isVisible() { + return getSectionState().visible; + } + + /** + * Removes the row at the given position. + * + * @param index + * the position of the row + * + * @throws IllegalArgumentException + * if no row exists at given index + * @see #removeRow(StaticRow) + * @see #addRowAt(int) + * @see #appendRow() + * @see #prependRow() + */ + public ROWTYPE removeRow(int rowIndex) { + if (rowIndex >= rows.size() || rowIndex < 0) { + throw new IllegalArgumentException("No row at given index " + + rowIndex); + } + ROWTYPE row = rows.remove(rowIndex); + getSectionState().rows.remove(rowIndex); + + markAsDirty(); + return row; + } + + /** + * Removes the given row from the section. + * + * @param row + * the row to be removed + * + * @throws IllegalArgumentException + * if the row does not exist in this section + * @see #removeRow(int) + * @see #addRowAt(int) + * @see #appendRow() + * @see #prependRow() + */ + public void removeRow(ROWTYPE row) { + try { + removeRow(rows.indexOf(row)); + } catch (IndexOutOfBoundsException e) { + throw new IllegalArgumentException( + "Section does not contain the given row"); + } + } + + /** + * Gets row at given index. + * + * @param rowIndex + * 0 based index for row. Counted from top to bottom + * @return row at given index + */ + public ROWTYPE getRow(int rowIndex) { + if (rowIndex >= rows.size() || rowIndex < 0) { + throw new IllegalArgumentException("No row at given index " + + rowIndex); + } + return rows.get(rowIndex); + } + + /** + * Adds a new row at the top of this section. + * + * @return the new row + * @see #appendRow() + * @see #addRowAt(int) + * @see #removeRow(StaticRow) + * @see #removeRow(int) + */ + public ROWTYPE prependRow() { + return addRowAt(0); + } + + /** + * Adds a new row at the bottom of this section. + * + * @return the new row + * @see #prependRow() + * @see #addRowAt(int) + * @see #removeRow(StaticRow) + * @see #removeRow(int) + */ + public ROWTYPE appendRow() { + return addRowAt(rows.size()); + } + + /** + * Inserts a new row at the given position. + * + * @param index + * the position at which to insert the row + * @return the new row + * + * @throws IndexOutOfBoundsException + * if the index is out of bounds + * @see #appendRow() + * @see #prependRow() + * @see #removeRow(StaticRow) + * @see #removeRow(int) + */ + public ROWTYPE addRowAt(int index) { + if (index > rows.size() || index < 0) { + throw new IllegalArgumentException( + "Unable to add row at index " + index); + } + ROWTYPE row = createRow(); + rows.add(index, row); + getSectionState().rows.add(index, row.getRowState()); + + Indexed dataSource = grid.getContainerDataSource(); + for (Object id : dataSource.getContainerPropertyIds()) { + row.addCell(id); + } + + markAsDirty(); + return row; + } + + /** + * Gets the amount of rows in this section. + * + * @return row count + */ + public int getRowCount() { + return rows.size(); + } + + protected abstract GridStaticSectionState getSectionState(); + + protected abstract ROWTYPE createRow(); + + /** + * Informs the grid that state has changed and it should be redrawn. + */ + protected void markAsDirty() { + grid.markAsDirty(); + } + + /** + * Removes a column for given property id from the section. + * + * @param propertyId + * property to be removed + */ + protected void removeColumn(Object propertyId) { + for (ROWTYPE row : rows) { + row.removeCell(propertyId); + } + } + + /** + * Adds a column for given property id to the section. + * + * @param propertyId + * property to be added + */ + protected void addColumn(Object propertyId) { + for (ROWTYPE row : rows) { + row.addCell(propertyId); + } + } + + /** + * Performs a sanity check that section is in correct state. + * + * @throws IllegalStateException + * if merged cells are not i n continuous range + */ + protected void sanityCheck() throws IllegalStateException { + List<String> columnOrder = grid.getState().columnOrder; + for (ROWTYPE row : rows) { + for (Set<String> cellGroup : row.getRowState().cellGroups + .keySet()) { + if (!checkCellGroupAndOrder(columnOrder, cellGroup)) { + throw new IllegalStateException( + "Not all merged cells were in a continuous range."); + } + } + } + } + + private boolean checkCellGroupAndOrder(List<String> columnOrder, + Set<String> cellGroup) { + if (!columnOrder.containsAll(cellGroup)) { + return false; + } + + for (int i = 0; i < columnOrder.size(); ++i) { + if (!cellGroup.contains(columnOrder.get(i))) { + continue; + } + + for (int j = 1; j < cellGroup.size(); ++j) { + if (!cellGroup.contains(columnOrder.get(i + j))) { + return false; + } + } + return true; + } + return false; + } + } + + /** + * Represents the header section of a Grid. + */ + protected static class Header extends StaticSection<HeaderRow> { + + private HeaderRow defaultRow = null; + private final GridStaticSectionState headerState = new GridStaticSectionState(); + + protected Header(Grid grid) { + this.grid = grid; + grid.getState(true).header = headerState; + HeaderRow row = createRow(); + rows.add(row); + setDefaultRow(row); + getSectionState().rows.add(row.getRowState()); + } + + /** + * Sets the default row of this header. The default row is a special + * header row providing a user interface for sorting columns. + * + * @param row + * the new default row, or null for no default row + * + * @throws IllegalArgumentException + * this header does not contain the row + */ + public void setDefaultRow(HeaderRow row) { + if (row == defaultRow) { + return; + } + + if (row != null && !rows.contains(row)) { + throw new IllegalArgumentException( + "Cannot set a default row that does not exist in the section"); + } + + if (defaultRow != null) { + defaultRow.setDefaultRow(false); + } + + if (row != null) { + row.setDefaultRow(true); + } + + defaultRow = row; + markAsDirty(); + } + + /** + * Returns the current default row of this header. The default row is a + * special header row providing a user interface for sorting columns. + * + * @return the default row or null if no default row set + */ + public HeaderRow getDefaultRow() { + return defaultRow; + } + + @Override + protected GridStaticSectionState getSectionState() { + return headerState; + } + + @Override + protected HeaderRow createRow() { + return new HeaderRow(this); + } + + @Override + public HeaderRow removeRow(int rowIndex) { + HeaderRow row = super.removeRow(rowIndex); + if (row == defaultRow) { + // Default Header Row was just removed. + setDefaultRow(null); + } + return row; + } + + @Override + protected void sanityCheck() throws IllegalStateException { + super.sanityCheck(); + + boolean hasDefaultRow = false; + for (HeaderRow row : rows) { + if (row.getRowState().defaultRow) { + if (!hasDefaultRow) { + hasDefaultRow = true; + } else { + throw new IllegalStateException( + "Multiple default rows in header"); + } + } + } + } + } + + /** + * Represents a header row in Grid. + */ + public static class HeaderRow extends StaticSection.StaticRow<HeaderCell> { + + protected HeaderRow(StaticSection<?> section) { + super(section); + } + + private void setDefaultRow(boolean value) { + getRowState().defaultRow = value; + } + + @Override + protected HeaderCell createCell() { + return new HeaderCell(this); + } + } + + /** + * Represents a header cell in Grid. Can be a merged cell for multiple + * columns. + */ + public static class HeaderCell extends StaticSection.StaticCell { + + protected HeaderCell(HeaderRow row) { + super(row); + } + } + + /** + * Represents the footer section of a Grid. By default Footer is not + * visible. + */ + protected static class Footer extends StaticSection<FooterRow> { + + private final GridStaticSectionState footerState = new GridStaticSectionState(); + + protected Footer(Grid grid) { + this.grid = grid; + grid.getState(true).footer = footerState; + } + + @Override + protected GridStaticSectionState getSectionState() { + return footerState; + } + + @Override + protected FooterRow createRow() { + return new FooterRow(this); + } + + @Override + protected void sanityCheck() throws IllegalStateException { + super.sanityCheck(); + } + } + + /** + * Represents a footer row in Grid. + */ + public static class FooterRow extends StaticSection.StaticRow<FooterCell> { + + protected FooterRow(StaticSection<?> section) { + super(section); + } + + @Override + protected FooterCell createCell() { + return new FooterCell(this); + } + + } + + /** + * Represents a footer cell in Grid. + */ + public static class FooterCell extends StaticSection.StaticCell { + + protected FooterCell(FooterRow row) { + super(row); + } + } + + /** + * A column in the grid. Can be obtained by calling + * {@link Grid#getColumn(Object propertyId)}. + */ + public static class Column implements Serializable { + + /** + * The state of the column shared to the client + */ + private final GridColumnState state; + + /** + * The grid this column is associated with + */ + private final Grid grid; + + /** + * Backing property for column + */ + private final Object columnProperty; + + private Converter<?, Object> converter; + + /** + * A check for allowing the {@link #Column(Grid, GridColumnState) + * constructor} to call {@link #setConverter(Converter)} with a + * <code>null</code>, even if model and renderer aren't compatible. + */ + private boolean isFirstConverterAssignment = true; + + /** + * Internally used constructor. + * + * @param grid + * The grid this column belongs to. Should not be null. + * @param state + * the shared state of this column + * @param columnProperty + * the backing property id for this column + */ + Column(Grid grid, GridColumnState state, Object columnProperty) { + this.grid = grid; + this.state = state; + this.columnProperty = columnProperty; + internalSetRenderer(new TextRenderer()); + } + + /** + * Returns the serializable state of this column that is sent to the + * client side connector. + * + * @return the internal state of the column + */ + GridColumnState getState() { + return state; + } + + /** + * Return the property id for the backing property of this Column + * + * @return property id + */ + public Object getColumnProperty() { + return columnProperty; + } + + /** + * Returns the caption of the header. By default the header caption is + * the property id of the column. + * + * @return the text in the default row of header, null if no default row + * + * @throws IllegalStateException + * if the column no longer is attached to the grid + */ + public String getHeaderCaption() throws IllegalStateException { + checkColumnIsAttached(); + HeaderRow row = grid.getHeader().getDefaultRow(); + if (row != null) { + return row.getCell(grid.getPropertyIdByColumnId(state.id)) + .getText(); + } + return null; + } + + /** + * Sets the caption of the header. + * + * @param caption + * the text to show in the caption + * @return the column itself + * + * @throws IllegalStateException + * if the column is no longer attached to any grid + */ + public Column setHeaderCaption(String caption) + throws IllegalStateException { + checkColumnIsAttached(); + HeaderRow row = grid.getHeader().getDefaultRow(); + if (row != null) { + row.getCell(grid.getPropertyIdByColumnId(state.id)).setText( + caption); + } + return this; + } + + /** + * Returns the width (in pixels). By default a column is 100px wide. + * + * @return the width in pixels of the column + * @throws IllegalStateException + * if the column is no longer attached to any grid + */ + public double getWidth() throws IllegalStateException { + checkColumnIsAttached(); + return state.width; + } + + /** + * Sets the width (in pixels). + * + * @param pixelWidth + * the new pixel width of the column + * @return the column itself + * + * @throws IllegalStateException + * if the column is no longer attached to any grid + * @throws IllegalArgumentException + * thrown if pixel width is less than zero + */ + public Column setWidth(double pixelWidth) throws IllegalStateException, + IllegalArgumentException { + checkColumnIsAttached(); + if (pixelWidth < 0) { + throw new IllegalArgumentException( + "Pixel width should be greated than 0 (in " + + toString() + ")"); + } + state.width = pixelWidth; + grid.markAsDirty(); + return this; + } + + /** + * Marks the column width as undefined meaning that the grid is free to + * resize the column based on the cell contents and available space in + * the grid. + * + * @return the column itself + */ + public Column setWidthUndefined() { + checkColumnIsAttached(); + state.width = -1; + grid.markAsDirty(); + return this; + } + + /** + * Checks if column is attached and throws an + * {@link IllegalStateException} if it is not + * + * @throws IllegalStateException + * if the column is no longer attached to any grid + */ + protected void checkColumnIsAttached() throws IllegalStateException { + if (grid.getColumnByColumnId(state.id) == null) { + throw new IllegalStateException("Column no longer exists."); + } + } + + /** + * Sets this column as the last frozen column in its grid. + * + * @return the column itself + * + * @throws IllegalArgumentException + * if the column is no longer attached to any grid + * @see Grid#setFrozenColumnCount(int) + */ + public Column setLastFrozenColumn() { + checkColumnIsAttached(); + grid.setFrozenColumnCount(grid.getState(false).columnOrder + .indexOf(this) + 1); + return this; + } + + /** + * Sets the renderer for this column. + * <p> + * If a suitable converter isn't defined explicitly, the session + * converter factory is used to find a compatible converter. + * + * @param renderer + * the renderer to use + * @return the column itself + * + * @throws IllegalArgumentException + * if no compatible converter could be found + * + * @see VaadinSession#getConverterFactory() + * @see ConverterUtil#getConverter(Class, Class, VaadinSession) + * @see #setConverter(Converter) + */ + public Column setRenderer(Renderer<?> renderer) { + if (!internalSetRenderer(renderer)) { + throw new IllegalArgumentException( + "Could not find a converter for converting from the model type " + + getModelType() + + " to the renderer presentation type " + + renderer.getPresentationType() + " (in " + + toString() + ")"); + } + return this; + } + + /** + * Sets the renderer for this column and the converter used to convert + * from the property value type to the renderer presentation type. + * + * @param renderer + * the renderer to use, cannot be null + * @param converter + * the converter to use + * @return the column itself + * + * @throws IllegalArgumentException + * if the renderer is already associated with a grid column + */ + public <T> Column setRenderer(Renderer<T> renderer, + Converter<? extends T, ?> converter) { + if (renderer.getParent() != null) { + throw new IllegalArgumentException( + "Cannot set a renderer that is already connected to a grid column (in " + + toString() + ")"); + } + + if (getRenderer() != null) { + grid.removeExtension(getRenderer()); + } + + grid.addRenderer(renderer); + state.rendererConnector = renderer; + setConverter(converter); + return this; + } + + /** + * Sets the converter used to convert from the property value type to + * the renderer presentation type. + * + * @param converter + * the converter to use, or {@code null} to not use any + * converters + * @return the column itself + * + * @throws IllegalArgumentException + * if the types are not compatible + */ + public Column setConverter(Converter<?, ?> converter) + throws IllegalArgumentException { + Class<?> modelType = getModelType(); + if (converter != null) { + if (!converter.getModelType().isAssignableFrom(modelType)) { + throw new IllegalArgumentException( + "The converter model type " + + converter.getModelType() + + " is not compatible with the property type " + + modelType + " (in " + toString() + ")"); + + } else if (!getRenderer().getPresentationType() + .isAssignableFrom(converter.getPresentationType())) { + throw new IllegalArgumentException( + "The converter presentation type " + + converter.getPresentationType() + + " is not compatible with the renderer presentation type " + + getRenderer().getPresentationType() + + " (in " + toString() + ")"); + } + } + + else { + /* + * Since the converter is null (i.e. will be removed), we need + * to know that the renderer and model are compatible. If not, + * we can't allow for this to happen. + * + * The constructor is allowed to call this method with null + * without any compatibility checks, therefore we have a special + * case for it. + */ + + Class<?> rendererPresentationType = getRenderer() + .getPresentationType(); + if (!isFirstConverterAssignment + && !rendererPresentationType + .isAssignableFrom(modelType)) { + throw new IllegalArgumentException( + "Cannot remove converter, " + + "as renderer's presentation type " + + rendererPresentationType.getName() + + " and column's " + + "model " + + modelType.getName() + + " type aren't " + + "directly compatible with each other (in " + + toString() + ")"); + } + } + + isFirstConverterAssignment = false; + + @SuppressWarnings("unchecked") + Converter<?, Object> castConverter = (Converter<?, Object>) converter; + this.converter = castConverter; + + return this; + } + + /** + * Returns the renderer instance used by this column. + * + * @return the renderer + */ + public Renderer<?> getRenderer() { + return (Renderer<?>) getState().rendererConnector; + } + + /** + * Returns the converter instance used by this column. + * + * @return the converter + */ + public Converter<?, ?> getConverter() { + return converter; + } + + private <T> boolean internalSetRenderer(Renderer<T> renderer) { + + Converter<? extends T, ?> converter; + if (isCompatibleWithProperty(renderer, getConverter())) { + // Use the existing converter (possibly none) if types + // compatible + converter = (Converter<? extends T, ?>) getConverter(); + } else { + converter = ConverterUtil.getConverter( + renderer.getPresentationType(), getModelType(), + getSession()); + } + setRenderer(renderer, converter); + return isCompatibleWithProperty(renderer, converter); + } + + private VaadinSession getSession() { + UI ui = grid.getUI(); + return ui != null ? ui.getSession() : null; + } + + private boolean isCompatibleWithProperty(Renderer<?> renderer, + Converter<?, ?> converter) { + Class<?> type; + if (converter == null) { + type = getModelType(); + } else { + type = converter.getPresentationType(); + } + return renderer.getPresentationType().isAssignableFrom(type); + } + + private Class<?> getModelType() { + return grid.getContainerDataSource().getType( + grid.getPropertyIdByColumnId(state.id)); + } + + /** + * Should sorting controls be available for the column + * + * @param sortable + * <code>true</code> if the sorting controls should be + * visible. + * @return the column itself + */ + public Column setSortable(boolean sortable) { + checkColumnIsAttached(); + state.sortable = sortable; + grid.markAsDirty(); + return this; + } + + /** + * Are the sorting controls visible in the column header + */ + public boolean isSortable() { + return state.sortable; + } + + @Override + public String toString() { + return getClass().getSimpleName() + "[propertyId:" + + grid.getPropertyIdByColumnId(state.id) + "]"; + } + } + + /** + * An abstract base class for server-side Grid renderers. + * {@link com.vaadin.client.ui.grid.Renderer Grid renderers}. This class + * currently extends the AbstractExtension superclass, but this fact should + * be regarded as an implementation detail and subject to change in a future + * major or minor Vaadin revision. + * + * @param <T> + * the type this renderer knows how to present + */ + public static abstract class AbstractRenderer<T> extends AbstractExtension + implements Renderer<T> { + + private final Class<T> presentationType; + + protected AbstractRenderer(Class<T> presentationType) { + this.presentationType = presentationType; + } + + /** + * This method is inherited from AbstractExtension but should never be + * called directly with an AbstractRenderer. + */ + @Deprecated + @Override + protected Class<Grid> getSupportedParentType() { + return Grid.class; + } + + /** + * This method is inherited from AbstractExtension but should never be + * called directly with an AbstractRenderer. + */ + @Deprecated + @Override + protected void extend(AbstractClientConnector target) { + super.extend(target); + } + + @Override + public Class<T> getPresentationType() { + return presentationType; + } + + @Override + public JsonValue encode(T value) { + return encode(value, getPresentationType()); + } + + /** + * Encodes the given value to JSON. + * <p> + * This is a helper method that can be invoked by an + * {@link #encode(Object) encode(T)} override if serializing a value of + * type other than {@link #getPresentationType() the presentation type} + * is desired. For instance, a {@code Renderer<Date>} could first turn a + * date value into a formatted string and return + * {@code encode(dateString, String.class)}. + * + * @param value + * the value to be encoded + * @param type + * the type of the value + * @return a JSON representation of the given value + */ + protected <U> JsonValue encode(U value, Class<U> type) { + return JsonCodec.encode(value, null, type, + getUI().getConnectorTracker()).getEncodedValue(); + } + + /** + * Gets the item id for a row key. + * <p> + * A key is used to identify a particular row on both a server and a + * client. This method can be used to get the item id for the row key + * that the client has sent. + * + * @param rowKey + * the row key for which to retrieve an item id + * @return the item id corresponding to {@code key} + */ + protected Object getItemId(String rowKey) { + if (getParent() instanceof Grid) { + Grid grid = (Grid) getParent(); + return grid.getKeyMapper().getItemId(rowKey); + } else { + throw new IllegalStateException( + "Renderers can be used only with Grid"); + } + } + } + + /** + * The data source attached to the grid + */ + private Container.Indexed datasource; + + /** + * Property id to column instance mapping + */ + private final Map<Object, Column> columns = new HashMap<Object, Column>(); + + /** + * Key generator for column server-to-client communication + */ + private final KeyMapper<Object> columnKeys = new KeyMapper<Object>(); + + /** + * The current sort order + */ + private final List<SortOrder> sortOrder = new ArrayList<SortOrder>(); + + /** + * Property listener for listening to changes in data source properties. + */ + private final PropertySetChangeListener propertyListener = new PropertySetChangeListener() { + + @Override + public void containerPropertySetChange(PropertySetChangeEvent event) { + Collection<?> properties = new HashSet<Object>(event.getContainer() + .getContainerPropertyIds()); + + // Cleanup columns that are no longer in grid + List<Object> removedColumns = new LinkedList<Object>(); + for (Object columnId : columns.keySet()) { + if (!properties.contains(columnId)) { + removedColumns.add(columnId); + } + } + for (Object columnId : removedColumns) { + removeColumn(columnId); + columnKeys.remove(columnId); + } + datasourceExtension.propertiesRemoved(removedColumns); + + // Add new columns + HashSet<Object> addedPropertyIds = new HashSet<Object>(); + for (Object propertyId : properties) { + if (!columns.containsKey(propertyId)) { + appendColumn(propertyId); + addedPropertyIds.add(propertyId); + } + } + datasourceExtension.propertiesAdded(addedPropertyIds); + + if (getFrozenColumnCount() > columns.size()) { + setFrozenColumnCount(columns.size()); + } + + // Update sortable columns + if (event.getContainer() instanceof Sortable) { + Collection<?> sortableProperties = ((Sortable) event + .getContainer()).getSortableContainerPropertyIds(); + for (Entry<Object, Column> columnEntry : columns.entrySet()) { + columnEntry.getValue().setSortable( + sortableProperties.contains(columnEntry.getKey())); + } + } + } + }; + + private RpcDataProviderExtension datasourceExtension; + + /** + * The selection model that is currently in use. Never <code>null</code> + * after the constructor has been run. + */ + private SelectionModel selectionModel; + + /** + * Used to know whether selection change events originate from the server or + * the client so the selection change handler knows whether the changes + * should be sent to the client. + */ + private boolean applyingSelectionFromClient; + + private final Header header = new Header(this); + private final Footer footer = new Footer(this); + + private Object editedItemId = null; + private FieldGroup editorRowFieldGroup = new FieldGroup(); + private HashSet<Object> uneditableProperties = new HashSet<Object>(); + private ErrorHandler editorRowErrorHandler; + + private CellStyleGenerator cellStyleGenerator; + + /** + * <code>true</code> if Grid is using the internal IndexedContainer created + * in Grid() constructor, or <code>false</code> if the user has set their + * own Container. + * + * @see #setContainerDataSource() + * @see #Grid() + */ + private boolean defaultContainer = true; + + private static final Method SELECTION_CHANGE_METHOD = ReflectTools + .findMethod(SelectionChangeListener.class, "selectionChange", + SelectionChangeEvent.class); + + private static final Method SORT_ORDER_CHANGE_METHOD = ReflectTools + .findMethod(SortOrderChangeListener.class, "sortOrderChange", + SortOrderChangeEvent.class); + + /** + * Creates a new Grid with a new {@link IndexedContainer} as the datasource. + */ + public Grid() { + internalSetContainerDataSource(new IndexedContainer()); + initGrid(); + } + + /** + * Creates a new Grid using the given datasource. + * + * @param datasource + * the data source for the grid + */ + public Grid(final Container.Indexed datasource) { + setContainerDataSource(datasource); + initGrid(); + } + + /** + * Grid initial setup + */ + private void initGrid() { + setSelectionMode(SelectionMode.MULTI); + addSelectionChangeListener(new SelectionChangeListener() { + @Override + public void selectionChange(SelectionChangeEvent event) { + if (applyingSelectionFromClient) { + /* + * Avoid sending changes back to the client if they + * originated from the client. Instead, the RPC handler is + * responsible for keeping track of the resulting selection + * state and notifying the client if it doens't match the + * expectation. + */ + return; + } + + /* + * The rows are pinned here to ensure that the client gets the + * correct key from server when the selected row is first + * loaded. + * + * Once the client has gotten info that it is supposed to select + * a row, it will pin the data from the client side as well and + * it will be unpinned once it gets deselected. Nothing on the + * server side should ever unpin anything from KeyMapper. + * Pinning is mostly a client feature and is only used when + * selecting something from the server side. + */ + for (Object addedItemId : event.getAdded()) { + if (!getKeyMapper().isPinned(addedItemId)) { + getKeyMapper().pin(addedItemId); + } + } + + getState().selectedKeys = getKeyMapper().getKeys( + getSelectedRows()); + } + }); + + registerRpc(new GridServerRpc() { + + @Override + public void selectionChange(List<String> selection) { + Collection<Object> receivedSelection = getKeyMapper() + .getItemIds(selection); + + final HashSet<Object> receivedSelectionSet = new HashSet<Object>( + receivedSelection); + final HashSet<Object> previousSelectionSet = new HashSet<Object>( + getSelectedRows()); + + applyingSelectionFromClient = true; + try { + SelectionModel selectionModel = getSelectionModel(); + + SetView<Object> removedItemIds = Sets.difference( + previousSelectionSet, receivedSelectionSet); + if (!removedItemIds.isEmpty()) { + if (removedItemIds.size() == 1) { + deselect(removedItemIds.iterator().next()); + } else { + assert selectionModel instanceof SelectionModel.Multi : "Got multiple deselections, but the selection model is not a SelectionModel.Multi"; + ((SelectionModel.Multi) selectionModel) + .deselect(removedItemIds); + } + } + + SetView<Object> addedItemIds = Sets.difference( + receivedSelectionSet, previousSelectionSet); + if (!addedItemIds.isEmpty()) { + if (addedItemIds.size() == 1) { + select(addedItemIds.iterator().next()); + } else { + assert selectionModel instanceof SelectionModel.Multi : "Got multiple selections, but the selection model is not a SelectionModel.Multi"; + ((SelectionModel.Multi) selectionModel) + .select(addedItemIds); + } + } + } finally { + applyingSelectionFromClient = false; + } + + Collection<Object> actualSelection = getSelectedRows(); + + // Make sure all selected rows are pinned + for (Object itemId : actualSelection) { + if (!getKeyMapper().isPinned(itemId)) { + getKeyMapper().pin(itemId); + } + } + + // Don't mark as dirty since this might be the expected state + getState(false).selectedKeys = getKeyMapper().getKeys( + actualSelection); + + JsonObject diffState = getUI().getConnectorTracker() + .getDiffState(Grid.this); + + final String diffstateKey = "selectedKeys"; + + assert diffState.hasKey(diffstateKey) : "Field name has changed"; + + if (receivedSelection.equals(actualSelection)) { + /* + * We ended up with the same selection state that the client + * sent us. There's nothing to send back to the client, just + * update the diffstate so subsequent changes will be + * detected. + */ + JsonArray diffSelected = Json.createArray(); + for (String rowKey : getState(false).selectedKeys) { + diffSelected.set(diffSelected.length(), rowKey); + } + diffState.put(diffstateKey, diffSelected); + } else { + /* + * Actual selection is not what the client expects. Make + * sure the client gets a state change event by clearing the + * diffstate and marking as dirty + */ + diffState.remove(diffstateKey); + markAsDirty(); + } + } + + @Override + public void sort(String[] columnIds, SortDirection[] directions, + SortEventOriginator originator) { + assert columnIds.length == directions.length; + + List<SortOrder> order = new ArrayList<SortOrder>( + columnIds.length); + for (int i = 0; i < columnIds.length; i++) { + Object propertyId = getPropertyIdByColumnId(columnIds[i]); + order.add(new SortOrder(propertyId, directions[i])); + } + + setSortOrder(order, originator); + } + + @Override + public void selectAll() { + assert getSelectionModel() instanceof SelectionModel.Multi : "Not a multi selection model!"; + + ((SelectionModel.Multi) getSelectionModel()).selectAll(); + } + }); + + registerRpc(new EditorRowServerRpc() { + + @Override + public void bind(int rowIndex) { + try { + Object id = getContainerDataSource().getIdByIndex(rowIndex); + doEditItem(id); + getEditorRowRpc().confirmBind(); + } catch (Exception e) { + handleError(e); + } + } + + @Override + public void cancel(int rowIndex) { + try { + // For future proofing even though cannot currently fail + doCancelEditorRow(); + } catch (Exception e) { + handleError(e); + } + } + + @Override + public void save(int rowIndex) { + try { + saveEditorRow(); + getEditorRowRpc().confirmSave(); + } catch (Exception e) { + handleError(e); + } + } + + private void handleError(Exception e) { + ErrorHandler handler = getEditorRowErrorHandler(); + if (handler == null) { + handler = com.vaadin.server.ErrorEvent + .findErrorHandler(Grid.this); + } + handler.error(new ConnectorErrorEvent(Grid.this, e)); + } + }); + } + + @Override + public void beforeClientResponse(boolean initial) { + try { + header.sanityCheck(); + footer.sanityCheck(); + } catch (Exception e) { + e.printStackTrace(); + setComponentError(new ErrorMessage() { + + @Override + public ErrorLevel getErrorLevel() { + return ErrorLevel.CRITICAL; + } + + @Override + public String getFormattedHtmlMessage() { + return "Incorrectly merged cells"; + } + + }); + } + + super.beforeClientResponse(initial); + } + + /** + * Sets the grid data source. + * + * @param container + * The container data source. Cannot be null. + * @throws IllegalArgumentException + * if the data source is null + */ + public void setContainerDataSource(Container.Indexed container) { + defaultContainer = false; + internalSetContainerDataSource(container); + } + + private void internalSetContainerDataSource(Container.Indexed container) { + if (container == null) { + throw new IllegalArgumentException( + "Cannot set the datasource to null"); + } + if (datasource == container) { + return; + } + + // Remove old listeners + if (datasource instanceof PropertySetChangeNotifier) { + ((PropertySetChangeNotifier) datasource) + .removePropertySetChangeListener(propertyListener); + } + + if (datasourceExtension != null) { + removeExtension(datasourceExtension); + } + + columnKeys.removeAll(); + datasource = container; + + resetEditorRow(); + + // + // Adjust sort order + // + + if (container instanceof Container.Sortable) { + + // If the container is sortable, go through the current sort order + // and match each item to the sortable properties of the new + // container. If the new container does not support an item in the + // current sort order, that item is removed from the current sort + // order list. + Collection<?> sortableProps = ((Container.Sortable) getContainerDataSource()) + .getSortableContainerPropertyIds(); + + Iterator<SortOrder> i = sortOrder.iterator(); + while (i.hasNext()) { + if (!sortableProps.contains(i.next().getPropertyId())) { + i.remove(); + } + } + + sort(SortEventOriginator.API); + } else { + + // If the new container is not sortable, we'll just re-set the sort + // order altogether. + clearSortOrder(); + } + + datasourceExtension = new RpcDataProviderExtension(container); + datasourceExtension.extend(this, columnKeys); + + /* + * selectionModel == null when the invocation comes from the + * constructor. + */ + if (selectionModel != null) { + selectionModel.reset(); + } + + // Listen to changes in properties and remove columns if needed + if (datasource instanceof PropertySetChangeNotifier) { + ((PropertySetChangeNotifier) datasource) + .addPropertySetChangeListener(propertyListener); + } + /* + * activeRowHandler will be updated by the client-side request that + * occurs on container change - no need to actively re-insert any + * ValueChangeListeners at this point. + */ + + setFrozenColumnCount(0); + + if (columns.isEmpty()) { + // Add columns + for (Object propertyId : datasource.getContainerPropertyIds()) { + Column column = appendColumn(propertyId); + + // Initial sorting is defined by container + if (datasource instanceof Sortable) { + column.setSortable(((Sortable) datasource) + .getSortableContainerPropertyIds().contains( + propertyId)); + } + } + } else { + Collection<?> properties = datasource.getContainerPropertyIds(); + for (Object property : columns.keySet()) { + if (!properties.contains(property)) { + throw new IllegalStateException( + "Found at least one column in Grid that does not exist in the given container: " + + property + + " with the header \"" + + getColumn(property).getHeaderCaption() + + "\""); + } + } + } + } + + /** + * Returns the grid data source. + * + * @return the container data source of the grid + */ + public Container.Indexed getContainerDataSource() { + return datasource; + } + + /** + * Returns a column based on the property id + * + * @param propertyId + * the property id of the column + * @return the column or <code>null</code> if not found + */ + public Column getColumn(Object propertyId) { + return columns.get(propertyId); + } + + /** + * Returns a copy of currently configures columns in their current visual + * order in this Grid. + * + * @return unmodifiable copy of current columns in visual order + */ + public List<Column> getColumns() { + List<Column> columns = new ArrayList<Grid.Column>(); + for (String columnId : getState(false).columnOrder) { + columns.add(getColumnByColumnId(columnId)); + } + return Collections.unmodifiableList(columns); + } + + /** + * Adds a new Column to Grid. Also adds the property to container with data + * type String, if property for column does not exist in it. Default value + * for the new property is an empty String. + * <p> + * Note that adding a new property is only done for the default container + * that Grid sets up with the default constructor. + * + * @param propertyId + * the property id of the new column + * @return the new column + * + * @throws IllegalStateException + * if column for given property already exists in this grid + */ + + public Column addColumn(Object propertyId) throws IllegalStateException { + if (datasource.getContainerPropertyIds().contains(propertyId) + && !columns.containsKey(propertyId)) { + appendColumn(propertyId); + } else { + addColumnProperty(propertyId, String.class, ""); + } + return getColumn(propertyId); + } + + /** + * Adds a new Column to Grid. This function makes sure that the property + * with the given id and data type exists in the container. If property does + * not exists, it will be created. + * <p> + * Default value for the new property is 0 if type is Integer, Double and + * Float. If type is String, default value is an empty string. For all other + * types the default value is null. + * <p> + * Note that adding a new property is only done for the default container + * that Grid sets up with the default constructor. + * + * @param propertyId + * the property id of the new column + * @param type + * the data type for the new property + * @return the new column + * + * @throws IllegalStateException + * if column for given property already exists in this grid or + * property already exists in the container with wrong type + */ + public Column addColumn(Object propertyId, Class<?> type) { + addColumnProperty(propertyId, type, null); + return getColumn(propertyId); + } + + protected void addColumnProperty(Object propertyId, Class<?> type, + Object defaultValue) throws IllegalStateException { + if (!defaultContainer) { + throw new IllegalStateException( + "Container for this Grid is not a default container from Grid() constructor"); + } + + if (!columns.containsKey(propertyId)) { + if (!datasource.getContainerPropertyIds().contains(propertyId)) { + datasource.addContainerProperty(propertyId, type, defaultValue); + } else { + Property<?> containerProperty = datasource + .getContainerProperty(datasource.firstItemId(), + propertyId); + if (containerProperty.getType() == type) { + appendColumn(propertyId); + } else { + throw new IllegalStateException( + "DataSource already has the given property " + + propertyId + " with a different type"); + } + } + } else { + throw new IllegalStateException( + "Grid already has a column for property " + propertyId); + } + } + + /** + * Removes all columns from this Grid. + */ + public void removeAllColumns() { + Set<Object> properties = new HashSet<Object>(columns.keySet()); + for (Object propertyId : properties) { + removeColumn(propertyId); + } + } + + /** + * Used internally by the {@link Grid} to get a {@link Column} by + * referencing its generated state id. Also used by {@link Column} to verify + * if it has been detached from the {@link Grid}. + * + * @param columnId + * the client id generated for the column when the column is + * added to the grid + * @return the column with the id or <code>null</code> if not found + */ + Column getColumnByColumnId(String columnId) { + Object propertyId = getPropertyIdByColumnId(columnId); + return getColumn(propertyId); + } + + /** + * Used internally by the {@link Grid} to get a property id by referencing + * the columns generated state id. + * + * @param columnId + * The state id of the column + * @return The column instance or null if not found + */ + Object getPropertyIdByColumnId(String columnId) { + return columnKeys.get(columnId); + } + + @Override + protected GridState getState() { + return (GridState) super.getState(); + } + + @Override + protected GridState getState(boolean markAsDirty) { + return (GridState) super.getState(markAsDirty); + } + + /** + * Creates a new column based on a property id and appends it as the last + * column. + * + * @param datasourcePropertyId + * The property id of a property in the datasource + */ + private Column appendColumn(Object datasourcePropertyId) { + if (datasourcePropertyId == null) { + throw new IllegalArgumentException("Property id cannot be null"); + } + assert datasource.getContainerPropertyIds().contains( + datasourcePropertyId) : "Datasource should contain the property id"; + + GridColumnState columnState = new GridColumnState(); + columnState.id = columnKeys.key(datasourcePropertyId); + + Column column = new Column(this, columnState, datasourcePropertyId); + columns.put(datasourcePropertyId, column); + + getState().columns.add(columnState); + getState().columnOrder.add(columnState.id); + header.addColumn(datasourcePropertyId); + footer.addColumn(datasourcePropertyId); + + column.setHeaderCaption(SharedUtil.camelCaseToHumanFriendly(String + .valueOf(datasourcePropertyId))); + + return column; + } + + /** + * Removes a column from Grid based on a property id. + * + * @param propertyId + * The property id of column to be removed + */ + public void removeColumn(Object propertyId) { + header.removeColumn(propertyId); + footer.removeColumn(propertyId); + Column column = columns.remove(propertyId); + getState().columnOrder.remove(columnKeys.key(propertyId)); + getState().columns.remove(column.getState()); + removeExtension(column.getRenderer()); + } + + /** + * Sets a new column order for the grid. All columns which are not ordered + * here will remain in the order they were before as the last columns of + * grid. + * + * @param propertyIds + * properties in the order columns should be + */ + public void setColumnOrder(Object... propertyIds) { + List<String> columnOrder = new ArrayList<String>(); + for (Object propertyId : propertyIds) { + if (columns.containsKey(propertyId)) { + columnOrder.add(columnKeys.key(propertyId)); + } else { + throw new IllegalArgumentException( + "Grid does not contain column for property " + + String.valueOf(propertyId)); + } + } + + List<String> stateColumnOrder = getState().columnOrder; + if (stateColumnOrder.size() != columnOrder.size()) { + stateColumnOrder.removeAll(columnOrder); + columnOrder.addAll(stateColumnOrder); + } + getState().columnOrder = columnOrder; + } + + /** + * Sets the number of frozen columns in this grid. Setting the count to 0 + * means that no data columns will be frozen, but the built-in selection + * checkbox column will still be frozen if it's in use. Setting the count to + * -1 will also disable the selection column. + * <p> + * The default value is 0. + * + * @param numberOfColumns + * the number of columns that should be frozen + * + * @throws IllegalArgumentException + * if the column count is < 0 or > the number of visible columns + */ + public void setFrozenColumnCount(int numberOfColumns) { + if (numberOfColumns < -1 || numberOfColumns > columns.size()) { + throw new IllegalArgumentException( + "count must be between -1 and the current number of columns (" + + columns + ")"); + } + + getState().frozenColumnCount = numberOfColumns; + } + + /** + * Gets the number of frozen columns in this grid. 0 means that no data + * columns will be frozen, but the built-in selection checkbox column will + * still be frozen if it's in use. -1 means that not even the selection + * column is frozen. + * + * @see #setFrozenColumnCount(int) + * + * @return the number of frozen columns + */ + public int getFrozenColumnCount() { + return getState(false).frozenColumnCount; + } + + /** + * Scrolls to a certain item, using {@link ScrollDestination#ANY}. + * + * @param itemId + * id of item to scroll to. + * @throws IllegalArgumentException + * if the provided id is not recognized by the data source. + */ + public void scrollTo(Object itemId) throws IllegalArgumentException { + scrollTo(itemId, ScrollDestination.ANY); + } + + /** + * Scrolls to a certain item, using user-specified scroll destination. + * + * @param itemId + * id of item to scroll to. + * @param destination + * value specifying desired position of scrolled-to row. + * @throws IllegalArgumentException + * if the provided id is not recognized by the data source. + */ + public void scrollTo(Object itemId, ScrollDestination destination) + throws IllegalArgumentException { + + int row = datasource.indexOfId(itemId); + + if (row == -1) { + throw new IllegalArgumentException( + "Item with specified ID does not exist in data source"); + } + + GridClientRpc clientRPC = getRpcProxy(GridClientRpc.class); + clientRPC.scrollToRow(row, destination); + } + + /** + * Scrolls to the beginning of the first data row. + */ + public void scrollToStart() { + GridClientRpc clientRPC = getRpcProxy(GridClientRpc.class); + clientRPC.scrollToStart(); + } + + /** + * Scrolls to the end of the last data row. + */ + public void scrollToEnd() { + GridClientRpc clientRPC = getRpcProxy(GridClientRpc.class); + clientRPC.scrollToEnd(); + } + + /** + * Sets the number of rows that should be visible in Grid's body, while + * {@link #getHeightMode()} is {@link HeightMode#ROW}. + * <p> + * If Grid is currently not in {@link HeightMode#ROW}, the given value is + * remembered, and applied once the mode is applied. + * + * @param rows + * The height in terms of number of rows displayed in Grid's + * body. If Grid doesn't contain enough rows, white space is + * displayed instead. If <code>null</code> is given, then Grid's + * height is undefined + * @throws IllegalArgumentException + * if {@code rows} is zero or less + * @throws IllegalArgumentException + * if {@code rows} is {@link Double#isInifinite(double) + * infinite} + * @throws IllegalArgumentException + * if {@code rows} is {@link Double#isNaN(double) NaN} + */ + public void setHeightByRows(double rows) { + if (rows <= 0.0d) { + throw new IllegalArgumentException( + "More than zero rows must be shown."); + } else if (Double.isInfinite(rows)) { + throw new IllegalArgumentException( + "Grid doesn't support infinite heights"); + } else if (Double.isNaN(rows)) { + throw new IllegalArgumentException("NaN is not a valid row count"); + } + + getState().heightByRows = rows; + } + + /** + * Gets the amount of rows in Grid's body that are shown, while + * {@link #getHeightMode()} is {@link HeightMode#ROW}. + * + * @return the amount of rows that are being shown in Grid's body + * @see #setHeightByRows(double) + */ + public double getHeightByRows() { + return getState(false).heightByRows; + } + + /** + * {@inheritDoc} + * <p> + * <em>Note:</em> This method will change the widget's size in the browser + * only if {@link #getHeightMode()} returns {@link HeightMode#CSS}. + * + * @see #setHeightMode(HeightMode) + */ + @Override + public void setHeight(float height, Unit unit) { + super.setHeight(height, unit); + } + + /** + * Defines the mode in which the Grid widget's height is calculated. + * <p> + * If {@link HeightMode#CSS} is given, Grid will respect the values given + * via a {@code setHeight}-method, and behave as a traditional Component. + * <p> + * If {@link HeightMode#ROW} is given, Grid will make sure that the body + * will display as many rows as {@link #getHeightByRows()} defines. + * <em>Note:</em> If headers/footers are inserted or removed, the widget + * will resize itself to still display the required amount of rows in its + * body. It also takes the horizontal scrollbar into account. + * + * @param heightMode + * the mode in to which Grid should be set + */ + public void setHeightMode(HeightMode heightMode) { + /* + * This method is a workaround for the fact that Vaadin re-applies + * widget dimensions (height/width) on each state change event. The + * original design was to have setHeight an setHeightByRow be equals, + * and whichever was called the latest was considered in effect. + * + * But, because of Vaadin always calling setHeight on the widget, this + * approach doesn't work. + */ + + getState().heightMode = heightMode; + } + + /** + * Returns the current {@link HeightMode} the Grid is in. + * <p> + * Defaults to {@link HeightMode#CSS}. + * + * @return the current HeightMode + */ + public HeightMode getHeightMode() { + return getState(false).heightMode; + } + + /* Selection related methods: */ + + /** + * Takes a new {@link SelectionModel} into use. + * <p> + * The SelectionModel that is previously in use will have all its items + * deselected. + * <p> + * If the given SelectionModel is already in use, this method does nothing. + * + * @param selectionModel + * the new SelectionModel to use + * @throws IllegalArgumentException + * if {@code selectionModel} is <code>null</code> + */ + public void setSelectionModel(SelectionModel selectionModel) + throws IllegalArgumentException { + if (selectionModel == null) { + throw new IllegalArgumentException( + "Selection model may not be null"); + } + + if (this.selectionModel != selectionModel) { + // this.selectionModel is null on init + if (this.selectionModel != null) { + this.selectionModel.reset(); + this.selectionModel.setGrid(null); + } + + this.selectionModel = selectionModel; + this.selectionModel.setGrid(this); + this.selectionModel.reset(); + + if (selectionModel.getClass().equals(SingleSelectionModel.class)) { + getState().selectionMode = SharedSelectionMode.SINGLE; + } else if (selectionModel.getClass().equals( + MultiSelectionModel.class)) { + getState().selectionMode = SharedSelectionMode.MULTI; + } else if (selectionModel.getClass().equals(NoSelectionModel.class)) { + getState().selectionMode = SharedSelectionMode.NONE; + } else { + throw new UnsupportedOperationException("Grid currently " + + "supports only its own bundled selection models"); + } + } + } + + /** + * Returns the currently used {@link SelectionModel}. + * + * @return the currently used SelectionModel + */ + public SelectionModel getSelectionModel() { + return selectionModel; + } + + /** + * Changes the Grid's selection mode. + * <p> + * Grid supports three selection modes: multiselect, single select and no + * selection, and this is a conveniency method for choosing between one of + * them. + * <P> + * Technically, this method is a shortcut that can be used instead of + * calling {@code setSelectionModel} with a specific SelectionModel + * instance. Grid comes with three built-in SelectionModel classes, and the + * {@link SelectionMode} enum represents each of them. + * <p> + * Essentially, the two following method calls are equivalent: + * <p> + * <code><pre> + * grid.setSelectionMode(SelectionMode.MULTI); + * grid.setSelectionModel(new MultiSelectionMode()); + * </pre></code> + * + * + * @param selectionMode + * the selection mode to switch to + * @return The {@link SelectionModel} instance that was taken into use + * @throws IllegalArgumentException + * if {@code selectionMode} is <code>null</code> + * @see SelectionModel + */ + public SelectionModel setSelectionMode(final SelectionMode selectionMode) + throws IllegalArgumentException { + if (selectionMode == null) { + throw new IllegalArgumentException("selection mode may not be null"); + } + final SelectionModel newSelectionModel = selectionMode.createModel(); + setSelectionModel(newSelectionModel); + return newSelectionModel; + } + + /** + * Checks whether an item is selected or not. + * + * @param itemId + * the item id to check for + * @return <code>true</code> iff the item is selected + */ + // keep this javadoc in sync with SelectionModel.isSelected + public boolean isSelected(Object itemId) { + return selectionModel.isSelected(itemId); + } + + /** + * Returns a collection of all the currently selected itemIds. + * <p> + * This method is a shorthand that is forwarded to the object that is + * returned by {@link #getSelectionModel()}. + * + * @return a collection of all the currently selected itemIds + */ + // keep this javadoc in sync with SelectionModel.getSelectedRows + public Collection<Object> getSelectedRows() { + return getSelectionModel().getSelectedRows(); + } + + /** + * Gets the item id of the currently selected item. + * <p> + * This method is a shorthand that is forwarded to the object that is + * returned by {@link #getSelectionModel()}. Only + * {@link SelectionModel.Single} is supported. + * + * @return the item id of the currently selected item, or <code>null</code> + * if nothing is selected + * @throws IllegalStateException + * if the object that is returned by + * {@link #getSelectionModel()} is not an instance of + * {@link SelectionModel.Single} + */ + // keep this javadoc in sync with SelectionModel.Single.getSelectedRow + public Object getSelectedRow() throws IllegalStateException { + if (selectionModel instanceof SelectionModel.Single) { + return ((SelectionModel.Single) selectionModel).getSelectedRow(); + } else { + throw new IllegalStateException(Grid.class.getSimpleName() + + " does not support the 'getSelectedRow' shortcut method " + + "unless the selection model implements " + + SelectionModel.Single.class.getName() + + ". The current one does not (" + + selectionModel.getClass().getName() + ")"); + } + } + + /** + * Marks an item as selected. + * <p> + * This method is a shorthand that is forwarded to the object that is + * returned by {@link #getSelectionModel()}. Only + * {@link SelectionModel.Single} or {@link SelectionModel.Multi} are + * supported. + * + * + * @param itemIds + * the itemId to mark as selected + * @return <code>true</code> if the selection state changed. + * <code>false</code> if the itemId already was selected + * @throws IllegalArgumentException + * if the {@code itemId} doesn't exist in the currently active + * Container + * @throws IllegalStateException + * if the selection was illegal. One such reason might be that + * the implementation already had an item selected, and that + * needs to be explicitly deselected before re-selecting + * something + * @throws IllegalStateException + * if the object that is returned by + * {@link #getSelectionModel()} does not implement + * {@link SelectionModel.Single} or {@link SelectionModel.Multi} + */ + // keep this javadoc in sync with SelectionModel.Single.select + public boolean select(Object itemId) throws IllegalArgumentException, + IllegalStateException { + if (selectionModel instanceof SelectionModel.Single) { + return ((SelectionModel.Single) selectionModel).select(itemId); + } else if (selectionModel instanceof SelectionModel.Multi) { + return ((SelectionModel.Multi) selectionModel).select(itemId); + } else { + throw new IllegalStateException(Grid.class.getSimpleName() + + " does not support the 'select' shortcut method " + + "unless the selection model implements " + + SelectionModel.Single.class.getName() + " or " + + SelectionModel.Multi.class.getName() + + ". The current one does not (" + + selectionModel.getClass().getName() + ")."); + } + } + + /** + * Marks an item as deselected. + * <p> + * This method is a shorthand that is forwarded to the object that is + * returned by {@link #getSelectionModel()}. Only + * {@link SelectionModel.Single} and {@link SelectionModel.Multi} are + * supported. + * + * @param itemId + * the itemId to remove from being selected + * @return <code>true</code> if the selection state changed. + * <code>false</code> if the itemId already was selected + * @throws IllegalArgumentException + * if the {@code itemId} doesn't exist in the currently active + * Container + * @throws IllegalStateException + * if the deselection was illegal. One such reason might be that + * the implementation already had an item selected, and that + * needs to be explicitly deselected before re-selecting + * something + * @throws IllegalStateException + * if the object that is returned by + * {@link #getSelectionModel()} does not implement + * {@link SelectionModel.Single} or {@link SelectionModel.Multi} + */ + // keep this javadoc in sync with SelectionModel.Single.deselect + public boolean deselect(Object itemId) throws IllegalStateException { + if (selectionModel instanceof SelectionModel.Single) { + if (isSelected(itemId)) { + return ((SelectionModel.Single) selectionModel).select(null); + } + return false; + } else if (selectionModel instanceof SelectionModel.Multi) { + return ((SelectionModel.Multi) selectionModel).deselect(itemId); + } else { + throw new IllegalStateException(Grid.class.getSimpleName() + + " does not support the 'deselect' shortcut method " + + "unless the selection model implements " + + SelectionModel.Single.class.getName() + " or " + + SelectionModel.Multi.class.getName() + + ". The current one does not (" + + selectionModel.getClass().getName() + ")."); + } + } + + /** + * Fires a selection change event. + * <p> + * <strong>Note:</strong> This is not a method that should be called by + * application logic. This method is publicly accessible only so that + * {@link SelectionModel SelectionModels} would be able to inform Grid of + * these events. + * + * @param addedSelections + * the selections that were added by this event + * @param removedSelections + * the selections that were removed by this event + */ + public void fireSelectionChangeEvent(Collection<Object> oldSelection, + Collection<Object> newSelection) { + fireEvent(new SelectionChangeEvent(this, oldSelection, newSelection)); + } + + @Override + public void addSelectionChangeListener(SelectionChangeListener listener) { + addListener(SelectionChangeEvent.class, listener, + SELECTION_CHANGE_METHOD); + } + + @Override + public void removeSelectionChangeListener(SelectionChangeListener listener) { + removeListener(SelectionChangeEvent.class, listener, + SELECTION_CHANGE_METHOD); + } + + /** + * Gets the + * {@link com.vaadin.data.RpcDataProviderExtension.DataProviderKeyMapper + * DataProviderKeyMapper} being used by the data source. + * + * @return the key mapper being used by the data source + */ + DataProviderKeyMapper getKeyMapper() { + return datasourceExtension.getKeyMapper(); + } + + /** + * Adds a renderer to this grid's connector hierarchy. + * + * @param renderer + * the renderer to add + */ + void addRenderer(Renderer<?> renderer) { + addExtension(renderer); + } + + /** + * Sets the current sort order using the fluid Sort API. Read the + * documentation for {@link Sort} for more information. + * + * @param s + * a sort instance + */ + public void sort(Sort s) { + setSortOrder(s.build()); + } + + /** + * Sort this Grid in ascending order by a specified property. + * + * @param propertyId + * a property ID + */ + public void sort(Object propertyId) { + sort(propertyId, SortDirection.ASCENDING); + } + + /** + * Sort this Grid in user-specified {@link SortOrder} by a property. + * + * @param propertyId + * a property ID + * @param direction + * a sort order value (ascending/descending) + */ + public void sort(Object propertyId, SortDirection direction) { + sort(Sort.by(propertyId, direction)); + } + + /** + * Clear the current sort order, and re-sort the grid. + */ + public void clearSortOrder() { + sortOrder.clear(); + sort(false); + } + + /** + * Sets the sort order to use. This method throws + * {@link IllegalStateException} if the attached container is not a + * {@link Container.Sortable}, and {@link IllegalArgumentException} if a + * property in the list is not recognized by the container, or if the + * 'order' parameter is null. + * + * @param order + * a sort order list. + */ + public void setSortOrder(List<SortOrder> order) { + setSortOrder(order, SortEventOriginator.API); + } + + private void setSortOrder(List<SortOrder> order, + SortEventOriginator originator) { + if (!(getContainerDataSource() instanceof Container.Sortable)) { + throw new IllegalStateException( + "Attached container is not sortable (does not implement Container.Sortable)"); + } + + if (order == null) { + throw new IllegalArgumentException("Order list may not be null!"); + } + + sortOrder.clear(); + + Collection<?> sortableProps = ((Container.Sortable) getContainerDataSource()) + .getSortableContainerPropertyIds(); + + for (SortOrder o : order) { + if (!sortableProps.contains(o.getPropertyId())) { + throw new IllegalArgumentException( + "Property " + + o.getPropertyId() + + " does not exist or is not sortable in the current container"); + } + } + + sortOrder.addAll(order); + sort(originator); + } + + /** + * Get the current sort order list. + * + * @return a sort order list + */ + public List<SortOrder> getSortOrder() { + return Collections.unmodifiableList(sortOrder); + } + + /** + * Apply sorting to data source. + */ + private void sort(SortEventOriginator originator) { + + Container c = getContainerDataSource(); + if (c instanceof Container.Sortable) { + Container.Sortable cs = (Container.Sortable) c; + + final int items = sortOrder.size(); + Object[] propertyIds = new Object[items]; + boolean[] directions = new boolean[items]; + + String[] columnKeys = new String[items]; + SortDirection[] stateDirs = new SortDirection[items]; + + for (int i = 0; i < items; ++i) { + SortOrder order = sortOrder.get(i); + + columnKeys[i] = this.columnKeys.key(order.getPropertyId()); + stateDirs[i] = order.getDirection(); + + propertyIds[i] = order.getPropertyId(); + switch (order.getDirection()) { + case ASCENDING: + directions[i] = true; + break; + case DESCENDING: + directions[i] = false; + break; + default: + throw new IllegalArgumentException("getDirection() of " + + order + " returned an unexpected value"); + } + } + + cs.sort(propertyIds, directions); + + fireEvent(new SortOrderChangeEvent(this, new ArrayList<SortOrder>( + sortOrder), originator)); + + getState().sortColumns = columnKeys; + getState(false).sortDirs = stateDirs; + } else { + throw new IllegalStateException( + "Container is not sortable (does not implement Container.Sortable)"); + } + } + + /** + * Adds a sort order change listener that gets notified when the sort order + * changes. + * + * @param listener + * the sort order change listener to add + */ + @Override + public void addSortOrderChangeListener(SortOrderChangeListener listener) { + addListener(SortOrderChangeEvent.class, listener, + SORT_ORDER_CHANGE_METHOD); + } + + /** + * Removes a sort order change listener previously added using + * {@link #addSortOrderChangeListener(SortOrderChangeListener)}. + * + * @param listener + * the sort order change listener to remove + */ + @Override + public void removeSortOrderChangeListener(SortOrderChangeListener listener) { + removeListener(SortOrderChangeEvent.class, listener, + SORT_ORDER_CHANGE_METHOD); + } + + /* Grid Headers */ + + /** + * Returns the header section of this grid. The default header contains a + * single row displaying the column captions. + * + * @return the header + */ + protected Header getHeader() { + return header; + } + + /** + * Gets the header row at given index. + * + * @param rowIndex + * 0 based index for row. Counted from top to bottom + * @return header row at given index + * @throws IllegalArgumentException + * if no row exists at given index + */ + public HeaderRow getHeaderRow(int rowIndex) { + return header.getRow(rowIndex); + } + + /** + * Inserts a new row at the given position to the header section. Shifts the + * row currently at that position and any subsequent rows down (adds one to + * their indices). + * + * @param index + * the position at which to insert the row + * @return the new row + * + * @throws IllegalArgumentException + * if the index is less than 0 or greater than row count + * @see #appendHeaderRow() + * @see #prependHeaderRow() + * @see #removeHeaderRow(HeaderRow) + * @see #removeHeaderRow(int) + */ + public HeaderRow addHeaderRowAt(int index) { + return header.addRowAt(index); + } + + /** + * Adds a new row at the bottom of the header section. + * + * @return the new row + * @see #prependHeaderRow() + * @see #addHeaderRowAt(int) + * @see #removeHeaderRow(HeaderRow) + * @see #removeHeaderRow(int) + */ + public HeaderRow appendHeaderRow() { + return header.appendRow(); + } + + /** + * Returns the current default row of the header section. The default row is + * a special header row providing a user interface for sorting columns. + * Setting a header text for column updates cells in the default header. + * + * @return the default row or null if no default row set + */ + public HeaderRow getDefaultHeaderRow() { + return header.getDefaultRow(); + } + + /** + * Gets the row count for the header section. + * + * @return row count + */ + public int getHeaderRowCount() { + return header.getRowCount(); + } + + /** + * Adds a new row at the top of the header section. + * + * @return the new row + * @see #appendHeaderRow() + * @see #addHeaderRowAt(int) + * @see #removeHeaderRow(HeaderRow) + * @see #removeHeaderRow(int) + */ + public HeaderRow prependHeaderRow() { + return header.prependRow(); + } + + /** + * Removes the given row from the header section. + * + * @param row + * the row to be removed + * + * @throws IllegalArgumentException + * if the row does not exist in this section + * @see #removeHeaderRow(int) + * @see #addHeaderRowAt(int) + * @see #appendHeaderRow() + * @see #prependHeaderRow() + */ + public void removeHeaderRow(HeaderRow row) { + header.removeRow(row); + } + + /** + * Removes the row at the given position from the header section. + * + * @param index + * the position of the row + * + * @throws IllegalArgumentException + * if no row exists at given index + * @see #removeHeaderRow(HeaderRow) + * @see #addHeaderRowAt(int) + * @see #appendHeaderRow() + * @see #prependHeaderRow() + */ + public void removeHeaderRow(int rowIndex) { + header.removeRow(rowIndex); + } + + /** + * Sets the default row of the header. The default row is a special header + * row providing a user interface for sorting columns. + * + * @param row + * the new default row, or null for no default row + * + * @throws IllegalArgumentException + * header does not contain the row + */ + public void setDefaultHeaderRow(HeaderRow row) { + header.setDefaultRow(row); + } + + /** + * Sets the visibility of the header section. + * + * @param visible + * true to show header section, false to hide + */ + public void setHeaderVisible(boolean visible) { + header.setVisible(visible); + } + + /** + * Returns the visibility of the header section. + * + * @return true if visible, false otherwise. + */ + public boolean isHeaderVisible() { + return header.isVisible(); + } + + /* Grid Footers */ + + /** + * Returns the footer section of this grid. The default header contains a + * single row displaying the column captions. + * + * @return the footer + */ + protected Footer getFooter() { + return footer; + } + + /** + * Gets the footer row at given index. + * + * @param rowIndex + * 0 based index for row. Counted from top to bottom + * @return footer row at given index + * @throws IllegalArgumentException + * if no row exists at given index + */ + public FooterRow getFooterRow(int rowIndex) { + return footer.getRow(rowIndex); + } + + /** + * Inserts a new row at the given position to the footer section. Shifts the + * row currently at that position and any subsequent rows down (adds one to + * their indices). + * + * @param index + * the position at which to insert the row + * @return the new row + * + * @throws IllegalArgumentException + * if the index is less than 0 or greater than row count + * @see #appendFooterRow() + * @see #prependFooterRow() + * @see #removeFooterRow(FooterRow) + * @see #removeFooterRow(int) + */ + public FooterRow addFooterRowAt(int index) { + return footer.addRowAt(index); + } + + /** + * Adds a new row at the bottom of the footer section. + * + * @return the new row + * @see #prependFooterRow() + * @see #addFooterRowAt(int) + * @see #removeFooterRow(FooterRow) + * @see #removeFooterRow(int) + */ + public FooterRow appendFooterRow() { + return footer.appendRow(); + } + + /** + * Gets the row count for the footer. + * + * @return row count + */ + public int getFooterRowCount() { + return footer.getRowCount(); + } + + /** + * Adds a new row at the top of the footer section. + * + * @return the new row + * @see #appendFooterRow() + * @see #addFooterRowAt(int) + * @see #removeFooterRow(FooterRow) + * @see #removeFooterRow(int) + */ + public FooterRow prependFooterRow() { + return footer.prependRow(); + } + + /** + * Removes the given row from the footer section. + * + * @param row + * the row to be removed + * + * @throws IllegalArgumentException + * if the row does not exist in this section + * @see #removeFooterRow(int) + * @see #addFooterRowAt(int) + * @see #appendFooterRow() + * @see #prependFooterRow() + */ + public void removeFooterRow(FooterRow row) { + footer.removeRow(row); + } + + /** + * Removes the row at the given position from the footer section. + * + * @param index + * the position of the row + * + * @throws IllegalArgumentException + * if no row exists at given index + * @see #removeFooterRow(FooterRow) + * @see #addFooterRowAt(int) + * @see #appendFooterRow() + * @see #prependFooterRow() + */ + public void removeFooterRow(int rowIndex) { + footer.removeRow(rowIndex); + } + + /** + * Sets the visibility of the footer section. + * + * @param visible + * true to show footer section, false to hide + */ + public void setFooterVisible(boolean visible) { + footer.setVisible(visible); + } + + /** + * Returns the visibility of the footer section. + * + * @return true if visible, false otherwise. + */ + public boolean isFooterVisible() { + return footer.isVisible(); + } + + @Override + public Iterator<Component> iterator() { + List<Component> componentList = new ArrayList<Component>(); + + Header header = getHeader(); + for (int i = 0; i < header.getRowCount(); ++i) { + HeaderRow row = header.getRow(i); + for (Object propId : columns.keySet()) { + HeaderCell cell = row.getCell(propId); + if (cell.getCellState().type == GridStaticCellType.WIDGET) { + componentList.add(cell.getComponent()); + } + } + } + + Footer footer = getFooter(); + for (int i = 0; i < footer.getRowCount(); ++i) { + FooterRow row = footer.getRow(i); + for (Object propId : columns.keySet()) { + FooterCell cell = row.getCell(propId); + if (cell.getCellState().type == GridStaticCellType.WIDGET) { + componentList.add(cell.getComponent()); + } + } + } + + componentList.addAll(getEditorRowFields()); + return componentList.iterator(); + } + + @Override + public boolean isRendered(Component childComponent) { + if (getEditorRowFields().contains(childComponent)) { + // Only render editor row fields if the editor is open + return isEditorRowActive(); + } else { + // TODO Header and footer components should also only be rendered if + // the header/footer is visible + return true; + } + } + + EditorRowClientRpc getEditorRowRpc() { + return getRpcProxy(EditorRowClientRpc.class); + } + + /** + * Sets the cell style generator that is used for generating styles for rows + * and cells. + * + * @param cellStyleGenerator + * the cell style generator to set, or <code>null</code> to + * remove a previously set generator + */ + public void setCellStyleGenerator(CellStyleGenerator cellStyleGenerator) { + this.cellStyleGenerator = cellStyleGenerator; + getState().hasCellStyleGenerator = (cellStyleGenerator != null); + + datasourceExtension.refreshCache(); + } + + /** + * Gets the cell style generator that is used for generating styles for rows + * and cells. + * + * @return the cell style generator, or <code>null</code> if no generator is + * set + */ + public CellStyleGenerator getCellStyleGenerator() { + return cellStyleGenerator; + } + + /** + * Adds a row to the underlying container. The order of the parameters + * should match the current visible column order. + * <p> + * Please note that it's generally only safe to use this method during + * initialization. After Grid has been initialized and the visible column + * order might have been changed, it's better to instead add items directly + * to the underlying container and use {@link Item#getItemProperty(Object)} + * to make sure each value is assigned to the intended property. + * + * @param values + * the cell values of the new row, in the same order as the + * visible column order, not <code>null</code>. + * @return the item id of the new row + * @throws IllegalArgumentException + * if values is null + * @throws IllegalArgumentException + * if its length does not match the number of visible columns + * @throws IllegalArgumentException + * if a parameter value is not an instance of the corresponding + * property type + * @throws UnsupportedOperationException + * if the container does not support adding new items + */ + public Object addRow(Object... values) { + if (values == null) { + throw new IllegalArgumentException("Values cannot be null"); + } + + Indexed dataSource = getContainerDataSource(); + List<String> columnOrder = getState(false).columnOrder; + + if (values.length != columnOrder.size()) { + throw new IllegalArgumentException("There are " + + columnOrder.size() + " visible columns, but " + + values.length + " cell values were provided."); + } + + // First verify all parameter types + for (int i = 0; i < columnOrder.size(); i++) { + Object propertyId = getPropertyIdByColumnId(columnOrder.get(i)); + + Class<?> propertyType = dataSource.getType(propertyId); + if (values[i] != null && !propertyType.isInstance(values[i])) { + throw new IllegalArgumentException("Parameter " + i + "(" + + values[i] + ") is not an instance of " + + propertyType.getCanonicalName()); + } + } + + Object itemId = dataSource.addItem(); + try { + Item item = dataSource.getItem(itemId); + for (int i = 0; i < columnOrder.size(); i++) { + Object propertyId = getPropertyIdByColumnId(columnOrder.get(i)); + Property<Object> property = item.getItemProperty(propertyId); + property.setValue(values[i]); + } + } catch (RuntimeException e) { + try { + dataSource.removeItem(itemId); + } catch (Exception e2) { + getLogger().log(Level.SEVERE, + "Error recovering from exception in addRow", e); + } + throw e; + } + + return itemId; + } + + private static Logger getLogger() { + return Logger.getLogger(Grid.class.getName()); + } + + /** + * Sets whether or not the editor row feature is enabled for this grid. + * + * @param isEnabled + * <code>true</code> to enable the feature, <code>false</code> + * otherwise + * @throws IllegalStateException + * if an item is currently being edited + * @see #getEditedItemId() + */ + public void setEditorRowEnabled(boolean isEnabled) + throws IllegalStateException { + if (isEditorRowActive()) { + throw new IllegalStateException( + "Cannot disable the editor row while an item (" + + getEditedItemId() + ") is being edited."); + } + if (isEditorRowEnabled() != isEnabled) { + getState().editorRowEnabled = isEnabled; + } + } + + /** + * Checks whether the editor row feature is enabled for this grid. + * + * @return <code>true</code> iff the editor row feature is enabled for this + * grid + * @see #getEditedItemId() + */ + public boolean isEditorRowEnabled() { + return getState(false).editorRowEnabled; + } + + /** + * Gets the id of the item that is currently being edited. + * + * @return the id of the item that is currently being edited, or + * <code>null</code> if no item is being edited at the moment + */ + public Object getEditedItemId() { + return editedItemId; + } + + /** + * Gets the field group that is backing the editor row of this grid. + * + * @return the backing field group + */ + public FieldGroup getEditorRowFieldGroup() { + return editorRowFieldGroup; + } + + /** + * Sets the field group that is backing this editor row. + * + * @param fieldGroup + * the backing field group + */ + public void setEditorRowFieldGroup(FieldGroup fieldGroup) { + editorRowFieldGroup = fieldGroup; + if (isEditorRowActive()) { + editorRowFieldGroup.setItemDataSource(getContainerDataSource() + .getItem(editedItemId)); + } + } + + /** + * Returns whether an item is currently being edited in the editor row. + * + * @return true iff the editor row is editing an item + */ + public boolean isEditorRowActive() { + return editedItemId != null; + } + + /** + * Sets a property editable or not. + * <p> + * In order for a user to edit a particular value with a Field, it needs to + * be both non-readonly and editable. + * <p> + * The difference between read-only and uneditable is that the read-only + * state is propagated back into the property, while the editable property + * is internal metadata for the editor row. + * + * @param propertyId + * the id of the property to set as editable state + * @param editable + * whether or not {@code propertyId} chould be editable + */ + public void setPropertyEditable(Object propertyId, boolean editable) { + checkPropertyExists(propertyId); + if (getEditorRowField(propertyId) != null) { + getEditorRowField(propertyId).setReadOnly(!editable); + } + if (editable) { + uneditableProperties.remove(propertyId); + } else { + uneditableProperties.add(propertyId); + } + } + + /** + * Checks whether a property is editable through the editor row. + * <p> + * This only checks whether the property is configured as uneditable in the + * editor row. The property's or field's readonly status will ultimately + * decide whether the value can be edited or not. + * + * @param propertyId + * the id of the property to check for editable status + * @return <code>true</code> iff the property is editable according to this + * editor row + */ + public boolean isPropertyEditable(Object propertyId) { + checkPropertyExists(propertyId); + return !uneditableProperties.contains(propertyId); + } + + private void checkPropertyExists(Object propertyId) { + if (!getContainerDataSource().getContainerPropertyIds().contains( + propertyId)) { + throw new IllegalArgumentException("Property with id " + propertyId + + " is not in the current Container"); + } + } + + /** + * Gets the field component that represents a property in the editor row. If + * the property is not yet bound to a field, null is returned. + * <p> + * When {@link #editItem(Object) editItem} is called, fields are + * automatically created and bound for any unbound properties. + * + * @param propertyId + * the property id of the property for which to find the field + * @return the bound field or null if not bound + */ + public Field<?> getEditorRowField(Object propertyId) { + return editorRowFieldGroup.getField(propertyId); + } + + /** + * Opens the editor row for the provided item. + * + * @param itemId + * the id of the item to edit + * @throws IllegalStateException + * if the editor row is not enabled + * @throws IllegalArgumentException + * if the {@code itemId} is not in the backing container + * @see #setEditorRowEnabled(boolean) + */ + public void editItem(Object itemId) throws IllegalStateException, + IllegalArgumentException { + doEditItem(itemId); + + getEditorRowRpc().bind(getContainerDataSource().indexOfId(itemId)); + } + + protected void doEditItem(Object itemId) { + if (!isEditorRowEnabled()) { + throw new IllegalStateException("Editor row is not enabled"); + } + + Item item = getContainerDataSource().getItem(itemId); + if (item == null) { + throw new IllegalArgumentException("Item with id " + itemId + + " not found in current container"); + } + + editorRowFieldGroup.setItemDataSource(item); + editedItemId = itemId; + + for (Object propertyId : item.getItemPropertyIds()) { + + final Field<?> editor; + if (editorRowFieldGroup.getUnboundPropertyIds() + .contains(propertyId)) { + editor = editorRowFieldGroup.buildAndBind(propertyId); + } else { + editor = editorRowFieldGroup.getField(propertyId); + } + + getColumn(propertyId).getState().editorConnector = editor; + + if (editor != null) { + editor.setReadOnly(!isPropertyEditable(propertyId)); + + if (editor.getParent() != Grid.this) { + assert editor.getParent() == null; + editor.setParent(Grid.this); + } + } + } + } + + /** + * Binds the field with the given propertyId from the current item. If an + * item has not been set then the binding is postponed until the item is set + * using {@link #editItem(Object)}. + * <p> + * This method also adds validators when applicable. + * <p> + * <em>Note:</em> This is a pass-through call to the backing field group. + * + * @param field + * The field to bind + * @param propertyId + * The propertyId to bind to the field + * @throws BindException + * If the property id is already bound to another field by this + * field binder + */ + public void bindEditorRowField(Object propertyId, Field<?> field) + throws BindException { + editorRowFieldGroup.bind(field, propertyId); + } + + /** + * Saves all changes done to the bound fields. + * <p> + * <em>Note:</em> This is a pass-through call to the backing field group. + * + * @throws CommitException + * If the commit was aborted + * + * @see FieldGroup#commit() + */ + public void saveEditorRow() throws CommitException { + editorRowFieldGroup.commit(); + } + + /** + * Cancels the currently active edit if any. + */ + public void cancelEditorRow() { + if (isEditorRowActive()) { + getEditorRowRpc().cancel( + getContainerDataSource().indexOfId(editedItemId)); + doCancelEditorRow(); + } + } + + protected void doCancelEditorRow() { + editedItemId = null; + } + + void resetEditorRow() { + if (isEditorRowActive()) { + /* + * Simply force cancel the editing; throwing here would just make + * Grid.setContainerDataSource semantics more complicated. + */ + cancelEditorRow(); + } + for (Field<?> editor : getEditorRowFields()) { + editor.setParent(null); + } + + editedItemId = null; + editorRowFieldGroup = new FieldGroup(); + uneditableProperties = new HashSet<Object>(); + } + + /** + * Gets a collection of all fields bound to the editor row of this grid. + * <p> + * All non-editable fields (either readonly or uneditable) are in read-only + * mode. + * <p> + * When {@link #editItem(Object) editItem} is called, fields are + * automatically created and bound to any unbound properties. + * + * @return a collection of all the fields bound to this editor row + */ + Collection<Field<?>> getEditorRowFields() { + return editorRowFieldGroup.getFields(); + } + + /** + * Sets the field factory for the {@link FieldGroup}. The field factory is + * only used when {@link FieldGroup} creates a new field. + * <p> + * <em>Note:</em> This is a pass-through call to the backing field group. + * + * @param fieldFactory + * The field factory to use + */ + public void setEditorRowFieldFactory(FieldGroupFieldFactory fieldFactory) { + editorRowFieldGroup.setFieldFactory(fieldFactory); + } + + /** + * Returns the error handler of this editor row. + * + * @return the error handler or null if there is no dedicated error handler + * + * @see #setEditorRowErrorHandler(ErrorHandler) + * @see ClientConnector#getErrorHandler() + */ + public ErrorHandler getEditorRowErrorHandler() { + return editorRowErrorHandler; + } + + /** + * Sets the error handler for this editor row. The error handler is invoked + * for exceptions thrown while processing client requests; specifically when + * {@link #saveEditorRow()} triggered by the client throws a + * CommitException. If the error handler is not set, one is looked up via + * Grid. + * + * @param errorHandler + * the error handler to use + * + * @see ClientConnector#setErrorHandler(ErrorHandler) + * @see ErrorEvent#findErrorHandler(ClientConnector) + */ + public void setEditorRowErrorHandler(ErrorHandler errorHandler) { + editorRowErrorHandler = errorHandler; + } + + /** + * Builds a field using the given caption and binds it to the given property + * id using the field binder. Ensures the new field is of the given type. + * <p> + * <em>Note:</em> This is a pass-through call to the backing field group. + * + * @param propertyId + * The property id to bind to. Must be present in the field + * finder + * @param fieldType + * The type of field that we want to create + * @throws BindException + * If the field could not be created + * @return The created and bound field. Can be any type of {@link Field} . + */ + public <T extends Field<?>> T buildAndBind(Object propertyId, + Class<T> fieldType) throws BindException { + return editorRowFieldGroup.buildAndBind(null, propertyId, fieldType); + } + +} diff --git a/server/src/com/vaadin/ui/renderer/ButtonRenderer.java b/server/src/com/vaadin/ui/renderer/ButtonRenderer.java new file mode 100644 index 0000000000..4a7a86daa2 --- /dev/null +++ b/server/src/com/vaadin/ui/renderer/ButtonRenderer.java @@ -0,0 +1,45 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.ui.renderer; + +/** + * A Renderer that displays a button with a textual caption. The value of the + * corresponding property is used as the caption. Click listeners can be added + * to the renderer, invoked when any of the rendered buttons is clicked. + * + * @since + * @author Vaadin Ltd + */ +public class ButtonRenderer extends ClickableRenderer<String> { + + /** + * Creates a new button renderer. + */ + public ButtonRenderer() { + super(String.class); + } + + /** + * Creates a new button renderer and adds the given click listener to it. + * + * @param listener + * the click listener to register + */ + public ButtonRenderer(RendererClickListener listener) { + this(); + addClickListener(listener); + } +} diff --git a/server/src/com/vaadin/ui/renderer/ClickableRenderer.java b/server/src/com/vaadin/ui/renderer/ClickableRenderer.java new file mode 100644 index 0000000000..0d745ab29e --- /dev/null +++ b/server/src/com/vaadin/ui/renderer/ClickableRenderer.java @@ -0,0 +1,121 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.ui.renderer; + +import java.lang.reflect.Method; + +import com.vaadin.event.ConnectorEventListener; +import com.vaadin.event.MouseEvents.ClickEvent; +import com.vaadin.shared.MouseEventDetails; +import com.vaadin.shared.ui.grid.renderers.RendererClickRpc; +import com.vaadin.ui.Grid; +import com.vaadin.ui.Grid.AbstractRenderer; +import com.vaadin.util.ReflectTools; + +/** + * An abstract superclass for Renderers that render clickable items. Click + * listeners can be added to a renderer to be notified when any of the rendered + * items is clicked. + * + * @param <T> + * the type presented by the renderer + * + * @since + * @author Vaadin Ltd + */ +public class ClickableRenderer<T> extends AbstractRenderer<T> { + + /** + * An interface for listening to {@link RendererClickEvent renderer click + * events}. + * + * @see {@link ButtonRenderer#addClickListener(RendererClickListener)} + */ + public interface RendererClickListener extends ConnectorEventListener { + + static final Method CLICK_METHOD = ReflectTools.findMethod( + RendererClickListener.class, "click", RendererClickEvent.class); + + /** + * Called when a rendered button is clicked. + * + * @param event + * the event representing the click + */ + void click(RendererClickEvent event); + } + + /** + * An event fired when a button rendered by a ButtonRenderer is clicked. + */ + public static class RendererClickEvent extends ClickEvent { + + private Object itemId; + + protected RendererClickEvent(Grid source, Object itemId, + MouseEventDetails mouseEventDetails) { + super(source, mouseEventDetails); + this.itemId = itemId; + } + + /** + * Returns the item ID of the row where the click event originated. + * + * @return the item ID of the clicked row + */ + public Object getItemId() { + return itemId; + } + } + + protected ClickableRenderer(Class<T> presentationType) { + super(presentationType); + registerRpc(new RendererClickRpc() { + @Override + public void click(int row, int column, + MouseEventDetails mouseDetails) { + + Grid grid = (Grid) getParent(); + Object itemId = grid.getContainerDataSource().getIdByIndex(row); + // TODO map column index to property ID or send column ID + // instead of index from the client + fireEvent(new RendererClickEvent(grid, itemId, mouseDetails)); + } + }); + } + + /** + * Adds a click listener to this button renderer. The listener is invoked + * every time one of the buttons rendered by this renderer is clicked. + * + * @param listener + * the click listener to be added + */ + public void addClickListener(RendererClickListener listener) { + addListener(RendererClickEvent.class, listener, + RendererClickListener.CLICK_METHOD); + } + + /** + * Removes the given click listener from this renderer. + * + * @param listener + * the click listener to be removed + */ + public void removeClickListener(RendererClickListener listener) { + removeListener(RendererClickEvent.class, listener); + } +} diff --git a/server/src/com/vaadin/ui/renderer/DateRenderer.java b/server/src/com/vaadin/ui/renderer/DateRenderer.java new file mode 100644 index 0000000000..4d1d702993 --- /dev/null +++ b/server/src/com/vaadin/ui/renderer/DateRenderer.java @@ -0,0 +1,156 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.ui.renderer; + +import java.text.DateFormat; +import java.util.Date; +import java.util.Locale; + +import com.vaadin.ui.Grid.AbstractRenderer; + +import elemental.json.JsonValue; + +/** + * A renderer for presenting date values. + * + * @since + * @author Vaadin Ltd + */ +public class DateRenderer extends AbstractRenderer<Date> { + private final Locale locale; + private final String formatString; + private final DateFormat dateFormat; + + /** + * Creates a new date renderer. + * <p> + * The renderer is configured to render with the {@link Date#toString()} + * representation for the default locale. + */ + public DateRenderer() { + this(Locale.getDefault()); + } + + /** + * Creates a new date renderer. + * <p> + * The renderer is configured to render with the {@link Date#toString()} + * representation for the given locale. + * + * @param locale + * the locale in which to present dates + * @throws IllegalArgumentException + * if {@code locale} is {@code null} + */ + public DateRenderer(Locale locale) throws IllegalArgumentException { + this("%s", locale); + } + + /** + * Creates a new date renderer. + * <p> + * The renderer is configured to render with the given string format, as + * displayed in the default locale. + * + * @param formatString + * the format string with which to format the date + * @throws IllegalArgumentException + * if {@code formatString} is {@code null} + * @see <a + * href="http://docs.oracle.com/javase/7/docs/api/java/util/Formatter.html#syntax">Format + * String Syntax</a> + */ + public DateRenderer(String formatString) throws IllegalArgumentException { + this(formatString, Locale.getDefault()); + } + + /** + * Creates a new date renderer. + * <p> + * The renderer is configured to render with the given string format, as + * displayed in the given locale. + * + * @param formatString + * the format string to format the date with + * @param locale + * the locale to use + * @throws IllegalArgumentException + * if either argument is {@code null} + * @see <a + * href="http://docs.oracle.com/javase/7/docs/api/java/util/Formatter.html#syntax">Format + * String Syntax</a> + */ + public DateRenderer(String formatString, Locale locale) + throws IllegalArgumentException { + super(Date.class); + + if (formatString == null) { + throw new IllegalArgumentException("format string may not be null"); + } + + if (locale == null) { + throw new IllegalArgumentException("locale may not be null"); + } + + this.locale = locale; + this.formatString = formatString; + dateFormat = null; + } + + /** + * Creates a new date renderer. + * <p> + * The renderer is configured to render with he given date format. + * + * @param dateFormat + * the date format to use when rendering dates + * @throws IllegalArgumentException + * if {@code dateFormat} is {@code null} + */ + public DateRenderer(DateFormat dateFormat) throws IllegalArgumentException { + super(Date.class); + if (dateFormat == null) { + throw new IllegalArgumentException("date format may not be null"); + } + + locale = null; + formatString = null; + this.dateFormat = dateFormat; + } + + @Override + public JsonValue encode(Date value) { + String dateString; + if (dateFormat != null) { + dateString = dateFormat.format(value); + } else { + dateString = String.format(locale, formatString, value); + } + return encode(dateString, String.class); + } + + @Override + public String toString() { + final String fieldInfo; + if (dateFormat != null) { + fieldInfo = "dateFormat: " + dateFormat.toString(); + } else { + fieldInfo = "locale: " + locale + ", formatString: " + formatString; + } + + return String.format("%s [%s]", getClass().getSimpleName(), fieldInfo); + } +} diff --git a/server/src/com/vaadin/ui/renderer/HtmlRenderer.java b/server/src/com/vaadin/ui/renderer/HtmlRenderer.java new file mode 100644 index 0000000000..bdab51991c --- /dev/null +++ b/server/src/com/vaadin/ui/renderer/HtmlRenderer.java @@ -0,0 +1,33 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.ui.renderer; + +import com.vaadin.ui.Grid.AbstractRenderer; + +/** + * A renderer for presenting HTML content. + * + * @since + * @author Vaadin Ltd + */ +public class HtmlRenderer extends AbstractRenderer<String> { + /** + * Creates a new HTML renderer. + */ + public HtmlRenderer() { + super(String.class); + } +} diff --git a/server/src/com/vaadin/ui/renderer/ImageRenderer.java b/server/src/com/vaadin/ui/renderer/ImageRenderer.java new file mode 100644 index 0000000000..87a044c4a6 --- /dev/null +++ b/server/src/com/vaadin/ui/renderer/ImageRenderer.java @@ -0,0 +1,67 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.ui.renderer; + +import com.vaadin.server.ExternalResource; +import com.vaadin.server.Resource; +import com.vaadin.server.ResourceReference; +import com.vaadin.server.ThemeResource; +import com.vaadin.shared.communication.URLReference; + +import elemental.json.JsonValue; + +/** + * A renderer for presenting images. + * <p> + * The image for each rendered cell is read from a Resource-typed property in + * the data source. Only {@link ExternalResource}s and {@link ThemeResource}s + * are currently supported. + * + * @since + * @author Vaadin Ltd + */ +public class ImageRenderer extends ClickableRenderer<Resource> { + + /** + * Creates a new image renderer. + */ + public ImageRenderer() { + super(Resource.class); + } + + /** + * Creates a new image renderer and adds the given click listener to it. + * + * @param listener + * the click listener to register + */ + public ImageRenderer(RendererClickListener listener) { + this(); + addClickListener(listener); + } + + @Override + public JsonValue encode(Resource resource) { + if (!(resource instanceof ExternalResource || resource instanceof ThemeResource)) { + throw new IllegalArgumentException( + "ImageRenderer only supports ExternalResource and ThemeResource (" + + resource.getClass().getSimpleName() + "given )"); + } + + return encode(ResourceReference.create(resource, this, null), + URLReference.class); + } +} diff --git a/server/src/com/vaadin/ui/renderer/NumberRenderer.java b/server/src/com/vaadin/ui/renderer/NumberRenderer.java new file mode 100644 index 0000000000..9af5c4cffb --- /dev/null +++ b/server/src/com/vaadin/ui/renderer/NumberRenderer.java @@ -0,0 +1,163 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.ui.renderer; + +import java.text.NumberFormat; +import java.util.Locale; + +import com.vaadin.ui.Grid.AbstractRenderer; + +import elemental.json.JsonValue; + +/** + * A renderer for presenting number values. + * + * @since + * @author Vaadin Ltd + */ +public class NumberRenderer extends AbstractRenderer<Number> { + private final Locale locale; + private final NumberFormat numberFormat; + private final String formatString; + + /** + * Creates a new number renderer. + * <p> + * The renderer is configured to render with the number's natural string + * representation in the default locale. + */ + public NumberRenderer() { + this(Locale.getDefault()); + } + + /** + * Creates a new number renderer. + * <p> + * The renderer is configured to render the number as defined with the given + * number format. + * + * @param numberFormat + * the number format with which to display numbers + * @throws IllegalArgumentException + * if {@code numberFormat} is {@code null} + */ + public NumberRenderer(NumberFormat numberFormat) + throws IllegalArgumentException { + super(Number.class); + + if (numberFormat == null) { + throw new IllegalArgumentException("Number format may not be null"); + } + + locale = null; + this.numberFormat = numberFormat; + formatString = null; + } + + /** + * Creates a new number renderer. + * <p> + * The renderer is configured to render with the number's natural string + * representation in the given locale. + * + * @param locale + * the locale in which to display numbers + * @throws IllegalArgumentException + * if {@code locale} is {@code null} + */ + public NumberRenderer(Locale locale) throws IllegalArgumentException { + this("%s", locale); + } + + /** + * Creates a new number renderer. + * <p> + * The renderer is configured to render with the given format string in the + * default locale. + * + * @param formatString + * the format string with which to format the number + * @throws IllegalArgumentException + * if {@code formatString} is {@code null} + * @see <a + * href="http://docs.oracle.com/javase/7/docs/api/java/util/Formatter.html#syntax">Format + * String Syntax</a> + */ + public NumberRenderer(String formatString) throws IllegalArgumentException { + this(formatString, Locale.getDefault()); + } + + /** + * Creates a new number renderer. + * <p> + * The renderer is configured to render with the given format string in the + * given locale. + * + * @param formatString + * the format string with which to format the number + * @param locale + * the locale in which to present numbers + * @throws IllegalArgumentException + * if either argument is {@code null} + * @see <a + * href="http://docs.oracle.com/javase/7/docs/api/java/util/Formatter.html#syntax">Format + * String Syntax</a> + */ + public NumberRenderer(String formatString, Locale locale) { + super(Number.class); + + if (formatString == null) { + throw new IllegalArgumentException("Format string may not be null"); + } + + if (locale == null) { + throw new IllegalArgumentException("Locale may not be null"); + } + + this.locale = locale; + numberFormat = null; + this.formatString = formatString; + } + + @Override + public JsonValue encode(Number value) { + String stringValue; + if (formatString != null && locale != null) { + stringValue = String.format(locale, formatString, value); + } else if (numberFormat != null) { + stringValue = numberFormat.format(value); + } else { + throw new IllegalStateException(String.format("Internal bug: " + + "%s is in an illegal state: " + + "[locale: %s, numberFormat: %s, formatString: %s]", + getClass().getSimpleName(), locale, numberFormat, + formatString)); + } + return encode(stringValue, String.class); + } + + @Override + public String toString() { + final String fieldInfo; + if (numberFormat != null) { + fieldInfo = "numberFormat: " + numberFormat.toString(); + } else { + fieldInfo = "locale: " + locale + ", formatString: " + formatString; + } + + return String.format("%s [%s]", getClass().getSimpleName(), fieldInfo); + } +} diff --git a/server/src/com/vaadin/ui/renderer/ProgressBarRenderer.java b/server/src/com/vaadin/ui/renderer/ProgressBarRenderer.java new file mode 100644 index 0000000000..cb688b178b --- /dev/null +++ b/server/src/com/vaadin/ui/renderer/ProgressBarRenderer.java @@ -0,0 +1,44 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.ui.renderer; + +import com.vaadin.ui.Grid.AbstractRenderer; + +import elemental.json.JsonValue; + +/** + * A renderer that represents a double values as a graphical progress bar. + * + * @since + * @author Vaadin Ltd + */ +public class ProgressBarRenderer extends AbstractRenderer<Double> { + + /** + * Creates a new text renderer + */ + public ProgressBarRenderer() { + super(Double.class); + } + + @Override + public JsonValue encode(Double value) { + if (value != null) { + value = Math.max(Math.min(value, 1), 0); + } + return super.encode(value); + } +} diff --git a/server/src/com/vaadin/ui/renderer/Renderer.java b/server/src/com/vaadin/ui/renderer/Renderer.java new file mode 100644 index 0000000000..6adddd1a20 --- /dev/null +++ b/server/src/com/vaadin/ui/renderer/Renderer.java @@ -0,0 +1,69 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.ui.renderer; + +import com.vaadin.server.ClientConnector; +import com.vaadin.server.Extension; + +import elemental.json.JsonValue; + +/** + * A ClientConnector for controlling client-side + * {@link com.vaadin.client.ui.grid.Renderer Grid renderers}. Renderers + * currently extend the Extension interface, but this fact should be regarded as + * an implementation detail and subject to change in a future major or minor + * Vaadin revision. + * + * @param <T> + * the type this renderer knows how to present + * + * @since + * @author Vaadin Ltd + */ +public interface Renderer<T> extends Extension { + + /** + * Returns the class literal corresponding to the presentation type T. + * + * @return the class literal of T + */ + Class<T> getPresentationType(); + + /** + * Encodes the given value into a {@link JsonValue}. + * + * @param value + * the value to encode + * @return a JSON representation of the given value + */ + JsonValue encode(T value); + + /** + * This method is inherited from Extension but should never be called + * directly with a Renderer. + */ + @Override + @Deprecated + void remove(); + + /** + * This method is inherited from Extension but should never be called + * directly with a Renderer. + */ + @Override + @Deprecated + void setParent(ClientConnector parent); +} diff --git a/server/src/com/vaadin/ui/renderer/TextRenderer.java b/server/src/com/vaadin/ui/renderer/TextRenderer.java new file mode 100644 index 0000000000..42bd02fa2a --- /dev/null +++ b/server/src/com/vaadin/ui/renderer/TextRenderer.java @@ -0,0 +1,34 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.ui.renderer; + +import com.vaadin.ui.Grid.AbstractRenderer; + +/** + * A renderer for presenting simple plain-text string values. + * + * @since + * @author Vaadin Ltd + */ +public class TextRenderer extends AbstractRenderer<String> { + + /** + * Creates a new text renderer + */ + public TextRenderer() { + super(String.class); + } +} diff --git a/server/tests/src/com/vaadin/data/util/BeanItemContainerTest.java b/server/tests/src/com/vaadin/data/util/BeanItemContainerTest.java index 514bf70416..2cf64bbc7c 100644 --- a/server/tests/src/com/vaadin/data/util/BeanItemContainerTest.java +++ b/server/tests/src/com/vaadin/data/util/BeanItemContainerTest.java @@ -8,11 +8,17 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import org.easymock.Capture; +import org.easymock.EasyMock; import org.junit.Assert; import com.vaadin.data.Container; +import com.vaadin.data.Container.Indexed.ItemAddEvent; +import com.vaadin.data.Container.Indexed.ItemRemoveEvent; +import com.vaadin.data.Container.ItemSetChangeListener; import com.vaadin.data.Item; import com.vaadin.data.util.NestedMethodPropertyTest.Address; +import com.vaadin.data.util.filter.Compare; /** * Test basic functionality of BeanItemContainer. @@ -742,6 +748,184 @@ public class BeanItemContainerTest extends AbstractBeanContainerTest { .getValue()); } + public void testItemAddedEvent() { + BeanItemContainer<Person> container = new BeanItemContainer<Person>( + Person.class); + Person bean = new Person("John"); + ItemSetChangeListener addListener = createListenerMockFor(container); + addListener.containerItemSetChange(EasyMock.isA(ItemAddEvent.class)); + EasyMock.replay(addListener); + + container.addItem(bean); + + EasyMock.verify(addListener); + } + + public void testItemAddedEvent_AddedItem() { + BeanItemContainer<Person> container = new BeanItemContainer<Person>( + Person.class); + Person bean = new Person("John"); + ItemSetChangeListener addListener = createListenerMockFor(container); + Capture<ItemAddEvent> capturedEvent = captureAddEvent(addListener); + EasyMock.replay(addListener); + + container.addItem(bean); + + assertEquals(bean, capturedEvent.getValue().getFirstItemId()); + } + + public void testItemAddedEvent_addItemAt_IndexOfAddedItem() { + BeanItemContainer<Person> container = new BeanItemContainer<Person>( + Person.class); + Person bean = new Person("John"); + container.addItem(bean); + ItemSetChangeListener addListener = createListenerMockFor(container); + Capture<ItemAddEvent> capturedEvent = captureAddEvent(addListener); + EasyMock.replay(addListener); + + container.addItemAt(1, new Person("")); + + assertEquals(1, capturedEvent.getValue().getFirstIndex()); + } + + public void testItemAddedEvent_addItemAfter_IndexOfAddedItem() { + BeanItemContainer<Person> container = new BeanItemContainer<Person>( + Person.class); + Person bean = new Person("John"); + container.addItem(bean); + ItemSetChangeListener addListener = createListenerMockFor(container); + Capture<ItemAddEvent> capturedEvent = captureAddEvent(addListener); + EasyMock.replay(addListener); + + container.addItemAfter(bean, new Person("")); + + assertEquals(1, capturedEvent.getValue().getFirstIndex()); + } + + public void testItemAddedEvent_amountOfAddedItems() { + BeanItemContainer<Person> container = new BeanItemContainer<Person>( + Person.class); + ItemSetChangeListener addListener = createListenerMockFor(container); + Capture<ItemAddEvent> capturedEvent = captureAddEvent(addListener); + EasyMock.replay(addListener); + List<Person> beans = Arrays.asList(new Person("Jack"), new Person( + "John")); + + container.addAll(beans); + + assertEquals(2, capturedEvent.getValue().getAddedItemsCount()); + } + + public void testItemAddedEvent_someItemsAreFiltered_amountOfAddedItemsIsReducedByAmountOfFilteredItems() { + BeanItemContainer<Person> container = new BeanItemContainer<Person>( + Person.class); + ItemSetChangeListener addListener = createListenerMockFor(container); + Capture<ItemAddEvent> capturedEvent = captureAddEvent(addListener); + EasyMock.replay(addListener); + List<Person> beans = Arrays.asList(new Person("Jack"), new Person( + "John")); + container.addFilter(new Compare.Equal("name", "John")); + + container.addAll(beans); + + assertEquals(1, capturedEvent.getValue().getAddedItemsCount()); + } + + public void testItemAddedEvent_someItemsAreFiltered_addedItemIsTheFirstVisibleItem() { + BeanItemContainer<Person> container = new BeanItemContainer<Person>( + Person.class); + Person bean = new Person("John"); + ItemSetChangeListener addListener = createListenerMockFor(container); + Capture<ItemAddEvent> capturedEvent = captureAddEvent(addListener); + EasyMock.replay(addListener); + List<Person> beans = Arrays.asList(new Person("Jack"), bean); + container.addFilter(new Compare.Equal("name", "John")); + + container.addAll(beans); + + assertEquals(bean, capturedEvent.getValue().getFirstItemId()); + } + + public void testItemRemovedEvent() { + BeanItemContainer<Person> container = new BeanItemContainer<Person>( + Person.class); + Person bean = new Person("John"); + container.addItem(bean); + ItemSetChangeListener removeListener = createListenerMockFor(container); + removeListener.containerItemSetChange(EasyMock + .isA(ItemRemoveEvent.class)); + EasyMock.replay(removeListener); + + container.removeItem(bean); + + EasyMock.verify(removeListener); + } + + public void testItemRemovedEvent_RemovedItem() { + BeanItemContainer<Person> container = new BeanItemContainer<Person>( + Person.class); + Person bean = new Person("John"); + container.addItem(bean); + ItemSetChangeListener removeListener = createListenerMockFor(container); + Capture<ItemRemoveEvent> capturedEvent = captureRemoveEvent(removeListener); + EasyMock.replay(removeListener); + + container.removeItem(bean); + + assertEquals(bean, capturedEvent.getValue().getFirstItemId()); + } + + public void testItemRemovedEvent_indexOfRemovedItem() { + BeanItemContainer<Person> container = new BeanItemContainer<Person>( + Person.class); + container.addItem(new Person("Jack")); + Person secondBean = new Person("John"); + container.addItem(secondBean); + ItemSetChangeListener removeListener = createListenerMockFor(container); + Capture<ItemRemoveEvent> capturedEvent = captureRemoveEvent(removeListener); + EasyMock.replay(removeListener); + + container.removeItem(secondBean); + + assertEquals(1, capturedEvent.getValue().getFirstIndex()); + } + + public void testItemRemovedEvent_amountOfRemovedItems() { + BeanItemContainer<Person> container = new BeanItemContainer<Person>( + Person.class); + container.addItem(new Person("Jack")); + container.addItem(new Person("John")); + ItemSetChangeListener removeListener = createListenerMockFor(container); + Capture<ItemRemoveEvent> capturedEvent = captureRemoveEvent(removeListener); + EasyMock.replay(removeListener); + + container.removeAllItems(); + + assertEquals(2, capturedEvent.getValue().getRemovedItemsCount()); + } + + private Capture<ItemAddEvent> captureAddEvent( + ItemSetChangeListener addListener) { + Capture<ItemAddEvent> capturedEvent = new Capture<ItemAddEvent>(); + addListener.containerItemSetChange(EasyMock.capture(capturedEvent)); + return capturedEvent; + } + + private Capture<ItemRemoveEvent> captureRemoveEvent( + ItemSetChangeListener removeListener) { + Capture<ItemRemoveEvent> capturedEvent = new Capture<ItemRemoveEvent>(); + removeListener.containerItemSetChange(EasyMock.capture(capturedEvent)); + return capturedEvent; + } + + private ItemSetChangeListener createListenerMockFor( + BeanItemContainer<Person> container) { + ItemSetChangeListener listener = EasyMock + .createNiceMock(ItemSetChangeListener.class); + container.addItemSetChangeListener(listener); + return listener; + } + public void testAddNestedContainerBeanBeforeData() { BeanItemContainer<NestedMethodPropertyTest.Person> container = new BeanItemContainer<NestedMethodPropertyTest.Person>( NestedMethodPropertyTest.Person.class); diff --git a/server/tests/src/com/vaadin/data/util/GeneratedPropertyContainerTest.java b/server/tests/src/com/vaadin/data/util/GeneratedPropertyContainerTest.java new file mode 100644 index 0000000000..bfa77eab52 --- /dev/null +++ b/server/tests/src/com/vaadin/data/util/GeneratedPropertyContainerTest.java @@ -0,0 +1,306 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.data.util; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import org.junit.Before; +import org.junit.Test; + +import com.vaadin.data.Container.Filter; +import com.vaadin.data.Container.Indexed; +import com.vaadin.data.Container.ItemSetChangeEvent; +import com.vaadin.data.Container.ItemSetChangeListener; +import com.vaadin.data.Container.PropertySetChangeEvent; +import com.vaadin.data.Container.PropertySetChangeListener; +import com.vaadin.data.Item; +import com.vaadin.data.sort.SortOrder; +import com.vaadin.data.util.filter.Compare; +import com.vaadin.data.util.filter.UnsupportedFilterException; + +public class GeneratedPropertyContainerTest { + + GeneratedPropertyContainer container; + Indexed wrappedContainer; + private static double MILES_CONVERSION = 0.6214d; + + private class GeneratedPropertyListener implements + PropertySetChangeListener { + + private int callCount = 0; + + public int getCallCount() { + return callCount; + } + + @Override + public void containerPropertySetChange(PropertySetChangeEvent event) { + ++callCount; + assertEquals( + "Container for event was not GeneratedPropertyContainer", + event.getContainer(), container); + } + } + + private class GeneratedItemSetListener implements ItemSetChangeListener { + + private int callCount = 0; + + public int getCallCount() { + return callCount; + } + + @Override + public void containerItemSetChange(ItemSetChangeEvent event) { + ++callCount; + assertEquals( + "Container for event was not GeneratedPropertyContainer", + event.getContainer(), container); + } + } + + @Before + public void setUp() { + container = new GeneratedPropertyContainer(createContainer()); + } + + @Test + public void testSimpleGeneratedProperty() { + container.addGeneratedProperty("hello", + new PropertyValueGenerator<String>() { + + @Override + public String getValue(Item item, Object itemId, + Object propertyId) { + return "Hello World!"; + } + + @Override + public Class<String> getType() { + return String.class; + } + }); + + Object itemId = container.addItem(); + assertEquals("Expected value not in item.", container.getItem(itemId) + .getItemProperty("hello").getValue(), "Hello World!"); + } + + @Test + public void testSortableProperties() { + container.addGeneratedProperty("baz", + new PropertyValueGenerator<String>() { + + @Override + public String getValue(Item item, Object itemId, + Object propertyId) { + return item.getItemProperty("foo").getValue() + " " + + item.getItemProperty("bar").getValue(); + } + + @Override + public Class<String> getType() { + return String.class; + } + + @Override + public SortOrder[] getSortProperties(SortOrder order) { + SortOrder[] sortOrder = new SortOrder[1]; + sortOrder[0] = new SortOrder("bar", order + .getDirection()); + return sortOrder; + } + }); + + container.sort(new Object[] { "baz" }, new boolean[] { true }); + assertEquals("foo 0", container.getItem(container.getIdByIndex(0)) + .getItemProperty("baz").getValue()); + + container.sort(new Object[] { "baz" }, new boolean[] { false }); + assertEquals("foo 10", container.getItem(container.getIdByIndex(0)) + .getItemProperty("baz").getValue()); + } + + @Test + public void testOverrideSortableProperties() { + + assertTrue(container.getSortableContainerPropertyIds().contains("bar")); + + container.addGeneratedProperty("bar", + new PropertyValueGenerator<String>() { + + @Override + public String getValue(Item item, Object itemId, + Object propertyId) { + return item.getItemProperty("foo").getValue() + " " + + item.getItemProperty("bar").getValue(); + } + + @Override + public Class<String> getType() { + return String.class; + } + }); + + assertFalse(container.getSortableContainerPropertyIds().contains("bar")); + } + + @Test + public void testFilterByMiles() { + container.addGeneratedProperty("miles", + new PropertyValueGenerator<Double>() { + + @Override + public Double getValue(Item item, Object itemId, + Object propertyId) { + return (Double) item.getItemProperty("km").getValue() + * MILES_CONVERSION; + } + + @Override + public Class<Double> getType() { + return Double.class; + } + + @Override + public Filter modifyFilter(Filter filter) + throws UnsupportedFilterException { + if (filter instanceof Compare.LessOrEqual) { + Double value = (Double) ((Compare.LessOrEqual) filter) + .getValue(); + value = value / MILES_CONVERSION; + return new Compare.LessOrEqual("km", value); + } + return super.modifyFilter(filter); + } + }); + + for (Object itemId : container.getItemIds()) { + Item item = container.getItem(itemId); + Double km = (Double) item.getItemProperty("km").getValue(); + Double miles = (Double) item.getItemProperty("miles").getValue(); + assertTrue(miles.equals(km * MILES_CONVERSION)); + } + + Filter filter = new Compare.LessOrEqual("miles", MILES_CONVERSION); + container.addContainerFilter(filter); + for (Object itemId : container.getItemIds()) { + Item item = container.getItem(itemId); + assertTrue("Item did not pass original filter.", + filter.passesFilter(itemId, item)); + } + + assertTrue(container.getContainerFilters().contains(filter)); + container.removeContainerFilter(filter); + assertFalse(container.getContainerFilters().contains(filter)); + + boolean allPass = true; + for (Object itemId : container.getItemIds()) { + Item item = container.getItem(itemId); + if (!filter.passesFilter(itemId, item)) { + allPass = false; + } + } + + if (allPass) { + fail("Removing filter did not introduce any previous filtered items"); + } + } + + @Test + public void testPropertySetChangeNotifier() { + GeneratedPropertyListener listener = new GeneratedPropertyListener(); + GeneratedPropertyListener removedListener = new GeneratedPropertyListener(); + container.addPropertySetChangeListener(listener); + container.addPropertySetChangeListener(removedListener); + + container.addGeneratedProperty("foo", + new PropertyValueGenerator<String>() { + + @Override + public String getValue(Item item, Object itemId, + Object propertyId) { + return ""; + } + + @Override + public Class<String> getType() { + return String.class; + } + }); + + // Adding property to wrapped container should cause an event + wrappedContainer.addContainerProperty("baz", String.class, ""); + container.removePropertySetChangeListener(removedListener); + container.removeGeneratedProperty("foo"); + + assertEquals("Listener was not called correctly.", 3, + listener.getCallCount()); + assertEquals("Removed listener was not called correctly.", 2, + removedListener.getCallCount()); + } + + @Test + public void testItemSetChangeNotifier() { + GeneratedItemSetListener listener = new GeneratedItemSetListener(); + container.addItemSetChangeListener(listener); + + container.sort(new Object[] { "foo" }, new boolean[] { true }); + container.sort(new Object[] { "foo" }, new boolean[] { false }); + + assertEquals("Listener was not called correctly.", 2, + listener.getCallCount()); + + } + + @Test + public void testRemoveProperty() { + container.removeContainerProperty("foo"); + assertFalse("Container contained removed property", container + .getContainerPropertyIds().contains("foo")); + assertTrue("Wrapped container did not contain removed property", + wrappedContainer.getContainerPropertyIds().contains("foo")); + + assertFalse(container.getItem(container.firstItemId()) + .getItemPropertyIds().contains("foo")); + + container.addContainerProperty("foo", null, null); + assertTrue("Container did not contain returned property", container + .getContainerPropertyIds().contains("foo")); + } + + private Indexed createContainer() { + wrappedContainer = new IndexedContainer(); + wrappedContainer.addContainerProperty("foo", String.class, "foo"); + wrappedContainer.addContainerProperty("bar", Integer.class, 0); + // km contains double values from 0.0 to 2.0 + wrappedContainer.addContainerProperty("km", Double.class, 0); + + for (int i = 0; i <= 10; ++i) { + Object itemId = wrappedContainer.addItem(); + Item item = wrappedContainer.getItem(itemId); + item.getItemProperty("foo").setValue("foo"); + item.getItemProperty("bar").setValue(i); + item.getItemProperty("km").setValue(i / 5.0d); + } + + return wrappedContainer; + } + +} diff --git a/server/tests/src/com/vaadin/data/util/TestIndexedContainer.java b/server/tests/src/com/vaadin/data/util/TestIndexedContainer.java index eacee7e301..91e222af77 100644 --- a/server/tests/src/com/vaadin/data/util/TestIndexedContainer.java +++ b/server/tests/src/com/vaadin/data/util/TestIndexedContainer.java @@ -2,8 +2,13 @@ package com.vaadin.data.util; import java.util.List; +import org.easymock.Capture; +import org.easymock.EasyMock; import org.junit.Assert; +import com.vaadin.data.Container.Indexed.ItemAddEvent; +import com.vaadin.data.Container.Indexed.ItemRemoveEvent; +import com.vaadin.data.Container.ItemSetChangeListener; import com.vaadin.data.Item; public class TestIndexedContainer extends AbstractInMemoryContainerTest { @@ -271,6 +276,145 @@ public class TestIndexedContainer extends AbstractInMemoryContainerTest { counter.assertNone(); } + public void testItemAdd_idSequence() { + IndexedContainer container = new IndexedContainer(); + Object itemId; + + itemId = container.addItem(); + assertEquals(Integer.valueOf(1), itemId); + + itemId = container.addItem(); + assertEquals(Integer.valueOf(2), itemId); + + itemId = container.addItemAfter(null); + assertEquals(Integer.valueOf(3), itemId); + + itemId = container.addItemAt(2); + assertEquals(Integer.valueOf(4), itemId); + } + + public void testItemAddRemove_idSequence() { + IndexedContainer container = new IndexedContainer(); + Object itemId; + + itemId = container.addItem(); + assertEquals(Integer.valueOf(1), itemId); + + container.removeItem(itemId); + + itemId = container.addItem(); + assertEquals( + "Id sequence should continue from the previous value even if an item is removed", + Integer.valueOf(2), itemId); + } + + public void testItemAddedEvent() { + IndexedContainer container = new IndexedContainer(); + ItemSetChangeListener addListener = createListenerMockFor(container); + addListener.containerItemSetChange(EasyMock.isA(ItemAddEvent.class)); + EasyMock.replay(addListener); + + container.addItem(); + + EasyMock.verify(addListener); + } + + public void testItemAddedEvent_AddedItem() { + IndexedContainer container = new IndexedContainer(); + ItemSetChangeListener addListener = createListenerMockFor(container); + Capture<ItemAddEvent> capturedEvent = captureAddEvent(addListener); + EasyMock.replay(addListener); + + Object itemId = container.addItem(); + + assertEquals(itemId, capturedEvent.getValue().getFirstItemId()); + } + + public void testItemAddedEvent_IndexOfAddedItem() { + IndexedContainer container = new IndexedContainer(); + ItemSetChangeListener addListener = createListenerMockFor(container); + container.addItem(); + Capture<ItemAddEvent> capturedEvent = captureAddEvent(addListener); + EasyMock.replay(addListener); + + Object itemId = container.addItemAt(1); + + assertEquals(1, capturedEvent.getValue().getFirstIndex()); + } + + public void testItemRemovedEvent() { + IndexedContainer container = new IndexedContainer(); + Object itemId = container.addItem(); + ItemSetChangeListener removeListener = createListenerMockFor(container); + removeListener.containerItemSetChange(EasyMock + .isA(ItemRemoveEvent.class)); + EasyMock.replay(removeListener); + + container.removeItem(itemId); + + EasyMock.verify(removeListener); + } + + public void testItemRemovedEvent_RemovedItem() { + IndexedContainer container = new IndexedContainer(); + Object itemId = container.addItem(); + ItemSetChangeListener removeListener = createListenerMockFor(container); + Capture<ItemRemoveEvent> capturedEvent = captureRemoveEvent(removeListener); + EasyMock.replay(removeListener); + + container.removeItem(itemId); + + assertEquals(itemId, capturedEvent.getValue().getFirstItemId()); + } + + public void testItemRemovedEvent_indexOfRemovedItem() { + IndexedContainer container = new IndexedContainer(); + container.addItem(); + Object secondItemId = container.addItem(); + ItemSetChangeListener removeListener = createListenerMockFor(container); + Capture<ItemRemoveEvent> capturedEvent = captureRemoveEvent(removeListener); + EasyMock.replay(removeListener); + + container.removeItem(secondItemId); + + assertEquals(1, capturedEvent.getValue().getFirstIndex()); + } + + public void testItemRemovedEvent_amountOfRemovedItems() { + IndexedContainer container = new IndexedContainer(); + container.addItem(); + container.addItem(); + ItemSetChangeListener removeListener = createListenerMockFor(container); + Capture<ItemRemoveEvent> capturedEvent = captureRemoveEvent(removeListener); + EasyMock.replay(removeListener); + + container.removeAllItems(); + + assertEquals(2, capturedEvent.getValue().getRemovedItemsCount()); + } + + private Capture<ItemAddEvent> captureAddEvent( + ItemSetChangeListener addListener) { + Capture<ItemAddEvent> capturedEvent = new Capture<ItemAddEvent>(); + addListener.containerItemSetChange(EasyMock.capture(capturedEvent)); + return capturedEvent; + } + + private Capture<ItemRemoveEvent> captureRemoveEvent( + ItemSetChangeListener removeListener) { + Capture<ItemRemoveEvent> capturedEvent = new Capture<ItemRemoveEvent>(); + removeListener.containerItemSetChange(EasyMock.capture(capturedEvent)); + return capturedEvent; + } + + private ItemSetChangeListener createListenerMockFor( + IndexedContainer container) { + ItemSetChangeListener listener = EasyMock + .createNiceMock(ItemSetChangeListener.class); + container.addItemSetChangeListener(listener); + return listener; + } + // Ticket 8028 public void testGetItemIdsRangeIndexOutOfBounds() { IndexedContainer ic = new IndexedContainer(); diff --git a/server/tests/src/com/vaadin/tests/server/component/grid/DataProviderExtension.java b/server/tests/src/com/vaadin/tests/server/component/grid/DataProviderExtension.java new file mode 100644 index 0000000000..9ecf131c5b --- /dev/null +++ b/server/tests/src/com/vaadin/tests/server/component/grid/DataProviderExtension.java @@ -0,0 +1,88 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.tests.server.component.grid; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.util.Arrays; + +import org.junit.Before; +import org.junit.Test; + +import com.vaadin.data.Container; +import com.vaadin.data.Container.Indexed; +import com.vaadin.data.Item; +import com.vaadin.data.Property; +import com.vaadin.data.RpcDataProviderExtension; +import com.vaadin.data.RpcDataProviderExtension.DataProviderKeyMapper; +import com.vaadin.data.util.IndexedContainer; + +public class DataProviderExtension { + private RpcDataProviderExtension dataProvider; + private DataProviderKeyMapper keyMapper; + private Container.Indexed container; + + private static final Object ITEM_ID1 = "itemid1"; + private static final Object ITEM_ID2 = "itemid2"; + private static final Object ITEM_ID3 = "itemid3"; + + private static final Object PROPERTY_ID1_STRING = "property1"; + + @Before + public void setup() { + container = new IndexedContainer(); + populate(container); + + dataProvider = new RpcDataProviderExtension(container); + keyMapper = dataProvider.getKeyMapper(); + } + + private static void populate(Indexed container) { + container.addContainerProperty(PROPERTY_ID1_STRING, String.class, ""); + for (Object itemId : Arrays.asList(ITEM_ID1, ITEM_ID2, ITEM_ID3)) { + final Item item = container.addItem(itemId); + @SuppressWarnings("unchecked") + final Property<String> stringProperty = item + .getItemProperty(PROPERTY_ID1_STRING); + stringProperty.setValue(itemId.toString()); + } + } + + @Test + public void pinBasics() { + assertFalse("itemId1 should not start as pinned", + keyMapper.isPinned(ITEM_ID2)); + + keyMapper.pin(ITEM_ID1); + assertTrue("itemId1 should now be pinned", keyMapper.isPinned(ITEM_ID1)); + + keyMapper.unpin(ITEM_ID1); + assertFalse("itemId1 should not be pinned anymore", + keyMapper.isPinned(ITEM_ID2)); + } + + @Test(expected = IllegalStateException.class) + public void doublePinning() { + keyMapper.pin(ITEM_ID1); + keyMapper.pin(ITEM_ID1); + } + + @Test(expected = IllegalStateException.class) + public void nonexistentUnpin() { + keyMapper.unpin(ITEM_ID1); + } +} diff --git a/server/tests/src/com/vaadin/tests/server/component/grid/EditorRowTests.java b/server/tests/src/com/vaadin/tests/server/component/grid/EditorRowTests.java new file mode 100644 index 0000000000..d521423187 --- /dev/null +++ b/server/tests/src/com/vaadin/tests/server/component/grid/EditorRowTests.java @@ -0,0 +1,257 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.tests.server.component.grid; + +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.assertSame; +import static org.junit.Assert.assertTrue; + +import org.easymock.EasyMock; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import com.vaadin.data.Item; +import com.vaadin.data.fieldgroup.FieldGroup; +import com.vaadin.data.util.IndexedContainer; +import com.vaadin.server.MockVaadinSession; +import com.vaadin.server.VaadinService; +import com.vaadin.server.VaadinSession; +import com.vaadin.ui.Field; +import com.vaadin.ui.Grid; +import com.vaadin.ui.TextField; + +public class EditorRowTests { + + private static final Object PROPERTY_NAME = "name"; + private static final Object PROPERTY_AGE = "age"; + private static final Object ITEM_ID = new Object(); + + private Grid grid; + + // Explicit field for the test session to save it from GC + private VaadinSession session; + + @Before + @SuppressWarnings("unchecked") + public void setup() { + IndexedContainer container = new IndexedContainer(); + container.addContainerProperty(PROPERTY_NAME, String.class, "[name]"); + container.addContainerProperty(PROPERTY_AGE, Integer.class, + Integer.valueOf(-1)); + + Item item = container.addItem(ITEM_ID); + item.getItemProperty(PROPERTY_NAME).setValue("Some Valid Name"); + item.getItemProperty(PROPERTY_AGE).setValue(Integer.valueOf(25)); + + grid = new Grid(container); + + // VaadinSession needed for ConverterFactory + VaadinService mockService = EasyMock + .createNiceMock(VaadinService.class); + session = new MockVaadinSession(mockService); + VaadinSession.setCurrent(session); + session.lock(); + } + + @After + public void tearDown() { + session.unlock(); + session = null; + VaadinSession.setCurrent(null); + } + + @Test + public void initAssumptions() throws Exception { + assertFalse(grid.isEditorRowEnabled()); + assertNull(grid.getEditedItemId()); + assertNotNull(grid.getEditorRowFieldGroup()); + } + + @Test + public void setEnabled() throws Exception { + assertFalse(grid.isEditorRowEnabled()); + grid.setEditorRowEnabled(true); + assertTrue(grid.isEditorRowEnabled()); + } + + @Test + public void setDisabled() throws Exception { + assertFalse(grid.isEditorRowEnabled()); + grid.setEditorRowEnabled(true); + grid.setEditorRowEnabled(false); + assertFalse(grid.isEditorRowEnabled()); + } + + @Test + public void setReEnabled() throws Exception { + assertFalse(grid.isEditorRowEnabled()); + grid.setEditorRowEnabled(true); + grid.setEditorRowEnabled(false); + grid.setEditorRowEnabled(true); + assertTrue(grid.isEditorRowEnabled()); + } + + @Test + public void detached() throws Exception { + FieldGroup oldFieldGroup = grid.getEditorRowFieldGroup(); + grid.removeAllColumns(); + grid.setContainerDataSource(new IndexedContainer()); + assertFalse(oldFieldGroup == grid.getEditorRowFieldGroup()); + } + + @Test + public void propertyUneditable() throws Exception { + assertTrue(grid.isPropertyEditable(PROPERTY_NAME)); + grid.setPropertyEditable(PROPERTY_NAME, false); + assertFalse(grid.isPropertyEditable(PROPERTY_NAME)); + } + + @Test(expected = IllegalArgumentException.class) + public void nonexistentPropertyUneditable() throws Exception { + grid.setPropertyEditable(new Object(), false); + } + + @Test(expected = IllegalStateException.class) + public void disabledEditItem() throws Exception { + grid.editItem(ITEM_ID); + } + + @Test + public void editItem() throws Exception { + startEdit(); + assertEquals(ITEM_ID, grid.getEditedItemId()); + } + + @Test(expected = IllegalArgumentException.class) + public void nonexistentEditItem() throws Exception { + grid.setEditorRowEnabled(true); + grid.editItem(new Object()); + } + + @Test + public void getField() throws Exception { + startEdit(); + + assertNotNull(grid.getEditorRowField(PROPERTY_NAME)); + } + + @Test + public void getFieldWithoutItem() throws Exception { + grid.setEditorRowEnabled(true); + assertNull(grid.getEditorRowField(PROPERTY_NAME)); + } + + @Test + public void getFieldAfterReSettingFieldAsEditable() throws Exception { + startEdit(); + + grid.setPropertyEditable(PROPERTY_NAME, false); + grid.setPropertyEditable(PROPERTY_NAME, true); + assertNotNull(grid.getEditorRowField(PROPERTY_NAME)); + } + + @Test + public void isEditable() { + assertTrue(grid.isPropertyEditable(PROPERTY_NAME)); + } + + @Test + public void isUneditable() { + grid.setPropertyEditable(PROPERTY_NAME, false); + assertFalse(grid.isPropertyEditable(PROPERTY_NAME)); + } + + @Test + public void isEditableAgain() { + grid.setPropertyEditable(PROPERTY_NAME, false); + grid.setPropertyEditable(PROPERTY_NAME, true); + assertTrue(grid.isPropertyEditable(PROPERTY_NAME)); + } + + @Test + public void isUneditableAgain() { + grid.setPropertyEditable(PROPERTY_NAME, false); + grid.setPropertyEditable(PROPERTY_NAME, true); + grid.setPropertyEditable(PROPERTY_NAME, false); + assertFalse(grid.isPropertyEditable(PROPERTY_NAME)); + } + + @Test(expected = IllegalArgumentException.class) + public void isNonexistentEditable() { + grid.isPropertyEditable(new Object()); + } + + @Test(expected = IllegalArgumentException.class) + public void setNonexistentUneditable() { + grid.setPropertyEditable(new Object(), false); + } + + @Test(expected = IllegalArgumentException.class) + public void setNonexistentEditable() { + grid.setPropertyEditable(new Object(), true); + } + + @Test + public void customBinding() { + TextField textField = new TextField(); + grid.bindEditorRowField(PROPERTY_NAME, textField); + + startEdit(); + + assertSame(textField, grid.getEditorRowField(PROPERTY_NAME)); + } + + @Test(expected = IllegalStateException.class) + public void disableWhileEditing() { + startEdit(); + grid.setEditorRowEnabled(false); + } + + @Test + public void fieldIsNotReadonly() { + startEdit(); + + Field<?> field = grid.getEditorRowField(PROPERTY_NAME); + assertFalse(field.isReadOnly()); + } + + @Test + public void fieldIsReadonlyWhenFieldGroupIsReadonly() { + startEdit(); + + grid.getEditorRowFieldGroup().setReadOnly(true); + Field<?> field = grid.getEditorRowField(PROPERTY_NAME); + assertTrue(field.isReadOnly()); + } + + @Test + public void fieldIsReadonlyWhenPropertyIsNotEditable() { + startEdit(); + + grid.setPropertyEditable(PROPERTY_NAME, false); + Field<?> field = grid.getEditorRowField(PROPERTY_NAME); + assertTrue(field.isReadOnly()); + } + + private void startEdit() { + grid.setEditorRowEnabled(true); + grid.editItem(ITEM_ID); + } +} diff --git a/server/tests/src/com/vaadin/tests/server/component/grid/GridAddRowBuiltinContainerTest.java b/server/tests/src/com/vaadin/tests/server/component/grid/GridAddRowBuiltinContainerTest.java new file mode 100644 index 0000000000..70c73eb516 --- /dev/null +++ b/server/tests/src/com/vaadin/tests/server/component/grid/GridAddRowBuiltinContainerTest.java @@ -0,0 +1,219 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.tests.server.component.grid; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import com.vaadin.data.Container; +import com.vaadin.data.Item; +import com.vaadin.data.util.BeanItem; +import com.vaadin.data.util.BeanItemContainer; +import com.vaadin.data.util.MethodProperty.MethodException; +import com.vaadin.tests.data.bean.Person; +import com.vaadin.ui.Grid; + +public class GridAddRowBuiltinContainerTest { + Grid grid = new Grid(); + Container.Indexed container; + + @Before + public void setUp() { + container = grid.getContainerDataSource(); + + grid.addColumn("myColumn"); + } + + @Test + public void testSimpleCase() { + Object itemId = grid.addRow("Hello"); + + Assert.assertEquals(Integer.valueOf(1), itemId); + + Assert.assertEquals("There should be one item in the container", 1, + container.size()); + + Assert.assertEquals("Hello", + container.getItem(itemId).getItemProperty("myColumn") + .getValue()); + } + + @Test(expected = IllegalArgumentException.class) + public void testNullParameter() { + // cast to Object[] to distinguish from one null varargs value + grid.addRow((Object[]) null); + } + + @Test + public void testNullValue() { + // cast to Object to distinguish from a null varargs array + Object itemId = grid.addRow((Object) null); + + Assert.assertEquals(null, + container.getItem(itemId).getItemProperty("myColumn") + .getValue()); + } + + @Test(expected = IllegalArgumentException.class) + public void testAddInvalidType() { + grid.addRow(Integer.valueOf(5)); + } + + @Test + public void testMultipleProperties() { + grid.addColumn("myOther", Integer.class); + + Object itemId = grid.addRow("Hello", Integer.valueOf(3)); + + Item item = container.getItem(itemId); + Assert.assertEquals("Hello", item.getItemProperty("myColumn") + .getValue()); + Assert.assertEquals(Integer.valueOf(3), item.getItemProperty("myOther") + .getValue()); + } + + @Test(expected = IllegalArgumentException.class) + public void testInvalidPropertyAmount() { + grid.addRow("Hello", Integer.valueOf(3)); + } + + @Test + public void testRemovedColumn() { + grid.addColumn("myOther", Integer.class); + grid.removeColumn("myColumn"); + + grid.addRow(Integer.valueOf(3)); + + Item item = container.getItem(Integer.valueOf(1)); + Assert.assertEquals("Default value should be used for removed column", + "", item.getItemProperty("myColumn").getValue()); + Assert.assertEquals(Integer.valueOf(3), item.getItemProperty("myOther") + .getValue()); + } + + @Test + public void testMultiplePropertiesAfterReorder() { + grid.addColumn("myOther", Integer.class); + + grid.setColumnOrder("myOther", "myColumn"); + + grid.addRow(Integer.valueOf(3), "Hello"); + + Item item = container.getItem(Integer.valueOf(1)); + Assert.assertEquals("Hello", item.getItemProperty("myColumn") + .getValue()); + Assert.assertEquals(Integer.valueOf(3), item.getItemProperty("myOther") + .getValue()); + } + + @Test + public void testInvalidType_NothingAdded() { + try { + grid.addRow(Integer.valueOf(5)); + + // Can't use @Test(expect = Foo.class) since we also want to verify + // state after exception was thrown + Assert.fail("Adding wrong type should throw ClassCastException"); + } catch (IllegalArgumentException e) { + Assert.assertEquals("No row should have been added", 0, + container.size()); + } + } + + @Test + public void testUnsupportingContainer() { + setContainerRemoveColumns(new BeanItemContainer<Person>(Person.class)); + try { + + grid.addRow("name"); + + // Can't use @Test(expect = Foo.class) since we also want to verify + // state after exception was thrown + Assert.fail("Adding to BeanItemContainer container should throw UnsupportedOperationException"); + } catch (UnsupportedOperationException e) { + Assert.assertEquals("No row should have been added", 0, + container.size()); + } + } + + @Test + public void testCustomContainer() { + BeanItemContainer<Person> container = new BeanItemContainer<Person>( + Person.class) { + @Override + public Object addItem() { + BeanItem<Person> item = addBean(new Person()); + return getBeanIdResolver().getIdForBean(item.getBean()); + } + }; + + setContainerRemoveColumns(container); + + grid.addRow("name"); + + Assert.assertEquals(1, container.size()); + + Assert.assertEquals("name", container.getIdByIndex(0).getFirstName()); + } + + @Test + public void testSetterThrowing() { + BeanItemContainer<Person> container = new BeanItemContainer<Person>( + Person.class) { + @Override + public Object addItem() { + BeanItem<Person> item = addBean(new Person() { + @Override + public void setFirstName(String firstName) { + if ("name".equals(firstName)) { + throw new RuntimeException(firstName); + } else { + super.setFirstName(firstName); + } + } + }); + return getBeanIdResolver().getIdForBean(item.getBean()); + } + }; + + setContainerRemoveColumns(container); + + try { + + grid.addRow("name"); + + // Can't use @Test(expect = Foo.class) since we also want to verify + // state after exception was thrown + Assert.fail("Adding row should throw MethodException"); + } catch (MethodException e) { + Assert.assertEquals("Got the wrong exception", "name", e.getCause() + .getMessage()); + + Assert.assertEquals("There should be no rows in the container", 0, + container.size()); + } + } + + private void setContainerRemoveColumns(BeanItemContainer<Person> container) { + // Remove predefined column so we can change container + grid.removeAllColumns(); + grid.setContainerDataSource(container); + grid.removeAllColumns(); + grid.addColumn("firstName"); + } + +} diff --git a/server/tests/src/com/vaadin/tests/server/component/grid/GridColumnAddingAndRemovingTest.java b/server/tests/src/com/vaadin/tests/server/component/grid/GridColumnAddingAndRemovingTest.java new file mode 100644 index 0000000000..f401fba1e3 --- /dev/null +++ b/server/tests/src/com/vaadin/tests/server/component/grid/GridColumnAddingAndRemovingTest.java @@ -0,0 +1,128 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.tests.server.component.grid; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + +import org.junit.Before; +import org.junit.Test; + +import com.vaadin.data.Container; +import com.vaadin.data.Property; +import com.vaadin.data.util.IndexedContainer; +import com.vaadin.ui.Grid; + +public class GridColumnAddingAndRemovingTest { + + Grid grid = new Grid(); + Container.Indexed container; + + @Before + public void setUp() { + container = grid.getContainerDataSource(); + container.addItem(); + } + + @Test + public void testAddColumn() { + grid.addColumn("foo"); + + Property<?> property = container.getContainerProperty( + container.firstItemId(), "foo"); + assertEquals(property.getType(), String.class); + } + + @Test(expected = IllegalStateException.class) + public void testAddColumnTwice() { + grid.addColumn("foo"); + grid.addColumn("foo"); + } + + @Test + public void testAddRemoveAndAddAgainColumn() { + grid.addColumn("foo"); + grid.removeColumn("foo"); + + // Removing a column, doesn't remove the property + Property<?> property = container.getContainerProperty( + container.firstItemId(), "foo"); + assertEquals(property.getType(), String.class); + grid.addColumn("foo"); + } + + @Test + public void testAddNumberColumns() { + grid.addColumn("bar", Integer.class); + grid.addColumn("baz", Double.class); + + Property<?> property = container.getContainerProperty( + container.firstItemId(), "bar"); + assertEquals(property.getType(), Integer.class); + assertEquals(null, property.getValue()); + property = container.getContainerProperty(container.firstItemId(), + "baz"); + assertEquals(property.getType(), Double.class); + assertEquals(null, property.getValue()); + } + + @Test(expected = IllegalStateException.class) + public void testAddDifferentTypeColumn() { + grid.addColumn("foo"); + grid.removeColumn("foo"); + grid.addColumn("foo", Integer.class); + } + + @Test(expected = IllegalStateException.class) + public void testAddColumnToNonDefaultContainer() { + grid.setContainerDataSource(new IndexedContainer()); + grid.addColumn("foo"); + } + + @Test + public void testAddColumnForExistingProperty() { + grid.addColumn("bar"); + IndexedContainer container2 = new IndexedContainer(); + container2.addContainerProperty("foo", Integer.class, 0); + container2.addContainerProperty("bar", String.class, ""); + grid.setContainerDataSource(container2); + assertNull("Grid should not have a column for property foo", + grid.getColumn("foo")); + grid.removeAllColumns(); + grid.addColumn("foo"); + assertNotNull("Grid should now have a column for property foo", + grid.getColumn("foo")); + assertNull("Grid should not have a column for property bar anymore", + grid.getColumn("bar")); + } + + @Test(expected = IllegalStateException.class) + public void testAddIncompatibleColumnProperty() { + grid.addColumn("bar"); + grid.removeAllColumns(); + grid.addColumn("bar", Integer.class); + } + + @Test + public void testAddBooleanColumnProperty() { + grid.addColumn("foo", Boolean.class); + Property<?> property = container.getContainerProperty( + container.firstItemId(), "foo"); + assertEquals(property.getType(), Boolean.class); + assertEquals(property.getValue(), null); + } +} diff --git a/server/tests/src/com/vaadin/tests/server/component/grid/GridColumns.java b/server/tests/src/com/vaadin/tests/server/component/grid/GridColumns.java new file mode 100644 index 0000000000..1204b1e396 --- /dev/null +++ b/server/tests/src/com/vaadin/tests/server/component/grid/GridColumns.java @@ -0,0 +1,240 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.tests.server.component.grid; + +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 static org.junit.Assert.fail; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.LinkedHashSet; +import java.util.Set; + +import org.junit.Before; +import org.junit.Test; + +import com.vaadin.data.util.IndexedContainer; +import com.vaadin.server.KeyMapper; +import com.vaadin.shared.ui.grid.GridColumnState; +import com.vaadin.shared.ui.grid.GridState; +import com.vaadin.shared.util.SharedUtil; +import com.vaadin.ui.Grid; +import com.vaadin.ui.Grid.Column; + +public class GridColumns { + + private Grid grid; + + private GridState state; + + private Method getStateMethod; + + private Field columnIdGeneratorField; + + private KeyMapper<Object> columnIdMapper; + + @Before + @SuppressWarnings("unchecked") + public void setup() throws Exception { + IndexedContainer ds = new IndexedContainer(); + for (int c = 0; c < 10; c++) { + ds.addContainerProperty("column" + c, String.class, ""); + } + grid = new Grid(ds); + + getStateMethod = Grid.class.getDeclaredMethod("getState"); + getStateMethod.setAccessible(true); + + state = (GridState) getStateMethod.invoke(grid); + + columnIdGeneratorField = Grid.class.getDeclaredField("columnKeys"); + columnIdGeneratorField.setAccessible(true); + + columnIdMapper = (KeyMapper<Object>) columnIdGeneratorField.get(grid); + } + + @Test + public void testColumnGeneration() throws Exception { + + for (Object propertyId : grid.getContainerDataSource() + .getContainerPropertyIds()) { + + // All property ids should get a column + Column column = grid.getColumn(propertyId); + assertNotNull(column); + + // Humanized property id should be the column header by default + assertEquals( + SharedUtil.camelCaseToHumanFriendly(propertyId.toString()), + grid.getDefaultHeaderRow().getCell(propertyId).getText()); + } + } + + @Test + public void testModifyingColumnProperties() throws Exception { + + // Modify first column + Column column = grid.getColumn("column1"); + assertNotNull(column); + + column.setHeaderCaption("CustomHeader"); + assertEquals("CustomHeader", column.getHeaderCaption()); + assertEquals(column.getHeaderCaption(), grid.getDefaultHeaderRow() + .getCell("column1").getText()); + + column.setWidth(100); + assertEquals(100, column.getWidth(), 0.49d); + assertEquals(column.getWidth(), getColumnState("column1").width, 0.49d); + + try { + column.setWidth(-1); + fail("Setting width to -1 should throw exception"); + } catch (IllegalArgumentException iae) { + // expected + } + + assertEquals(100, column.getWidth(), 0.49d); + assertEquals(100, getColumnState("column1").width, 0.49d); + } + + @Test + public void testRemovingColumnByRemovingPropertyFromContainer() + throws Exception { + + Column column = grid.getColumn("column1"); + assertNotNull(column); + + // Remove column + grid.getContainerDataSource().removeContainerProperty("column1"); + + try { + column.setHeaderCaption("asd"); + + fail("Succeeded in modifying a detached column"); + } catch (IllegalStateException ise) { + // Detached state should throw exception + } + + try { + column.setWidth(123); + fail("Succeeded in modifying a detached column"); + } catch (IllegalStateException ise) { + // Detached state should throw exception + } + + assertNull(grid.getColumn("column1")); + assertNull(getColumnState("column1")); + } + + @Test + public void testAddingColumnByAddingPropertyToContainer() throws Exception { + grid.getContainerDataSource().addContainerProperty("columnX", + String.class, ""); + Column column = grid.getColumn("columnX"); + assertNotNull(column); + } + + @Test + public void testHeaderVisiblility() throws Exception { + + assertTrue(grid.isHeaderVisible()); + assertTrue(state.header.visible); + + grid.setHeaderVisible(false); + assertFalse(grid.isHeaderVisible()); + assertFalse(state.header.visible); + + grid.setHeaderVisible(true); + assertTrue(grid.isHeaderVisible()); + assertTrue(state.header.visible); + } + + @Test + public void testFooterVisibility() throws Exception { + + assertTrue(grid.isFooterVisible()); + assertTrue(state.footer.visible); + + grid.setFooterVisible(false); + assertFalse(grid.isFooterVisible()); + assertFalse(state.footer.visible); + + grid.setFooterVisible(true); + assertTrue(grid.isFooterVisible()); + assertTrue(state.footer.visible); + } + + @Test + public void testFrozenColumnRemoveColumn() { + assertEquals("Grid should not start with a frozen column", 0, + grid.getFrozenColumnCount()); + + int containerSize = grid.getContainerDataSource() + .getContainerPropertyIds().size(); + grid.setFrozenColumnCount(containerSize); + + Object propertyId = grid.getContainerDataSource() + .getContainerPropertyIds().iterator().next(); + + grid.getContainerDataSource().removeContainerProperty(propertyId); + assertEquals( + "Frozen column count should update when removing last row", + containerSize - 1, grid.getFrozenColumnCount()); + } + + @Test + public void testReorderColumns() { + Set<?> containerProperties = new LinkedHashSet<Object>(grid + .getContainerDataSource().getContainerPropertyIds()); + Object[] properties = new Object[] { "column3", "column2", "column6" }; + grid.setColumnOrder(properties); + + int i = 0; + // Test sorted columns are first in order + for (Object property : properties) { + containerProperties.remove(property); + assertEquals(columnIdMapper.key(property), + state.columnOrder.get(i++)); + } + + // Test remaining columns are in original order + for (Object property : containerProperties) { + assertEquals(columnIdMapper.key(property), + state.columnOrder.get(i++)); + } + + try { + grid.setColumnOrder("foo", "bar", "baz"); + fail("Grid allowed sorting with non-existent properties"); + } catch (IllegalArgumentException e) { + // All ok + } + } + + private GridColumnState getColumnState(Object propertyId) { + String columnId = columnIdMapper.key(propertyId); + for (GridColumnState columnState : state.columns) { + if (columnState.id.equals(columnId)) { + return columnState; + } + } + return null; + } +} diff --git a/server/tests/src/com/vaadin/tests/server/component/grid/GridSelection.java b/server/tests/src/com/vaadin/tests/server/component/grid/GridSelection.java new file mode 100644 index 0000000000..a941a92117 --- /dev/null +++ b/server/tests/src/com/vaadin/tests/server/component/grid/GridSelection.java @@ -0,0 +1,301 @@ +/* + * 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.grid; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.util.Collection; + +import org.junit.Before; +import org.junit.Test; + +import com.vaadin.data.util.IndexedContainer; +import com.vaadin.event.SelectionChangeEvent; +import com.vaadin.event.SelectionChangeEvent.SelectionChangeListener; +import com.vaadin.ui.Grid; +import com.vaadin.ui.Grid.SelectionMode; +import com.vaadin.ui.Grid.SelectionModel; + +public class GridSelection { + + private static class MockSelectionChangeListener implements + SelectionChangeListener { + private SelectionChangeEvent event; + + @Override + public void selectionChange(final SelectionChangeEvent event) { + this.event = event; + } + + public Collection<?> getAdded() { + return event.getAdded(); + } + + public Collection<?> getRemoved() { + return event.getRemoved(); + } + + public void clearEvent() { + /* + * This method is not strictly needed as the event will simply be + * overridden, but it's good practice, and makes the code more + * obvious. + */ + event = null; + } + + public boolean eventHasHappened() { + return event != null; + } + } + + private Grid grid; + private MockSelectionChangeListener mockListener; + + private final Object itemId1Present = "itemId1Present"; + private final Object itemId2Present = "itemId2Present"; + + private final Object itemId1NotPresent = "itemId1NotPresent"; + private final Object itemId2NotPresent = "itemId2NotPresent"; + + @Before + public void setup() { + final IndexedContainer container = new IndexedContainer(); + container.addItem(itemId1Present); + container.addItem(itemId2Present); + for (int i = 2; i < 10; i++) { + container.addItem(new Object()); + } + + assertEquals("init size", 10, container.size()); + assertTrue("itemId1Present", container.containsId(itemId1Present)); + assertTrue("itemId2Present", container.containsId(itemId2Present)); + assertFalse("itemId1NotPresent", + container.containsId(itemId1NotPresent)); + assertFalse("itemId2NotPresent", + container.containsId(itemId2NotPresent)); + + grid = new Grid(container); + + mockListener = new MockSelectionChangeListener(); + grid.addSelectionChangeListener(mockListener); + + assertFalse("eventHasHappened", mockListener.eventHasHappened()); + } + + @Test + public void defaultSelectionModeIsMulti() { + assertTrue(grid.getSelectionModel() instanceof SelectionModel.Multi); + } + + @Test(expected = IllegalStateException.class) + public void getSelectedRowThrowsExceptionMulti() { + grid.setSelectionMode(SelectionMode.MULTI); + grid.getSelectedRow(); + } + + @Test(expected = IllegalStateException.class) + public void getSelectedRowThrowsExceptionNone() { + grid.setSelectionMode(SelectionMode.NONE); + grid.getSelectedRow(); + } + + @Test(expected = IllegalStateException.class) + public void selectThrowsExceptionNone() { + grid.setSelectionMode(SelectionMode.NONE); + grid.select(itemId1Present); + } + + @Test(expected = IllegalStateException.class) + public void deselectRowThrowsExceptionNone() { + grid.setSelectionMode(SelectionMode.NONE); + grid.deselect(itemId1Present); + } + + @Test + public void selectionModeMapsToMulti() { + assertTrue(grid.setSelectionMode(SelectionMode.MULTI) instanceof SelectionModel.Multi); + } + + @Test + public void selectionModeMapsToSingle() { + assertTrue(grid.setSelectionMode(SelectionMode.SINGLE) instanceof SelectionModel.Single); + } + + @Test + public void selectionModeMapsToNone() { + assertTrue(grid.setSelectionMode(SelectionMode.NONE) instanceof SelectionModel.None); + } + + @Test(expected = IllegalArgumentException.class) + public void selectionModeNullThrowsException() { + grid.setSelectionMode(null); + } + + @Test + public void noSelectModel_isSelected() { + grid.setSelectionMode(SelectionMode.NONE); + assertFalse("itemId1Present", grid.isSelected(itemId1Present)); + assertFalse("itemId1NotPresent", grid.isSelected(itemId1NotPresent)); + } + + @Test(expected = IllegalStateException.class) + public void noSelectModel_getSelectedRow() { + grid.setSelectionMode(SelectionMode.NONE); + grid.getSelectedRow(); + } + + @Test + public void noSelectModel_getSelectedRows() { + grid.setSelectionMode(SelectionMode.NONE); + assertTrue(grid.getSelectedRows().isEmpty()); + } + + @Test + public void selectionCallsListenerMulti() { + grid.setSelectionMode(SelectionMode.MULTI); + selectionCallsListener(); + } + + @Test + public void selectionCallsListenerSingle() { + grid.setSelectionMode(SelectionMode.SINGLE); + selectionCallsListener(); + } + + private void selectionCallsListener() { + grid.select(itemId1Present); + assertEquals("added size", 1, mockListener.getAdded().size()); + assertEquals("added item", itemId1Present, mockListener.getAdded() + .iterator().next()); + assertEquals("removed size", 0, mockListener.getRemoved().size()); + } + + @Test + public void deselectionCallsListenerMulti() { + grid.setSelectionMode(SelectionMode.MULTI); + deselectionCallsListener(); + } + + @Test + public void deselectionCallsListenerSingle() { + grid.setSelectionMode(SelectionMode.SINGLE); + deselectionCallsListener(); + } + + private void deselectionCallsListener() { + grid.select(itemId1Present); + mockListener.clearEvent(); + + grid.deselect(itemId1Present); + assertEquals("removed size", 1, mockListener.getRemoved().size()); + assertEquals("removed item", itemId1Present, mockListener.getRemoved() + .iterator().next()); + assertEquals("removed size", 0, mockListener.getAdded().size()); + } + + @Test + public void deselectPresentButNotSelectedItemIdShouldntFireListenerMulti() { + grid.setSelectionMode(SelectionMode.MULTI); + deselectPresentButNotSelectedItemIdShouldntFireListener(); + } + + @Test + public void deselectPresentButNotSelectedItemIdShouldntFireListenerSingle() { + grid.setSelectionMode(SelectionMode.SINGLE); + deselectPresentButNotSelectedItemIdShouldntFireListener(); + } + + private void deselectPresentButNotSelectedItemIdShouldntFireListener() { + grid.deselect(itemId1Present); + assertFalse(mockListener.eventHasHappened()); + } + + @Test + public void deselectNotPresentItemIdShouldNotThrowExceptionMulti() { + grid.setSelectionMode(SelectionMode.MULTI); + grid.deselect(itemId1NotPresent); + } + + @Test + public void deselectNotPresentItemIdShouldNotThrowExceptionSingle() { + grid.setSelectionMode(SelectionMode.SINGLE); + grid.deselect(itemId1NotPresent); + } + + @Test(expected = IllegalArgumentException.class) + public void selectNotPresentItemIdShouldThrowExceptionMulti() { + grid.setSelectionMode(SelectionMode.MULTI); + grid.select(itemId1NotPresent); + } + + @Test(expected = IllegalArgumentException.class) + public void selectNotPresentItemIdShouldThrowExceptionSingle() { + grid.setSelectionMode(SelectionMode.SINGLE); + grid.select(itemId1NotPresent); + } + + @Test + public void selectAllMulti() { + grid.setSelectionMode(SelectionMode.MULTI); + final SelectionModel.Multi select = (SelectionModel.Multi) grid + .getSelectionModel(); + select.selectAll(); + assertEquals("added size", 10, mockListener.getAdded().size()); + assertEquals("removed size", 0, mockListener.getRemoved().size()); + assertTrue("itemId1Present", + mockListener.getAdded().contains(itemId1Present)); + assertTrue("itemId2Present", + mockListener.getAdded().contains(itemId2Present)); + } + + @Test + public void deselectAllMulti() { + grid.setSelectionMode(SelectionMode.MULTI); + final SelectionModel.Multi select = (SelectionModel.Multi) grid + .getSelectionModel(); + select.selectAll(); + mockListener.clearEvent(); + + select.deselectAll(); + assertEquals("removed size", 10, mockListener.getRemoved().size()); + assertEquals("added size", 0, mockListener.getAdded().size()); + assertTrue("itemId1Present", + mockListener.getRemoved().contains(itemId1Present)); + assertTrue("itemId2Present", + mockListener.getRemoved().contains(itemId2Present)); + assertTrue("selectedRows is empty", grid.getSelectedRows().isEmpty()); + } + + @Test + public void reselectionDeselectsPreviousSingle() { + grid.setSelectionMode(SelectionMode.SINGLE); + grid.select(itemId1Present); + mockListener.clearEvent(); + + grid.select(itemId2Present); + assertEquals("added size", 1, mockListener.getAdded().size()); + assertEquals("removed size", 1, mockListener.getRemoved().size()); + assertEquals("added item", itemId2Present, mockListener.getAdded() + .iterator().next()); + assertEquals("removed item", itemId1Present, mockListener.getRemoved() + .iterator().next()); + assertEquals("selectedRows is correct", itemId2Present, + grid.getSelectedRow()); + } +} diff --git a/server/tests/src/com/vaadin/tests/server/component/grid/GridStaticSectionTest.java b/server/tests/src/com/vaadin/tests/server/component/grid/GridStaticSectionTest.java new file mode 100644 index 0000000000..c0b4afbdbe --- /dev/null +++ b/server/tests/src/com/vaadin/tests/server/component/grid/GridStaticSectionTest.java @@ -0,0 +1,112 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.tests.server.component.grid; + +import static org.junit.Assert.assertEquals; + +import java.lang.reflect.Method; + +import org.junit.Before; +import org.junit.Test; + +import com.vaadin.data.Container.Indexed; +import com.vaadin.data.util.IndexedContainer; +import com.vaadin.ui.Grid; + +public class GridStaticSectionTest extends Grid { + + private Indexed dataSource = new IndexedContainer(); + + @Before + public void setUp() { + dataSource.addContainerProperty("firstName", String.class, ""); + dataSource.addContainerProperty("lastName", String.class, ""); + dataSource.addContainerProperty("streetAddress", String.class, ""); + dataSource.addContainerProperty("zipCode", Integer.class, null); + setContainerDataSource(dataSource); + } + + @Test + public void testAddAndRemoveHeaders() { + assertEquals(1, getHeaderRowCount()); + prependHeaderRow(); + assertEquals(2, getHeaderRowCount()); + removeHeaderRow(0); + assertEquals(1, getHeaderRowCount()); + removeHeaderRow(0); + assertEquals(0, getHeaderRowCount()); + assertEquals(null, getDefaultHeaderRow()); + HeaderRow row = appendHeaderRow(); + assertEquals(1, getHeaderRowCount()); + assertEquals(null, getDefaultHeaderRow()); + setDefaultHeaderRow(row); + assertEquals(row, getDefaultHeaderRow()); + } + + @Test + public void testAddAndRemoveFooters() { + // By default there are no footer rows + assertEquals(0, getFooterRowCount()); + FooterRow row = appendFooterRow(); + + assertEquals(1, getFooterRowCount()); + prependFooterRow(); + assertEquals(2, getFooterRowCount()); + assertEquals(row, getFooterRow(1)); + removeFooterRow(0); + assertEquals(1, getFooterRowCount()); + removeFooterRow(0); + assertEquals(0, getFooterRowCount()); + } + + @Test + public void testJoinHeaderCells() { + HeaderRow mergeRow = prependHeaderRow(); + mergeRow.join("firstName", "lastName").setText("Name"); + mergeRow.join(mergeRow.getCell("streetAddress"), + mergeRow.getCell("zipCode")); + } + + @Test(expected = IllegalStateException.class) + public void testJoinHeaderCellsIncorrectly() throws Throwable { + HeaderRow mergeRow = prependHeaderRow(); + mergeRow.join("firstName", "zipCode").setText("Name"); + sanityCheck(); + } + + @Test + public void testJoinAllFooterCells() { + FooterRow mergeRow = prependFooterRow(); + mergeRow.join(dataSource.getContainerPropertyIds().toArray()).setText( + "All the stuff."); + } + + private void sanityCheck() throws Throwable { + Method sanityCheckHeader; + try { + sanityCheckHeader = Grid.Header.class + .getDeclaredMethod("sanityCheck"); + sanityCheckHeader.setAccessible(true); + Method sanityCheckFooter = Grid.Footer.class + .getDeclaredMethod("sanityCheck"); + sanityCheckFooter.setAccessible(true); + sanityCheckHeader.invoke(getHeader()); + sanityCheckFooter.invoke(getFooter()); + } catch (Exception e) { + throw e.getCause(); + } + } +} diff --git a/server/tests/src/com/vaadin/tests/server/component/grid/SingleSelectionModelTest.java b/server/tests/src/com/vaadin/tests/server/component/grid/SingleSelectionModelTest.java new file mode 100644 index 0000000000..9f54930ac8 --- /dev/null +++ b/server/tests/src/com/vaadin/tests/server/component/grid/SingleSelectionModelTest.java @@ -0,0 +1,154 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.tests.server.component.grid; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import com.vaadin.data.Container; +import com.vaadin.data.util.IndexedContainer; +import com.vaadin.event.SelectionChangeEvent; +import com.vaadin.event.SelectionChangeEvent.SelectionChangeListener; +import com.vaadin.ui.Grid; +import com.vaadin.ui.Grid.SelectionMode; +import com.vaadin.ui.Grid.SingleSelectionModel; + +public class SingleSelectionModelTest { + + private Object itemId1Present = "itemId1Present"; + private Object itemId2Present = "itemId2Present"; + + private Object itemIdNotPresent = "itemIdNotPresent"; + private Container.Indexed dataSource; + private SingleSelectionModel model; + private Grid grid; + + private boolean expectingEvent = false; + + @Before + public void setUp() { + dataSource = createDataSource(); + grid = new Grid(dataSource); + grid.setSelectionMode(SelectionMode.SINGLE); + model = (SingleSelectionModel) grid.getSelectionModel(); + } + + @After + public void tearDown() { + Assert.assertFalse("Some expected event did not happen.", + expectingEvent); + } + + private IndexedContainer createDataSource() { + final IndexedContainer container = new IndexedContainer(); + container.addItem(itemId1Present); + container.addItem(itemId2Present); + for (int i = 2; i < 10; i++) { + container.addItem(new Object()); + } + + return container; + } + + @Test + public void testSelectAndDeselctRow() throws Throwable { + try { + expectEvent(itemId1Present, null); + model.select(itemId1Present); + expectEvent(null, itemId1Present); + model.select(null); + } catch (Exception e) { + throw e.getCause(); + } + } + + @Test + public void testSelectAndChangeSelectedRow() throws Throwable { + try { + expectEvent(itemId1Present, null); + model.select(itemId1Present); + expectEvent(itemId2Present, itemId1Present); + model.select(itemId2Present); + } catch (Exception e) { + throw e.getCause(); + } + } + + @Test + public void testRemovingSelectedRowAndThenDeselecting() throws Throwable { + try { + expectEvent(itemId2Present, null); + model.select(itemId2Present); + dataSource.removeItem(itemId2Present); + expectEvent(null, itemId2Present); + model.select(null); + } catch (Exception e) { + throw e.getCause(); + } + } + + @Test + public void testSelectAndReSelectRow() throws Throwable { + try { + expectEvent(itemId1Present, null); + model.select(itemId1Present); + expectEvent(null, null); + // This is no-op. Nothing should happen. + model.select(itemId1Present); + } catch (Exception e) { + throw e.getCause(); + } + Assert.assertTrue("Should still wait for event", expectingEvent); + expectingEvent = false; + } + + @Test(expected = IllegalArgumentException.class) + public void testSelectNonExistentRow() { + model.select(itemIdNotPresent); + } + + private void expectEvent(final Object selected, final Object deselected) { + expectingEvent = true; + grid.addSelectionChangeListener(new SelectionChangeListener() { + + @Override + public void selectionChange(SelectionChangeEvent event) { + if (selected != null) { + Assert.assertTrue( + "Selection did not contain expected item", event + .getAdded().contains(selected)); + } else { + Assert.assertTrue("Unexpected selection", event.getAdded() + .isEmpty()); + } + + if (deselected != null) { + Assert.assertTrue( + "DeSelection did not contain expected item", event + .getRemoved().contains(deselected)); + } else { + Assert.assertTrue("Unexpected selection", event + .getRemoved().isEmpty()); + } + + grid.removeSelectionChangeListener(this); + expectingEvent = false; + } + }); + } +} diff --git a/server/tests/src/com/vaadin/tests/server/component/grid/sort/SortTest.java b/server/tests/src/com/vaadin/tests/server/component/grid/sort/SortTest.java new file mode 100644 index 0000000000..b339cb9aff --- /dev/null +++ b/server/tests/src/com/vaadin/tests/server/component/grid/sort/SortTest.java @@ -0,0 +1,203 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.tests.server.component.grid.sort; + +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.sort.Sort; +import com.vaadin.data.sort.SortOrder; +import com.vaadin.data.util.IndexedContainer; +import com.vaadin.event.SortOrderChangeEvent; +import com.vaadin.event.SortOrderChangeEvent.SortOrderChangeListener; +import com.vaadin.shared.ui.grid.SortDirection; +import com.vaadin.ui.Grid; + +public class SortTest { + + class DummySortingIndexedContainer extends IndexedContainer { + + private Object[] expectedProperties; + private boolean[] expectedAscending; + private boolean sorted = true; + + @Override + public void sort(Object[] propertyId, boolean[] ascending) { + Assert.assertEquals( + "Different amount of expected and actual properties,", + expectedProperties.length, propertyId.length); + Assert.assertEquals( + "Different amount of expected and actual directions", + expectedAscending.length, ascending.length); + for (int i = 0; i < propertyId.length; ++i) { + Assert.assertEquals("Sorting properties differ", + expectedProperties[i], propertyId[i]); + Assert.assertEquals("Sorting directions differ", + expectedAscending[i], ascending[i]); + } + sorted = true; + } + + public void expectedSort(Object[] properties, SortDirection[] directions) { + assert directions.length == properties.length : "Array dimensions differ"; + expectedProperties = properties; + expectedAscending = new boolean[directions.length]; + for (int i = 0; i < directions.length; ++i) { + expectedAscending[i] = (directions[i] == SortDirection.ASCENDING); + } + sorted = false; + } + + public boolean isSorted() { + return sorted; + } + } + + class RegisteringSortChangeListener implements SortOrderChangeListener { + private List<SortOrder> order; + + @Override + public void sortOrderChange(SortOrderChangeEvent event) { + assert order == null : "The same listener was notified multipe times without checking"; + + order = event.getSortOrder(); + } + + public void assertEventFired(SortOrder... expectedOrder) { + Assert.assertEquals(Arrays.asList(expectedOrder), order); + + // Reset for nest test + order = null; + } + + } + + private DummySortingIndexedContainer container; + private RegisteringSortChangeListener listener; + private Grid grid; + + @Before + public void setUp() { + container = createContainer(); + container.expectedSort(new Object[] {}, new SortDirection[] {}); + + listener = new RegisteringSortChangeListener(); + + grid = new Grid(container); + grid.addSortOrderChangeListener(listener); + } + + @After + public void tearDown() { + Assert.assertTrue("Container was not sorted after the test.", + container.isSorted()); + } + + @Test(expected = IllegalArgumentException.class) + public void testInvalidSortDirection() { + Sort.by("foo", null); + } + + @Test(expected = IllegalStateException.class) + public void testSortOneColumnMultipleTimes() { + Sort.by("foo").then("bar").then("foo"); + } + + @Test(expected = IllegalArgumentException.class) + public void testSortingByUnexistingProperty() { + grid.sort("foobar"); + } + + @Test(expected = IllegalArgumentException.class) + public void testSortingByUnsortableProperty() { + container.addContainerProperty("foobar", Object.class, null); + grid.sort("foobar"); + } + + @Test + public void testGridDirectSortAscending() { + container.expectedSort(new Object[] { "foo" }, + new SortDirection[] { SortDirection.ASCENDING }); + grid.sort("foo"); + + listener.assertEventFired(new SortOrder("foo", SortDirection.ASCENDING)); + } + + @Test + public void testGridDirectSortDescending() { + container.expectedSort(new Object[] { "foo" }, + new SortDirection[] { SortDirection.DESCENDING }); + grid.sort("foo", SortDirection.DESCENDING); + + listener.assertEventFired(new SortOrder("foo", SortDirection.DESCENDING)); + } + + @Test + public void testGridSortBy() { + container.expectedSort(new Object[] { "foo", "bar", "baz" }, + new SortDirection[] { SortDirection.ASCENDING, + SortDirection.ASCENDING, SortDirection.DESCENDING }); + grid.sort(Sort.by("foo").then("bar") + .then("baz", SortDirection.DESCENDING)); + + listener.assertEventFired( + new SortOrder("foo", SortDirection.ASCENDING), new SortOrder( + "bar", SortDirection.ASCENDING), new SortOrder("baz", + SortDirection.DESCENDING)); + + } + + @Test + public void testChangeContainerAfterSorting() { + class Person { + } + + container.expectedSort(new Object[] { "foo", "bar", "baz" }, + new SortDirection[] { SortDirection.ASCENDING, + SortDirection.ASCENDING, SortDirection.DESCENDING }); + grid.sort(Sort.by("foo").then("bar") + .then("baz", SortDirection.DESCENDING)); + + listener.assertEventFired( + new SortOrder("foo", SortDirection.ASCENDING), new SortOrder( + "bar", SortDirection.ASCENDING), new SortOrder("baz", + SortDirection.DESCENDING)); + + container = new DummySortingIndexedContainer(); + container.addContainerProperty("foo", Person.class, null); + container.addContainerProperty("baz", String.class, ""); + container.addContainerProperty("bar", Person.class, null); + container.expectedSort(new Object[] { "baz" }, + new SortDirection[] { SortDirection.DESCENDING }); + grid.setContainerDataSource(container); + + listener.assertEventFired(new SortOrder("baz", SortDirection.DESCENDING)); + + } + + private DummySortingIndexedContainer createContainer() { + DummySortingIndexedContainer container = new DummySortingIndexedContainer(); + container.addContainerProperty("foo", Integer.class, 0); + container.addContainerProperty("bar", Integer.class, 0); + container.addContainerProperty("baz", Integer.class, 0); + return container; + } +} diff --git a/server/tests/src/com/vaadin/tests/server/renderer/ImageRendererTest.java b/server/tests/src/com/vaadin/tests/server/renderer/ImageRendererTest.java new file mode 100644 index 0000000000..08f045277d --- /dev/null +++ b/server/tests/src/com/vaadin/tests/server/renderer/ImageRendererTest.java @@ -0,0 +1,85 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.tests.server.renderer; + +import static org.junit.Assert.assertEquals; + +import java.io.File; + +import org.easymock.EasyMock; +import org.junit.Before; +import org.junit.Test; + +import com.vaadin.server.ClassResource; +import com.vaadin.server.ExternalResource; +import com.vaadin.server.FileResource; +import com.vaadin.server.FontAwesome; +import com.vaadin.server.ThemeResource; +import com.vaadin.ui.Grid; +import com.vaadin.ui.UI; +import com.vaadin.ui.renderer.ImageRenderer; + +import elemental.json.JsonObject; +import elemental.json.JsonValue; + +public class ImageRendererTest { + + private ImageRenderer renderer; + + @Before + public void setUp() { + UI mockUI = EasyMock.createNiceMock(UI.class); + EasyMock.replay(mockUI); + + Grid grid = new Grid(); + grid.setParent(mockUI); + + renderer = new ImageRenderer(); + renderer.setParent(grid); + } + + @Test + public void testThemeResource() { + JsonValue v = renderer.encode(new ThemeResource("foo.png")); + assertEquals("theme://foo.png", getUrl(v)); + } + + @Test + public void testExternalResource() { + JsonValue v = renderer.encode(new ExternalResource( + "http://example.com/foo.png")); + assertEquals("http://example.com/foo.png", getUrl(v)); + } + + @Test(expected = IllegalArgumentException.class) + public void testFileResource() { + renderer.encode(new FileResource(new File("/tmp/foo.png"))); + } + + @Test(expected = IllegalArgumentException.class) + public void testClassResource() { + renderer.encode(new ClassResource("img/foo.png")); + } + + @Test(expected = IllegalArgumentException.class) + public void testFontIcon() { + renderer.encode(FontAwesome.AMBULANCE); + } + + private String getUrl(JsonValue v) { + return ((JsonObject) v).get("uRL").asString(); + } +} diff --git a/server/tests/src/com/vaadin/tests/server/renderer/RendererTest.java b/server/tests/src/com/vaadin/tests/server/renderer/RendererTest.java new file mode 100644 index 0000000000..464d409543 --- /dev/null +++ b/server/tests/src/com/vaadin/tests/server/renderer/RendererTest.java @@ -0,0 +1,209 @@ +/* + * Copyright 2000-2014 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.tests.server.renderer; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; + +import java.util.Locale; + +import org.easymock.EasyMock; +import org.junit.Before; +import org.junit.Test; + +import com.vaadin.data.Item; +import com.vaadin.data.RpcDataProviderExtension; +import com.vaadin.data.util.IndexedContainer; +import com.vaadin.data.util.converter.Converter; +import com.vaadin.data.util.converter.StringToIntegerConverter; +import com.vaadin.server.VaadinSession; +import com.vaadin.tests.util.AlwaysLockedVaadinSession; +import com.vaadin.ui.ConnectorTracker; +import com.vaadin.ui.Grid; +import com.vaadin.ui.Grid.Column; +import com.vaadin.ui.UI; +import com.vaadin.ui.renderer.TextRenderer; + +import elemental.json.JsonValue; + +public class RendererTest { + + private static class TestBean { + int i = 42; + } + + private static class ExtendedBean extends TestBean { + float f = 3.14f; + } + + private static class TestRenderer extends TextRenderer { + @Override + public JsonValue encode(String value) { + return super.encode("renderer(" + value + ")"); + } + } + + private static class TestConverter implements Converter<String, TestBean> { + + @Override + public TestBean convertToModel(String value, + Class<? extends TestBean> targetType, Locale locale) + throws ConversionException { + return null; + } + + @Override + public String convertToPresentation(TestBean value, + Class<? extends String> targetType, Locale locale) + throws ConversionException { + if (value instanceof ExtendedBean) { + return "ExtendedBean(" + value.i + ", " + + ((ExtendedBean) value).f + ")"; + } else { + return "TestBean(" + value.i + ")"; + } + } + + @Override + public Class<TestBean> getModelType() { + return TestBean.class; + } + + @Override + public Class<String> getPresentationType() { + return String.class; + } + } + + private Grid grid; + + private Column foo; + private Column bar; + private Column baz; + private Column bah; + + @Before + public void setUp() { + VaadinSession.setCurrent(new AlwaysLockedVaadinSession(null)); + + IndexedContainer c = new IndexedContainer(); + + c.addContainerProperty("foo", Integer.class, 0); + c.addContainerProperty("bar", String.class, ""); + c.addContainerProperty("baz", TestBean.class, null); + c.addContainerProperty("bah", ExtendedBean.class, null); + + Object id = c.addItem(); + Item item = c.getItem(id); + item.getItemProperty("foo").setValue(123); + item.getItemProperty("bar").setValue("321"); + item.getItemProperty("baz").setValue(new TestBean()); + item.getItemProperty("bah").setValue(new ExtendedBean()); + + UI ui = EasyMock.createNiceMock(UI.class); + ConnectorTracker ct = EasyMock.createNiceMock(ConnectorTracker.class); + EasyMock.expect(ui.getConnectorTracker()).andReturn(ct).anyTimes(); + EasyMock.replay(ui, ct); + + grid = new Grid(c); + grid.setParent(ui); + + foo = grid.getColumn("foo"); + bar = grid.getColumn("bar"); + baz = grid.getColumn("baz"); + bah = grid.getColumn("bah"); + } + + @Test + public void testDefaultRendererAndConverter() throws Exception { + assertSame(TextRenderer.class, foo.getRenderer().getClass()); + assertSame(StringToIntegerConverter.class, foo.getConverter() + .getClass()); + + assertSame(TextRenderer.class, bar.getRenderer().getClass()); + // String->String; converter not needed + assertNull(bar.getConverter()); + + assertSame(TextRenderer.class, baz.getRenderer().getClass()); + // MyBean->String; converter not found + assertNull(baz.getConverter()); + } + + @Test + public void testFindCompatibleConverter() throws Exception { + foo.setRenderer(renderer()); + assertSame(StringToIntegerConverter.class, foo.getConverter() + .getClass()); + + bar.setRenderer(renderer()); + assertNull(bar.getConverter()); + } + + @Test(expected = IllegalArgumentException.class) + public void testCannotFindConverter() { + baz.setRenderer(renderer()); + } + + @Test + public void testExplicitConverter() throws Exception { + baz.setRenderer(renderer(), converter()); + bah.setRenderer(renderer(), converter()); + } + + @Test + public void testEncoding() throws Exception { + assertEquals("42", render(foo, 42).asString()); + foo.setRenderer(renderer()); + assertEquals("renderer(42)", render(foo, 42).asString()); + + assertEquals("2.72", render(bar, "2.72").asString()); + bar.setRenderer(new TestRenderer()); + assertEquals("renderer(2.72)", render(bar, "2.72").asString()); + } + + @Test + public void testEncodingWithoutConverter() throws Exception { + assertEquals("", render(baz, new TestBean()).asString()); + } + + @Test + public void testBeanEncoding() throws Exception { + baz.setRenderer(renderer(), converter()); + bah.setRenderer(renderer(), converter()); + + assertEquals("renderer(TestBean(42))", render(baz, new TestBean()) + .asString()); + assertEquals("renderer(ExtendedBean(42, 3.14))", + render(baz, new ExtendedBean()).asString()); + + assertEquals("renderer(ExtendedBean(42, 3.14))", + render(bah, new ExtendedBean()).asString()); + } + + private TestConverter converter() { + return new TestConverter(); + } + + private TestRenderer renderer() { + return new TestRenderer(); + } + + private JsonValue render(Column column, Object value) { + return RpcDataProviderExtension.encodeValue(value, + column.getRenderer(), column.getConverter(), grid.getLocale()); + } +} |