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