diff options
Diffstat (limited to 'server/src/com')
30 files changed, 8259 insertions, 80 deletions
diff --git a/server/src/com/vaadin/data/Container.java b/server/src/com/vaadin/data/Container.java index 8e99bac541..fb7a93e832 100644 --- a/server/src/com/vaadin/data/Container.java +++ b/server/src/com/vaadin/data/Container.java @@ -582,6 +582,64 @@ 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. + * + * @since 7.4 + */ + 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. + * + * @since 7.4 + */ + 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..5da95c3b5c --- /dev/null +++ b/server/src/com/vaadin/data/RpcDataProviderExtension.java @@ -0,0 +1,1045 @@ +/* + * 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.CellReference; +import com.vaadin.ui.Grid.CellStyleGenerator; +import com.vaadin.ui.Grid.Column; +import com.vaadin.ui.Grid.RowReference; +import com.vaadin.ui.Grid.RowStyleGenerator; +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 7.4 + * @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 setActiveRange(Range newActiveRange) { + final Range[] removed = activeRange.partitionWith(newActiveRange); + final Range[] added = newActiveRange.partitionWith(activeRange); + + removeActiveRows(removed[0]); + removeActiveRows(removed[2]); + addActiveRows(added[0]); + addActiveRows(added[2]); + + 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(Range added) { + if (added.isEmpty()) { + // Some container.getItemIds() implementations just might be + // expensive even for an empty range, so bail out early + return; + } + + List<?> newItemIds = container.getItemIds(added.getStart(), + added.length()); + Integer index = added.getStart(); + for (Object itemId : newItemIds) { + /* + * 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 inside this + * if-state. But it sounds too stupid (and most often too + * insignificant) to try out. + */ + if (!indexToItemId.containsKey(index)) { + /* + * 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. + */ + if (!itemIdToKey.containsKey(itemId)) { + itemIdToKey.put(itemId, nextKey()); + } + + indexToItemId.forcePut(index, itemId); + } + index++; + } + } + + 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.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; + + assert valueChangeListeners.size() == newActiveRange.length() : "Value change listeners not set up correctly!"; + } + + 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, item); + valueChangeListeners.put(itemId, listener); + } + } + + private void removeValueChangeListeners(Range range) { + for (int i = range.getStart(); i < range.getEnd(); i++) { + final Object itemId = container.getIdByIndex(i); + final GridValueChangeListener listener = valueChangeListeners + .remove(itemId); + + if (listener != null) { + listener.removeListener(); + } + } + } + + /** + * Manages removed columns in active rows. + * <p> + * This method does <em>not</em> send data again to the client. + * + * @param removedColumns + * the columns that have been removed from the grid + */ + public void columnsRemoved(Collection<Column> removedColumns) { + if (removedColumns.isEmpty()) { + return; + } + + for (GridValueChangeListener listener : valueChangeListeners + .values()) { + listener.removeColumns(removedColumns); + } + } + + /** + * Manages added columns in active rows. + * <p> + * This method sends the data for the changed rows to client side. + * + * @param addedColumns + * the columns that have been added to the grid + */ + public void columnsAdded(Collection<Column> addedColumns) { + if (addedColumns.isEmpty()) { + return; + } + + for (GridValueChangeListener listener : valueChangeListeners + .values()) { + listener.addColumns(addedColumns); + } + } + + /** + * 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.withLength(firstIndex, count); + addValueChangeListeners(freshRange); + } else { + // out of view, noop + } + } + + /** + * Handles the removal of rows. + * <p> + * This method's responsibilities are to: + * <ul> + * <li>shift the internal bookkeeping by <code>count</code> if the + * removal happens above currently active range + * <li>ignore rows removed below the currently active range + * </ul> + * + * @param firstIndex + * the index of the first removed rows + * @param count + * the number of rows removed at <code>firstIndex</code> + */ + public void removeRows(int firstIndex, int count) { + int lastRemoved = firstIndex + count; + if (lastRemoved < activeRange.getStart()) { + /* firstIndex < lastIndex < start */ + activeRange = activeRange.offsetBy(-count); + } else if (firstIndex < activeRange.getEnd()) { + final Range deprecated = Range.between( + Math.max(activeRange.getStart(), firstIndex), + Math.min(activeRange.getEnd(), lastRemoved + 1)); + for (int i = deprecated.getStart(); i < deprecated.getEnd(); ++i) { + Object itemId = keyMapper.itemIdAtIndex(i); + // Item doesn't exist anymore. + valueChangeListeners.remove(itemId); + } + + activeRange = Range.withLength(activeRange.getStart(), + activeRange.length() - deprecated.length()); + } else { + /* end <= firstIndex, no need to do anything */ + } + } + } + + /** + * 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; + private final Item item; + + public GridValueChangeListener(Object itemId, Item item) { + /* + * Using an assert instead of an exception throw, just to optimize + * prematurely + */ + assert itemId != null : "null itemId not accepted"; + this.itemId = itemId; + this.item = item; + + internalAddColumns(getGrid().getColumns()); + } + + @Override + public void valueChange(ValueChangeEvent event) { + updateRowData(itemId); + } + + public void removeListener() { + removeColumns(getGrid().getColumns()); + } + + public void addColumns(Collection<Column> addedColumns) { + internalAddColumns(addedColumns); + updateRowData(itemId); + } + + private void internalAddColumns(Collection<Column> addedColumns) { + for (final Column column : addedColumns) { + final Property<?> property = item.getItemProperty(column + .getPropertyId()); + if (property instanceof ValueChangeNotifier) { + ((ValueChangeNotifier) property) + .addValueChangeListener(this); + } + } + } + + public void removeColumns(Collection<Column> removedColumns) { + for (final Column column : removedColumns) { + final Property<?> property = item.getItemProperty(column + .getPropertyId()); + if (property instanceof ValueChangeNotifier) { + ((ValueChangeNotifier) property) + .removeValueChangeListener(this); + } + } + } + } + + 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. + */ + + Map<Object, GridValueChangeListener> listeners = activeRowHandler.valueChangeListeners; + for (GridValueChangeListener listener : listeners.values()) { + listener.removeListener(); + } + listeners.clear(); + activeRowHandler.activeRange = Range.withLength(0, 0); + keyMapper.setActiveRange(Range.withLength(0, 0)); + keyMapper.indexToItemId.clear(); + rpc.resetDataAndSize(event.getContainer().size()); + } + } + }; + + private final DataProviderKeyMapper keyMapper = new DataProviderKeyMapper(); + + private KeyMapper<Object> columnKeys; + + /* Has client been initialized */ + private boolean clientInitialized = false; + + private RowReference rowReference; + private CellReference cellReference; + + private Set<Object> updatedItemIds = new HashSet<Object>(); + + /** + * 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) { + if (initial) { + 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); + } + + for (Object itemId : updatedItemIds) { + internalUpdateRowData(itemId); + } + updatedItemIds.clear(); + + super.beforeClientResponse(initial); + } + + 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); + } + + keyMapper.setActiveRange(active); + + List<?> itemIds = container.getItemIds(firstRowToPush, numberOfRows); + 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); + + 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.getPropertyId(); + + 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)); + + rowReference.set(itemId); + + CellStyleGenerator cellStyleGenerator = grid.getCellStyleGenerator(); + if (cellStyleGenerator != null) { + setGeneratedCellStyles(cellStyleGenerator, rowObject, columns); + } + RowStyleGenerator rowStyleGenerator = grid.getRowStyleGenerator(); + if (rowStyleGenerator != null) { + setGeneratedRowStyles(rowStyleGenerator, rowObject); + } + + return rowObject; + } + + private void setGeneratedCellStyles(CellStyleGenerator generator, + JsonObject rowObject, Collection<Column> columns) { + JsonObject cellStyles = null; + for (Column column : columns) { + Object propertyId = column.getPropertyId(); + cellReference.set(propertyId); + String style = generator.getStyle(cellReference); + 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); + } + + } + + private void setGeneratedRowStyles(RowStyleGenerator generator, + JsonObject rowObject) { + String rowStyle = generator.getStyle(rowReference); + 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); + } + + activeRowHandler.removeRows(firstIndex, count); + } + + /** + * Informs the client side that data of a row has been modified in the data + * source. + * + * @param itemId + * the item Id the row that was updated + */ + public void updateRowData(Object itemId) { + if (updatedItemIds.isEmpty()) { + // At least one new item will be updated. Mark as dirty to actually + // update before response to client. + markAsDirty(); + } + + updatedItemIds.add(itemId); + } + + private void internalUpdateRowData(Object itemId) { + int index = container.indexOfId(itemId); + if (index >= 0) { + JsonValue row = getRowData(getGrid().getColumns(), itemId); + JsonArray rowArray = Json.createArray(); + rowArray.set(0, row); + rpc.setRowData(index, rowArray); + } + } + + /** + * 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); + } + + } else if (parent instanceof Grid) { + Grid grid = (Grid) parent; + rowReference = new RowReference(grid); + cellReference = new CellReference(rowReference); + } else { + throw new IllegalStateException( + "Grid is the only accepted parent type"); + } + } + + /** + * Informs this data provider that given columns have been removed from + * grid. + * + * @param removedColumns + * a list of removed columns + */ + public void columnsRemoved(List<Column> removedColumns) { + activeRowHandler.columnsRemoved(removedColumns); + } + + /** + * Informs this data provider that given columns have been added to grid. + * + * @param addedColumns + * a list of added columns + */ + public void columnsAdded(List<Column> addedColumns) { + activeRowHandler.columnsAdded(addedColumns); + } + + 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/fieldgroup/FieldGroup.java b/server/src/com/vaadin/data/fieldgroup/FieldGroup.java index 4790d786e7..069cb2e153 100644 --- a/server/src/com/vaadin/data/fieldgroup/FieldGroup.java +++ b/server/src/com/vaadin/data/fieldgroup/FieldGroup.java @@ -344,7 +344,8 @@ public class FieldGroup implements Serializable { .getWrappedProperty(); } - if (fieldDataSource == getItemProperty(propertyId)) { + if (getItemDataSource() != null + && fieldDataSource == getItemProperty(propertyId)) { if (null != wrapper) { wrapper.detachFromProperty(); } 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..81e0d08c73 --- /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.data.sort.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..2c419f88b7 --- /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.data.sort.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 27168694e6..f7b1a4b0d8 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,87 @@ 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> + * + * @since 7.4 + */ + 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> + * + * @since 7.4 + */ + 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. * @@ -897,33 +980,45 @@ public abstract class AbstractInMemoryContainer<ITEMIDTYPE, PROPERTYIDCLASS, ITE /** * Notify item set change listeners that an item has been added to the * container. - * <p> - * Unless subclasses specify otherwise, the default notification indicates a - * full refresh. * * @since 7.4 * - * @param postion - * position of the added item in the view (if visible) + * @param position + * 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. - * <p> - * Unless subclasses specify otherwise, the default notification indicates a - * full refresh. * * @since 7.4 * - * @param postion + * @param position * position of the removed item in the view prior to removal (if * was visible) * @param itemId @@ -931,7 +1026,28 @@ public abstract class AbstractInMemoryContainer<ITEMIDTYPE, PROPERTYIDCLASS, ITE * {@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 @@ -950,6 +1066,23 @@ 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. + * <p> + * For internal use only. + * + * @since 7.4 + * + * @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..fd2ce609b8 --- /dev/null +++ b/server/src/com/vaadin/data/util/GeneratedPropertyContainer.java @@ -0,0 +1,724 @@ +/* + * 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.data.sort.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 7.4 + * @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); + } + + /** + * Returns the original underlying container. + * + * @return the original underlying container + */ + public Container.Indexed getWrappedContainer() { + return wrappedContainer; + } +} 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..453e45b1db --- /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 7.4 + * @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/SelectionEvent.java b/server/src/com/vaadin/event/SelectionEvent.java new file mode 100644 index 0000000000..e75369e6da --- /dev/null +++ b/server/src/com/vaadin/event/SelectionEvent.java @@ -0,0 +1,114 @@ +/* + * 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.Collections; +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 7.4 + * @author Vaadin Ltd + */ +public class SelectionEvent extends EventObject { + + private LinkedHashSet<Object> oldSelection; + private LinkedHashSet<Object> newSelection; + + public SelectionEvent(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); + } + + /** + * A {@link Collection} of all the itemIds that are currently selected. + * + * @return a Collection of the itemIds that are currently selected + */ + public Set<Object> getSelected() { + return Collections.unmodifiableSet(newSelection); + } + + /** + * The listener interface for receiving {@link SelectionEvent + * SelectionEvents}. + */ + public interface SelectionListener extends Serializable { + /** + * Notifies the listener that the selection state has changed. + * + * @param event + * the selection change event + */ + void select(SelectionEvent event); + } + + /** + * The interface for adding and removing listeners for + * {@link SelectionEvent SelectionEvents}. + */ + public interface SelectionNotifier extends Serializable { + /** + * Registers a new selection listener + * + * @param listener + * the listener to register + */ + void addSelectionListener(SelectionListener listener); + + /** + * Removes a previously registered selection change listener + * + * @param listener + * the listener to remove + */ + void removeSelectionListener(SelectionListener listener); + } +} diff --git a/server/src/com/vaadin/event/SortEvent.java b/server/src/com/vaadin/event/SortEvent.java new file mode 100644 index 0000000000..f303e47781 --- /dev/null +++ b/server/src/com/vaadin/event/SortEvent.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.List; + +import com.vaadin.data.sort.SortOrder; +import com.vaadin.ui.Component; + +/** + * Event describing a change in sorting of a {@link Container}. Fired by + * {@link SortNotifier SortNotifiers}. + * + * @see SortListener + * + * @since 7.4 + * @author Vaadin Ltd + */ +public class SortEvent extends Component.Event { + + private final List<SortOrder> sortOrder; + private final boolean userOriginated; + + /** + * 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 userOriginated + * <code>true</code> if event is a result of user interaction, + * <code>false</code> if from API call + */ + public SortEvent(Component source, List<SortOrder> sortOrder, + boolean userOriginated) { + super(source); + this.sortOrder = sortOrder; + this.userOriginated = userOriginated; + } + + /** + * 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 userOriginated; + } + + /** + * Listener for sort order change events. + */ + public interface SortListener extends Serializable { + /** + * Called when the sort order has changed. + * + * @param event + * the sort order change event + */ + public void sort(SortEvent event); + } + + /** + * The interface for adding and removing listeners for {@link SortEvent + * SortEvents}. + */ + public interface SortNotifier 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 addSortListener(SortListener listener); + + /** + * Removes a sort order change listener previously added using + * {@link #addSortListener(SortListener)}. + * + * @param listener + * the sort order change listener to remove + */ + public void removeSortListener(SortListener listener); + } +} diff --git a/server/src/com/vaadin/server/AbstractJavaScriptExtension.java b/server/src/com/vaadin/server/AbstractJavaScriptExtension.java index e182319c85..e9cf2c5e33 100644 --- a/server/src/com/vaadin/server/AbstractJavaScriptExtension.java +++ b/server/src/com/vaadin/server/AbstractJavaScriptExtension.java @@ -106,8 +106,8 @@ import com.vaadin.ui.JavaScriptFunction; * <li>Java Dates are represented by JavaScript numbers containing the timestamp * </li> * <li>List, Set and all arrays in Java are represented by JavaScript arrays.</li> - * <li>Map<String, ?> in Java is represented by JavaScript object with fields - * corresponding to the map keys.</li> + * <li>Map<String, ?> in Java is represented by JavaScript object with + * fields corresponding to the map keys.</li> * <li>Any other Java Map is represented by a JavaScript array containing two * arrays, the first contains the keys and the second contains the values in the * same order.</li> diff --git a/server/src/com/vaadin/server/JsonCodec.java b/server/src/com/vaadin/server/JsonCodec.java index 6833cef1f7..498bf3e6b2 100644 --- a/server/src/com/vaadin/server/JsonCodec.java +++ b/server/src/com/vaadin/server/JsonCodec.java @@ -300,7 +300,9 @@ public class JsonCodec implements Serializable { } // Try to decode object using fields - if (value.getType() == JsonType.NULL) { + if (isJsonType(targetType)) { + return value; + } else if (value.getType() == JsonType.NULL) { return null; } else if (targetType == byte.class || targetType == Byte.class) { return Byte.valueOf((byte) value.asNumber()); @@ -334,6 +336,11 @@ public class JsonCodec implements Serializable { } } + private static boolean isJsonType(Type type) { + return type instanceof Class<?> + && JsonValue.class.isAssignableFrom((Class<?>) type); + } + private static Object decodeArray(Type componentType, JsonArray value, ConnectorTracker connectorTracker) { Class<?> componentClass = getClassForType(componentType); diff --git a/server/src/com/vaadin/ui/AbstractJavaScriptComponent.java b/server/src/com/vaadin/ui/AbstractJavaScriptComponent.java index f3cbf47b62..84023555bb 100644 --- a/server/src/com/vaadin/ui/AbstractJavaScriptComponent.java +++ b/server/src/com/vaadin/ui/AbstractJavaScriptComponent.java @@ -119,8 +119,8 @@ import com.vaadin.shared.ui.JavaScriptComponentState; * <li>Java Dates are represented by JavaScript numbers containing the timestamp * </li> * <li>List, Set and all arrays in Java are represented by JavaScript arrays.</li> - * <li>Map<String, ?> in Java is represented by JavaScript object with fields - * corresponding to the map keys.</li> + * <li>Map<String, ?> in Java is represented by JavaScript object with + * fields corresponding to the map keys.</li> * <li>Any other Java Map is represented by a JavaScript array containing two * arrays, the first contains the keys and the second contains the values in the * same order.</li> 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..adb08e0fcc --- /dev/null +++ b/server/src/com/vaadin/ui/Grid.java @@ -0,0 +1,4678 @@ +/* + * 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.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.ItemClickEvent; +import com.vaadin.event.ItemClickEvent.ItemClickListener; +import com.vaadin.event.ItemClickEvent.ItemClickNotifier; +import com.vaadin.event.SelectionEvent; +import com.vaadin.event.SelectionEvent.SelectionListener; +import com.vaadin.event.SelectionEvent.SelectionNotifier; +import com.vaadin.event.SortEvent; +import com.vaadin.event.SortEvent.SortListener; +import com.vaadin.event.SortEvent.SortNotifier; +import com.vaadin.server.AbstractClientConnector; +import com.vaadin.server.AbstractExtension; +import com.vaadin.server.ErrorMessage; +import com.vaadin.server.JsonCodec; +import com.vaadin.server.KeyMapper; +import com.vaadin.server.VaadinSession; +import com.vaadin.shared.MouseEventDetails; +import com.vaadin.shared.data.sort.SortDirection; +import com.vaadin.shared.ui.grid.EditorClientRpc; +import com.vaadin.shared.ui.grid.EditorServerRpc; +import com.vaadin.shared.ui.grid.GridClientRpc; +import com.vaadin.shared.ui.grid.GridColumnState; +import com.vaadin.shared.ui.grid.GridConstants; +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.util.SharedUtil; +import com.vaadin.ui.renderer.ObjectRenderer; +import com.vaadin.ui.renderer.Renderer; +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 7.4 + * @author Vaadin Ltd + */ +public class Grid extends AbstractComponent implements SelectionNotifier, + SortNotifier, SelectiveRenderer, ItemClickNotifier { + + /** + * Custom field group that allows finding property types before an item has + * been bound. + */ + private final class CustomFieldGroup extends FieldGroup { + @Override + protected Class<?> getPropertyType(Object propertyId) + throws BindException { + if (getItemDataSource() == null) { + return datasource.getType(propertyId); + } else { + return super.getPropertyType(propertyId); + } + } + } + + /** + * 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. + */ + 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(); + + /** + * Marks items as selected while deselecting all items not in the + * given Collection. + * + * @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 + */ + boolean setSelected(Collection<?> itemIds) + throws IllegalArgumentException; + + /** + * Marks items as selected while deselecting all items not in the + * varargs array. + * + * @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 the <code>itemIds</code> varargs array is + * <code>null</code> or given itemIds don't exist in the + * container of Grid + */ + boolean setSelected(Object... itemIds) + throws IllegalArgumentException; + } + + /** + * 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. + */ + 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 SelectionEvent} to all the {@link SelectionListener + * SelectionListeners} 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 SelectionNotifier} + * + * @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 fireSelectionEvent( + final Collection<Object> oldSelection, + final Collection<Object> newSelection) { + grid.fireSelectionEvent(oldSelection, newSelection); + } + } + + /** + * A default implementation of a {@link SelectionModel.Single} + */ + 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(); + } + + fireSelectionEvent(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) { + fireSelectionEvent(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} + */ + 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} + */ + 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); + } + fireSelectionEvent(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); + fireSelectionEvent(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(); + } + + @Override + public boolean setSelected(Collection<?> itemIds) + throws IllegalArgumentException { + if (itemIds == null) { + throw new IllegalArgumentException("itemIds may not be null"); + } + + checkItemIdsExist(itemIds); + + boolean changed = false; + Set<Object> selectedRows = new HashSet<Object>(itemIds); + final Collection<Object> oldSelection = getSelectedRows(); + SetView<?> added = Sets.difference(selectedRows, selection); + if (!added.isEmpty()) { + changed = true; + selection.addAll(added.immutableCopy()); + } + + SetView<?> removed = Sets.difference(selection, selectedRows); + if (!removed.isEmpty()) { + changed = true; + selection.removeAll(removed.immutableCopy()); + } + + if (changed) { + fireSelectionEvent(oldSelection, selection); + } + + return changed; + } + + @Override + public boolean setSelected(Object... itemIds) + throws IllegalArgumentException { + if (itemIds != null) { + return setSelected(Arrays.asList(itemIds)); + } else { + throw new IllegalArgumentException( + "Vararg array of itemIds may not be null"); + } + } + } + + /** + * A data class which contains information which identifies a row in a + * {@link Grid}. + * <p> + * Since this class follows the <code>Flyweight</code>-pattern any instance + * of this object is subject to change without the user knowing it and so + * should not be stored anywhere outside of the method providing these + * instances. + */ + public static class RowReference implements Serializable { + private final Grid grid; + + private Object itemId; + + /** + * Creates a new row reference for the given grid. + * + * @param grid + * the grid that the row belongs to + */ + public RowReference(Grid grid) { + this.grid = grid; + } + + /** + * Sets the identifying information for this row + * + * @param itemId + * the item id of the row + */ + public void set(Object itemId) { + this.itemId = itemId; + } + + /** + * Gets the grid that contains the referenced row. + * + * @return the grid that contains referenced row + */ + public Grid getGrid() { + return grid; + } + + /** + * Gets the item id of the row. + * + * @return the item id of the row + */ + public Object getItemId() { + return itemId; + } + + /** + * Gets the item for the row. + * + * @return the item for the row + */ + public Item getItem() { + return grid.getContainerDataSource().getItem(itemId); + } + } + + /** + * A data class which contains information which identifies a cell in a + * {@link Grid}. + * <p> + * Since this class follows the <code>Flyweight</code>-pattern any instance + * of this object is subject to change without the user knowing it and so + * should not be stored anywhere outside of the method providing these + * instances. + */ + public static class CellReference implements Serializable { + private final RowReference rowReference; + + private Object propertyId; + + public CellReference(RowReference rowReference) { + this.rowReference = rowReference; + } + + /** + * Sets the identifying information for this cell + * + * @param propertyId + * the property id of the column + */ + public void set(Object propertyId) { + this.propertyId = propertyId; + } + + /** + * Gets the grid that contains the referenced cell. + * + * @return the grid that contains referenced cell + */ + public Grid getGrid() { + return rowReference.getGrid(); + } + + /** + * @return the property id of the column + */ + public Object getPropertyId() { + return propertyId; + } + + /** + * @return the property for the cell + */ + public Property<?> getProperty() { + return getItem().getItemProperty(propertyId); + } + + /** + * Gets the item id of the row of the cell. + * + * @return the item id of the row + */ + public Object getItemId() { + return rowReference.getItemId(); + } + + /** + * Gets the item for the row of the cell. + * + * @return the item for the row + */ + public Item getItem() { + return rowReference.getItem(); + } + + /** + * Gets the value of the cell. + * + * @return the value of the cell + */ + public Object getValue() { + return getProperty().getValue(); + } + } + + /** + * Callback interface for generating custom style names for data rows + * + * @see Grid#setRowStyleGenerator(RowStyleGenerator) + */ + public interface RowStyleGenerator extends Serializable { + + /** + * Called by Grid to generate a style name for a row + * + * @param rowReference + * The row to generate a style for + * @return the style name to add to this row, or {@code null} to not set + * any style + */ + public String getStyle(RowReference rowReference); + } + + /** + * Callback interface for generating custom style names for cells + * + * @see Grid#setCellStyleGenerator(CellStyleGenerator) + */ + public interface CellStyleGenerator extends Serializable { + + /** + * Called by Grid to generate a style name for a column + * + * @param cellReference + * The cell to generate a style for + * @return the style name to add to this cell, or {@code null} to not + * set any style + */ + public String getStyle(CellReference cellReference); + } + + /** + * 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) { + removeComponentIfPresent(); + 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) { + removeComponentIfPresent(); + 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) { + removeComponentIfPresent(); + 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(); + } + + private void removeComponentIfPresent() { + Component component = (Component) cellState.connector; + if (component != null) { + component.setParent(null); + cellState.connector = null; + } + } + } + + 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()); + + for (Object id : grid.columns.keySet()) { + 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 propertyId; + + 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 propertyId + * the backing property id for this column + */ + Column(Grid grid, GridColumnState state, Object propertyId) { + this.grid = grid; + this.state = state; + this.propertyId = propertyId; + + internalSetRenderer(new ObjectRenderer() { + private boolean warned = false; + private final String DEFAULT_RENDERER_WARNING = "This column uses " + + "a dummy default ObjectRenderer. A more suitable " + + "renderer should be set using the setRenderer() " + + "method."; + + @Override + public JsonValue encode(Object value) { + if (!warned && !(value instanceof String)) { + getLogger().warning( + Column.this.toString() + ": " + + DEFAULT_RENDERER_WARNING); + warned = true; + } + return super.encode(value); + } + }); + } + + /** + * 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 getPropertyId() { + return propertyId; + } + + /** + * 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). + * <p> + * This overrides any configuration set by any of + * {@link #setExpandRatio(int)}, {@link #setMinimumWidth(double)} or + * {@link #setMaximumWidth(double)}. + * + * @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) { + boolean success = internalSetRenderer(renderer); + if (!success) { + 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; + } + + @SuppressWarnings("unchecked") + 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(); + + if (sortable) { + if (!(grid.datasource instanceof Sortable)) { + throw new IllegalStateException( + "Can't set column " + + toString() + + " sortable. The Container of Grid does not implement Sortable"); + } else if (!((Sortable) grid.datasource) + .getSortableContainerPropertyIds().contains(propertyId)) { + throw new IllegalStateException( + "Can't set column " + + toString() + + " sortable. Container doesn't support sorting by property " + + propertyId); + } + } + + 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) + "]"; + } + + /** + * Sets the ratio with which the column expands. + * <p> + * By default, all columns expand equally (treated as if all of them had + * an expand ratio of 1). Once at least one column gets a defined expand + * ratio, the implicit expand ratio is removed, and only the defined + * expand ratios are taken into account. + * <p> + * If a column has a defined width ({@link #setWidth(double)}), it + * overrides this method's effects. + * <p> + * <em>Example:</em> A grid with three columns, with expand ratios 0, 1 + * and 2, respectively. The column with a <strong>ratio of 0 is exactly + * as wide as its contents requires</strong>. The column with a ratio of + * 1 is as wide as it needs, <strong>plus a third of any excess + * space</strong>, bceause we have 3 parts total, and this column + * reservs only one of those. The column with a ratio of 2, is as wide + * as it needs to be, <strong>plus two thirds</strong> of the excess + * width. + * + * @param expandRatio + * the expand ratio of this column. {@code 0} to not have it + * expand at all. A negative number to clear the expand + * value. + * @throws IllegalStateException + * if the column is no longer attached to any grid + * @see #setWidth(double) + */ + public Column setExpandRatio(int expandRatio) + throws IllegalStateException { + checkColumnIsAttached(); + + getState().expandRatio = expandRatio; + grid.markAsDirty(); + return this; + } + + /** + * Gets the column's expand ratio. + * + * @return the column's expand ratio + * @see #setExpandRatio(int) + */ + public int getExpandRatio() { + return getState().expandRatio; + } + + /** + * Clears the expand ratio for this column. + * <p> + * Equal to calling {@link #setExpandRatio(int) setExpandRatio(-1)} + * + * @throws IllegalStateException + * if the column is no longer attached to any grid + */ + public Column clearExpandRatio() throws IllegalStateException { + return setExpandRatio(-1); + } + + /** + * Sets the minimum width for this column. + * <p> + * This defines the minimum guaranteed pixel width of the column + * <em>when it is set to expand</em>. + * + * @throws IllegalStateException + * if the column is no longer attached to any grid + * @see #setExpandRatio(int) + */ + public Column setMinimumWidth(double pixels) + throws IllegalStateException { + checkColumnIsAttached(); + + final double maxwidth = getMaximumWidth(); + if (pixels >= 0 && pixels > maxwidth && maxwidth >= 0) { + throw new IllegalArgumentException("New minimum width (" + + pixels + ") was greater than maximum width (" + + maxwidth + ")"); + } + getState().minWidth = pixels; + grid.markAsDirty(); + return this; + } + + /** + * Gets the minimum width for this column. + * + * @return the minimum width for this column + * @see #setMinimumWidth(double) + */ + public double getMinimumWidth() { + return getState().minWidth; + } + + /** + * Sets the maximum width for this column. + * <p> + * This defines the maximum allowed pixel width of the column + * <em>when it is set to expand</em>. + * + * @param pixels + * the maximum width + * @throws IllegalStateException + * if the column is no longer attached to any grid + * @see #setExpandRatio(int) + */ + public Column setMaximumWidth(double pixels) { + checkColumnIsAttached(); + + final double minwidth = getMinimumWidth(); + if (pixels >= 0 && pixels < minwidth && minwidth >= 0) { + throw new IllegalArgumentException("New maximum width (" + + pixels + ") was less than minimum width (" + minwidth + + ")"); + } + + getState().maxWidth = pixels; + grid.markAsDirty(); + return this; + } + + /** + * Gets the maximum width for this column. + * + * @return the maximum width for this column + * @see #setMaximumWidth(double) + */ + public double getMaximumWidth() { + return getState().maxWidth; + } + } + + /** + * An abstract base class for server-side Grid renderers. + * {@link com.vaadin.client.widget.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) { + return getParentGrid().getKeyMapper().getItemId(rowKey); + } + + /** + * Gets the column for a column id. + * <p> + * An id is used to identify a particular column on both a server and a + * client. This method can be used to get the column for the column id + * that the client has sent. + * + * @param columnId + * the column id for which to retrieve a column + * @return the column corresponding to {@code columnId} + */ + protected Column getColumn(String columnId) { + return getParentGrid().getColumnByColumnId(columnId); + } + + /** + * Gets the parent Grid of the renderer. + * + * @return parent grid + * @throws IllegalStateException + * if parent is not Grid + */ + protected Grid getParentGrid() { + if (getParent() instanceof Grid) { + Grid grid = (Grid) getParent(); + return grid; + } 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()); + + // Find columns that need to be removed. + List<Column> removedColumns = new LinkedList<Column>(); + for (Object propertyId : columns.keySet()) { + if (!properties.contains(propertyId)) { + removedColumns.add(getColumn(propertyId)); + } + } + + // Actually remove columns. + for (Column column : removedColumns) { + Object propertyId = column.getPropertyId(); + internalRemoveColumn(propertyId); + columnKeys.remove(propertyId); + } + datasourceExtension.columnsRemoved(removedColumns); + + // Add new columns + List<Column> addedColumns = new LinkedList<Column>(); + for (Object propertyId : properties) { + if (!columns.containsKey(propertyId)) { + addedColumns.add(appendColumn(propertyId)); + } + } + datasourceExtension.columnsAdded(addedColumns); + + 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 editorFieldGroup = new CustomFieldGroup(); + + private CellStyleGenerator cellStyleGenerator; + private RowStyleGenerator rowStyleGenerator; + + /** + * <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(SelectionListener.class, "select", SelectionEvent.class); + + private static final Method SORT_ORDER_CHANGE_METHOD = ReflectTools + .findMethod(SortListener.class, "sort", SortEvent.class); + + /** + * Creates a new Grid with a new {@link IndexedContainer} as the data + * source. + */ + public Grid() { + this(null, null); + } + + /** + * Creates a new Grid using the given data source. + * + * @param dataSource + * the indexed container to use as a data source + */ + public Grid(final Container.Indexed dataSource) { + this(null, dataSource); + } + + /** + * Creates a new Grid with the given caption and a new + * {@link IndexedContainer} data source. + * + * @param caption + * the caption of the grid + */ + public Grid(String caption) { + this(caption, null); + } + + /** + * Creates a new Grid with the given caption and data source. If the data + * source is null, a new {@link IndexedContainer} will be used. + * + * @param caption + * the caption of the grid + * @param dataSource + * the indexed container to use as a data source + */ + public Grid(String caption, Container.Indexed dataSource) { + if (dataSource == null) { + internalSetContainerDataSource(new IndexedContainer()); + } else { + setContainerDataSource(dataSource); + } + setCaption(caption); + initGrid(); + } + + /** + * Grid initial setup + */ + private void initGrid() { + setSelectionMode(SelectionMode.SINGLE); + addSelectionListener(new SelectionListener() { + @Override + public void select(SelectionEvent 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 select(List<String> selection) { + Collection<Object> receivedSelection = getKeyMapper() + .getItemIds(selection); + + applyingSelectionFromClient = true; + try { + SelectionModel selectionModel = getSelectionModel(); + if (selectionModel instanceof SelectionModel.Single + && selection.size() <= 1) { + Object select = null; + if (selection.size() == 1) { + select = getKeyMapper().getItemId(selection.get(0)); + } + ((SelectionModel.Single) selectionModel).select(select); + } else if (selectionModel instanceof SelectionModel.Multi) { + ((SelectionModel.Multi) selectionModel) + .setSelected(receivedSelection); + } else { + throw new IllegalStateException("SelectionModel " + + selectionModel.getClass().getSimpleName() + + " does not support selecting the given " + + selection.size() + " items."); + } + } 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, + boolean userOriginated) { + 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, userOriginated); + } + + @Override + public void selectAll() { + assert getSelectionModel() instanceof SelectionModel.Multi : "Not a multi selection model!"; + + ((SelectionModel.Multi) getSelectionModel()).selectAll(); + } + + @Override + public void itemClick(String rowKey, String columnId, + MouseEventDetails details) { + Object itemId = getKeyMapper().getItemId(rowKey); + Item item = datasource.getItem(itemId); + Object propertyId = getPropertyIdByColumnId(columnId); + fireEvent(new ItemClickEvent(Grid.this, item, itemId, + propertyId, details)); + } + }); + + registerRpc(new EditorServerRpc() { + + @Override + public void bind(int rowIndex) { + boolean success = false; + try { + Object id = getContainerDataSource().getIdByIndex(rowIndex); + if (editedItemId == null) { + editedItemId = id; + } + + if (editedItemId.equals(id)) { + success = true; + doEditItem(); + } + } catch (Exception e) { + handleError(e); + } + getEditorRpc().confirmBind(success); + } + + @Override + public void cancel(int rowIndex) { + try { + // For future proofing even though cannot currently fail + doCancelEditor(); + } catch (Exception e) { + handleError(e); + } + } + + @Override + public void save(int rowIndex) { + boolean success = false; + try { + saveEditor(); + success = true; + } catch (Exception e) { + handleError(e); + } + getEditorRpc().confirmSave(success); + } + + private void handleError(Exception e) { + com.vaadin.server.ErrorEvent.findErrorHandler(Grid.this).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); + } + + datasource = container; + + resetEditor(); + + // + // 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(false); + } else { + // Clear sorting order. Don't sort. + sortOrder.clear(); + } + + 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 { + column.setSortable(false); + } + } + } 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() + + "\""); + } + + if (!(datasource instanceof Sortable) + || !((Sortable) datasource) + .getSortableContainerPropertyIds().contains( + property)) { + columns.get(property).setSortable(false); + } + } + } + } + + /** + * 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, ""); + } + + // Inform the data provider of this new column. + Column column = getColumn(propertyId); + List<Column> addedColumns = new ArrayList<Column>(); + addedColumns.add(column); + datasourceExtension.columnsAdded(addedColumns); + + return column; + } + + /** + * 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() { + List<Column> removed = new ArrayList<Column>(columns.values()); + Set<Object> properties = new HashSet<Object>(columns.keySet()); + for (Object propertyId : properties) { + removeColumn(propertyId); + } + datasourceExtension.columnsRemoved(removed); + } + + /** + * 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.propertyIdToHumanFriendly(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 + * + * @throws IllegalArgumentException + * if there is no column for given property id in this grid + */ + public void removeColumn(Object propertyId) throws IllegalArgumentException { + if (!columns.keySet().contains(propertyId)) { + throw new IllegalArgumentException( + "There is no column for given property id " + propertyId); + } + + List<Column> removed = new ArrayList<Column>(); + removed.add(getColumn(propertyId)); + internalRemoveColumn(propertyId); + datasourceExtension.columnsRemoved(removed); + } + + private void internalRemoveColumn(Object propertyId) { + setEditorField(propertyId, null); + 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; + } + + /** + * Sets the Grid's selection mode. + * <p> + * Grid supports three selection modes: multiselect, single select and no + * selection, and this is a convenience 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 delegates to the + * {@link #getSelectionModel() selection model}. + * + * @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 delegates to the + * {@link #getSelectionModel() selection model}. 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 selection model does not implement + * {@code 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 if (selectionModel instanceof SelectionModel.Multi) { + throw new IllegalStateException("Cannot get unique selected row: " + + "Grid is in multiselect mode " + + "(the current selection model is " + + selectionModel.getClass().getName() + ")."); + } else if (selectionModel instanceof SelectionModel.None) { + throw new IllegalStateException("Cannot get selected row: " + + "Grid selection is disabled " + + "(the current selection model is " + + selectionModel.getClass().getName() + ")."); + } else { + throw new IllegalStateException("Cannot get selected row: " + + "Grid selection model does not implement " + + SelectionModel.Single.class.getName() + " or " + + SelectionModel.Multi.class.getName() + + "(the current model is " + + selectionModel.getClass().getName() + ")."); + } + } + + /** + * Marks an item as selected. + * <p> + * This method is a shorthand that delegates to the + * {@link #getSelectionModel() selection model}. Only + * {@link SelectionModel.Single} and {@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 selection model does not implement + * {@code SelectionModel.Single} or {@code 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 if (selectionModel instanceof SelectionModel.None) { + throw new IllegalStateException("Cannot select row '" + itemId + + "': Grid selection is disabled " + + "(the current selection model is " + + selectionModel.getClass().getName() + ")."); + } else { + throw new IllegalStateException("Cannot select row '" + itemId + + "': Grid selection model does not implement " + + SelectionModel.Single.class.getName() + " or " + + SelectionModel.Multi.class.getName() + + "(the current model is " + + selectionModel.getClass().getName() + ")."); + } + } + + /** + * Marks an item as unselected. + * <p> + * This method is a shorthand that delegates to the + * {@link #getSelectionModel() selection model}. 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 was already 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 requires one or more items to be selected + * at all times. + * @throws IllegalStateException + * if the selection model does not implement + * {@code SelectionModel.Single} or {code 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 if (selectionModel instanceof SelectionModel.None) { + throw new IllegalStateException("Cannot deselect row '" + itemId + + "': Grid selection is disabled " + + "(the current selection model is " + + selectionModel.getClass().getName() + ")."); + } else { + throw new IllegalStateException("Cannot deselect row '" + itemId + + "': Grid selection model does not implement " + + SelectionModel.Single.class.getName() + " or " + + SelectionModel.Multi.class.getName() + + "(the current model is " + + 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 fireSelectionEvent(Collection<Object> oldSelection, + Collection<Object> newSelection) { + fireEvent(new SelectionEvent(this, oldSelection, newSelection)); + } + + @Override + public void addSelectionListener(SelectionListener listener) { + addListener(SelectionEvent.class, listener, SELECTION_CHANGE_METHOD); + } + + @Override + public void removeSelectionListener(SelectionListener listener) { + removeListener(SelectionEvent.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. + * <p> + * <em>Note:</em> Sorting by a property that has no column in Grid will hide + * all possible sorting indicators. + * + * @param s + * a sort instance + * + * @throws IllegalStateException + * if container is not sortable (does not implement + * Container.Sortable) + * @throws IllegalArgumentException + * if trying to sort by non-existing property + */ + public void sort(Sort s) { + setSortOrder(s.build()); + } + + /** + * Sort this Grid in ascending order by a specified property. + * <p> + * <em>Note:</em> Sorting by a property that has no column in Grid will hide + * all possible sorting indicators. + * + * @param propertyId + * a property ID + * + * @throws IllegalStateException + * if container is not sortable (does not implement + * Container.Sortable) + * @throws IllegalArgumentException + * if trying to sort by non-existing property + */ + public void sort(Object propertyId) { + sort(propertyId, SortDirection.ASCENDING); + } + + /** + * Sort this Grid in user-specified {@link SortOrder} by a property. + * <p> + * <em>Note:</em> Sorting by a property that has no column in Grid will hide + * all possible sorting indicators. + * + * @param propertyId + * a property ID + * @param direction + * a sort order value (ascending/descending) + * + * @throws IllegalStateException + * if container is not sortable (does not implement + * Container.Sortable) + * @throws IllegalArgumentException + * if trying to sort by non-existing property + */ + 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. + * <p> + * <em>Note:</em> Sorting by a property that has no column in Grid will hide + * all possible sorting indicators. + * + * @param order + * a sort order list. + * + * @throws IllegalStateException + * if container is not sortable (does not implement + * Container.Sortable) + * @throws IllegalArgumentException + * if order is null or trying to sort by non-existing property + */ + public void setSortOrder(List<SortOrder> order) { + setSortOrder(order, false); + } + + private void setSortOrder(List<SortOrder> order, boolean userOriginated) + throws IllegalStateException, IllegalArgumentException { + 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(userOriginated); + } + + /** + * 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(boolean userOriginated) { + + Container c = getContainerDataSource(); + if (c instanceof Container.Sortable) { + Container.Sortable cs = (Container.Sortable) c; + + final int items = sortOrder.size(); + Object[] propertyIds = new Object[items]; + boolean[] directions = new boolean[items]; + + SortDirection[] stateDirs = new SortDirection[items]; + + for (int i = 0; i < items; ++i) { + SortOrder order = sortOrder.get(i); + + 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 SortEvent(this, new ArrayList<SortOrder>(sortOrder), + userOriginated)); + + if (columns.keySet().containsAll(Arrays.asList(propertyIds))) { + String[] columnKeys = new String[items]; + for (int i = 0; i < items; ++i) { + columnKeys[i] = this.columnKeys.key(propertyIds[i]); + } + getState().sortColumns = columnKeys; + getState(false).sortDirs = stateDirs; + } else { + // Not all sorted properties are in Grid. Remove any indicators. + getState().sortColumns = new String[] {}; + getState(false).sortDirs = new SortDirection[] {}; + } + } 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 addSortListener(SortListener listener) { + addListener(SortEvent.class, listener, SORT_ORDER_CHANGE_METHOD); + } + + /** + * Removes a sort order change listener previously added using + * {@link #addSortListener(SortListener)}. + * + * @param listener + * the sort order change listener to remove + */ + @Override + public void removeSortListener(SortListener listener) { + removeListener(SortEvent.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(getEditorFields()); + return componentList.iterator(); + } + + @Override + public boolean isRendered(Component childComponent) { + if (getEditorFields().contains(childComponent)) { + // Only render editor fields if the editor is open + return isEditorActive(); + } else { + // TODO Header and footer components should also only be rendered if + // the header/footer is visible + return true; + } + } + + EditorClientRpc getEditorRpc() { + return getRpcProxy(EditorClientRpc.class); + } + + /** + * Sets the style generator that is used for generating styles for 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 style generator that is used for generating styles for cells + * + * @return the cell style generator, or <code>null</code> if no generator is + * set + */ + public CellStyleGenerator getCellStyleGenerator() { + return cellStyleGenerator; + } + + /** + * Sets the style generator that is used for generating styles for rows + * + * @param rowStyleGenerator + * the row style generator to set, or <code>null</code> to remove + * a previously set generator + */ + public void setRowStyleGenerator(RowStyleGenerator rowStyleGenerator) { + this.rowStyleGenerator = rowStyleGenerator; + getState().hasRowStyleGenerator = (rowStyleGenerator != null); + + datasourceExtension.refreshCache(); + } + + /** + * Gets the style generator that is used for generating styles for rows + * + * @return the row style generator, or <code>null</code> if no generator is + * set + */ + public RowStyleGenerator getRowStyleGenerator() { + return rowStyleGenerator; + } + + /** + * 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 item editor UI is enabled for this grid. When the + * editor is enabled, the user can open it by double-clicking a row or + * hitting enter when a row is focused. The editor can also be opened + * programmatically using the {@link #editItem(Object)} method. + * + * @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 setEditorEnabled(boolean isEnabled) + throws IllegalStateException { + if (isEditorActive()) { + throw new IllegalStateException( + "Cannot disable the editor while an item (" + + getEditedItemId() + ") is being edited"); + } + if (isEditorEnabled() != isEnabled) { + getState().editorEnabled = isEnabled; + } + } + + /** + * Checks whether the item editor UI is enabled for this grid. + * + * @return <code>true</code> iff the editor is enabled for this grid + * + * @see #setEditorEnabled(boolean) + * @see #getEditedItemId() + */ + public boolean isEditorEnabled() { + return getState(false).editorEnabled; + } + + /** + * 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 item editor of this grid. + * + * @return the backing field group + */ + public FieldGroup getEditorFieldGroup() { + return editorFieldGroup; + } + + /** + * Sets the field group that is backing the item editor of this grid. + * + * @param fieldGroup + * the backing field group + * + * @throws IllegalStateException + * if the editor is currently active + */ + public void setEditorFieldGroup(FieldGroup fieldGroup) { + if (isEditorActive()) { + throw new IllegalStateException( + "Cannot change field group while an item (" + + getEditedItemId() + ") is being edited"); + } + editorFieldGroup = fieldGroup; + } + + /** + * Returns whether an item is currently being edited in the editor. + * + * @return true iff the editor is open + */ + public boolean isEditorActive() { + return editedItemId != null; + } + + private void checkColumnExists(Object propertyId) { + if (getColumn(propertyId) == null) { + throw new IllegalArgumentException( + "There is no column with the property id " + propertyId); + } + } + + /** + * Gets the field component that represents a property in the item editor. + * <p> + * When {@link #editItem(Object) editItem} is called, fields are + * automatically created and bound for any unbound properties. + * <p> + * Getting a field before the editor has been opened depends on special + * support from the {@link FieldGroup} in use. Using this method with a + * user-provided <code>FieldGroup</code> might cause {@link BindException} + * to be thrown. + * + * @param propertyId + * the property id of the property for which to find the field + * @return the bound field, never null + * + * @throws IllegalArgumentException + * if there is no column for the provided property id + * @throws BindException + * if no field has been configured and there is a problem + * building or binding + */ + public Field<?> getEditorField(Object propertyId) { + checkColumnExists(propertyId); + + Field<?> editor = editorFieldGroup.getField(propertyId); + if (editor == null) { + editor = editorFieldGroup.buildAndBind(propertyId); + } + + if (editor.getParent() != Grid.this) { + assert editor.getParent() == null; + editor.setParent(this); + } + return editor; + } + + /** + * Opens the editor interface for the provided item. + * + * @param itemId + * the id of the item to edit + * @throws IllegalStateException + * if the editor is not enabled or already editing an item + * @throws IllegalArgumentException + * if the {@code itemId} is not in the backing container + * @see #setEditorEnabled(boolean) + */ + public void editItem(Object itemId) throws IllegalStateException, + IllegalArgumentException { + if (!isEditorEnabled()) { + throw new IllegalStateException("Item editor is not enabled"); + } else if (editedItemId != null) { + throw new IllegalStateException("Editing item + " + itemId + + " failed. Item editor is already editing item " + + editedItemId); + } else if (!getContainerDataSource().containsId(itemId)) { + throw new IllegalArgumentException("Item with id " + itemId + + " not found in current container"); + } + editedItemId = itemId; + getEditorRpc().bind(getContainerDataSource().indexOfId(itemId)); + } + + protected void doEditItem() { + Item item = getContainerDataSource().getItem(editedItemId); + + editorFieldGroup.setItemDataSource(item); + + for (Column column : getColumns()) { + Object propertyId = column.getPropertyId(); + + Field<?> editor = getEditorField(propertyId); + + getColumn(propertyId).getState().editorConnector = editor; + } + } + + /** + * Binds the field to the given propertyId. If an item has not been set, + * then the binding is postponed until the item is set using + * {@link #editItem(Object)}. + * <p> + * Setting the field to <code>null</code> clears any previously set field, + * causing a new field to be created the next time the item editor is + * opened. + * + * @param field + * The field to bind + * @param propertyId + * The propertyId to bind the field to + */ + public void setEditorField(Object propertyId, Field<?> field) { + checkColumnExists(propertyId); + + Field<?> oldField = editorFieldGroup.getField(propertyId); + if (oldField != null) { + editorFieldGroup.unbind(oldField); + oldField.setParent(null); + } + + if (field != null) { + field.setParent(this); + editorFieldGroup.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 saveEditor() throws CommitException { + editorFieldGroup.commit(); + } + + /** + * Cancels the currently active edit if any. Hides the editor and discards + * possible unsaved changes in the editor fields. + */ + public void cancelEditor() { + if (isEditorActive()) { + getEditorRpc().cancel( + getContainerDataSource().indexOfId(editedItemId)); + doCancelEditor(); + } + } + + protected void doCancelEditor() { + editedItemId = null; + editorFieldGroup.discard(); + } + + void resetEditor() { + if (isEditorActive()) { + /* + * Simply force cancel the editing; throwing here would just make + * Grid.setContainerDataSource semantics more complicated. + */ + cancelEditor(); + } + for (Field<?> editor : getEditorFields()) { + editor.setParent(null); + } + + editedItemId = null; + editorFieldGroup = new CustomFieldGroup(); + } + + /** + * Gets a collection of all fields bound to the item editor of this grid. + * <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 the item editor + */ + Collection<Field<?>> getEditorFields() { + Collection<Field<?>> fields = editorFieldGroup.getFields(); + assert allAttached(fields); + return fields; + } + + private boolean allAttached(Collection<? extends Component> components) { + for (Component component : components) { + if (component.getParent() != this) { + return false; + } + } + return true; + } + + /** + * 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 setEditorFieldFactory(FieldGroupFieldFactory fieldFactory) { + editorFieldGroup.setFieldFactory(fieldFactory); + } + + @Override + public void addItemClickListener(ItemClickListener listener) { + addListener(GridConstants.ITEM_CLICK_EVENT_ID, ItemClickEvent.class, + listener, ItemClickEvent.ITEM_CLICK_METHOD); + } + + @Override + @Deprecated + public void addListener(ItemClickListener listener) { + addItemClickListener(listener); + } + + @Override + public void removeItemClickListener(ItemClickListener listener) { + removeListener(GridConstants.ITEM_CLICK_EVENT_ID, ItemClickEvent.class, + listener); + } + + @Override + @Deprecated + public void removeListener(ItemClickListener listener) { + removeItemClickListener(listener); + } +} diff --git a/server/src/com/vaadin/ui/renderer/AbstractJavaScriptRenderer.java b/server/src/com/vaadin/ui/renderer/AbstractJavaScriptRenderer.java new file mode 100644 index 0000000000..8fabded536 --- /dev/null +++ b/server/src/com/vaadin/ui/renderer/AbstractJavaScriptRenderer.java @@ -0,0 +1,157 @@ +/* + * 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.AbstractJavaScriptExtension; +import com.vaadin.server.JavaScriptCallbackHelper; +import com.vaadin.shared.JavaScriptExtensionState; +import com.vaadin.shared.communication.ServerRpc; +import com.vaadin.ui.Grid.AbstractRenderer; +import com.vaadin.ui.JavaScriptFunction; + +/** + * Base class for Renderers with all client-side logic implemented using + * JavaScript. + * <p> + * When a new JavaScript renderer is initialized in the browser, the framework + * will look for a globally defined JavaScript function that will initialize the + * renderer. The name of the initialization function is formed by replacing . + * with _ in the name of the server-side class. If no such function is defined, + * each super class is used in turn until a match is found. The framework will + * thus first attempt with <code>com_example_MyRenderer</code> for the + * server-side + * <code>com.example.MyRenderer extends AbstractJavaScriptRenderer</code> class. + * If MyRenderer instead extends <code>com.example.SuperRenderer</code> , then + * <code>com_example_SuperRenderer</code> will also be attempted if + * <code>com_example_MyRenderer</code> has not been defined. + * <p> + * + * In addition to the general JavaScript extension functionality explained in + * {@link AbstractJavaScriptExtension}, this class also provides some + * functionality specific for renderers. + * <p> + * The initialization function will be called with <code>this</code> pointing to + * a connector wrapper object providing integration to Vaadin with the following + * functions: + * <ul> + * <li><code>getRowKey(rowIndex)</code> - Gets a unique identifier for the row + * at the given index. This identifier can be used on the server to retrieve the + * corresponding ItemId using {@link #getItemId(String)}.</li> + * </ul> + * The connector wrapper also supports these special functions that can be + * implemented by the connector: + * <ul> + * <li><code>render(cell, data)</code> - Callback for rendering the given data + * into the given cell. The structure of cell and data are described in separate + * sections below. The renderer is required to implement this function. + * Corresponds to + * {@link com.vaadin.client.renderers.Renderer#render(com.vaadin.client.widget.grid.RendererCellReference, Object)} + * .</li> + * <li><code>init(cell)</code> - Prepares a cell for rendering. Corresponds to + * {@link com.vaadin.client.renderers.ComplexRenderer#init(com.vaadin.client.widget.grid.RendererCellReference)} + * .</li> + * <li><code>destory(cell)</code> - Allows the renderer to release resources + * allocate for a cell that will no longer be used. Corresponds to + * {@link com.vaadin.client.renderers.ComplexRenderer#destroy(com.vaadin.client.widget.grid.RendererCellReference)} + * .</li> + * <li><code>onActivate(cell)</code> - Called when the cell is activated by the + * user e.g. by double clicking on the cell or pressing enter with the cell + * focused. Corresponds to + * {@link com.vaadin.client.renderers.ComplexRenderer#onActivate(com.vaadin.client.widget.grid.CellReference)} + * .</li> + * <li><code>getConsumedEvents()</code> - Returns a JavaScript array of event + * names that should cause onBrowserEvent to be invoked whenever an event is + * fired for a cell managed by this renderer. Corresponds to + * {@link com.vaadin.client.renderers.ComplexRenderer#getConsumedEvents()}.</li> + * <li><code>onBrowserEvent(cell, event)</code> - Called by Grid when an event + * of a type returned by getConsumedEvents is fired for a cell managed by this + * renderer. Corresponds to + * {@link com.vaadin.client.renderers.ComplexRenderer#onBrowserEvent(com.vaadin.client.widget.grid.CellReference, com.google.gwt.dom.client.NativeEvent)} + * .</li> + * </ul> + * + * <p> + * The cell object passed to functions defined by the renderer has these + * properties: + * <ul> + * <li><code>element</code> - The DOM element corresponding to this cell. + * Readonly.</li> + * <li><code>rowIndex</code> - The current index of the row of this cell. + * Readonly.</li> + * <li><code>columnIndex</code> - The current index of the column of this cell. + * Readonly.</li> + * <li><code>colSpan</code> - The number of columns spanned by this cell. Only + * supported in the object passed to the <code>render</code> function - other + * functions should not use the property. Readable and writable. + * </ul> + * + * @author Vaadin Ltd + * @since 7.4 + */ +public abstract class AbstractJavaScriptRenderer<T> extends AbstractRenderer<T> { + private JavaScriptCallbackHelper callbackHelper = new JavaScriptCallbackHelper( + this); + + protected AbstractJavaScriptRenderer(Class<T> presentationType) { + super(presentationType); + } + + @Override + protected <R extends ServerRpc> void registerRpc(R implementation, + Class<R> rpcInterfaceType) { + super.registerRpc(implementation, rpcInterfaceType); + callbackHelper.registerRpc(rpcInterfaceType); + } + + /** + * Register a {@link JavaScriptFunction} that can be called from the + * JavaScript using the provided name. A JavaScript function with the + * provided name will be added to the connector wrapper object (initially + * available as <code>this</code>). Calling that JavaScript function will + * cause the call method in the registered {@link JavaScriptFunction} to be + * invoked with the same arguments. + * + * @param functionName + * the name that should be used for client-side callback + * @param function + * the {@link JavaScriptFunction} object that will be invoked + * when the JavaScript function is called + */ + protected void addFunction(String functionName, JavaScriptFunction function) { + callbackHelper.registerCallback(functionName, function); + } + + /** + * Invoke a named function that the connector JavaScript has added to the + * JavaScript connector wrapper object. The arguments should only contain + * data types that can be represented in JavaScript including primitives, + * their boxed types, arrays, String, List, Set, Map, Connector and + * JavaBeans. + * + * @param name + * the name of the function + * @param arguments + * function arguments + */ + protected void callFunction(String name, Object... arguments) { + callbackHelper.invokeCallback(name, arguments); + } + + @Override + protected JavaScriptExtensionState getState() { + return (JavaScriptExtensionState) super.getState(); + } +} 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..b0819794c0 --- /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 7.4 + * @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..d640ce8b71 --- /dev/null +++ b/server/src/com/vaadin/ui/renderer/ClickableRenderer.java @@ -0,0 +1,138 @@ +/* + * 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.ui.Grid.Column; +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 7.4 + * @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; + private Column column; + + protected RendererClickEvent(Grid source, Object itemId, Column column, + MouseEventDetails mouseEventDetails) { + super(source, mouseEventDetails); + this.itemId = itemId; + this.column = column; + } + + /** + * 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; + } + + /** + * Returns the {@link Column} where the click event originated. + * + * @return the column of the click event + */ + public Column getColumn() { + return column; + } + + /** + * Returns the property ID where the click event originated. + * + * @return the property ID of the clicked cell + */ + public Object getPropertyId() { + return column.getPropertyId(); + } + } + + protected ClickableRenderer(Class<T> presentationType) { + super(presentationType); + registerRpc(new RendererClickRpc() { + @Override + public void click(String rowKey, String columnId, + MouseEventDetails mouseDetails) { + fireEvent(new RendererClickEvent(getParentGrid(), + getItemId(rowKey), getColumn(columnId), 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..d3d2df573d --- /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 7.4 + * @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..02d153dedf --- /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 7.4 + * @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..3ef3eed3e5 --- /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 7.4 + * @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..3406e1837a --- /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 7.4 + * @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/ObjectRenderer.java b/server/src/com/vaadin/ui/renderer/ObjectRenderer.java new file mode 100644 index 0000000000..9f8b44162c --- /dev/null +++ b/server/src/com/vaadin/ui/renderer/ObjectRenderer.java @@ -0,0 +1,46 @@ +/* + * 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 for displaying an object to a string using the + * {@link Object#toString()} method. + * <p> + * If the object is <code>null</code>, then it is rendered as an empty string + * instead. + * + * @since + * @author Vaadin Ltd + */ +public class ObjectRenderer extends AbstractRenderer<Object> { + + /** + * Creates a new <code>toString</code> renderer. + */ + public ObjectRenderer() { + super(Object.class); + } + + @Override + public JsonValue encode(Object value) { + String text = (value != null) ? value.toString() : ""; + return super.encode(text); + } +} 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..9bdc0b299a --- /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 7.4 + * @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..cab1cdfe3c --- /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.widget.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 7.4 + * @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..154a09ccd8 --- /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 7.4 + * @author Vaadin Ltd + */ +public class TextRenderer extends AbstractRenderer<String> { + + /** + * Creates a new text renderer + */ + public TextRenderer() { + super(String.class); + } +} diff --git a/server/src/com/vaadin/ui/themes/Reindeer.java b/server/src/com/vaadin/ui/themes/Reindeer.java index 6eeebd8a03..e0ab792a15 100644 --- a/server/src/com/vaadin/ui/themes/Reindeer.java +++ b/server/src/com/vaadin/ui/themes/Reindeer.java @@ -15,14 +15,6 @@ */ package com.vaadin.ui.themes; -import com.vaadin.ui.CssLayout; -import com.vaadin.ui.FormLayout; -import com.vaadin.ui.GridLayout; -import com.vaadin.ui.HorizontalLayout; -import com.vaadin.ui.HorizontalSplitPanel; -import com.vaadin.ui.VerticalLayout; -import com.vaadin.ui.VerticalSplitPanel; - public class Reindeer extends BaseTheme { public static final String THEME_NAME = "reindeer"; @@ -90,6 +82,18 @@ public class Reindeer extends BaseTheme { /*************************************************************************** * + * ProgressBar Styles + * + **************************************************************************/ + + /** + * Displays the progress bar with a static background, instead of an + * animated one. + */ + public static final String PROGRESSBAR_STATIC = "static"; + + /*************************************************************************** + * * SplitPanel styles * **************************************************************************/ diff --git a/server/src/com/vaadin/ui/themes/Runo.java b/server/src/com/vaadin/ui/themes/Runo.java index 11f1bae682..6f8d5f37d9 100644 --- a/server/src/com/vaadin/ui/themes/Runo.java +++ b/server/src/com/vaadin/ui/themes/Runo.java @@ -59,6 +59,18 @@ public class Runo extends BaseTheme { /*************************************************************************** * + * ProgressBar Styles + * + **************************************************************************/ + + /** + * Displays the progress bar with a static background, instead of an + * animated one. + */ + public static final String PROGRESSBAR_STATIC = "static"; + + /*************************************************************************** + * * TabSheet styles * **************************************************************************/ |