diff options
Diffstat (limited to 'server/src/com/vaadin/data/RpcDataProviderExtension.java')
-rw-r--r-- | server/src/com/vaadin/data/RpcDataProviderExtension.java | 958 |
1 files changed, 165 insertions, 793 deletions
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(); - } } |