diff options
author | Artur Signell <artur@vaadin.com> | 2015-09-04 15:05:27 +0300 |
---|---|---|
committer | Artur Signell <artur@vaadin.com> | 2015-09-04 15:05:27 +0300 |
commit | f46be1b2792755cb7d9b111068dd6cf202398c4a (patch) | |
tree | 74f7098faee688cc1f1ca1b6ffe9e912338bbcaf /server | |
parent | e603ea3cf91e5dd0893b766f27d400947527fba9 (diff) | |
parent | ebceef4d44bcd61605fa92fcf7be8d3678537599 (diff) | |
download | vaadin-framework-f46be1b2792755cb7d9b111068dd6cf202398c4a.tar.gz vaadin-framework-f46be1b2792755cb7d9b111068dd6cf202398c4a.zip |
Merge remote-tracking branch 'origin/master' into reconnect-dialog
Change-Id: Ie622160a83116c83b255a26bec297f73f3223ac7
Diffstat (limited to 'server')
38 files changed, 2107 insertions, 1394 deletions
diff --git a/server/build.xml b/server/build.xml index 4bb2bde730..95c0b0add4 100644 --- a/server/build.xml +++ b/server/build.xml @@ -34,7 +34,7 @@ </target> <target name="jar" depends="compress-files"> <property name="server.osgi.import" - value="javax.servlet;version="2.4.0",javax.servlet.http;version="2.4.0",javax.validation;version="1.0.0.GA";resolution:=optional,org.jsoup;version="1.6.3",org.jsoup.parser;version="1.6.3",org.jsoup.nodes;version="1.6.3",org.jsoup.helper;version="1.6.3",org.jsoup.safety;version="1.6.3",org.jsoup.select;version="1.6.3"" /> + value="javax.servlet;version="2.4.0",javax.servlet.http;version="2.4.0",javax.validation;version="1.0.0.GA";resolution:=optional,org.jsoup;version="1.6.3",org.jsoup.parser;version="1.6.3",org.jsoup.nodes;version="1.6.3",org.jsoup.helper;version="1.6.3",org.jsoup.safety;version="1.6.3",org.jsoup.select;version="1.6.3",javax.portlet;version="[2.0,3)";resolution:=optional,javax.portlet.filter;version="[2.0,3)";resolution:=optional,com.liferay.portal.kernel.util;resolution:=optional" /> <property name="server.osgi.require" value="com.google.gwt.thirdparty.guava;bundle-version="16.0.1.vaadin1",com.vaadin.shared;bundle-version="${vaadin.version}",com.vaadin.push;bundle-version="${vaadin.version}";resolution:=optional,com.vaadin.sass-compiler;bundle-version="${vaadin.sass.version}";resolution:=optional" /> <antcall target="common.jar"> diff --git a/server/ivy.xml b/server/ivy.xml index e9bc8b818d..0711b4b2ea 100644 --- a/server/ivy.xml +++ b/server/ivy.xml @@ -36,7 +36,7 @@ <!-- Google App Engine --> <dependency org="com.google.appengine" name="appengine-api-1.0-sdk" - rev="1.2.1" conf="build-provided,ide,test -> default" /> + rev="1.7.7" conf="build-provided,ide,test -> default" /> <!-- Bean Validation API --> <dependency org="javax.validation" name="validation-api" diff --git a/server/src/com/vaadin/data/DataGenerator.java b/server/src/com/vaadin/data/DataGenerator.java new file mode 100644 index 0000000000..a5333b8523 --- /dev/null +++ b/server/src/com/vaadin/data/DataGenerator.java @@ -0,0 +1,49 @@ +/* + * 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 com.vaadin.ui.Grid.AbstractGridExtension; +import com.vaadin.ui.Grid.AbstractRenderer; + +import elemental.json.JsonObject; + +/** + * Interface for {@link AbstractGridExtension}s that allows adding data to row + * objects being sent to client by the {@link RpcDataProviderExtension}. + * <p> + * {@link AbstractRenderer} implements this interface to provide encoded data to + * client for {@link Renderer}s automatically. + * + * @since 7.6 + * @author Vaadin Ltd + */ +public interface DataGenerator extends Serializable { + + /** + * Adds data to row object for given item and item id being sent to client. + * + * @param itemId + * item id of item + * @param item + * item being sent to client + * @param rowData + * row object being sent to client + */ + public void generateData(Object itemId, Item item, JsonObject rowData); + +} diff --git a/server/src/com/vaadin/data/RpcDataProviderExtension.java b/server/src/com/vaadin/data/RpcDataProviderExtension.java index b3c7972b52..78c87ab23d 100644 --- a/server/src/com/vaadin/data/RpcDataProviderExtension.java +++ b/server/src/com/vaadin/data/RpcDataProviderExtension.java @@ -23,14 +23,9 @@ import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; -import java.util.Locale; import java.util.Map; -import java.util.Map.Entry; import java.util.Set; -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.google.gwt.thirdparty.guava.common.collect.ImmutableSet; import com.google.gwt.thirdparty.guava.common.collect.Maps; import com.google.gwt.thirdparty.guava.common.collect.Sets; @@ -43,31 +38,23 @@ 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.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.DetailsConnectorChange; import com.vaadin.shared.ui.grid.GridClientRpc; import com.vaadin.shared.ui.grid.GridState; import com.vaadin.shared.ui.grid.Range; -import com.vaadin.shared.util.SharedUtil; import com.vaadin.ui.Component; import com.vaadin.ui.Grid; -import com.vaadin.ui.Grid.CellReference; -import com.vaadin.ui.Grid.CellStyleGenerator; import com.vaadin.ui.Grid.Column; import com.vaadin.ui.Grid.DetailsGenerator; import com.vaadin.ui.Grid.RowReference; -import com.vaadin.ui.Grid.RowStyleGenerator; -import com.vaadin.ui.renderers.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 @@ -82,445 +69,84 @@ import elemental.json.JsonValue; public class RpcDataProviderExtension extends AbstractExtension { /** - * ItemId to Key to ItemId mapper. - * <p> - * This class is used when transmitting information about items in container - * related to Grid. It introduces a consistent way of mapping ItemIds and - * its container to a String that can be mapped back to ItemId. - * <p> - * <em>Technical note:</em> This class also keeps tabs on which indices are - * being shown/selected, and is able to clean up after itself once the - * itemId ⇆ key mapping is not needed anymore. In other words, this - * doesn't leak memory. + * Class for keeping track of current items and ValueChangeListeners. + * + * @since 7.6 */ - public class DataProviderKeyMapper implements Serializable { - private final BiMap<Object, String> itemIdToKey = HashBiMap.create(); - private Set<Object> pinnedItemIds = new HashSet<Object>(); - private long rollingIndex = 0; - - private DataProviderKeyMapper() { - // private implementation - } - - /** - * Sets the currently active rows. This will purge any unpinned rows - * from cache. - * - * @param itemIds - * collection of itemIds to map to row keys - */ - void setActiveRows(Collection<?> itemIds) { - Set<Object> itemSet = new HashSet<Object>(itemIds); - Set<Object> itemsRemoved = new HashSet<Object>(); - for (Object itemId : itemIdToKey.keySet()) { - if (!itemSet.contains(itemId) && !isPinned(itemId)) { - itemsRemoved.add(itemId); - } - } - - for (Object itemId : itemsRemoved) { - detailComponentManager.destroyDetails(itemId); - itemIdToKey.remove(itemId); - } - - for (Object itemId : itemSet) { - itemIdToKey.put(itemId, getKey(itemId)); - if (visibleDetails.contains(itemId)) { - detailComponentManager.createDetails(itemId, - indexOf(itemId)); - } - } - } - - private String nextKey() { - return String.valueOf(rollingIndex++); - } + private class ActiveItemHandler implements Serializable, DataGenerator { - /** - * Gets the key for a given item id. Creates a new key mapping if no - * existing mapping was found for the given item id. - * - * @since 7.5.0 - * @param itemId - * the item id to get the key for - * @return the key for the given item id - */ - public String getKey(Object itemId) { - String key = itemIdToKey.get(itemId); - if (key == null) { - key = nextKey(); - itemIdToKey.put(itemId, key); - } - return key; - } + private final Map<Object, GridValueChangeListener> activeItemMap = new HashMap<Object, GridValueChangeListener>(); + private final KeyMapper<Object> keyMapper = new KeyMapper<Object>(); + private final Set<Object> droppedItems = new HashSet<Object>(); /** - * Gets keys for a collection of item ids. + * Registers ValueChangeListeners for given item ids. * <p> - * If the itemIds are currently cached, the existing keys will be used. - * Otherwise new ones will be created. + * Note: This method will clean up any unneeded listeners and key + * mappings * * @param itemIds - * the item ids for which to get keys - * @return keys for the {@code itemIds} + * collection of new active item ids */ - 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()); + public void addActiveItems(Collection<?> itemIds) { 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"); + if (!activeItemMap.containsKey(itemId)) { + activeItemMap.put(itemId, new GridValueChangeListener( + itemId, container.getItem(itemId))); + } } - pinnedItemIds.remove(itemId); + // Remove still active rows that were "dropped" + droppedItems.removeAll(itemIds); + internalDropActiveItems(droppedItems); + droppedItems.clear(); } /** - * Checks whether an item id is pinned or not. + * Marks given item id as dropped. Dropped items are cleared when adding + * new active items. * * @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); - } - } - - /** - * 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 index to the value change listener used for all of column - * properties - */ - private final Map<Integer, GridValueChangeListener> valueChangeListeners = new HashMap<Integer, 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(Range newActiveRange) { - - // TODO [[Components]] attach and detach components - - /*- - * Example - * - * New Range: [3, 4, 5, 6, 7] - * Old Range: [1, 2, 3, 4, 5] - * Result: [1, 2][3, 4, 5] [] - */ - final Range[] depractionPartition = activeRange - .partitionWith(newActiveRange); - removeValueChangeListeners(depractionPartition[0]); - removeValueChangeListeners(depractionPartition[2]); - - /*- - * Example - * - * Old Range: [1, 2, 3, 4, 5] - * New Range: [3, 4, 5, 6, 7] - * Result: [] [3, 4, 5][6, 7] - */ - final Range[] activationPartition = newActiveRange - .partitionWith(activeRange); - addValueChangeListeners(activationPartition[0]); - addValueChangeListeners(activationPartition[2]); - - activeRange = newActiveRange; - - assert valueChangeListeners.size() == newActiveRange.length() : "Value change listeners not set up correctly!"; - } - - private void addValueChangeListeners(Range range) { - for (Integer i = range.getStart(); i < range.getEnd(); i++) { - - final Object itemId = container.getIdByIndex(i); - final Item item = container.getItem(itemId); - - assert valueChangeListeners.get(i) == null : "Overwriting existing listener"; - - GridValueChangeListener listener = new GridValueChangeListener( - itemId, item); - valueChangeListeners.put(i, listener); - } - } - - private void removeValueChangeListeners(Range range) { - for (Integer i = range.getStart(); i < range.getEnd(); i++) { - final GridValueChangeListener listener = valueChangeListeners - .remove(i); - - assert listener != null : "Trying to remove nonexisting listener"; - - listener.removeListener(); - } - } - - /** - * Manages removed columns in active rows. - * <p> - * This method does <em>not</em> send data again to the client. - * - * @param removedColumns - * the columns that have been removed from the grid + * dropped item id */ - public void columnsRemoved(Collection<Column> removedColumns) { - if (removedColumns.isEmpty()) { - return; - } - - for (GridValueChangeListener listener : valueChangeListeners - .values()) { - listener.removeColumns(removedColumns); + public void dropActiveItem(Object itemId) { + if (activeItemMap.containsKey(itemId)) { + droppedItems.add(itemId); } } - /** - * Manages added columns in active rows. - * <p> - * This method sends the data for the changed rows to client side. - * - * @param addedColumns - * the columns that have been added to the grid - */ - public void columnsAdded(Collection<Column> addedColumns) { - if (addedColumns.isEmpty()) { - return; - } + private void internalDropActiveItems(Collection<Object> itemIds) { + for (Object itemId : droppedItems) { + assert activeItemMap.containsKey(itemId) : "Item ID should exist in the activeItemMap"; - for (GridValueChangeListener listener : valueChangeListeners - .values()) { - listener.addColumns(addedColumns); + activeItemMap.remove(itemId).removeListener(); + keyMapper.remove(itemId); } } /** - * 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> + * Gets a collection copy of currently active item ids. * - * @param firstIndex - * the index of the first inserted rows - * @param count - * the number of rows inserted at <code>firstIndex</code> + * @return collection of item ids */ - public void insertRows(int firstIndex, int count) { - if (firstIndex < activeRange.getStart()) { - moveListeners(activeRange, count); - activeRange = activeRange.offsetBy(count); - } else if (firstIndex < activeRange.getEnd()) { - int end = activeRange.getEnd(); - // Move rows from first added index by count - Range movedRange = Range.between(firstIndex, end); - moveListeners(movedRange, count); - // Remove excess listeners from extra rows - removeValueChangeListeners(Range.withLength(end, count)); - // Add listeners for new rows - final Range freshRange = Range.withLength(firstIndex, count); - addValueChangeListeners(freshRange); - } else { - // out of view, noop - } + public Collection<Object> getActiveItemIds() { + return new HashSet<Object>(activeItemMap.keySet()); } /** - * Handles the removal of rows. - * <p> - * This method's responsibilities are to: - * <ul> - * <li>shift the internal bookkeeping by <code>count</code> if the - * removal happens above currently active range - * <li>ignore rows removed below the currently active range - * </ul> + * Gets a collection copy of currently active ValueChangeListeners. * - * @param firstIndex - * the index of the first removed rows - * @param count - * the number of rows removed at <code>firstIndex</code> + * @return collection of value change listeners */ - public void removeRows(int firstIndex, int count) { - Range removed = Range.withLength(firstIndex, count); - if (removed.intersects(activeRange)) { - final Range[] deprecated = activeRange.partitionWith(removed); - // Remove the listeners that are no longer existing - removeValueChangeListeners(deprecated[1]); - - // Move remaining listeners to fill the listener map correctly - moveListeners(deprecated[2], -deprecated[1].length()); - activeRange = Range.withLength(activeRange.getStart(), - activeRange.length() - deprecated[1].length()); - - } else { - if (removed.getEnd() < activeRange.getStart()) { - /* firstIndex < lastIndex < start */ - moveListeners(activeRange, -count); - activeRange = activeRange.offsetBy(-count); - } - /* else: end <= firstIndex, no need to do anything */ - } + public Collection<GridValueChangeListener> getValueChangeListeners() { + return new HashSet<GridValueChangeListener>(activeItemMap.values()); } - /** - * Moves value change listeners in map with given index range by count - */ - private void moveListeners(Range movedRange, int diff) { - if (diff < 0) { - for (Integer i = movedRange.getStart(); i < movedRange.getEnd(); ++i) { - moveListener(i, i + diff); - } - } else if (diff > 0) { - for (Integer i = movedRange.getEnd() - 1; i >= movedRange - .getStart(); --i) { - moveListener(i, i + diff); - } - } else { - // diff == 0 should not happen. If it does, should be no-op - return; - } + @Override + public void generateData(Object itemId, Item item, JsonObject rowData) { + rowData.put(GridState.JSONKEY_ROWKEY, keyMapper.key(itemId)); } - private void moveListener(Integer oldIndex, Integer newIndex) { - assert valueChangeListeners.get(newIndex) == null : "Overwriting existing listener"; - - GridValueChangeListener listener = valueChangeListeners - .remove(oldIndex); - assert listener != null : "Moving nonexisting listener."; - valueChangeListeners.put(newIndex, listener); - } } /** @@ -601,7 +227,8 @@ public class RpcDataProviderExtension extends AbstractExtension { * @since 7.5.0 * @author Vaadin Ltd */ - public static final class DetailComponentManager implements Serializable { + // TODO this should probably be a static nested class + public final class DetailComponentManager implements DataGenerator { /** * This map represents all the components that have been requested for * each item id. @@ -609,9 +236,8 @@ public class RpcDataProviderExtension extends AbstractExtension { * Normally this map is consistent with what is displayed in the * component hierarchy (and thus the DOM). The only time this map is out * of sync with the DOM is between the any calls to - * {@link #createDetails(Object, int)} or - * {@link #destroyDetails(Object)}, and - * {@link GridClientRpc#setDetailsConnectorChanges(Set)}. + * {@link #createDetails(Object)} or {@link #destroyDetails(Object)}, + * and {@link GridClientRpc#setDetailsConnectorChanges(Set)}. * <p> * This is easily checked: if {@link #unattachedComponents} is * {@link Collection#isEmpty() empty}, then this field is consistent @@ -620,34 +246,11 @@ public class RpcDataProviderExtension extends AbstractExtension { private final Map<Object, Component> visibleDetailsComponents = Maps .newHashMap(); - /** A lookup map for which row contains which details component. */ - private BiMap<Integer, Component> rowIndexToDetails = HashBiMap - .create(); - - /** - * A copy of {@link #rowIndexToDetails} from its last stable state. Used - * for creating a diff against {@link #rowIndexToDetails}. - * - * @see #getAndResetConnectorChanges() - */ - private BiMap<Integer, Component> prevRowIndexToDetails = HashBiMap - .create(); - - /** - * A set keeping track on components that have been created, but not - * attached. They should be attached at some later point in time. - * <p> - * This isn't strictly requried, but it's a handy explicit log. You - * could find out the same thing by taking out all the other components - * and checking whether Grid is their parent or not. - */ - private final Set<Component> unattachedComponents = Sets.newHashSet(); - /** * Keeps tabs on all the details that did not get a component during - * {@link #createDetails(Object, int)}. + * {@link #createDetails(Object)}. */ - private final Map<Object, Integer> emptyDetails = Maps.newHashMap(); + private final Set<Object> emptyDetails = Sets.newHashSet(); private Grid grid; @@ -661,19 +264,16 @@ public class RpcDataProviderExtension extends AbstractExtension { * the item id for which to create the details component. * Assumed not <code>null</code> and that a component is not * currently present for this item previously - * @param rowIndex - * the row index for {@code itemId} * @throws IllegalStateException * if the current details generator provides a component * that was manually attached, or if the same instance has * already been provided */ - public void createDetails(Object itemId, int rowIndex) - throws IllegalStateException { + public void createDetails(Object itemId) throws IllegalStateException { assert itemId != null : "itemId was null"; - Integer newRowIndex = Integer.valueOf(rowIndex); - if (visibleDetailsComponents.containsKey(itemId)) { + if (visibleDetailsComponents.containsKey(itemId) + || emptyDetails.contains(itemId)) { // Don't overwrite existing components return; } @@ -684,58 +284,26 @@ public class RpcDataProviderExtension extends AbstractExtension { DetailsGenerator detailsGenerator = grid.getDetailsGenerator(); Component details = detailsGenerator.getDetails(rowReference); if (details != null) { - String generatorName = detailsGenerator.getClass().getName(); if (details.getParent() != null) { - throw new IllegalStateException(generatorName + String name = detailsGenerator.getClass().getName(); + throw new IllegalStateException(name + " generated a details component that already " - + "was attached. (itemId: " + itemId + ", row: " - + rowIndex + ", component: " + details); - } - - if (rowIndexToDetails.containsValue(details)) { - throw new IllegalStateException(generatorName - + " provided a details component that already " - + "exists in Grid. (itemId: " + itemId + ", row: " - + rowIndex + ", component: " + details); + + "was attached. (itemId: " + itemId + + ", component: " + details + ")"); } visibleDetailsComponents.put(itemId, details); - rowIndexToDetails.put(newRowIndex, details); - unattachedComponents.add(details); - assert !emptyDetails.containsKey(itemId) : "Bookeeping thinks " + details.setParent(grid); + grid.markAsDirty(); + + assert !emptyDetails.contains(itemId) : "Bookeeping thinks " + "itemId is empty even though we just created a " + "component for it (" + itemId + ")"; } else { - assert assertItemIdHasNotMovedAndNothingIsOverwritten(itemId, - newRowIndex); - emptyDetails.put(itemId, newRowIndex); - } - - /* - * Don't attach the components here. It's done by - * GridServerRpc.sendDetailsComponents in a separate roundtrip. - */ - } - - private boolean assertItemIdHasNotMovedAndNothingIsOverwritten( - Object itemId, Integer newRowIndex) { - - Integer oldRowIndex = emptyDetails.get(itemId); - if (!SharedUtil.equals(oldRowIndex, newRowIndex)) { - - assert !emptyDetails.containsKey(itemId) : "Unexpected " - + "change of empty details row index for itemId " - + itemId + " from " + oldRowIndex + " to " - + newRowIndex; - - assert !emptyDetails.containsValue(newRowIndex) : "Bookkeeping" - + " already had another itemId for this empty index " - + "(index: " + newRowIndex + ", new itemId: " + itemId - + ")"; + emptyDetails.add(itemId); } - return true; } /** @@ -756,8 +324,6 @@ public class RpcDataProviderExtension extends AbstractExtension { return; } - rowIndexToDetails.inverse().remove(removedComponent); - removedComponent.setParent(null); grid.markAsDirty(); } @@ -773,81 +339,12 @@ public class RpcDataProviderExtension extends AbstractExtension { public Collection<Component> getComponents() { Set<Component> components = new HashSet<Component>( visibleDetailsComponents.values()); - components.removeAll(unattachedComponents); return components; } - /** - * Gets information on how the connectors have changed. - * <p> - * This method only returns the changes that have been made between two - * calls of this method. I.e. Calling this method once will reset the - * state for the next state. - * <p> - * Used internally by the Grid object. - * - * @return information on how the connectors have changed - */ - public Set<DetailsConnectorChange> getAndResetConnectorChanges() { - Set<DetailsConnectorChange> changes = new HashSet<DetailsConnectorChange>(); - - // populate diff with added/changed - for (Entry<Integer, Component> entry : rowIndexToDetails.entrySet()) { - Component component = entry.getValue(); - assert component != null : "rowIndexToDetails contains a null component"; - - Integer newIndex = entry.getKey(); - Integer oldIndex = prevRowIndexToDetails.inverse().get( - component); - - /* - * only attach components. Detaching already happened in - * destroyDetails. - */ - if (newIndex != null && oldIndex == null) { - assert unattachedComponents.contains(component) : "unattachedComponents does not contain component for index " - + newIndex + " (" + component + ")"; - component.setParent(grid); - unattachedComponents.remove(component); - } - - if (!SharedUtil.equals(oldIndex, newIndex)) { - changes.add(new DetailsConnectorChange(component, oldIndex, - newIndex, emptyDetails.containsKey(component))); - } - } - - // populate diff with removed - for (Entry<Integer, Component> entry : prevRowIndexToDetails - .entrySet()) { - Integer oldIndex = entry.getKey(); - Component component = entry.getValue(); - Integer newIndex = rowIndexToDetails.inverse().get(component); - if (newIndex == null) { - changes.add(new DetailsConnectorChange(null, oldIndex, - null, emptyDetails.containsValue(oldIndex))); - } - } - - // reset diff map - prevRowIndexToDetails = HashBiMap.create(rowIndexToDetails); - - return changes; - } - public void refresh(Object itemId) { - Component component = visibleDetailsComponents.get(itemId); - Integer rowIndex = null; - if (component != null) { - rowIndex = rowIndexToDetails.inverse().get(component); - destroyDetails(itemId); - } else { - rowIndex = emptyDetails.remove(itemId); - } - - assert rowIndex != null : "Given itemId does not map to an " - + "existing detail row (" + itemId + ")"; - createDetails(itemId, rowIndex.intValue()); + destroyDetails(itemId); + createDetails(itemId); } void setGrid(Grid grid) { @@ -856,12 +353,29 @@ public class RpcDataProviderExtension extends AbstractExtension { } this.grid = grid; } + + /** + * {@inheritDoc} + * + * @since 7.6 + */ + @Override + public void generateData(Object itemId, Item item, JsonObject rowData) { + if (visibleDetails.contains(itemId)) { + // Double check to be sure details component exists. + detailComponentManager.createDetails(itemId); + Component detailsComponent = visibleDetailsComponents + .get(itemId); + rowData.put( + GridState.JSONKEY_DETAILS_VISIBLE, + (detailsComponent != null ? detailsComponent + .getConnectorId() : "")); + } + } } private final Indexed container; - private final ActiveRowHandler activeRowHandler = new ActiveRowHandler(); - private DataProviderRpc rpc; private final ItemSetChangeListener itemListener = new ItemSetChangeListener() { @@ -922,22 +436,10 @@ public class RpcDataProviderExtension extends AbstractExtension { * taking all the corner cases into account. */ - Map<Integer, GridValueChangeListener> listeners = activeRowHandler.valueChangeListeners; - for (GridValueChangeListener listener : listeners.values()) { - listener.removeListener(); - } - - // Wipe clean all details. - HashSet<Object> detailItemIds = new HashSet<Object>( - detailComponentManager.visibleDetailsComponents - .keySet()); - for (Object itemId : detailItemIds) { + for (Object itemId : visibleDetails) { detailComponentManager.destroyDetails(itemId); } - listeners.clear(); - activeRowHandler.activeRange = Range.withLength(0, 0); - /* Mark as dirty to push changes in beforeClientResponse */ bareItemSetTriggeredSizeChange = true; markAsDirty(); @@ -945,16 +447,9 @@ public class RpcDataProviderExtension extends AbstractExtension { } }; - private final DataProviderKeyMapper keyMapper = new DataProviderKeyMapper(); - - private KeyMapper<Object> columnKeys; - /** RpcDataProvider should send the current cache again. */ private boolean refreshCache = false; - private RowReference rowReference; - private CellReference cellReference; - /** Set of updated item ids */ private Set<Object> updatedItemIds = new LinkedHashSet<Object>(); @@ -971,10 +466,15 @@ public class RpcDataProviderExtension extends AbstractExtension { * This map represents all the details that are user-defined as visible. * This does not reflect the status in the DOM. */ - private Set<Object> visibleDetails = new HashSet<Object>(); + // TODO this should probably be inside DetailComponentManager + private final Set<Object> visibleDetails = new HashSet<Object>(); private final DetailComponentManager detailComponentManager = new DetailComponentManager(); + private final Set<DataGenerator> dataGenerators = new LinkedHashSet<DataGenerator>(); + + private final ActiveItemHandler activeItemHandler = new ActiveItemHandler(); + /** * Creates a new data provider using the given container. * @@ -989,22 +489,15 @@ public class RpcDataProviderExtension extends AbstractExtension { @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); + public void dropRows(JsonArray rowKeys) { + for (int i = 0; i < rowKeys.length(); ++i) { + activeItemHandler.dropActiveItem(getKeyMapper().get( + rowKeys.getString(i))); } } }); @@ -1014,6 +507,8 @@ public class RpcDataProviderExtension extends AbstractExtension { .addItemSetChangeListener(itemListener); } + addDataGenerator(activeItemHandler); + addDataGenerator(detailComponentManager); } /** @@ -1045,16 +540,11 @@ public class RpcDataProviderExtension extends AbstractExtension { // Send current rows again if needed. if (refreshCache) { - int firstRow = activeRowHandler.activeRange.getStart(); - int numberOfRows = activeRowHandler.activeRange.length(); - - pushRowData(firstRow, numberOfRows, firstRow, numberOfRows); + updatedItemIds.addAll(activeItemHandler.getActiveItemIds()); } } - for (Object itemId : updatedItemIds) { - internalUpdateRowData(itemId); - } + internalUpdateRows(updatedItemIds); // Clear all changes. rowChanges.clear(); @@ -1076,7 +566,6 @@ public class RpcDataProviderExtension extends AbstractExtension { List<?> itemIds = container.getItemIds(fullRange.getStart(), fullRange.length()); - keyMapper.setActiveRows(itemIds); JsonArray rows = Json.createArray(); @@ -1088,83 +577,25 @@ public class RpcDataProviderExtension extends AbstractExtension { for (int i = 0; i < newRange.length() && i + diff < itemIds.size(); ++i) { Object itemId = itemIds.get(i + diff); + rows.set(i, getRowData(getGrid().getColumns(), itemId)); } rpc.setRowData(firstRowToPush, rows); - activeRowHandler.setActiveRows(fullRange); + activeItemHandler.addActiveItems(itemIds); } - private JsonValue getRowData(Collection<Column> columns, Object itemId) { + private JsonObject getRowData(Collection<Column> columns, Object itemId) { Item item = container.getItem(itemId); - JsonObject rowData = Json.createObject(); - - Grid grid = getGrid(); - - for (Column column : columns) { - Object propertyId = column.getPropertyId(); - - Object propertyValue = item.getItemProperty(propertyId).getValue(); - JsonValue encodedValue = encodeValue(propertyValue, - column.getRenderer(), column.getConverter(), - grid.getLocale()); - - rowData.put(columnKeys.key(propertyId), encodedValue); - } - final JsonObject rowObject = Json.createObject(); - rowObject.put(GridState.JSONKEY_DATA, rowData); - rowObject.put(GridState.JSONKEY_ROWKEY, keyMapper.getKey(itemId)); - - if (visibleDetails.contains(itemId)) { - rowObject.put(GridState.JSONKEY_DETAILS_VISIBLE, true); - } - - rowReference.set(itemId); - - CellStyleGenerator cellStyleGenerator = grid.getCellStyleGenerator(); - if (cellStyleGenerator != null) { - setGeneratedCellStyles(cellStyleGenerator, rowObject, columns); - } - RowStyleGenerator rowStyleGenerator = grid.getRowStyleGenerator(); - if (rowStyleGenerator != null) { - setGeneratedRowStyles(rowStyleGenerator, rowObject); + for (DataGenerator dg : dataGenerators) { + dg.generateData(itemId, item, rowObject); } return rowObject; } - private void setGeneratedCellStyles(CellStyleGenerator generator, - JsonObject rowObject, Collection<Column> columns) { - JsonObject cellStyles = null; - for (Column column : columns) { - Object propertyId = column.getPropertyId(); - cellReference.set(propertyId); - String style = generator.getStyle(cellReference); - if (style != null && !style.isEmpty()) { - if (cellStyles == null) { - cellStyles = Json.createObject(); - } - - String columnKey = columnKeys.key(propertyId); - cellStyles.put(columnKey, style); - } - } - if (cellStyles != null) { - rowObject.put(GridState.JSONKEY_CELLSTYLES, cellStyles); - } - - } - - private void setGeneratedRowStyles(RowStyleGenerator generator, - JsonObject rowObject) { - String rowStyle = generator.getStyle(rowReference); - if (rowStyle != null && !rowStyle.isEmpty()) { - rowObject.put(GridState.JSONKEY_ROWSTYLE, rowStyle); - } - } - /** * Makes the data source available to the given {@link Grid} component. * @@ -1173,13 +604,38 @@ public class RpcDataProviderExtension extends AbstractExtension { * @param columnKeys * the key mapper for columns */ - public void extend(Grid component, KeyMapper<Object> columnKeys) { - this.columnKeys = columnKeys; + public void extend(Grid component) { detailComponentManager.setGrid(component); super.extend(component); } /** + * Adds a {@link DataGenerator} for this {@code RpcDataProviderExtension}. + * DataGenerators are called when sending row data to client. If given + * DataGenerator is already added, this method does nothing. + * + * @since 7.6 + * @param generator + * generator to add + */ + public void addDataGenerator(DataGenerator generator) { + dataGenerators.add(generator); + } + + /** + * Removes a {@link DataGenerator} from this + * {@code RpcDataProviderExtension}. If given DataGenerator is not added to + * this data provider, this method does nothing. + * + * @since 7.6 + * @param generator + * generator to remove + */ + public void removeDataGenerator(DataGenerator generator) { + dataGenerators.remove(generator); + } + + /** * Informs the client side that new rows have been inserted into the data * source. * @@ -1205,8 +661,6 @@ public class RpcDataProviderExtension extends AbstractExtension { rpc.insertRowData(index, count); } }); - - activeRowHandler.insertRows(index, count); } /** @@ -1231,8 +685,6 @@ public class RpcDataProviderExtension extends AbstractExtension { rpc.removeRowData(index, count); } }); - - activeRowHandler.removeRows(index, count); } /** @@ -1252,18 +704,20 @@ public class RpcDataProviderExtension extends AbstractExtension { updatedItemIds.add(itemId); } - private void internalUpdateRowData(Object itemId) { - int index = container.indexOfId(itemId); - if (index >= 0) { - JsonValue row = getRowData(getGrid().getColumns(), itemId); - JsonArray rowArray = Json.createArray(); - rowArray.set(0, row); - rpc.setRowData(index, rowArray); + private void internalUpdateRows(Set<Object> itemIds) { + if (itemIds.isEmpty()) { + return; + } - if (isDetailsVisible(itemId)) { - detailComponentManager.createDetails(itemId, index); + JsonArray rowData = Json.createArray(); + int i = 0; + for (Object itemId : itemIds) { + if (activeItemHandler.getActiveItemIds().contains(itemId)) { + JsonObject row = getRowData(getGrid().getColumns(), itemId); + rowData.set(i++, row); } } + rpc.updateRowData(rowData); } /** @@ -1280,20 +734,15 @@ public class RpcDataProviderExtension extends AbstractExtension { public void setParent(ClientConnector parent) { if (parent == null) { // We're being detached, release various listeners - - activeRowHandler - .removeValueChangeListeners(activeRowHandler.activeRange); + activeItemHandler.internalDropActiveItems(activeItemHandler + .getActiveItemIds()); if (container instanceof ItemSetChangeNotifier) { ((ItemSetChangeNotifier) container) .removeItemSetChangeListener(itemListener); } - } else if (parent instanceof Grid) { - Grid grid = (Grid) parent; - rowReference = new RowReference(grid); - cellReference = new CellReference(rowReference); - } else { + } else if (!(parent instanceof Grid)) { throw new IllegalStateException( "Grid is the only accepted parent type"); } @@ -1308,7 +757,13 @@ public class RpcDataProviderExtension extends AbstractExtension { * a list of removed columns */ public void columnsRemoved(List<Column> removedColumns) { - activeRowHandler.columnsRemoved(removedColumns); + for (GridValueChangeListener l : activeItemHandler + .getValueChangeListeners()) { + l.removeColumns(removedColumns); + } + + // No need to resend unchanged data. Client will remember the old + // columns until next set of rows is sent. } /** @@ -1318,11 +773,17 @@ public class RpcDataProviderExtension extends AbstractExtension { * a list of added columns */ public void columnsAdded(List<Column> addedColumns) { - activeRowHandler.columnsAdded(addedColumns); + for (GridValueChangeListener l : activeItemHandler + .getValueChangeListeners()) { + l.addColumns(addedColumns); + } + + // Resend all rows to contain new data. + refreshCache(); } - public DataProviderKeyMapper getKeyMapper() { - return keyMapper; + public KeyMapper<Object> getKeyMapper() { + return activeItemHandler.keyMapper; } protected Grid getGrid() { @@ -1330,62 +791,6 @@ public class RpcDataProviderExtension extends AbstractExtension { } /** - * 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) { - if (presentationType == String.class) { - // If there is no converter, just fallback to using - // toString(). - // modelValue can't be null as Class.cast(null) will always - // succeed - presentationValue = (T) modelValue.toString(); - } else { - throw 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."); - } - } - } 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()); - } - - /** * Marks a row's details to be visible or hidden. * <p> * If that row is currently in the client side's cache, this information @@ -1399,37 +804,21 @@ public class RpcDataProviderExtension extends AbstractExtension { * hide */ public void setDetailsVisible(Object itemId, boolean visible) { - final boolean modified; - if (visible) { - modified = visibleDetails.add(itemId); + visibleDetails.add(itemId); /* - * We don't want to create the component here, since the component - * might be out of view, and thus we don't know where the details - * should end up on the client side. This is also a great thing to - * optimize away, so that in case a lot of things would be opened at - * once, a huge chunk of data doesn't get sent over immediately. + * This might be an issue with a huge number of open rows, but as of + * now this works in most of the cases. */ - + detailComponentManager.createDetails(itemId); } else { - modified = visibleDetails.remove(itemId); + visibleDetails.remove(itemId); - /* - * Here we can try to destroy the component no matter what. The - * component has been removed and should be detached from the - * component hierarchy. The details row will be closed on the client - * side automatically. - */ detailComponentManager.destroyDetails(itemId); } - int rowIndex = indexOf(itemId); - boolean modifiedRowIsActive = activeRowHandler.activeRange - .contains(rowIndex); - if (modified && modifiedRowIsActive) { - updateRowData(itemId); - } + updateRowData(itemId); } /** @@ -1454,18 +843,10 @@ public class RpcDataProviderExtension extends AbstractExtension { public void refreshDetails() { for (Object itemId : ImmutableSet.copyOf(visibleDetails)) { detailComponentManager.refresh(itemId); + updateRowData(itemId); } } - private int indexOf(Object itemId) { - /* - * It would be great if we could optimize this method away, since the - * normal usage of Grid doesn't need any indices to be known. It was - * already optimized away once, maybe we can do away with these as well. - */ - return container.indexOfId(itemId); - } - /** * Gets the detail component manager for this data provider * @@ -1475,13 +856,4 @@ public class RpcDataProviderExtension extends AbstractExtension { public DetailComponentManager getDetailComponentManager() { return detailComponentManager; } - - @Override - public void detach() { - for (Object itemId : ImmutableSet.copyOf(visibleDetails)) { - detailComponentManager.destroyDetails(itemId); - } - - super.detach(); - } } diff --git a/server/src/com/vaadin/data/util/ContainerOrderedWrapper.java b/server/src/com/vaadin/data/util/ContainerOrderedWrapper.java index 4bb4e4c1b2..4329219e96 100644 --- a/server/src/com/vaadin/data/util/ContainerOrderedWrapper.java +++ b/server/src/com/vaadin/data/util/ContainerOrderedWrapper.java @@ -51,12 +51,14 @@ public class ContainerOrderedWrapper implements Container.Ordered, private final Container container; /** - * Ordering information, ie. the mapping from Item ID to the next item ID + * Ordering information, ie. the mapping from Item ID to the next item ID. + * The last item id should not be present */ private Hashtable<Object, Object> next; /** - * Reverse ordering information for convenience and performance reasons. + * Reverse ordering information for convenience and performance reasons. The + * first item id should not be present */ private Hashtable<Object, Object> prev; @@ -124,13 +126,21 @@ public class ContainerOrderedWrapper implements Container.Ordered, first = nid; } if (last.equals(id)) { - first = pid; + last = pid; } if (nid != null) { - prev.put(nid, pid); + if (pid == null) { + prev.remove(nid); + } else { + prev.put(nid, pid); + } } if (pid != null) { - next.put(pid, nid); + if (nid == null) { + next.remove(pid); + } else { + next.put(pid, nid); + } } next.remove(id); prev.remove(id); @@ -200,7 +210,7 @@ public class ContainerOrderedWrapper implements Container.Ordered, final Collection<?> ids = container.getItemIds(); // Recreates ordering if some parts of it are missing - if (next == null || first == null || last == null || prev != null) { + if (next == null || first == null || last == null || prev == null) { first = null; last = null; next = new Hashtable<Object, Object>(); @@ -219,7 +229,7 @@ public class ContainerOrderedWrapper implements Container.Ordered, // Adds missing items for (final Iterator<?> i = ids.iterator(); i.hasNext();) { final Object id = i.next(); - if (!next.containsKey(id)) { + if (!next.containsKey(id) && last != id) { addToOrderWrapper(id); } } diff --git a/server/src/com/vaadin/data/util/converter/StringToBooleanConverter.java b/server/src/com/vaadin/data/util/converter/StringToBooleanConverter.java index 0e802da879..f965cfcc6a 100644 --- a/server/src/com/vaadin/data/util/converter/StringToBooleanConverter.java +++ b/server/src/com/vaadin/data/util/converter/StringToBooleanConverter.java @@ -19,20 +19,43 @@ package com.vaadin.data.util.converter; import java.util.Locale; /** - * A converter that converts from {@link String} to {@link Boolean} and back. - * The String representation is given by Boolean.toString(). - * <p> - * Leading and trailing white spaces are ignored when converting from a String. - * </p> - * + * A converter that converts from {@link String} to {@link Boolean} and back. The String representation is given by + * {@link Boolean#toString()} or provided in constructor {@link #StringToBooleanConverter(String, String)}. + * <p> Leading and trailing white spaces are ignored when converting from a String. </p> + * <p> For language-dependent representation, subclasses should overwrite {@link #getFalseString(Locale)} and {@link #getTrueString(Locale)}</p> + * * @author Vaadin Ltd * @since 7.0 */ public class StringToBooleanConverter implements Converter<String, Boolean> { + private final String trueString; + + private final String falseString; + + /** + * Creates converter with default string representations - "true" and "false" + * + */ + public StringToBooleanConverter() { + this(Boolean.TRUE.toString(), Boolean.FALSE.toString()); + } + + /** + * Creates converter with custom string representation. + * + * @since 7.5.4 + * @param falseString string representation for <code>false</code> + * @param trueString string representation for <code>true</code> + */ + public StringToBooleanConverter(String trueString, String falseString) { + this.trueString = trueString; + this.falseString = falseString; + } + /* * (non-Javadoc) - * + * * @see * com.vaadin.data.util.converter.Converter#convertToModel(java.lang.Object, * java.lang.Class, java.util.Locale) @@ -59,26 +82,26 @@ public class StringToBooleanConverter implements Converter<String, Boolean> { } /** - * Gets the string representation for true. Default is "true". - * + * Gets the string representation for true. Default is "true", if not set in constructor. + * * @return the string representation for true */ protected String getTrueString() { - return Boolean.TRUE.toString(); + return trueString; } /** - * Gets the string representation for false. Default is "false". - * + * Gets the string representation for false. Default is "false", if not set in constructor. + * * @return the string representation for false */ protected String getFalseString() { - return Boolean.FALSE.toString(); + return falseString; } /* * (non-Javadoc) - * + * * @see * com.vaadin.data.util.converter.Converter#convertToPresentation(java.lang * .Object, java.lang.Class, java.util.Locale) @@ -91,15 +114,39 @@ public class StringToBooleanConverter implements Converter<String, Boolean> { return null; } if (value) { - return getTrueString(); + return getTrueString(locale); } else { - return getFalseString(); + return getFalseString(locale); } } + /** + * Gets the locale-depended string representation for false. + * Default is locale-independent value provided by {@link #getFalseString()} + * + * @since 7.5.4 + * @param locale to be used + * @return the string representation for false + */ + protected String getFalseString(Locale locale) { + return getFalseString(); + } + + /** + * Gets the locale-depended string representation for true. + * Default is locale-independent value provided by {@link #getTrueString()} + * + * @since 7.5.4 + * @param locale to be used + * @return the string representation for true + */ + protected String getTrueString(Locale locale) { + return getTrueString(); + } + /* * (non-Javadoc) - * + * * @see com.vaadin.data.util.converter.Converter#getModelType() */ @Override @@ -109,7 +156,7 @@ public class StringToBooleanConverter implements Converter<String, Boolean> { /* * (non-Javadoc) - * + * * @see com.vaadin.data.util.converter.Converter#getPresentationType() */ @Override diff --git a/server/src/com/vaadin/event/ShortcutAction.java b/server/src/com/vaadin/event/ShortcutAction.java index 09accae1c7..dd511c23c0 100644 --- a/server/src/com/vaadin/event/ShortcutAction.java +++ b/server/src/com/vaadin/event/ShortcutAction.java @@ -17,6 +17,7 @@ package com.vaadin.event; import java.io.Serializable; +import java.util.Arrays; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -55,7 +56,7 @@ public class ShortcutAction extends Action { private final int keyCode; - private final int[] modifiers; + private int[] modifiers; /** * Creates a shortcut that reacts to the given {@link KeyCode} and @@ -73,7 +74,7 @@ public class ShortcutAction extends Action { public ShortcutAction(String caption, int kc, int... m) { super(caption); keyCode = kc; - modifiers = m; + setModifiers(m); } /** @@ -94,7 +95,7 @@ public class ShortcutAction extends Action { public ShortcutAction(String caption, Resource icon, int kc, int... m) { super(caption, icon); keyCode = kc; - modifiers = m; + setModifiers(m); } /** @@ -190,7 +191,7 @@ public class ShortcutAction extends Action { // Given modifiers override this indicated in the caption if (modifierKeys != null) { - modifiers = modifierKeys; + setModifiers(modifierKeys); } else { // Read modifiers from caption int[] mod = new int[match.length() - 1]; @@ -208,13 +209,30 @@ public class ShortcutAction extends Action { break; } } - modifiers = mod; + setModifiers(mod); } } else { keyCode = -1; - modifiers = modifierKeys; + setModifiers(modifierKeys); } + + } + + /** + * When setting modifiers, make sure that modifiers is a valid array AND + * that it's sorted. + * + * @param modifiers + * the modifier keys for this shortcut + */ + private void setModifiers(int... modifiers) { + if (modifiers == null) { + this.modifiers = new int[0]; + } else { + this.modifiers = modifiers; + } + Arrays.sort(this.modifiers); } /** diff --git a/server/src/com/vaadin/server/Constants.java b/server/src/com/vaadin/server/Constants.java index 5a0d852299..77a1a3134e 100644 --- a/server/src/com/vaadin/server/Constants.java +++ b/server/src/com/vaadin/server/Constants.java @@ -137,6 +137,7 @@ public interface Constants { static final String SERVLET_PARAMETER_LEGACY_PROPERTY_TOSTRING = "legacyPropertyToString"; static final String SERVLET_PARAMETER_SYNC_ID_CHECK = "syncIdCheck"; static final String SERVLET_PARAMETER_SENDURLSASPARAMETERS = "sendUrlsAsParameters"; + static final String SERVLET_PARAMETER_PUSH_SUSPEND_TIMEOUT_LONGPOLLING = "pushLongPollingSuspendTimeout"; // Configurable parameter names static final String PARAMETER_VAADIN_RESOURCES = "Resources"; diff --git a/server/src/com/vaadin/server/VaadinServlet.java b/server/src/com/vaadin/server/VaadinServlet.java index 7aada2402d..61df02feaa 100644 --- a/server/src/com/vaadin/server/VaadinServlet.java +++ b/server/src/com/vaadin/server/VaadinServlet.java @@ -28,6 +28,7 @@ import java.lang.reflect.Method; import java.net.MalformedURLException; import java.net.URL; import java.net.URLConnection; +import java.net.URLDecoder; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -694,17 +695,20 @@ public class VaadinServlet extends HttpServlet implements Constants { return false; } + String decodedRequestURI = URLDecoder.decode(request.getRequestURI(), + "UTF-8"); if ((request.getContextPath() != null) - && (request.getRequestURI().startsWith("/VAADIN/"))) { - serveStaticResourcesInVAADIN(request.getRequestURI(), request, - response); + && (decodedRequestURI.startsWith("/VAADIN/"))) { + serveStaticResourcesInVAADIN(decodedRequestURI, request, response); return true; - } else if (request.getRequestURI().startsWith( - request.getContextPath() + "/VAADIN/")) { + } + + String decodedContextPath = URLDecoder.decode(request.getContextPath(), + "UTF-8"); + if (decodedRequestURI.startsWith(decodedContextPath + "/VAADIN/")) { serveStaticResourcesInVAADIN( - request.getRequestURI().substring( - request.getContextPath().length()), request, - response); + decodedRequestURI.substring(decodedContextPath.length()), + request, response); return true; } diff --git a/server/src/com/vaadin/server/WebBrowser.java b/server/src/com/vaadin/server/WebBrowser.java index 66018b02f2..9bf30cb3db 100644 --- a/server/src/com/vaadin/server/WebBrowser.java +++ b/server/src/com/vaadin/server/WebBrowser.java @@ -126,6 +126,20 @@ public class WebBrowser implements Serializable { } /** + * Tests whether the user is using Edge. + * + * @return true if the user is using Edge, false if the user is not using + * Edge or if no information on the browser is present + */ + public boolean isEdge() { + if (browserDetails == null) { + return false; + } + + return browserDetails.isEdge(); + } + + /** * Tests whether the user is using Safari. * * @return true if the user is using Safari, false if the user is not using diff --git a/server/src/com/vaadin/server/communication/AtmospherePushConnection.java b/server/src/com/vaadin/server/communication/AtmospherePushConnection.java index 87ce9ba81a..5c0d2e14d4 100644 --- a/server/src/com/vaadin/server/communication/AtmospherePushConnection.java +++ b/server/src/com/vaadin/server/communication/AtmospherePushConnection.java @@ -26,7 +26,9 @@ import java.io.Writer; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import java.util.logging.ConsoleHandler; import java.util.logging.Level; +import java.util.logging.LogRecord; import java.util.logging.Logger; import org.atmosphere.cpr.AtmosphereResource; @@ -274,6 +276,13 @@ public class AtmospherePushConnection implements PushConnection { public void disconnect() { assert isConnected(); + if (resource == null) { + // Already disconnected. Should not happen but if it does, we don't + // want to cause NPEs + getLogger() + .fine("AtmospherePushConnection.disconnect() called twice, this should not happen"); + return; + } if (resource.isResumed()) { // This can happen for long polling because of // http://dev.vaadin.com/ticket/16919 @@ -345,4 +354,32 @@ public class AtmospherePushConnection implements PushConnection { private static Logger getLogger() { return Logger.getLogger(AtmospherePushConnection.class.getName()); } + + /** + * Internal method used for reconfiguring loggers to show all Atmosphere log + * messages in the console. + * + * @since 7.6 + */ + public static void enableAtmosphereDebugLogging() { + Level level = Level.FINEST; + + Logger atmosphereLogger = Logger.getLogger("org.atmosphere"); + if (atmosphereLogger.getLevel() == level) { + // Already enabled + return; + } + + atmosphereLogger.setLevel(level); + + // Without this logging, we will have a ClassCircularityError + LogRecord record = new LogRecord(Level.INFO, + "Enabling Atmosphere debug logging"); + atmosphereLogger.log(record); + + ConsoleHandler ch = new ConsoleHandler(); + ch.setLevel(Level.ALL); + atmosphereLogger.addHandler(ch); + } + } diff --git a/server/src/com/vaadin/server/communication/FileUploadHandler.java b/server/src/com/vaadin/server/communication/FileUploadHandler.java index 576cbd8411..532c7fe95b 100644 --- a/server/src/com/vaadin/server/communication/FileUploadHandler.java +++ b/server/src/com/vaadin/server/communication/FileUploadHandler.java @@ -269,7 +269,7 @@ public class FileUploadHandler implements RequestHandler { streamVariable = uI.getConnectorTracker().getStreamVariable( connectorId, variableName); String secKey = uI.getConnectorTracker().getSeckey(streamVariable); - if (!secKey.equals(parts[3])) { + if (secKey == null || !secKey.equals(parts[3])) { // TODO Should rethink error handling return true; } diff --git a/server/src/com/vaadin/server/communication/JSR356WebsocketInitializer.java b/server/src/com/vaadin/server/communication/JSR356WebsocketInitializer.java index f36d403dd5..6d2843a4fc 100644 --- a/server/src/com/vaadin/server/communication/JSR356WebsocketInitializer.java +++ b/server/src/com/vaadin/server/communication/JSR356WebsocketInitializer.java @@ -181,8 +181,13 @@ public class JSR356WebsocketInitializer implements ServletContextListener { */ protected boolean isVaadinServlet(ServletRegistration servletRegistration) { try { - Class<?> servletClass = Class.forName(servletRegistration - .getClassName()); + String servletClassName = servletRegistration.getClassName(); + if (servletClassName.equals("com.ibm.ws.wsoc.WsocServlet")) { + // Websphere servlet which implements websocket endpoints, + // dynamically added + return false; + } + Class<?> servletClass = Class.forName(servletClassName); return VaadinServlet.class.isAssignableFrom(servletClass); } catch (Exception e) { // This will fail in OSGi environments, assume everything is a diff --git a/server/src/com/vaadin/server/communication/PushHandler.java b/server/src/com/vaadin/server/communication/PushHandler.java index 01077c3f86..994415a0b4 100644 --- a/server/src/com/vaadin/server/communication/PushHandler.java +++ b/server/src/com/vaadin/server/communication/PushHandler.java @@ -55,6 +55,8 @@ import elemental.json.JsonException; */ public class PushHandler { + private int longPollingSuspendTimeout = -1; + /** * Callback interface used internally to process an event with the * corresponding UI properly locked. @@ -107,7 +109,7 @@ public class PushHandler { return; } - resource.suspend(); + suspend(resource); AtmospherePushConnection connection = getConnectionForUI(ui); assert (connection != null); @@ -174,6 +176,21 @@ public class PushHandler { } /** + * Suspends the given resource + * + * @since + * @param resource + * the resource to suspend + */ + protected void suspend(AtmosphereResource resource) { + if (resource.transport() == TRANSPORT.LONG_POLLING) { + resource.suspend(getLongPollingSuspendTimeout()); + } else { + resource.suspend(-1); + } + } + + /** * Find the UI for the atmosphere resource, lock it and invoke the callback. * * @param resource @@ -493,4 +510,26 @@ public class PushHandler { resource.transport() == TRANSPORT.WEBSOCKET); } + /** + * Sets the timeout used for suspend calls when using long polling. + * + * If you are using a proxy with a defined idle timeout, set the suspend + * timeout to a value smaller than the proxy timeout so that the server is + * aware of a reconnect taking place. + * + * @param suspendTimeout + * the timeout to use for suspended AtmosphereResources + */ + public void setLongPollingSuspendTimeout(int longPollingSuspendTimeout) { + this.longPollingSuspendTimeout = longPollingSuspendTimeout; + } + + /** + * Gets the timeout used for suspend calls when using long polling. + * + * @return the timeout to use for suspended AtmosphereResources + */ + public int getLongPollingSuspendTimeout() { + return longPollingSuspendTimeout; + } } diff --git a/server/src/com/vaadin/server/communication/PushRequestHandler.java b/server/src/com/vaadin/server/communication/PushRequestHandler.java index c01c74e5cd..c44fcd9ef3 100644 --- a/server/src/com/vaadin/server/communication/PushRequestHandler.java +++ b/server/src/com/vaadin/server/communication/PushRequestHandler.java @@ -77,7 +77,7 @@ public class PushRequestHandler implements RequestHandler, final ServletConfig vaadinServletConfig = service.getServlet() .getServletConfig(); - pushHandler = new PushHandler(service); + pushHandler = createPushHandler(service); atmosphere = getPreInitializedAtmosphere(vaadinServletConfig); if (atmosphere == null) { @@ -100,7 +100,12 @@ public class PushRequestHandler implements RequestHandler, "Using pre-initialized Atmosphere for servlet " + vaadinServletConfig.getServletName()); } - + pushHandler + .setLongPollingSuspendTimeout(atmosphere + .getAtmosphereConfig() + .getInitParameter( + com.vaadin.server.Constants.SERVLET_PARAMETER_PUSH_SUSPEND_TIMEOUT_LONGPOLLING, + -1)); for (AtmosphereHandlerWrapper handlerWrapper : atmosphere .getAtmosphereHandlers().values()) { AtmosphereHandler handler = handlerWrapper.atmosphereHandler; @@ -113,6 +118,22 @@ public class PushRequestHandler implements RequestHandler, } } + /** + * Creates a push handler for this request handler. + * <p> + * Create your own request handler and override this method if you want to + * customize the {@link PushHandler}, e.g. to dynamically decide the suspend + * timeout. + * + * @since + * @param service + * the vaadin service + * @return the push handler to use for this service + */ + protected PushHandler createPushHandler(VaadinServletService service) { + return new PushHandler(service); + } + private static final Logger getLogger() { return Logger.getLogger(PushRequestHandler.class.getName()); } diff --git a/server/src/com/vaadin/ui/AbstractFocusable.java b/server/src/com/vaadin/ui/AbstractFocusable.java new file mode 100644 index 0000000000..ad3b96f29b --- /dev/null +++ b/server/src/com/vaadin/ui/AbstractFocusable.java @@ -0,0 +1,134 @@ +/* + * 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 com.vaadin.event.FieldEvents.BlurEvent; +import com.vaadin.event.FieldEvents.BlurListener; +import com.vaadin.event.FieldEvents.BlurNotifier; +import com.vaadin.event.FieldEvents.FocusAndBlurServerRpcImpl; +import com.vaadin.event.FieldEvents.FocusEvent; +import com.vaadin.event.FieldEvents.FocusListener; +import com.vaadin.event.FieldEvents.FocusNotifier; +import com.vaadin.shared.ui.TabIndexState; +import com.vaadin.ui.Component.Focusable; + +/** + * An abstract base class for focusable components. Includes API for setting the + * tab index, programmatic focusing, and adding focus and blur listeners. + * + * @since 7.6 + * @author Vaadin Ltd + */ +public abstract class AbstractFocusable extends AbstractComponent implements + Focusable, FocusNotifier, BlurNotifier { + + protected AbstractFocusable() { + registerRpc(new FocusAndBlurServerRpcImpl(this) { + @Override + protected void fireEvent(Event event) { + AbstractFocusable.this.fireEvent(event); + } + }); + } + + @Override + public void addBlurListener(BlurListener listener) { + addListener(BlurEvent.EVENT_ID, BlurEvent.class, listener, + BlurListener.blurMethod); + } + + /** + * @deprecated As of 7.0, replaced by {@link #addBlurListener(BlurListener)} + */ + @Override + @Deprecated + public void addListener(BlurListener listener) { + addBlurListener(listener); + } + + @Override + public void removeBlurListener(BlurListener listener) { + removeListener(BlurEvent.EVENT_ID, BlurEvent.class, listener); + } + + /** + * @deprecated As of 7.0, replaced by + * {@link #removeBlurListener(BlurListener)} + */ + @Override + @Deprecated + public void removeListener(BlurListener listener) { + removeBlurListener(listener); + + } + + @Override + public void addFocusListener(FocusListener listener) { + addListener(FocusEvent.EVENT_ID, FocusEvent.class, listener, + FocusListener.focusMethod); + + } + + /** + * @deprecated As of 7.0, replaced by + * {@link #addFocusListener(FocusListener)} + */ + @Override + @Deprecated + public void addListener(FocusListener listener) { + addFocusListener(listener); + } + + @Override + public void removeFocusListener(FocusListener listener) { + removeListener(FocusEvent.EVENT_ID, FocusEvent.class, listener); + } + + /** + * @deprecated As of 7.0, replaced by + * {@link #removeFocusListener(FocusListener)} + */ + @Override + @Deprecated + public void removeListener(FocusListener listener) { + removeFocusListener(listener); + } + + @Override + public void focus() { + super.focus(); + } + + @Override + public int getTabIndex() { + return getState(false).tabIndex; + } + + @Override + public void setTabIndex(int tabIndex) { + getState().tabIndex = tabIndex; + } + + @Override + protected TabIndexState getState() { + return (TabIndexState) super.getState(); + } + + @Override + protected TabIndexState getState(boolean markAsDirty) { + return (TabIndexState) super.getState(markAsDirty); + } +} diff --git a/server/src/com/vaadin/ui/Button.java b/server/src/com/vaadin/ui/Button.java index 6beb6ed686..a918780a60 100644 --- a/server/src/com/vaadin/ui/Button.java +++ b/server/src/com/vaadin/ui/Button.java @@ -24,12 +24,7 @@ import org.jsoup.nodes.Attributes; import org.jsoup.nodes.Element; import com.vaadin.event.Action; -import com.vaadin.event.FieldEvents; -import com.vaadin.event.FieldEvents.BlurEvent; -import com.vaadin.event.FieldEvents.BlurListener; import com.vaadin.event.FieldEvents.FocusAndBlurServerRpcImpl; -import com.vaadin.event.FieldEvents.FocusEvent; -import com.vaadin.event.FieldEvents.FocusListener; import com.vaadin.event.ShortcutAction; import com.vaadin.event.ShortcutAction.KeyCode; import com.vaadin.event.ShortcutAction.ModifierKey; @@ -38,7 +33,6 @@ import com.vaadin.server.Resource; import com.vaadin.shared.MouseEventDetails; import com.vaadin.shared.ui.button.ButtonServerRpc; import com.vaadin.shared.ui.button.ButtonState; -import com.vaadin.ui.Component.Focusable; import com.vaadin.ui.declarative.DesignAttributeHandler; import com.vaadin.ui.declarative.DesignContext; import com.vaadin.util.ReflectTools; @@ -50,8 +44,7 @@ import com.vaadin.util.ReflectTools; * @since 3.0 */ @SuppressWarnings("serial") -public class Button extends AbstractComponent implements - FieldEvents.BlurNotifier, FieldEvents.FocusNotifier, Focusable, +public class Button extends AbstractFocusable implements Action.ShortcutNotifier { private ButtonServerRpc rpc = new ButtonServerRpc() { @@ -72,20 +65,11 @@ public class Button extends AbstractComponent implements } }; - FocusAndBlurServerRpcImpl focusBlurRpc = new FocusAndBlurServerRpcImpl(this) { - - @Override - protected void fireEvent(Event event) { - Button.this.fireEvent(event); - } - }; - /** * Creates a new push button. */ public Button() { registerRpc(rpc); - registerRpc(focusBlurRpc); } /** @@ -393,67 +377,6 @@ public class Button extends AbstractComponent implements fireEvent(new Button.ClickEvent(this, details)); } - @Override - public void addBlurListener(BlurListener listener) { - addListener(BlurEvent.EVENT_ID, BlurEvent.class, listener, - BlurListener.blurMethod); - } - - /** - * @deprecated As of 7.0, replaced by {@link #addBlurListener(BlurListener)} - **/ - @Override - @Deprecated - public void addListener(BlurListener listener) { - addBlurListener(listener); - } - - @Override - public void removeBlurListener(BlurListener listener) { - removeListener(BlurEvent.EVENT_ID, BlurEvent.class, listener); - } - - /** - * @deprecated As of 7.0, replaced by - * {@link #removeBlurListener(BlurListener)} - **/ - @Override - @Deprecated - public void removeListener(BlurListener listener) { - removeBlurListener(listener); - } - - @Override - public void addFocusListener(FocusListener listener) { - addListener(FocusEvent.EVENT_ID, FocusEvent.class, listener, - FocusListener.focusMethod); - } - - /** - * @deprecated As of 7.0, replaced by - * {@link #addFocusListener(FocusListener)} - **/ - @Override - @Deprecated - public void addListener(FocusListener listener) { - addFocusListener(listener); - } - - @Override - public void removeFocusListener(FocusListener listener) { - removeListener(FocusEvent.EVENT_ID, FocusEvent.class, listener); - } - - /** - * @deprecated As of 7.0, replaced by - * {@link #removeFocusListener(FocusListener)} - **/ - @Override - @Deprecated - public void removeListener(FocusListener listener) { - removeFocusListener(listener); - } - /* * Actions */ @@ -575,32 +498,6 @@ public class Button extends AbstractComponent implements getState().disableOnClick = disableOnClick; } - /* - * (non-Javadoc) - * - * @see com.vaadin.ui.Component.Focusable#getTabIndex() - */ - @Override - public int getTabIndex() { - return getState(false).tabIndex; - } - - /* - * (non-Javadoc) - * - * @see com.vaadin.ui.Component.Focusable#setTabIndex(int) - */ - @Override - public void setTabIndex(int tabIndex) { - getState().tabIndex = tabIndex; - } - - @Override - public void focus() { - // Overridden only to make public - super.focus(); - } - @Override protected ButtonState getState() { return (ButtonState) super.getState(); diff --git a/server/src/com/vaadin/ui/Grid.java b/server/src/com/vaadin/ui/Grid.java index e9469c5bca..215df3a6a3 100644 --- a/server/src/com/vaadin/ui/Grid.java +++ b/server/src/com/vaadin/ui/Grid.java @@ -31,6 +31,7 @@ import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Map.Entry; import java.util.Set; @@ -45,14 +46,17 @@ 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.ItemSetChangeEvent; +import com.vaadin.data.Container.ItemSetChangeListener; +import com.vaadin.data.Container.ItemSetChangeNotifier; 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.DataGenerator; 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.RpcDataProviderExtension.DetailComponentManager; import com.vaadin.data.Validator.InvalidValueException; import com.vaadin.data.fieldgroup.DefaultFieldGroupFieldFactory; @@ -77,6 +81,7 @@ import com.vaadin.server.AbstractClientConnector; import com.vaadin.server.AbstractExtension; import com.vaadin.server.EncodeResult; import com.vaadin.server.ErrorMessage; +import com.vaadin.server.Extension; import com.vaadin.server.JsonCodec; import com.vaadin.server.KeyMapper; import com.vaadin.server.VaadinSession; @@ -89,13 +94,16 @@ import com.vaadin.shared.ui.grid.GridColumnState; import com.vaadin.shared.ui.grid.GridConstants; import com.vaadin.shared.ui.grid.GridServerRpc; import com.vaadin.shared.ui.grid.GridState; -import com.vaadin.shared.ui.grid.GridState.SharedSelectionMode; import com.vaadin.shared.ui.grid.GridStaticCellType; import com.vaadin.shared.ui.grid.GridStaticSectionState; import com.vaadin.shared.ui.grid.GridStaticSectionState.CellState; import com.vaadin.shared.ui.grid.GridStaticSectionState.RowState; import com.vaadin.shared.ui.grid.HeightMode; import com.vaadin.shared.ui.grid.ScrollDestination; +import com.vaadin.shared.ui.grid.selection.MultiSelectionModelServerRpc; +import com.vaadin.shared.ui.grid.selection.MultiSelectionModelState; +import com.vaadin.shared.ui.grid.selection.SingleSelectionModelServerRpc; +import com.vaadin.shared.ui.grid.selection.SingleSelectionModelState; import com.vaadin.shared.util.SharedUtil; import com.vaadin.ui.declarative.DesignAttributeHandler; import com.vaadin.ui.declarative.DesignContext; @@ -106,7 +114,6 @@ import com.vaadin.ui.renderers.TextRenderer; import com.vaadin.util.ReflectTools; import elemental.json.Json; -import elemental.json.JsonArray; import elemental.json.JsonObject; import elemental.json.JsonValue; @@ -172,7 +179,7 @@ import elemental.json.JsonValue; * @since 7.4 * @author Vaadin Ltd */ -public class Grid extends AbstractComponent implements SelectionNotifier, +public class Grid extends AbstractFocusable implements SelectionNotifier, SortNotifier, SelectiveRenderer, ItemClickNotifier { /** @@ -511,6 +518,100 @@ public class Grid extends AbstractComponent implements SelectionNotifier, } /** + * Interface for an editor event listener + */ + public interface EditorListener extends Serializable { + + public static final Method EDITOR_OPEN_METHOD = ReflectTools + .findMethod(EditorListener.class, "editorOpened", + EditorOpenEvent.class); + public static final Method EDITOR_MOVE_METHOD = ReflectTools + .findMethod(EditorListener.class, "editorMoved", + EditorMoveEvent.class); + public static final Method EDITOR_CLOSE_METHOD = ReflectTools + .findMethod(EditorListener.class, "editorClosed", + EditorCloseEvent.class); + + /** + * Called when an editor is opened + * + * @param e + * an editor open event object + */ + public void editorOpened(EditorOpenEvent e); + + /** + * Called when an editor is reopened without closing it first + * + * @param e + * an editor move event object + */ + public void editorMoved(EditorMoveEvent e); + + /** + * Called when an editor is closed + * + * @param e + * an editor close event object + */ + public void editorClosed(EditorCloseEvent e); + + } + + /** + * Base class for editor related events + */ + public static abstract class EditorEvent extends Component.Event { + + private Object itemID; + + protected EditorEvent(Grid source, Object itemID) { + super(source); + this.itemID = itemID; + } + + /** + * Get the item (row) for which this editor was opened + */ + public Object getItem() { + return itemID; + } + + } + + /** + * This event gets fired when an editor is opened + */ + public static class EditorOpenEvent extends EditorEvent { + + public EditorOpenEvent(Grid source, Object itemID) { + super(source, itemID); + } + } + + /** + * This event gets fired when an editor is opened while another row is being + * edited (i.e. editor focus moves elsewhere) + */ + public static class EditorMoveEvent extends EditorEvent { + + public EditorMoveEvent(Grid source, Object itemID) { + super(source, itemID); + } + } + + /** + * This event gets fired when an editor is dismissed or closed by other + * means. + */ + public static class EditorCloseEvent extends EditorEvent { + + public EditorCloseEvent(Grid source, Object itemID) { + super(source, itemID); + } + } + + /** * Default error handler for the editor * */ @@ -611,8 +712,9 @@ public class Grid extends AbstractComponent implements SelectionNotifier, /** * The server-side interface that controls Grid's selection state. + * SelectionModel should extend {@link AbstractGridExtension}. */ - public interface SelectionModel extends Serializable { + public interface SelectionModel extends Serializable, Extension { /** * Checks whether an item is selected or not. * @@ -631,6 +733,8 @@ public class Grid extends AbstractComponent implements SelectionNotifier, /** * Injects the current {@link Grid} instance into the SelectionModel. + * This method should usually call the extend method of + * {@link AbstractExtension}. * <p> * <em>Note:</em> This method should not be called manually. * @@ -872,10 +976,9 @@ public class Grid extends AbstractComponent implements SelectionNotifier, * A base class for SelectionModels that contains some of the logic that is * reusable. */ - public static abstract class AbstractSelectionModel implements - SelectionModel { + public static abstract class AbstractSelectionModel extends + AbstractGridExtension implements SelectionModel, DataGenerator { protected final LinkedHashSet<Object> selection = new LinkedHashSet<Object>(); - protected Grid grid = null; @Override public boolean isSelected(final Object itemId) { @@ -889,7 +992,9 @@ public class Grid extends AbstractComponent implements SelectionNotifier, @Override public void setGrid(final Grid grid) { - this.grid = grid; + if (grid != null) { + extend(grid); + } } /** @@ -903,7 +1008,7 @@ public class Grid extends AbstractComponent implements SelectionNotifier, */ protected void checkItemIdExists(Object itemId) throws IllegalArgumentException { - if (!grid.getContainerDataSource().containsId(itemId)) { + if (!getParentGrid().getContainerDataSource().containsId(itemId)) { throw new IllegalArgumentException("Given item id (" + itemId + ") does not exist in the container"); } @@ -945,7 +1050,19 @@ public class Grid extends AbstractComponent implements SelectionNotifier, protected void fireSelectionEvent( final Collection<Object> oldSelection, final Collection<Object> newSelection) { - grid.fireSelectionEvent(oldSelection, newSelection); + getParentGrid().fireSelectionEvent(oldSelection, newSelection); + } + + @Override + public void generateData(Object itemId, Item item, JsonObject rowData) { + if (isSelected(itemId)) { + rowData.put(GridState.JSONKEY_SELECTED, true); + } + } + + @Override + protected Object getItemId(String rowKey) { + return rowKey != null ? super.getItemId(rowKey) : null; } } @@ -954,8 +1071,25 @@ public class Grid extends AbstractComponent implements SelectionNotifier, */ public static class SingleSelectionModel extends AbstractSelectionModel implements SelectionModel.Single { + + @Override + protected void extend(AbstractClientConnector target) { + super.extend(target); + registerRpc(new SingleSelectionModelServerRpc() { + + @Override + public void select(String rowKey) { + SingleSelectionModel.this.select(getItemId(rowKey), false); + } + }); + } + @Override public boolean select(final Object itemId) { + return select(itemId, true); + } + + protected boolean select(final Object itemId, boolean refresh) { if (itemId == null) { return deselect(getSelectedRow()); } @@ -967,7 +1101,7 @@ public class Grid extends AbstractComponent implements SelectionNotifier, if (modified) { final Collection<Object> deselected; if (selectedRow != null) { - deselectInternal(selectedRow, false); + deselectInternal(selectedRow, false, true); deselected = Collections.singleton(selectedRow); } else { deselected = Collections.emptySet(); @@ -976,19 +1110,28 @@ public class Grid extends AbstractComponent implements SelectionNotifier, fireSelectionEvent(deselected, selection); } + if (refresh) { + refreshRow(itemId); + } + return modified; } private boolean deselect(final Object itemId) { - return deselectInternal(itemId, true); + return deselectInternal(itemId, true, true); } private boolean deselectInternal(final Object itemId, - boolean fireEventIfNeeded) { + boolean fireEventIfNeeded, boolean refresh) { final boolean modified = selection.remove(itemId); - if (fireEventIfNeeded && modified) { - fireSelectionEvent(Collections.singleton(itemId), - Collections.emptySet()); + if (modified) { + if (refresh) { + refreshRow(itemId); + } + if (fireEventIfNeeded) { + fireSelectionEvent(Collections.singleton(itemId), + Collections.emptySet()); + } } return modified; } @@ -1014,23 +1157,25 @@ public class Grid extends AbstractComponent implements SelectionNotifier, @Override public void setDeselectAllowed(boolean deselectAllowed) { - grid.getState().singleSelectDeselectAllowed = deselectAllowed; + getState().deselectAllowed = deselectAllowed; } @Override public boolean isDeselectAllowed() { - return grid.getState(false).singleSelectDeselectAllowed; + return getState().deselectAllowed; + } + + @Override + protected SingleSelectionModelState getState() { + return (SingleSelectionModelState) super.getState(); } } /** * A default implementation for a {@link SelectionModel.None} */ - public static class NoSelectionModel implements SelectionModel.None { - @Override - public void setGrid(final Grid grid) { - // NOOP, not needed for anything - } + public static class NoSelectionModel extends AbstractSelectionModel + implements SelectionModel.None { @Override public boolean isSelected(final Object itemId) { @@ -1069,6 +1214,41 @@ public class Grid extends AbstractComponent implements SelectionNotifier, private int selectionLimit = DEFAULT_MAX_SELECTIONS; @Override + protected void extend(AbstractClientConnector target) { + super.extend(target); + registerRpc(new MultiSelectionModelServerRpc() { + + @Override + public void select(List<String> rowKeys) { + List<Object> items = new ArrayList<Object>(); + for (String rowKey : rowKeys) { + items.add(getItemId(rowKey)); + } + MultiSelectionModel.this.select(items, false); + } + + @Override + public void deselect(List<String> rowKeys) { + List<Object> items = new ArrayList<Object>(); + for (String rowKey : rowKeys) { + items.add(getItemId(rowKey)); + } + MultiSelectionModel.this.deselect(items, false); + } + + @Override + public void selectAll() { + MultiSelectionModel.this.selectAll(false); + } + + @Override + public void deselectAll() { + MultiSelectionModel.this.deselectAll(false); + } + }); + } + + @Override public boolean select(final Object... itemIds) throws IllegalArgumentException { if (itemIds != null) { @@ -1089,6 +1269,10 @@ public class Grid extends AbstractComponent implements SelectionNotifier, @Override public boolean select(final Collection<?> itemIds) throws IllegalArgumentException { + return select(itemIds, true); + } + + protected boolean select(final Collection<?> itemIds, boolean refresh) { if (itemIds == null) { throw new IllegalArgumentException("itemIds may not be null"); } @@ -1113,6 +1297,15 @@ public class Grid extends AbstractComponent implements SelectionNotifier, } fireSelectionEvent(oldSelection, selection); } + + updateAllSelectedState(); + + if (refresh) { + for (Object itemId : itemIds) { + refreshRow(itemId); + } + } + return selectionWillChange; } @@ -1166,6 +1359,10 @@ public class Grid extends AbstractComponent implements SelectionNotifier, @Override public boolean deselect(final Collection<?> itemIds) throws IllegalArgumentException { + return deselect(itemIds, true); + } + + protected boolean deselect(final Collection<?> itemIds, boolean refresh) { if (itemIds == null) { throw new IllegalArgumentException("itemIds may not be null"); } @@ -1178,15 +1375,28 @@ public class Grid extends AbstractComponent implements SelectionNotifier, selection.removeAll(itemIds); fireSelectionEvent(oldSelection, selection); } + + updateAllSelectedState(); + + if (refresh) { + for (Object itemId : itemIds) { + refreshRow(itemId); + } + } + return hasCommonElements; } @Override public boolean selectAll() { + return selectAll(true); + } + + protected boolean selectAll(boolean refresh) { // select will fire the event - final Indexed container = grid.getContainerDataSource(); + final Indexed container = getParentGrid().getContainerDataSource(); if (container != null) { - return select(container.getItemIds()); + return select(container.getItemIds(), refresh); } else if (selection.isEmpty()) { return false; } else { @@ -1195,14 +1405,18 @@ public class Grid extends AbstractComponent implements SelectionNotifier, * but I guess the only theoretically correct course of * action... */ - return deselectAll(); + return deselectAll(false); } } @Override public boolean deselectAll() { + return deselectAll(true); + } + + protected boolean deselectAll(boolean refresh) { // deselect will fire the event - return deselect(getSelectedRows()); + return deselect(getSelectedRows(), refresh); } /** @@ -1258,6 +1472,8 @@ public class Grid extends AbstractComponent implements SelectionNotifier, fireSelectionEvent(oldSelection, selection); } + updateAllSelectedState(); + return changed; } @@ -1271,6 +1487,17 @@ public class Grid extends AbstractComponent implements SelectionNotifier, "Vararg array of itemIds may not be null"); } } + + private void updateAllSelectedState() { + if (getState().allSelected != selection.size() >= selectionLimit) { + getState().allSelected = selection.size() >= selectionLimit; + } + } + + @Override + protected MultiSelectionModelState getState() { + return (MultiSelectionModelState) super.getState(); + } } /** @@ -1415,39 +1642,174 @@ public class Grid extends AbstractComponent implements SelectionNotifier, } /** - * Callback interface for generating custom style names for data rows + * A callback interface for generating custom style names for Grid rows. * * @see Grid#setRowStyleGenerator(RowStyleGenerator) */ public interface RowStyleGenerator extends Serializable { /** - * Called by Grid to generate a style name for a row + * Called by Grid to generate a style name for a row. * - * @param rowReference - * The row to generate a style for + * @param row + * the row to generate a style for * @return the style name to add to this row, or {@code null} to not set * any style */ - public String getStyle(RowReference rowReference); + public String getStyle(RowReference row); } /** - * Callback interface for generating custom style names for cells + * A callback interface for generating custom style names for Grid cells. * * @see Grid#setCellStyleGenerator(CellStyleGenerator) */ public interface CellStyleGenerator extends Serializable { /** - * Called by Grid to generate a style name for a column + * Called by Grid to generate a style name for a column. * - * @param cellReference - * The cell to generate a style for + * @param cell + * the cell to generate a style for * @return the style name to add to this cell, or {@code null} to not * set any style */ - public String getStyle(CellReference cellReference); + public String getStyle(CellReference cell); + } + + /** + * A callback interface for generating optional descriptions (tooltips) for + * Grid rows. If a description is generated for a row, it is used for all + * the cells in the row for which a {@link CellDescriptionGenerator cell + * description} is not generated. + * + * @see Grid#setRowDescriptionGenerator(CellDescriptionGenerator) + * + * @since 7.6 + */ + public interface RowDescriptionGenerator extends Serializable { + + /** + * Called by Grid to generate a description (tooltip) for a row. The + * description may contain HTML which is rendered directly; if this is + * not desired the returned string must be escaped by the implementing + * method. + * + * @param row + * the row to generate a description for + * @return the row description or {@code null} for no description + */ + public String getDescription(RowReference row); + } + + /** + * A callback interface for generating optional descriptions (tooltips) for + * Grid cells. If a cell has both a {@link RowDescriptionGenerator row + * description}Â and a cell description, the latter has precedence. + * + * @see Grid#setCellDescriptionGenerator(CellDescriptionGenerator) + * + * @since 7.6 + */ + public interface CellDescriptionGenerator extends Serializable { + + /** + * Called by Grid to generate a description (tooltip) for a cell. The + * description may contain HTML which is rendered directly; if this is + * not desired the returned string must be escaped by the implementing + * method. + * + * @param cell + * the cell to generate a description for + * @return the cell description or {@code null} for no description + */ + public String getDescription(CellReference cell); + } + + /** + * Class for generating all row and cell related data for the essential + * parts of Grid. + */ + private class RowDataGenerator implements DataGenerator { + + private void put(String key, String value, JsonObject object) { + if (value != null && !value.isEmpty()) { + object.put(key, value); + } + } + + @Override + public void generateData(Object itemId, Item item, JsonObject rowData) { + RowReference row = new RowReference(Grid.this); + row.set(itemId); + + if (rowStyleGenerator != null) { + String style = rowStyleGenerator.getStyle(row); + put(GridState.JSONKEY_ROWSTYLE, style, rowData); + } + + if (rowDescriptionGenerator != null) { + String description = rowDescriptionGenerator + .getDescription(row); + put(GridState.JSONKEY_ROWDESCRIPTION, description, rowData); + + } + + JsonObject cellStyles = Json.createObject(); + JsonObject cellData = Json.createObject(); + JsonObject cellDescriptions = Json.createObject(); + + CellReference cell = new CellReference(row); + + for (Column column : getColumns()) { + cell.set(column.getPropertyId()); + + writeData(cell, cellData); + writeStyles(cell, cellStyles); + writeDescriptions(cell, cellDescriptions); + } + + if (cellDescriptionGenerator != null + && cellDescriptions.keys().length > 0) { + rowData.put(GridState.JSONKEY_CELLDESCRIPTION, cellDescriptions); + } + + if (cellStyleGenerator != null && cellStyles.keys().length > 0) { + rowData.put(GridState.JSONKEY_CELLSTYLES, cellStyles); + } + + rowData.put(GridState.JSONKEY_DATA, cellData); + } + + private void writeStyles(CellReference cell, JsonObject styles) { + if (cellStyleGenerator != null) { + String style = cellStyleGenerator.getStyle(cell); + put(columnKeys.key(cell.getPropertyId()), style, styles); + } + } + + private void writeDescriptions(CellReference cell, + JsonObject descriptions) { + if (cellDescriptionGenerator != null) { + String description = cellDescriptionGenerator + .getDescription(cell); + put(columnKeys.key(cell.getPropertyId()), description, + descriptions); + } + } + + private void writeData(CellReference cell, JsonObject data) { + Column column = getColumn(cell.getPropertyId()); + Converter<?, ?> converter = column.getConverter(); + Renderer<?> renderer = column.getRenderer(); + + Item item = cell.getItem(); + Object modelValue = item.getItemProperty(cell.getPropertyId()) + .getValue(); + + data.put(columnKeys.key(cell.getPropertyId()), AbstractRenderer + .encodeValue(modelValue, renderer, converter, getLocale())); + } } /** @@ -3281,7 +3643,7 @@ public class Grid extends AbstractComponent implements SelectionNotifier, * 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 */ @@ -3354,7 +3716,7 @@ public class Grid extends AbstractComponent implements SelectionNotifier, * 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 @@ -3365,11 +3727,79 @@ public class Grid extends AbstractComponent implements SelectionNotifier, return JsonCodec.encode(value, null, type, getUI().getConnectorTracker()).getEncodedValue(); } + + /** + * 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) { + if (presentationType == String.class) { + // If there is no converter, just fallback to using + // toString(). modelValue can't be null as + // Class.cast(null) will always succeed + presentationValue = (T) modelValue.toString(); + } else { + throw 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."); + } + } + } 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; + try { + encodedValue = renderer.encode(presentationValue); + } catch (Exception e) { + getLogger().log(Level.SEVERE, "Unable to encode data", e); + encodedValue = renderer.encode(null); + } + + return encodedValue; + } + + private static Logger getLogger() { + return Logger.getLogger(AbstractRenderer.class.getName()); + } + } /** * An abstract base class for server-side Grid extensions. - * + * <p> + * Note: If the extension is an instance of {@link DataGenerator} it will + * automatically register itself to {@link RpcDataProviderExtension} of + * extended Grid. On remove this registration is automatically removed. + * * @since 7.5 */ public static abstract class AbstractGridExtension extends @@ -3384,7 +3814,7 @@ public class Grid extends AbstractComponent implements SelectionNotifier, /** * Constructs a new Grid extension and extends given Grid. - * + * * @param grid * a grid instance */ @@ -3393,6 +3823,26 @@ public class Grid extends AbstractComponent implements SelectionNotifier, extend(grid); } + @Override + protected void extend(AbstractClientConnector target) { + super.extend(target); + + if (this instanceof DataGenerator) { + getParentGrid().datasourceExtension + .addDataGenerator((DataGenerator) this); + } + } + + @Override + public void remove() { + if (this instanceof DataGenerator) { + getParentGrid().datasourceExtension + .removeDataGenerator((DataGenerator) this); + } + + super.remove(); + } + /** * Gets the item id for a row key. * <p> @@ -3405,7 +3855,7 @@ public class Grid extends AbstractComponent implements SelectionNotifier, * @return the item id corresponding to {@code key} */ protected Object getItemId(String rowKey) { - return getParentGrid().getKeyMapper().getItemId(rowKey); + return getParentGrid().getKeyMapper().get(rowKey); } /** @@ -3434,11 +3884,27 @@ public class Grid extends AbstractComponent implements SelectionNotifier, if (getParent() instanceof Grid) { Grid grid = (Grid) getParent(); return grid; + } else if (getParent() == null) { + throw new IllegalStateException( + "Renderer is not attached to any parent"); } else { throw new IllegalStateException( - "Renderers can be used only with Grid"); + "Renderers can be used only with Grid. Extended " + + getParent().getClass().getSimpleName() + + " instead"); } } + + /** + * Resends the row data for given item id to the client. + * + * @since + * @param itemId + * row to refresh + */ + protected void refreshRow(Object itemId) { + getParentGrid().datasourceExtension.updateRowData(itemId); + } } /** @@ -3514,6 +3980,13 @@ public class Grid extends AbstractComponent implements SelectionNotifier, } }; + private final ItemSetChangeListener editorClosingItemSetListener = new ItemSetChangeListener() { + @Override + public void containerItemSetChange(ItemSetChangeEvent event) { + cancelEditor(); + } + }; + private RpcDataProviderExtension datasourceExtension; /** @@ -3539,6 +4012,9 @@ public class Grid extends AbstractComponent implements SelectionNotifier, private CellStyleGenerator cellStyleGenerator; private RowStyleGenerator rowStyleGenerator; + private CellDescriptionGenerator cellDescriptionGenerator; + private RowDescriptionGenerator rowDescriptionGenerator; + /** * <code>true</code> if Grid is using the internal IndexedContainer created * in Grid() constructor, or <code>false</code> if the user has set their @@ -3628,117 +4104,10 @@ public class Grid extends AbstractComponent implements SelectionNotifier, */ private void initGrid() { setSelectionMode(getDefaultSelectionMode()); - addSelectionListener(new SelectionListener() { - @Override - public void select(SelectionEvent event) { - if (applyingSelectionFromClient) { - /* - * Avoid sending changes back to the client if they - * originated from the client. Instead, the RPC handler is - * responsible for keeping track of the resulting selection - * state and notifying the client if it doens't match the - * expectation. - */ - return; - } - - /* - * The rows are pinned here to ensure that the client gets the - * correct key from server when the selected row is first - * loaded. - * - * Once the client has gotten info that it is supposed to select - * a row, it will pin the data from the client side as well and - * it will be unpinned once it gets deselected. Nothing on the - * server side should ever unpin anything from KeyMapper. - * Pinning is mostly a client feature and is only used when - * selecting something from the server side. - */ - for (Object addedItemId : event.getAdded()) { - if (!getKeyMapper().isPinned(addedItemId)) { - getKeyMapper().pin(addedItemId); - } - } - - getState().selectedKeys = getKeyMapper().getKeys( - getSelectedRows()); - } - }); registerRpc(new GridServerRpc() { @Override - public void select(List<String> selection) { - Collection<Object> receivedSelection = getKeyMapper() - .getItemIds(selection); - - applyingSelectionFromClient = true; - try { - SelectionModel selectionModel = getSelectionModel(); - if (selectionModel instanceof SelectionModel.Single - && selection.size() <= 1) { - Object select = null; - if (selection.size() == 1) { - select = getKeyMapper().getItemId(selection.get(0)); - } - ((SelectionModel.Single) selectionModel).select(select); - } else if (selectionModel instanceof SelectionModel.Multi) { - ((SelectionModel.Multi) selectionModel) - .setSelected(receivedSelection); - } else { - throw new IllegalStateException("SelectionModel " - + selectionModel.getClass().getSimpleName() - + " does not support selecting the given " - + selection.size() + " items."); - } - } finally { - applyingSelectionFromClient = false; - } - - Collection<Object> actualSelection = getSelectedRows(); - - // Make sure all selected rows are pinned - for (Object itemId : actualSelection) { - if (!getKeyMapper().isPinned(itemId)) { - getKeyMapper().pin(itemId); - } - } - - // Don't mark as dirty since this might be the expected state - getState(false).selectedKeys = getKeyMapper().getKeys( - actualSelection); - - JsonObject diffState = getUI().getConnectorTracker() - .getDiffState(Grid.this); - - final String diffstateKey = "selectedKeys"; - - assert diffState.hasKey(diffstateKey) : "Field name has changed"; - - if (receivedSelection.equals(actualSelection)) { - /* - * We ended up with the same selection state that the client - * sent us. There's nothing to send back to the client, just - * update the diffstate so subsequent changes will be - * detected. - */ - JsonArray diffSelected = Json.createArray(); - for (String rowKey : getState(false).selectedKeys) { - diffSelected.set(diffSelected.length(), rowKey); - } - diffState.put(diffstateKey, diffSelected); - } else { - /* - * Actual selection is not what the client expects. Make - * sure the client gets a state change event by clearing the - * diffstate and marking as dirty - */ - diffState.remove(diffstateKey); - markAsDirty(); - } - } - - @Override public void sort(String[] columnIds, SortDirection[] directions, boolean userOriginated) { assert columnIds.length == directions.length; @@ -3767,16 +4136,9 @@ public class Grid extends AbstractComponent implements SelectionNotifier, } @Override - public void selectAll() { - assert getSelectionModel() instanceof SelectionModel.Multi : "Not a multi selection model!"; - - ((SelectionModel.Multi) getSelectionModel()).selectAll(); - } - - @Override public void itemClick(String rowKey, String columnId, MouseEventDetails details) { - Object itemId = getKeyMapper().getItemId(rowKey); + Object itemId = getKeyMapper().get(rowKey); Item item = datasource.getItem(itemId); Object propertyId = getPropertyIdByColumnId(columnId); fireEvent(new ItemClickEvent(Grid.this, item, itemId, @@ -3858,10 +4220,21 @@ public class Grid extends AbstractComponent implements SelectionNotifier, } @Override - public void sendDetailsComponents(int fetchId) { - getRpcProxy(GridClientRpc.class).setDetailsConnectorChanges( - detailComponentManager.getAndResetConnectorChanges(), - fetchId); + public void editorOpen(String rowKey) { + fireEvent(new EditorOpenEvent(Grid.this, getKeyMapper().get( + rowKey))); + } + + @Override + public void editorMove(String rowKey) { + fireEvent(new EditorMoveEvent(Grid.this, getKeyMapper().get( + rowKey))); + } + + @Override + public void editorClose(String rowKey) { + fireEvent(new EditorCloseEvent(Grid.this, getKeyMapper().get( + rowKey))); } }); @@ -3869,28 +4242,37 @@ public class Grid extends AbstractComponent implements SelectionNotifier, @Override public void bind(int rowIndex) { - Exception exception = null; try { Object id = getContainerDataSource().getIdByIndex(rowIndex); - if (editedItemId == null) { - editedItemId = id; - } - if (editedItemId.equals(id)) { - doEditItem(); + final boolean opening = editedItemId == null; + + final boolean moving = !opening && !editedItemId.equals(id); + + final boolean allowMove = !isEditorBuffered() + && getEditorFieldGroup().isValid(); + + if (opening || !moving || allowMove) { + doBind(id); + } else { + failBind(null); } } catch (Exception e) { - exception = e; + failBind(e); } + } - if (exception != null) { - handleError(exception); - doCancelEditor(); - getEditorRpc().confirmBind(false); - } else { - doEditItem(); - getEditorRpc().confirmBind(true); + private void doBind(Object id) { + editedItemId = id; + doEditItem(); + getEditorRpc().confirmBind(true); + } + + private void failBind(Exception e) { + if (e != null) { + handleError(e); } + getEditorRpc().confirmBind(false); } @Override @@ -3999,10 +4381,10 @@ public class Grid extends AbstractComponent implements SelectionNotifier, removeExtension(datasourceExtension); } - datasource = container; - resetEditor(); + datasource = container; + // // Adjust sort order // @@ -4031,7 +4413,8 @@ public class Grid extends AbstractComponent implements SelectionNotifier, } datasourceExtension = new RpcDataProviderExtension(container); - datasourceExtension.extend(this, columnKeys); + datasourceExtension.extend(this); + datasourceExtension.addDataGenerator(new RowDataGenerator()); detailComponentManager = datasourceExtension .getDetailComponentManager(); @@ -4049,6 +4432,7 @@ public class Grid extends AbstractComponent implements SelectionNotifier, ((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 @@ -4643,25 +5027,11 @@ public class Grid extends AbstractComponent implements SelectionNotifier, if (this.selectionModel != selectionModel) { // this.selectionModel is null on init if (this.selectionModel != null) { - this.selectionModel.reset(); - this.selectionModel.setGrid(null); + this.selectionModel.remove(); } 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"); - } + selectionModel.setGrid(this); } } @@ -4928,7 +5298,7 @@ public class Grid extends AbstractComponent implements SelectionNotifier, * * @return the key mapper being used by the data source */ - DataProviderKeyMapper getKeyMapper() { + KeyMapper<Object> getKeyMapper() { return datasourceExtension.getKeyMapper(); } @@ -5489,6 +5859,73 @@ public class Grid extends AbstractComponent implements SelectionNotifier, } /** + * Sets the {@code CellDescriptionGenerator} instance for generating + * optional descriptions (tooltips) for individual Grid cells. If a + * {@link RowDescriptionGenerator} is also set, the row description it + * generates is displayed for cells for which {@code generator} returns + * null. + * + * @param generator + * the description generator to use or {@code null} to remove a + * previously set generator if any + * + * @see #setRowDescriptionGenerator(RowDescriptionGenerator) + * + * @since 7.6 + */ + public void setCellDescriptionGenerator(CellDescriptionGenerator generator) { + cellDescriptionGenerator = generator; + getState().hasDescriptions = (generator != null || rowDescriptionGenerator != null); + datasourceExtension.refreshCache(); + } + + /** + * Returns the {@code CellDescriptionGenerator} instance used to generate + * descriptions (tooltips) for Grid cells. + * + * @return the description generator or {@code null} if no generator is set + * + * @since 7.6 + */ + public CellDescriptionGenerator getCellDescriptionGenerator() { + return cellDescriptionGenerator; + } + + /** + * Sets the {@code RowDescriptionGenerator} instance for generating optional + * descriptions (tooltips) for Grid rows. If a + * {@link CellDescriptionGenerator} is also set, the row description + * generated by {@code generator} is used for cells for which the cell + * description generator returns null. + * + * + * @param generator + * the description generator to use or {@code null} to remove a + * previously set generator if any + * + * @see #setCellDescriptionGenerator(CellDescriptionGenerator) + * + * @since 7.6 + */ + public void setRowDescriptionGenerator(RowDescriptionGenerator generator) { + rowDescriptionGenerator = generator; + getState().hasDescriptions = (generator != null || cellDescriptionGenerator != null); + datasourceExtension.refreshCache(); + } + + /** + * Returns the {@code RowDescriptionGenerator} instance used to generate + * descriptions (tooltips) for Grid rows + * + * @return the description generator or {@code} null if no generator is set + * + * @since 7.6 + */ + public RowDescriptionGenerator getRowDescriptionGenerator() { + return rowDescriptionGenerator; + } + + /** * Sets the style generator that is used for generating styles for cells * * @param cellStyleGenerator @@ -5497,8 +5934,6 @@ public class Grid extends AbstractComponent implements SelectionNotifier, */ public void setCellStyleGenerator(CellStyleGenerator cellStyleGenerator) { this.cellStyleGenerator = cellStyleGenerator; - getState().hasCellStyleGenerator = (cellStyleGenerator != null); - datasourceExtension.refreshCache(); } @@ -5521,8 +5956,6 @@ public class Grid extends AbstractComponent implements SelectionNotifier, */ public void setRowStyleGenerator(RowStyleGenerator rowStyleGenerator) { this.rowStyleGenerator = rowStyleGenerator; - getState().hasRowStyleGenerator = (rowStyleGenerator != null); - datasourceExtension.refreshCache(); } @@ -5732,10 +6165,14 @@ public class Grid extends AbstractComponent implements SelectionNotifier, * Opens the editor interface for the provided item. Scrolls the Grid to * bring the item to view if it is not already visible. * + * Note that any cell content rendered by a WidgetRenderer will not be + * visible in the editor row. + * * @param itemId * the id of the item to edit * @throws IllegalStateException - * if the editor is not enabled or already editing an item + * if the editor is not enabled or already editing an item in + * buffered mode * @throws IllegalArgumentException * if the {@code itemId} is not in the backing container * @see #setEditorEnabled(boolean) @@ -5744,8 +6181,8 @@ public class Grid extends AbstractComponent implements SelectionNotifier, IllegalArgumentException { if (!isEditorEnabled()) { throw new IllegalStateException("Item editor is not enabled"); - } else if (editedItemId != null) { - throw new IllegalStateException("Editing item + " + itemId + } else if (isEditorBuffered() && editedItemId != null) { + throw new IllegalStateException("Editing item " + itemId + " failed. Item editor is already editing item " + editedItemId); } else if (!getContainerDataSource().containsId(itemId)) { @@ -5773,6 +6210,10 @@ public class Grid extends AbstractComponent implements SelectionNotifier, f.markAsDirtyRecursive(); } + if (datasource instanceof ItemSetChangeNotifier) { + ((ItemSetChangeNotifier) datasource) + .addItemSetChangeListener(editorClosingItemSetListener); + } } private void setEditorField(Object propertyId, Field<?> field) { @@ -5822,6 +6263,11 @@ public class Grid extends AbstractComponent implements SelectionNotifier, editorFieldGroup.discard(); editorFieldGroup.setItemDataSource(null); + if (datasource instanceof ItemSetChangeNotifier) { + ((ItemSetChangeNotifier) datasource) + .removeItemSetChangeListener(editorClosingItemSetListener); + } + // Mark Grid as dirty so the client side gets to know that the editors // are no longer attached markAsDirty(); @@ -5971,6 +6417,70 @@ public class Grid extends AbstractComponent implements SelectionNotifier, return getState(false).editorCancelCaption; } + /** + * Add an editor event listener + * + * @param listener + * the event listener object to add + */ + public void addEditorListener(EditorListener listener) { + addListener(GridConstants.EDITOR_OPEN_EVENT_ID, EditorOpenEvent.class, + listener, EditorListener.EDITOR_OPEN_METHOD); + addListener(GridConstants.EDITOR_MOVE_EVENT_ID, EditorMoveEvent.class, + listener, EditorListener.EDITOR_MOVE_METHOD); + addListener(GridConstants.EDITOR_CLOSE_EVENT_ID, + EditorCloseEvent.class, listener, + EditorListener.EDITOR_CLOSE_METHOD); + } + + /** + * Remove an editor event listener + * + * @param listener + * the event listener object to remove + */ + public void removeEditorListener(EditorListener listener) { + removeListener(GridConstants.EDITOR_OPEN_EVENT_ID, + EditorOpenEvent.class, listener); + removeListener(GridConstants.EDITOR_MOVE_EVENT_ID, + EditorMoveEvent.class, listener); + removeListener(GridConstants.EDITOR_CLOSE_EVENT_ID, + EditorCloseEvent.class, listener); + } + + /** + * Sets the buffered editor mode. The default mode is buffered ( + * <code>true</code>). + * + * @since 7.6 + * @param editorBuffered + * <code>true</code> to enable buffered editor, + * <code>false</code> to disable it + * @throws IllegalStateException + * If editor is active while attempting to change the buffered + * mode. + */ + public void setEditorBuffered(boolean editorBuffered) + throws IllegalStateException { + if (isEditorActive()) { + throw new IllegalStateException( + "Can't change editor unbuffered mode while editor is active."); + } + getState().editorBuffered = editorBuffered; + editorFieldGroup.setBuffered(editorBuffered); + } + + /** + * Gets the buffered editor mode. + * + * @since 7.6 + * @return <code>true</code> if buffered editor is enabled, + * <code>false</code> otherwise + */ + public boolean isEditorBuffered() { + return getState(false).editorBuffered; + } + @Override public void addItemClickListener(ItemClickListener listener) { addListener(GridConstants.ITEM_CLICK_EVENT_ID, ItemClickEvent.class, @@ -6063,8 +6573,6 @@ public class Grid extends AbstractComponent implements SelectionNotifier, this.detailsGenerator = detailsGenerator; datasourceExtension.refreshDetails(); - getRpcProxy(GridClientRpc.class).setDetailsConnectorChanges( - detailComponentManager.getAndResetConnectorChanges(), -1); } /** @@ -6088,6 +6596,9 @@ public class Grid extends AbstractComponent implements SelectionNotifier, * to hide them */ public void setDetailsVisible(Object itemId, boolean visible) { + if (DetailsGenerator.NULL.equals(detailsGenerator)) { + return; + } datasourceExtension.setDetailsVisible(itemId, visible); } @@ -6265,6 +6776,7 @@ public class Grid extends AbstractComponent implements SelectionNotifier, result.add("footer-visible"); result.add("editor-error-handler"); result.add("height-mode"); + return result; } } diff --git a/server/src/com/vaadin/ui/HorizontalLayout.java b/server/src/com/vaadin/ui/HorizontalLayout.java index 54569570b9..616fa09225 100644 --- a/server/src/com/vaadin/ui/HorizontalLayout.java +++ b/server/src/com/vaadin/ui/HorizontalLayout.java @@ -15,6 +15,8 @@ */ package com.vaadin.ui; +import com.vaadin.shared.ui.orderedlayout.HorizontalLayoutState; + /** * Horizontal layout * @@ -48,4 +50,9 @@ public class HorizontalLayout extends AbstractOrderedLayout { addComponents(children); } + @Override + protected HorizontalLayoutState getState() { + return (HorizontalLayoutState) super.getState(); + } + } diff --git a/server/src/com/vaadin/ui/Table.java b/server/src/com/vaadin/ui/Table.java index 42c4beab6c..10d1c45ab6 100644 --- a/server/src/com/vaadin/ui/Table.java +++ b/server/src/com/vaadin/ui/Table.java @@ -69,6 +69,7 @@ import com.vaadin.shared.util.SharedUtil; import com.vaadin.ui.declarative.DesignAttributeHandler; import com.vaadin.ui.declarative.DesignContext; import com.vaadin.ui.declarative.DesignException; +import com.vaadin.util.ReflectTools; /** * <p> @@ -1310,6 +1311,8 @@ public class Table extends AbstractSelect implements Action.Container, * the desired collapsedness. * @throws IllegalStateException * if column collapsing is not allowed + * @throws IllegalArgumentException + * if the property id does not exist */ public void setColumnCollapsed(Object propertyId, boolean collapsed) throws IllegalStateException { @@ -1319,11 +1322,20 @@ public class Table extends AbstractSelect implements Action.Container, if (collapsed && noncollapsibleColumns.contains(propertyId)) { throw new IllegalStateException("The column is noncollapsible!"); } + if (!getContainerPropertyIds().contains(propertyId) + && !columnGenerators.containsKey(propertyId)) { + throw new IllegalArgumentException("Property '" + propertyId + + "' was not found in the container"); + } if (collapsed) { - collapsedColumns.add(propertyId); + if (collapsedColumns.add(propertyId)) { + fireColumnCollapseEvent(propertyId); + } } else { - collapsedColumns.remove(propertyId); + if (collapsedColumns.remove(propertyId)) { + fireColumnCollapseEvent(propertyId); + } } // Assures the visual refresh @@ -3182,6 +3194,10 @@ public class Table extends AbstractSelect implements Action.Container, } } + private void fireColumnCollapseEvent(Object propertyId) { + fireEvent(new ColumnCollapseEvent(this, propertyId)); + } + private void fireColumnResizeEvent(Object propertyId, int previousWidth, int currentWidth) { /* @@ -5742,6 +5758,53 @@ public class Table extends AbstractSelect implements Action.Container, } /** + * This event is fired when the collapse state of a column changes + */ + public static class ColumnCollapseEvent extends Component.Event { + + public static final Method METHOD = ReflectTools.findMethod( + ColumnCollapseListener.class, "columnCollapseStateChange", + ColumnCollapseEvent.class); + private Object propertyId; + + /** + * Constructor + * + * @param source + * The source of the event + * @param propertyId + * The id of the column + */ + public ColumnCollapseEvent(Component source, Object propertyId) { + super(source); + this.propertyId = propertyId; + } + + /** + * Gets the id of the column whose collapse state changed + * + * @return the property id of the column + */ + public Object getPropertyId() { + return propertyId; + } + } + + /** + * Interface for listening to column collapse events. + */ + public interface ColumnCollapseListener extends Serializable { + + /** + * This method is triggered when the collapse state for a column has + * changed + * + * @param event + */ + public void columnCollapseStateChange(ColumnCollapseEvent event); + } + + /** * Adds a column reorder listener to the Table. A column reorder listener is * called when a user reorders columns. * @@ -5783,6 +5846,29 @@ public class Table extends AbstractSelect implements Action.Container, } /** + * Adds a column collapse listener to the Table. A column collapse listener + * is called when the collapsed state of a column changes. + * + * @param listener + * The listener to attach + */ + public void addColumnCollapseListener(ColumnCollapseListener listener) { + addListener(TableConstants.COLUMN_COLLAPSE_EVENT_ID, + ColumnCollapseEvent.class, listener, ColumnCollapseEvent.METHOD); + } + + /** + * Removes a column reorder listener from the Table. + * + * @param listener + * The listener to remove + */ + public void removeColumnCollapseListener(ColumnCollapseListener listener) { + removeListener(TableConstants.COLUMN_COLLAPSE_EVENT_ID, + ColumnCollapseEvent.class, listener); + } + + /** * Set the item description generator which generates tooltips for cells and * rows in the Table * diff --git a/server/src/com/vaadin/ui/VerticalLayout.java b/server/src/com/vaadin/ui/VerticalLayout.java index 12819e50bc..7002fbc7e6 100644 --- a/server/src/com/vaadin/ui/VerticalLayout.java +++ b/server/src/com/vaadin/ui/VerticalLayout.java @@ -15,6 +15,8 @@ */ package com.vaadin.ui; +import com.vaadin.shared.ui.orderedlayout.VerticalLayoutState; + /** * Vertical layout * @@ -48,4 +50,9 @@ public class VerticalLayout extends AbstractOrderedLayout { this(); addComponents(children); } + + @Override + protected VerticalLayoutState getState() { + return (VerticalLayoutState) super.getState(); + } } diff --git a/server/src/com/vaadin/ui/Window.java b/server/src/com/vaadin/ui/Window.java index 61ba5826b8..fd5565f900 100644 --- a/server/src/com/vaadin/ui/Window.java +++ b/server/src/com/vaadin/ui/Window.java @@ -21,6 +21,7 @@ import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.List; import java.util.Map; @@ -74,7 +75,7 @@ import com.vaadin.util.ReflectTools; * @author Vaadin Ltd. * @since 3.0 */ -@SuppressWarnings("serial") +@SuppressWarnings({ "serial", "deprecation" }) public class Window extends Panel implements FocusNotifier, BlurNotifier, LegacyComponent { @@ -102,6 +103,11 @@ public class Window extends Panel implements FocusNotifier, BlurNotifier, }; /** + * Holds registered CloseShortcut instances for query and later removal + */ + private List<CloseShortcut> closeShortcuts = new ArrayList<CloseShortcut>(4); + + /** * Creates a new, empty window */ public Window() { @@ -130,6 +136,7 @@ public class Window extends Panel implements FocusNotifier, BlurNotifier, super(caption, content); registerRpc(rpc); setSizeUndefined(); + setCloseShortcut(KeyCode.ESCAPE); } /* ********************************************************************* */ @@ -828,14 +835,22 @@ public class Window extends Panel implements FocusNotifier, BlurNotifier, } } - /* - * Actions - */ - protected CloseShortcut closeShortcut; - /** - * Makes is possible to close the window by pressing the given - * {@link KeyCode} and (optional) {@link ModifierKey}s.<br/> + * This is the old way of adding a keyboard shortcut to close a + * {@link Window} - to preserve compatibility with existing code under the + * new functionality, this method now first removes all registered close + * shortcuts, then adds the default ESCAPE shortcut key, and then attempts + * to add the shortcut provided as parameters to this method. This method, + * and its companion {@link #removeCloseShortcut()}, are now considered + * deprecated, as their main function is to preserve exact backwards + * compatibility with old code. For all new code, use the new keyboard + * shortcuts API: {@link #addCloseShortcut(int,int...)}, + * {@link #removeCloseShortcut(int,int...)}, + * {@link #removeAllCloseShortcuts()}, {@link #hasCloseShortcut(int,int...)} + * and {@link #getCloseShortcuts()}. + * <p> + * Original description: Makes it possible to close the window by pressing + * the given {@link KeyCode} and (optional) {@link ModifierKey}s.<br/> * Note that this shortcut only reacts while the window has focus, closing * itself - if you want to close a window from a UI, use * {@link UI#addAction(com.vaadin.event.Action)} of the UI instead. @@ -843,29 +858,137 @@ public class Window extends Panel implements FocusNotifier, BlurNotifier, * @param keyCode * the keycode for invoking the shortcut * @param modifiers - * the (optional) modifiers for invoking the shortcut, null for - * none + * the (optional) modifiers for invoking the shortcut. Can be set + * to null to be explicit about not having modifiers. + * + * @deprecated Use {@link #addCloseShortcut(int, int...)} instead. */ + @Deprecated public void setCloseShortcut(int keyCode, int... modifiers) { - if (closeShortcut != null) { - removeAction(closeShortcut); - } - closeShortcut = new CloseShortcut(this, keyCode, modifiers); - addAction(closeShortcut); + removeCloseShortcut(); + addCloseShortcut(keyCode, modifiers); } /** - * Removes the keyboard shortcut previously set with - * {@link #setCloseShortcut(int, int...)}. + * Removes all keyboard shortcuts previously set with + * {@link #setCloseShortcut(int, int...)} and + * {@link #addCloseShortcut(int, int...)}, then adds the default + * {@link KeyCode#ESCAPE} shortcut. + * <p> + * This is the old way of removing the (single) keyboard close shortcut, and + * is retained only for exact backwards compatibility. For all new code, use + * the new keyboard shortcuts API: {@link #addCloseShortcut(int,int...)}, + * {@link #removeCloseShortcut(int,int...)}, + * {@link #removeAllCloseShortcuts()}, {@link #hasCloseShortcut(int,int...)} + * and {@link #getCloseShortcuts()}. + * + * @deprecated Use {@link #removeCloseShortcut(int, int...)} instead. */ + @Deprecated public void removeCloseShortcut() { - if (closeShortcut != null) { - removeAction(closeShortcut); - closeShortcut = null; + for (int i = 0; i < closeShortcuts.size(); ++i) { + CloseShortcut sc = closeShortcuts.get(i); + removeAction(sc); + } + closeShortcuts.clear(); + addCloseShortcut(KeyCode.ESCAPE); + } + + /** + * Adds a close shortcut - pressing this key while holding down all (if any) + * modifiers specified while this Window is in focus will close the Window. + * + * @since + * @param keyCode + * the keycode for invoking the shortcut + * @param modifiers + * the (optional) modifiers for invoking the shortcut. Can be set + * to null to be explicit about not having modifiers. + */ + public void addCloseShortcut(int keyCode, int... modifiers) { + + // Ignore attempts to re-add existing shortcuts + if (hasCloseShortcut(keyCode, modifiers)) { + return; + } + + // Actually add the shortcut + CloseShortcut shortcut = new CloseShortcut(this, keyCode, modifiers); + addAction(shortcut); + closeShortcuts.add(shortcut); + } + + /** + * Removes a close shortcut previously added with + * {@link #addCloseShortcut(int, int...)}. + * + * @since + * @param keyCode + * the keycode for invoking the shortcut + * @param modifiers + * the (optional) modifiers for invoking the shortcut. Can be set + * to null to be explicit about not having modifiers. + */ + public void removeCloseShortcut(int keyCode, int... modifiers) { + for (CloseShortcut shortcut : closeShortcuts) { + if (shortcut.equals(keyCode, modifiers)) { + removeAction(shortcut); + closeShortcuts.remove(shortcut); + return; + } } } /** + * Removes all close shortcuts. This includes the default ESCAPE shortcut. + * It is up to the user to add back any and all keyboard close shortcuts + * they may require. For more fine-grained control over shortcuts, use + * {@link #removeCloseShortcut(int, int...)}. + * + * @since + */ + public void removeAllCloseShortcuts() { + for (CloseShortcut shortcut : closeShortcuts) { + removeAction(shortcut); + } + closeShortcuts.clear(); + } + + /** + * Checks if a close window shortcut key has already been registered. + * + * @since + * @param keyCode + * the keycode for invoking the shortcut + * @param modifiers + * the (optional) modifiers for invoking the shortcut. Can be set + * to null to be explicit about not having modifiers. + * @return true, if an exactly matching shortcut has been registered. + */ + public boolean hasCloseShortcut(int keyCode, int... modifiers) { + for (CloseShortcut shortcut : closeShortcuts) { + if (shortcut.equals(keyCode, modifiers)) { + return true; + } + } + return false; + } + + /** + * Returns an unmodifiable collection of {@link CloseShortcut} objects + * currently registered with this {@link Window}. This method is provided + * mainly so that users can implement their own serialization routines. To + * check if a certain combination of keys has been registered as a close + * shortcut, use the {@link #hasCloseShortcut(int, int...)} method instead. + * + * @since + * @return an unmodifiable Collection of CloseShortcut objects. + */ + public Collection<CloseShortcut> getCloseShortcuts() { + return Collections.unmodifiableCollection(closeShortcuts); + } + + /** * A {@link ShortcutListener} specifically made to define a keyboard * shortcut that closes the window. * @@ -930,6 +1053,25 @@ public class Window extends Panel implements FocusNotifier, BlurNotifier, public void handleAction(Object sender, Object target) { window.close(); } + + public boolean equals(int keyCode, int... modifiers) { + if (keyCode != getKeyCode()) { + return false; + } + + if (getModifiers() != null) { + int[] mods = null; + if (modifiers != null) { + // Modifiers provided by the parent ShortcutAction class + // are guaranteed to be sorted. We still need to sort + // the modifiers passed in as argument. + mods = Arrays.copyOf(modifiers, modifiers.length); + Arrays.sort(mods); + } + return Arrays.equals(mods, getModifiers()); + } + return true; + } } /* @@ -1244,11 +1386,26 @@ public class Window extends Panel implements FocusNotifier, BlurNotifier, setPositionX(Integer.parseInt(position[0])); setPositionY(Integer.parseInt(position[1])); } + + // Parse shortcuts if defined, otherwise rely on default behavior if (design.hasAttr("close-shortcut")) { - ShortcutAction shortcut = DesignAttributeHandler - .readAttribute("close-shortcut", design.attributes(), - ShortcutAction.class); - setCloseShortcut(shortcut.getKeyCode(), shortcut.getModifiers()); + + // Parse shortcuts + String[] shortcutStrings = DesignAttributeHandler.readAttribute( + "close-shortcut", design.attributes(), String.class).split( + "\\s+"); + + removeAllCloseShortcuts(); + + for (String part : shortcutStrings) { + if (!part.isEmpty()) { + ShortcutAction shortcut = DesignAttributeHandler + .getFormatter().parse(part.trim(), + ShortcutAction.class); + addCloseShortcut(shortcut.getKeyCode(), + shortcut.getModifiers()); + } + } } } @@ -1302,19 +1459,24 @@ public class Window extends Panel implements FocusNotifier, BlurNotifier, DesignAttributeHandler.writeAttribute("position", design.attributes(), getPosition(), def.getPosition(), String.class); - CloseShortcut shortcut = getCloseShortcut(); - if (shortcut != null) { - // TODO What if several close shortcuts?? - - CloseShortcut defShortcut = def.getCloseShortcut(); - if (defShortcut == null - || shortcut.getKeyCode() != defShortcut.getKeyCode() - || !Arrays.equals(shortcut.getModifiers(), - defShortcut.getModifiers())) { - DesignAttributeHandler.writeAttribute("close-shortcut", - design.attributes(), shortcut, null, - CloseShortcut.class); + // Process keyboard shortcuts + if (closeShortcuts.size() == 1 && hasCloseShortcut(KeyCode.ESCAPE)) { + // By default, we won't write anything if we're relying on default + // shortcut behavior + } else { + // Dump all close shortcuts to a string... + String attrString = ""; + + // TODO: add canonical support for array data in XML attributes + for (CloseShortcut shortcut : closeShortcuts) { + String shortcutString = DesignAttributeHandler.getFormatter() + .format(shortcut, CloseShortcut.class); + attrString += shortcutString + " "; } + + // Write everything except the last "," to the design + DesignAttributeHandler.writeAttribute("close-shortcut", + design.attributes(), attrString.trim(), null, String.class); } for (Component c : getAssistiveDescription()) { @@ -1328,10 +1490,6 @@ public class Window extends Panel implements FocusNotifier, BlurNotifier, return getPositionX() + "," + getPositionY(); } - private CloseShortcut getCloseShortcut() { - return closeShortcut; - } - @Override protected Collection<String> getCustomAttributes() { Collection<String> result = super.getCustomAttributes(); diff --git a/server/src/com/vaadin/ui/renderers/ImageRenderer.java b/server/src/com/vaadin/ui/renderers/ImageRenderer.java index 2fb872583e..ad7d5cae2b 100644 --- a/server/src/com/vaadin/ui/renderers/ImageRenderer.java +++ b/server/src/com/vaadin/ui/renderers/ImageRenderer.java @@ -58,7 +58,7 @@ public class ImageRenderer extends ClickableRenderer<Resource> { if (!(resource == null || resource instanceof ExternalResource || resource instanceof ThemeResource)) { throw new IllegalArgumentException( "ImageRenderer only supports ExternalResource and ThemeResource (" - + resource.getClass().getSimpleName() + "given )"); + + resource.getClass().getSimpleName() + " given)"); } return encode(ResourceReference.create(resource, this, null), diff --git a/server/src/com/vaadin/ui/themes/ValoTheme.java b/server/src/com/vaadin/ui/themes/ValoTheme.java index 1285bf7f67..3a9986c632 100644 --- a/server/src/com/vaadin/ui/themes/ValoTheme.java +++ b/server/src/com/vaadin/ui/themes/ValoTheme.java @@ -709,7 +709,7 @@ public class ValoTheme { * area is scrolled. Suitable with the {@link #PANEL_BORDERLESS} style. Can * be combined with any other Panel style. */ - public static final String PANEL_SCROLL_INDICATOR = "scroll-indicator"; + public static final String PANEL_SCROLL_INDICATOR = "scroll-divider"; /** * Inset panel style. Can be combined with any other Panel style. diff --git a/server/tests/src/com/vaadin/data/util/AbstractContainerTestBase.java b/server/tests/src/com/vaadin/data/util/AbstractContainerTestBase.java index 54cbc5305d..55dccc6455 100644 --- a/server/tests/src/com/vaadin/data/util/AbstractContainerTestBase.java +++ b/server/tests/src/com/vaadin/data/util/AbstractContainerTestBase.java @@ -11,6 +11,7 @@ import com.vaadin.data.Container; import com.vaadin.data.Container.Filterable; import com.vaadin.data.Container.ItemSetChangeEvent; import com.vaadin.data.Container.ItemSetChangeListener; +import com.vaadin.data.Container.Ordered; import com.vaadin.data.Container.Sortable; import com.vaadin.data.Item; import com.vaadin.data.util.filter.SimpleStringFilter; @@ -161,54 +162,54 @@ public abstract class AbstractContainerTestBase extends TestCase { validateContainer(container, sampleData[0], sampleData[sampleData.length - 1], sampleData[10], "abc", true, sampleData.length); + + validateRemovingItems(container); + } + + protected void validateRemovingItems(Container container) { + int sizeBeforeRemoving = container.size(); + + List<Object> itemIdList = new ArrayList<Object>(container.getItemIds()); + // There should be at least four items in the list + Object first = itemIdList.get(0); + Object middle = itemIdList.get(2); + Object last = itemIdList.get(itemIdList.size() - 1); + + container.removeItem(first); + container.removeItem(middle); // Middle now that first has been removed + container.removeItem(last); + + assertEquals(sizeBeforeRemoving - 3, container.size()); + + container.removeAllItems(); + + assertEquals(0, container.size()); } protected void testContainerOrdered(Container.Ordered container) { + // addItem with empty container Object id = container.addItem(); - assertNotNull(id); + assertOrderedContents(container, id); Item item = container.getItem(id); assertNotNull(item); - assertEquals(id, container.firstItemId()); - assertEquals(id, container.lastItemId()); - - // isFirstId - assertTrue(container.isFirstId(id)); - assertTrue(container.isFirstId(container.firstItemId())); - // isLastId - assertTrue(container.isLastId(id)); - assertTrue(container.isLastId(container.lastItemId())); + // addItemAfter with empty container + container.removeAllItems(); + assertOrderedContents(container); + id = container.addItemAfter(null); + assertOrderedContents(container, id); + item = container.getItem(id); + assertNotNull(item); // Add a new item before the first // addItemAfter Object newFirstId = container.addItemAfter(null); - assertNotNull(newFirstId); - assertNotNull(container.getItem(newFirstId)); - - // isFirstId - assertTrue(container.isFirstId(newFirstId)); - assertTrue(container.isFirstId(container.firstItemId())); - // isLastId - assertTrue(container.isLastId(id)); - assertTrue(container.isLastId(container.lastItemId())); - - // nextItemId - assertEquals(id, container.nextItemId(newFirstId)); - assertNull(container.nextItemId(id)); - assertNull(container.nextItemId("not-in-container")); - - // prevItemId - assertEquals(newFirstId, container.prevItemId(id)); - assertNull(container.prevItemId(newFirstId)); - assertNull(container.prevItemId("not-in-container")); + assertOrderedContents(container, newFirstId, id); // addItemAfter(Object) Object newSecondItemId = container.addItemAfter(newFirstId); // order is now: newFirstId, newSecondItemId, id - assertNotNull(newSecondItemId); - assertNotNull(container.getItem(newSecondItemId)); - assertEquals(id, container.nextItemId(newSecondItemId)); - assertEquals(newFirstId, container.prevItemId(newSecondItemId)); + assertOrderedContents(container, newFirstId, newSecondItemId, id); // addItemAfter(Object,Object) String fourthId = "id of the fourth item"; @@ -216,8 +217,8 @@ public abstract class AbstractContainerTestBase extends TestCase { // order is now: newFirstId, fourthId, newSecondItemId, id assertNotNull(fourth); assertEquals(fourth, container.getItem(fourthId)); - assertEquals(newSecondItemId, container.nextItemId(fourthId)); - assertEquals(newFirstId, container.prevItemId(fourthId)); + assertOrderedContents(container, newFirstId, fourthId, newSecondItemId, + id); // addItemAfter(Object,Object) Object fifthId = new Object(); @@ -225,8 +226,86 @@ public abstract class AbstractContainerTestBase extends TestCase { // order is now: fifthId, newFirstId, fourthId, newSecondItemId, id assertNotNull(fifth); assertEquals(fifth, container.getItem(fifthId)); - assertEquals(newFirstId, container.nextItemId(fifthId)); - assertNull(container.prevItemId(fifthId)); + assertOrderedContents(container, fifthId, newFirstId, fourthId, + newSecondItemId, id); + + // addItemAfter(Object,Object) + Object sixthId = new Object(); + Item sixth = container.addItemAfter(id, sixthId); + // order is now: fifthId, newFirstId, fourthId, newSecondItemId, id, + // sixthId + assertNotNull(sixth); + assertEquals(sixth, container.getItem(sixthId)); + assertOrderedContents(container, fifthId, newFirstId, fourthId, + newSecondItemId, id, sixthId); + + // Test order after removing first item 'fifthId' + container.removeItem(fifthId); + // order is now: newFirstId, fourthId, newSecondItemId, id, sixthId + assertOrderedContents(container, newFirstId, fourthId, newSecondItemId, + id, sixthId); + + // Test order after removing last item 'sixthId' + container.removeItem(sixthId); + // order is now: newFirstId, fourthId, newSecondItemId, id + assertOrderedContents(container, newFirstId, fourthId, newSecondItemId, + id); + + // Test order after removing item from the middle 'fourthId' + container.removeItem(fourthId); + // order is now: newFirstId, newSecondItemId, id + assertOrderedContents(container, newFirstId, newSecondItemId, id); + + // Delete remaining items + container.removeItem(newFirstId); + container.removeItem(newSecondItemId); + container.removeItem(id); + assertOrderedContents(container); + + Object finalItem = container.addItem(); + assertOrderedContents(container, finalItem); + } + + private void assertOrderedContents(Ordered container, Object... ids) { + assertEquals(ids.length, container.size()); + for (int i = 0; i < ids.length - 1; i++) { + assertNotNull("The item id should not be null", ids[i]); + } + if (ids.length == 0) { + assertNull("The first id is wrong", container.firstItemId()); + assertNull("The last id is wrong", container.lastItemId()); + return; + } + + assertEquals("The first id is wrong", ids[0], container.firstItemId()); + assertEquals("The last id is wrong", ids[ids.length - 1], + container.lastItemId()); + + // isFirstId & isLastId + assertTrue(container.isFirstId(container.firstItemId())); + assertTrue(container.isLastId(container.lastItemId())); + + // nextId + Object ref = container.firstItemId(); + for (int i = 1; i < ids.length; i++) { + Object next = container.nextItemId(ref); + assertEquals("The id after " + ref + " is wrong", ids[i], next); + ref = next; + } + assertNull("The last id should not have a next id", + container.nextItemId(ids[ids.length - 1])); + assertNull(container.nextItemId("not-in-container")); + + // prevId + ref = container.lastItemId(); + for (int i = ids.length - 2; i >= 0; i--) { + Object prev = container.prevItemId(ref); + assertEquals("The id before " + ref + " is wrong", ids[i], prev); + ref = prev; + } + assertNull("The first id should not have a prev id", + container.prevItemId(ids[0])); + assertNull(container.prevItemId("not-in-container")); } diff --git a/server/tests/src/com/vaadin/data/util/AbstractHierarchicalContainerTestBase.java b/server/tests/src/com/vaadin/data/util/AbstractHierarchicalContainerTestBase.java index 3bd00cce3c..9cede77162 100644 --- a/server/tests/src/com/vaadin/data/util/AbstractHierarchicalContainerTestBase.java +++ b/server/tests/src/com/vaadin/data/util/AbstractHierarchicalContainerTestBase.java @@ -253,4 +253,33 @@ public abstract class AbstractHierarchicalContainerTestBase extends } } + protected void testRemoveHierarchicalWrapperSubtree( + Container.Hierarchical container) { + initializeContainer(container); + + // remove root item + removeItemRecursively(container, "org"); + + int packages = 21 + 3 - 3; + int expectedSize = sampleData.length + packages - 1; + + validateContainer(container, "com", "com.vaadin.util.SerializerHelper", + "com.vaadin.server.ApplicationResource", "blah", true, + expectedSize); + + // rootItemIds + Collection<?> rootIds = container.rootItemIds(); + assertEquals(1, rootIds.size()); + } + + private void removeItemRecursively(Container.Hierarchical container, + Object itemId) { + if (container instanceof ContainerHierarchicalWrapper) { + ((ContainerHierarchicalWrapper) container) + .removeItemRecursively("org"); + } else { + HierarchicalContainer.removeItemRecursively(container, itemId); + } + } + } diff --git a/server/tests/src/com/vaadin/data/util/ContainerHierarchicalWrapperTest.java b/server/tests/src/com/vaadin/data/util/ContainerHierarchicalWrapperTest.java index 2fd21ef118..8e554d5030 100644 --- a/server/tests/src/com/vaadin/data/util/ContainerHierarchicalWrapperTest.java +++ b/server/tests/src/com/vaadin/data/util/ContainerHierarchicalWrapperTest.java @@ -1,6 +1,5 @@ package com.vaadin.data.util; -import java.util.Collection; public class ContainerHierarchicalWrapperTest extends AbstractHierarchicalContainerTestBase { @@ -20,23 +19,4 @@ public class ContainerHierarchicalWrapperTest extends new IndexedContainer())); } - protected void testRemoveHierarchicalWrapperSubtree( - ContainerHierarchicalWrapper container) { - initializeContainer(container); - - // remove root item - container.removeItemRecursively("org"); - - int packages = 21 + 3 - 3; - int expectedSize = sampleData.length + packages - 1; - - validateContainer(container, "com", "com.vaadin.util.SerializerHelper", - "com.vaadin.server.ApplicationResource", "blah", true, - expectedSize); - - // rootItemIds - Collection<?> rootIds = container.rootItemIds(); - assertEquals(1, rootIds.size()); - } - } diff --git a/server/tests/src/com/vaadin/data/util/ContainerOrderedWrapperTest.java b/server/tests/src/com/vaadin/data/util/ContainerOrderedWrapperTest.java new file mode 100644 index 0000000000..422e9756f8 --- /dev/null +++ b/server/tests/src/com/vaadin/data/util/ContainerOrderedWrapperTest.java @@ -0,0 +1,102 @@ +package com.vaadin.data.util; + +import java.util.Collection; + +import com.vaadin.data.Container; +import com.vaadin.data.Item; +import com.vaadin.data.Property; + +public class ContainerOrderedWrapperTest extends AbstractContainerTestBase { + + // This class is needed to get an implementation of container + // which is not an implementation of Ordered interface. + private class NotOrderedContainer implements Container { + + private IndexedContainer container; + + public NotOrderedContainer() { + container = new IndexedContainer(); + } + + @Override + public Item getItem(Object itemId) { + return container.getItem(itemId); + } + + @Override + public Collection<?> getContainerPropertyIds() { + return container.getContainerPropertyIds(); + } + + @Override + public Collection<?> getItemIds() { + return container.getItemIds(); + } + + @Override + public Property getContainerProperty(Object itemId, Object propertyId) { + return container.getContainerProperty(itemId, propertyId); + } + + @Override + public Class<?> getType(Object propertyId) { + return container.getType(propertyId); + } + + @Override + public int size() { + return container.size(); + } + + @Override + public boolean containsId(Object itemId) { + return container.containsId(itemId); + } + + @Override + public Item addItem(Object itemId) throws UnsupportedOperationException { + return container.addItem(itemId); + } + + @Override + public Object addItem() throws UnsupportedOperationException { + return container.addItem(); + } + + @Override + public boolean removeItem(Object itemId) + throws UnsupportedOperationException { + return container.removeItem(itemId); + } + + @Override + public boolean addContainerProperty(Object propertyId, Class<?> type, + Object defaultValue) throws UnsupportedOperationException { + return container.addContainerProperty(propertyId, type, + defaultValue); + } + + @Override + public boolean removeContainerProperty(Object propertyId) + throws UnsupportedOperationException { + return container.removeContainerProperty(propertyId); + } + + @Override + public boolean removeAllItems() throws UnsupportedOperationException { + return container.removeAllItems(); + } + + } + + public void testBasicOperations() { + testBasicContainerOperations(new ContainerOrderedWrapper( + new NotOrderedContainer())); + } + + public void testOrdered() { + testContainerOrdered(new ContainerOrderedWrapper( + new NotOrderedContainer())); + } + +} diff --git a/server/tests/src/com/vaadin/data/util/HierarchicalContainerOrderedWrapperTest.java b/server/tests/src/com/vaadin/data/util/HierarchicalContainerOrderedWrapperTest.java new file mode 100644 index 0000000000..7ecf59c309 --- /dev/null +++ b/server/tests/src/com/vaadin/data/util/HierarchicalContainerOrderedWrapperTest.java @@ -0,0 +1,27 @@ +package com.vaadin.data.util; + +public class HierarchicalContainerOrderedWrapperTest extends + AbstractHierarchicalContainerTestBase { + + private HierarchicalContainerOrderedWrapper createContainer() { + return new HierarchicalContainerOrderedWrapper( + new ContainerHierarchicalWrapper(new IndexedContainer())); + } + + public void testBasicOperations() { + testBasicContainerOperations(createContainer()); + } + + public void testHierarchicalContainer() { + testHierarchicalContainer(createContainer()); + } + + public void testContainerOrdered() { + testContainerOrdered(createContainer()); + } + + public void testRemoveSubtree() { + testRemoveHierarchicalWrapperSubtree(createContainer()); + } + +} diff --git a/server/tests/src/com/vaadin/server/communication/FileUploadHandlerTest.java b/server/tests/src/com/vaadin/server/communication/FileUploadHandlerTest.java index 2cb4c3bf4d..286163541e 100644 --- a/server/tests/src/com/vaadin/server/communication/FileUploadHandlerTest.java +++ b/server/tests/src/com/vaadin/server/communication/FileUploadHandlerTest.java @@ -15,50 +15,136 @@ */ package com.vaadin.server.communication; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; + import java.io.IOException; import java.io.InputStream; +import java.io.OutputStream; import org.junit.Before; import org.junit.Test; -import org.mockito.Mockito; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import com.vaadin.server.ClientConnector; +import com.vaadin.server.ServletPortletHelper; +import com.vaadin.server.StreamVariable; import com.vaadin.server.VaadinRequest; +import com.vaadin.server.VaadinResponse; +import com.vaadin.server.VaadinSession; +import com.vaadin.ui.ConnectorTracker; +import com.vaadin.ui.UI; -/** - * Tests whether we get infinite loop if InputStream is already read (#10096) - */ public class FileUploadHandlerTest { private FileUploadHandler handler; - private VaadinRequest request; + @Mock private VaadinResponse response; + @Mock private StreamVariable streamVariable; + @Mock private ClientConnector clientConnector; + @Mock private VaadinRequest request; + @Mock private UI ui; + @Mock private ConnectorTracker connectorTracker; + @Mock private VaadinSession session; + @Mock private OutputStream responseOutput; + + private int uiId = 123; + private final String connectorId = "connectorId"; + private final String variableName = "name"; + private final String expectedSecurityKey = "key"; @Before public void setup() throws Exception { + MockitoAnnotations.initMocks(this); + handler = new FileUploadHandler(); - InputStream inputStream = new InputStream() { - private int counter = 0; + + mockRequest(); + mockConnectorTracker(); + mockUi(); + + when(clientConnector.isConnectorEnabled()).thenReturn(true); + when(streamVariable.getOutputStream()).thenReturn(mock(OutputStream.class)); + when(response.getOutputStream()).thenReturn(responseOutput); + } + + private void mockConnectorTracker() { + when(connectorTracker.getSeckey(streamVariable)).thenReturn(expectedSecurityKey); + when(connectorTracker.getStreamVariable(connectorId, variableName)).thenReturn(streamVariable); + when(connectorTracker.getConnector(connectorId)).thenReturn(clientConnector); + } + + private void mockRequest() throws IOException { + when(request.getPathInfo()).thenReturn("/" + ServletPortletHelper.UPLOAD_URL_PREFIX + uiId + "/"+ connectorId + "/" + variableName + "/" + expectedSecurityKey); + when(request.getInputStream()).thenReturn(createInputStream("foobar")); + when(request.getHeader("Content-Length")).thenReturn("6"); + when(request.getContentType()).thenReturn("foobar"); + } + + private InputStream createInputStream(final String content) { + return new InputStream() { + int counter = 0; + byte[] msg = content.getBytes(); @Override public int read() throws IOException { - counter++; - if (counter > 6) { - throw new RuntimeException( - "-1 is ignored by FileUploadHandler"); + if(counter > msg.length + 1) { + throw new AssertionError("-1 was ignored by FileUploadHandler."); + } + + if(counter >= msg.length) { + counter++; + return -1; } - return -1; - } + return msg[counter++]; + } }; - request = Mockito.mock(VaadinRequest.class); - Mockito.when(request.getInputStream()).thenReturn(inputStream); - Mockito.when(request.getHeader("Content-Length")).thenReturn("211"); } + private void mockUi() { + when(ui.getConnectorTracker()).thenReturn(connectorTracker); + when(session.getUIById(uiId)).thenReturn(ui); + } + + /** + * Tests whether we get infinite loop if InputStream is already read (#10096) + */ @Test(expected = IOException.class) - public void testStreamEnded() throws IOException { + public void exceptionIsThrownOnUnexpectedEnd() throws IOException { + when(request.getInputStream()).thenReturn(createInputStream("")); + when(request.getHeader("Content-Length")).thenReturn("1"); + handler.doHandleSimpleMultipartFileUpload(null, request, null, null, null, null, null); + } + + @Test + public void responseIsSentOnCorrectSecurityKey() throws IOException { + when(connectorTracker.getSeckey(streamVariable)).thenReturn(expectedSecurityKey); + + handler.handleRequest(session, request, response); + verify(responseOutput).close(); } + @Test + public void responseIsNotSentOnIncorrectSecurityKey() throws IOException { + when(connectorTracker.getSeckey(streamVariable)).thenReturn("another key expected"); + + handler.handleRequest(session, request, response); + + verifyZeroInteractions(responseOutput); + } + + @Test + public void responseIsNotSentOnMissingSecurityKey() throws IOException { + when(connectorTracker.getSeckey(streamVariable)).thenReturn(null); + + handler.handleRequest(session, request, response); + + verifyZeroInteractions(responseOutput); + } } diff --git a/server/tests/src/com/vaadin/tests/data/converter/StringToBooleanConverterTest.java b/server/tests/src/com/vaadin/tests/data/converter/StringToBooleanConverterTest.java index f734d76633..6e81af97a3 100644 --- a/server/tests/src/com/vaadin/tests/data/converter/StringToBooleanConverterTest.java +++ b/server/tests/src/com/vaadin/tests/data/converter/StringToBooleanConverterTest.java @@ -4,9 +4,25 @@ import junit.framework.TestCase; import com.vaadin.data.util.converter.StringToBooleanConverter; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; + public class StringToBooleanConverterTest extends TestCase { StringToBooleanConverter converter = new StringToBooleanConverter(); + StringToBooleanConverter yesNoConverter = new StringToBooleanConverter("yes","no"); + StringToBooleanConverter localeConverter = new StringToBooleanConverter() { + @Override + public String getFalseString(Locale locale) { + return SimpleDateFormat.getDateInstance(SimpleDateFormat.LONG,locale).format(new Date(3000000000000L)); + } + + @Override + public String getTrueString(Locale locale) { + return SimpleDateFormat.getDateInstance(SimpleDateFormat.LONG,locale).format(new Date(2000000000000L)); + } + }; public void testNullConversion() { assertEquals(null, converter.convertToModel(null, Boolean.class, null)); @@ -20,4 +36,22 @@ public class StringToBooleanConverterTest extends TestCase { assertTrue(converter.convertToModel("true", Boolean.class, null)); assertFalse(converter.convertToModel("false", Boolean.class, null)); } + + public void testYesNoValueConversion() { + assertTrue(yesNoConverter.convertToModel("yes", Boolean.class, null)); + assertFalse(yesNoConverter.convertToModel("no", Boolean.class, null)); + + assertEquals("yes", yesNoConverter.convertToPresentation(true, String.class, null)); + assertEquals("no", yesNoConverter.convertToPresentation(false, String.class, null)); + } + + + public void testLocale() { + assertEquals("May 18, 2033", localeConverter.convertToPresentation(true, String.class, Locale.US)); + assertEquals("January 24, 2065", localeConverter.convertToPresentation(false, String.class, Locale.US)); + + assertEquals("18. Mai 2033", localeConverter.convertToPresentation(true, String.class, Locale.GERMANY)); + assertEquals("24. Januar 2065", localeConverter.convertToPresentation(false, String.class, Locale.GERMANY)); + } + } diff --git a/server/tests/src/com/vaadin/tests/data/converter/StringToEnumConverterTest.java b/server/tests/src/com/vaadin/tests/data/converter/StringToEnumConverterTest.java index a4c3732e1f..59c1ed133a 100644 --- a/server/tests/src/com/vaadin/tests/data/converter/StringToEnumConverterTest.java +++ b/server/tests/src/com/vaadin/tests/data/converter/StringToEnumConverterTest.java @@ -1,13 +1,14 @@ package com.vaadin.tests.data.converter; -import junit.framework.TestCase; +import org.junit.Assert; +import org.junit.Test; import com.vaadin.data.util.converter.Converter; import com.vaadin.data.util.converter.Converter.ConversionException; import com.vaadin.data.util.converter.ReverseConverter; import com.vaadin.data.util.converter.StringToEnumConverter; -public class StringToEnumConverterTest extends TestCase { +public class StringToEnumConverterTest { public static enum FooEnum { VALUE1, SOME_VALUE, FOO_BAR_BAZ, Bar, nonStandardCase, _HUGH; @@ -17,56 +18,64 @@ public class StringToEnumConverterTest extends TestCase { Converter<Enum, String> reverseConverter = new ReverseConverter<Enum, String>( converter); + @Test public void testEmptyStringConversion() { - assertEquals(null, converter.convertToModel("", Enum.class, null)); + Assert.assertEquals(null, + converter.convertToModel("", Enum.class, null)); } + @Test public void testInvalidEnumClassConversion() { try { converter.convertToModel("Foo", Enum.class, null); - fail("No exception thrown"); + Assert.fail("No exception thrown"); } catch (ConversionException e) { // OK } } + @Test public void testNullConversion() { - assertEquals(null, converter.convertToModel(null, Enum.class, null)); + Assert.assertEquals(null, + converter.convertToModel(null, Enum.class, null)); } + @Test public void testReverseNullConversion() { - assertEquals(null, + Assert.assertEquals(null, reverseConverter.convertToModel(null, String.class, null)); } + @Test public void testValueConversion() { - assertEquals(FooEnum.VALUE1, + Assert.assertEquals(FooEnum.VALUE1, converter.convertToModel("Value1", FooEnum.class, null)); - assertEquals(FooEnum.SOME_VALUE, + Assert.assertEquals(FooEnum.SOME_VALUE, converter.convertToModel("Some value", FooEnum.class, null)); - assertEquals(FooEnum.FOO_BAR_BAZ, + Assert.assertEquals(FooEnum.FOO_BAR_BAZ, converter.convertToModel("Foo bar baz", FooEnum.class, null)); - assertEquals(FooEnum.Bar, + Assert.assertEquals(FooEnum.Bar, converter.convertToModel("Bar", FooEnum.class, null)); - assertEquals(FooEnum.nonStandardCase, converter.convertToModel( + Assert.assertEquals(FooEnum.nonStandardCase, converter.convertToModel( "Nonstandardcase", FooEnum.class, null)); - assertEquals(FooEnum._HUGH, + Assert.assertEquals(FooEnum._HUGH, converter.convertToModel("_hugh", FooEnum.class, null)); } + @Test public void testReverseValueConversion() { - assertEquals("Value1", reverseConverter.convertToModel(FooEnum.VALUE1, - String.class, null)); - assertEquals("Some value", reverseConverter.convertToModel( + Assert.assertEquals("Value1", reverseConverter.convertToModel( + FooEnum.VALUE1, String.class, null)); + Assert.assertEquals("Some value", reverseConverter.convertToModel( FooEnum.SOME_VALUE, String.class, null)); - assertEquals("Foo bar baz", reverseConverter.convertToModel( + Assert.assertEquals("Foo bar baz", reverseConverter.convertToModel( FooEnum.FOO_BAR_BAZ, String.class, null)); - assertEquals("Bar", reverseConverter.convertToModel(FooEnum.Bar, + Assert.assertEquals("Bar", reverseConverter.convertToModel(FooEnum.Bar, String.class, null)); - assertEquals("Nonstandardcase", reverseConverter.convertToModel( + Assert.assertEquals("Nonstandardcase", reverseConverter.convertToModel( FooEnum.nonStandardCase, String.class, null)); - assertEquals("_hugh", reverseConverter.convertToModel(FooEnum._HUGH, - String.class, null)); + Assert.assertEquals("_hugh", reverseConverter.convertToModel( + FooEnum._HUGH, String.class, null)); } diff --git a/server/tests/src/com/vaadin/tests/design/DesignFormatterTest.java b/server/tests/src/com/vaadin/tests/design/DesignFormatterTest.java index acee3e2ca8..6510d8ad40 100644 --- a/server/tests/src/com/vaadin/tests/design/DesignFormatterTest.java +++ b/server/tests/src/com/vaadin/tests/design/DesignFormatterTest.java @@ -203,7 +203,7 @@ public class DesignFormatterTest { ShortcutAction action = new ShortcutAction("&^d"); String formatted = formatter.format(action); // note the space here - it separates key combination from caption - assertEquals("alt-ctrl-d d", formatted); + assertEquals("ctrl-alt-d d", formatted); ShortcutAction result = formatter .parse(formatted, ShortcutAction.class); @@ -215,7 +215,7 @@ public class DesignFormatterTest { ShortcutAction action = new ShortcutAction(null, KeyCode.D, new int[] { ModifierKey.ALT, ModifierKey.CTRL }); String formatted = formatter.format(action); - assertEquals("alt-ctrl-d", formatted); + assertEquals("ctrl-alt-d", formatted); ShortcutAction result = formatter .parse(formatted, ShortcutAction.class); diff --git a/server/tests/src/com/vaadin/tests/server/ClassesSerializableTest.java b/server/tests/src/com/vaadin/tests/server/ClassesSerializableTest.java index 9603032ce5..829ad0455d 100644 --- a/server/tests/src/com/vaadin/tests/server/ClassesSerializableTest.java +++ b/server/tests/src/com/vaadin/tests/server/ClassesSerializableTest.java @@ -73,7 +73,7 @@ public class ClassesSerializableTest extends TestCase { "com\\.vaadin\\.server\\.AbstractClientConnector\\$1\\$1", // "com\\.vaadin\\.server\\.JsonCodec\\$1", // "com\\.vaadin\\.server\\.communication\\.PushConnection", // - "com\\.vaadin\\.server\\.communication\\.AtmospherePushConnection", // + "com\\.vaadin\\.server\\.communication\\.AtmospherePushConnection.*", // "com\\.vaadin\\.util\\.ConnectorHelper", // "com\\.vaadin\\.server\\.VaadinSession\\$FutureAccess", // "com\\.vaadin\\.external\\..*", // diff --git a/server/tests/src/com/vaadin/tests/server/component/button/ButtonDeclarativeTest.java b/server/tests/src/com/vaadin/tests/server/component/button/ButtonDeclarativeTest.java index 51abf6be96..b1c10789b4 100644 --- a/server/tests/src/com/vaadin/tests/server/component/button/ButtonDeclarativeTest.java +++ b/server/tests/src/com/vaadin/tests/server/component/button/ButtonDeclarativeTest.java @@ -98,7 +98,7 @@ public class ButtonDeclarativeTest extends DeclarativeTestBase<Button> { @Test public void testAttributes() { String design = "<v-button tabindex=3 plain-text='' icon-alt=OK " - + "click-shortcut=ctrl-shift-o></v-button>"; + + "click-shortcut=shift-ctrl-o></v-button>"; Button b = new Button(""); b.setTabIndex(3); b.setIconAlternateText("OK"); 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 deleted file mode 100644 index 9ecf131c5b..0000000000 --- a/server/tests/src/com/vaadin/tests/server/component/grid/DataProviderExtension.java +++ /dev/null @@ -1,88 +0,0 @@ -/* - * 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/window/WindowDeclarativeTest.java b/server/tests/src/com/vaadin/tests/server/component/window/WindowDeclarativeTest.java index 1d233af494..607fb471b9 100644 --- a/server/tests/src/com/vaadin/tests/server/component/window/WindowDeclarativeTest.java +++ b/server/tests/src/com/vaadin/tests/server/component/window/WindowDeclarativeTest.java @@ -71,8 +71,9 @@ public class WindowDeclarativeTest extends DeclarativeTestBase<Window> { expected.setClosable(!expected.isClosable()); expected.setDraggable(!expected.isDraggable()); - expected.setCloseShortcut(KeyCode.ESCAPE, ModifierKey.CTRL, - ModifierKey.ALT); + expected.removeAllCloseShortcuts(); + expected.addCloseShortcut(KeyCode.ESCAPE, ModifierKey.ALT, + ModifierKey.CTRL); expected.setAssistivePrefix("Hello"); expected.setAssistivePostfix("World"); @@ -86,6 +87,40 @@ public class WindowDeclarativeTest extends DeclarativeTestBase<Window> { } @Test + public void testMultiCloseShortcuts() { + + Window expected = new Window(); + + // Add two shortcuts - should now contain three (default escape + two + // added) + expected.addCloseShortcut(KeyCode.SPACEBAR); + expected.addCloseShortcut(KeyCode.ARROW_LEFT, ModifierKey.ALT, + ModifierKey.CTRL); + + // Try to add the same shortcut again, should be no-op + expected.addCloseShortcut(KeyCode.ARROW_LEFT, ModifierKey.CTRL, + ModifierKey.ALT); + + // Add a third shortcut, should total four (default escape + three + // added) + expected.addCloseShortcut(KeyCode.ARROW_RIGHT, ModifierKey.CTRL); + + // Test validity + String design = "<v-window close-shortcut='escape spacebar ctrl-alt-left ctrl-right' />"; + testRead(design, expected); + testWrite(design, expected); + + // Try removing the spacebar shortcut + expected.removeCloseShortcut(KeyCode.SPACEBAR); + + // Test again + design = "<v-window close-shortcut='escape ctrl-alt-left ctrl-right' />"; + testRead(design, expected); + testWrite(design, expected); + + } + + @Test public void testInvalidPosition() { assertInvalidPosition(""); assertInvalidPosition("1"); diff --git a/server/tests/src/com/vaadin/tests/server/renderer/RendererTest.java b/server/tests/src/com/vaadin/tests/server/renderer/RendererTest.java index eb07fae07f..cea8df0ba6 100644 --- a/server/tests/src/com/vaadin/tests/server/renderer/RendererTest.java +++ b/server/tests/src/com/vaadin/tests/server/renderer/RendererTest.java @@ -15,8 +15,17 @@ */ 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.Date; +import java.util.Locale; + +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; @@ -24,22 +33,15 @@ import com.vaadin.server.VaadinSession; import com.vaadin.tests.server.component.grid.TestGrid; import com.vaadin.tests.util.AlwaysLockedVaadinSession; import com.vaadin.ui.Grid; +import com.vaadin.ui.Grid.AbstractRenderer; import com.vaadin.ui.Grid.Column; import com.vaadin.ui.renderers.ButtonRenderer; import com.vaadin.ui.renderers.DateRenderer; import com.vaadin.ui.renderers.HtmlRenderer; import com.vaadin.ui.renderers.NumberRenderer; import com.vaadin.ui.renderers.TextRenderer; -import elemental.json.JsonValue; -import org.junit.Before; -import org.junit.Test; - -import java.util.Date; -import java.util.Locale; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertSame; +import elemental.json.JsonValue; public class RendererTest { @@ -259,7 +261,7 @@ public class RendererTest { } private JsonValue render(Column column, Object value) { - return RpcDataProviderExtension.encodeValue(value, - column.getRenderer(), column.getConverter(), grid.getLocale()); + return AbstractRenderer.encodeValue(value, column.getRenderer(), + column.getConverter(), grid.getLocale()); } } |