]> source.dussan.org Git - vaadin-framework.git/commitdiff
Add APIs to inform components of stale objects in DataProvider (#8271)
authorTeemu Suo-Anttila <tsuoanttila@users.noreply.github.com>
Wed, 25 Jan 2017 10:27:44 +0000 (12:27 +0200)
committerDenis <denis@vaadin.com>
Wed, 25 Jan 2017 10:27:44 +0000 (12:27 +0200)
* Add DataProvider refreshItem for single item update

* Add 'id' concept for DataProviders

This patch also adds a simplified data provider that can replace items
based on their id. This can be used to simulate stale objects from an actual
backend.

* Add refresh logic to Grid SelectionModels

* Remove broken equals and hashCode

* Refresh KeyMapper, clean up some methods

* Fix UI.access in test

* Fix tests and Grid single selection model

* Do clean up before replacing data provider

* Check correct variable for null value

* Fix other selects, add generic tests

* Code style fixes, removed assert

* Merge remote-tracking branch 'origin/master' into 286_refresh_items

* Fix documentation for refreshing an item

* Improve introduction chapter, minor clarifications

* Merge remote-tracking branch 'origin/master' into 287_refresh_items

* Add missing parameters in unit tests

25 files changed:
documentation/datamodel/datamodel-providers.asciidoc
server/src/main/java/com/vaadin/data/provider/AbstractDataProvider.java
server/src/main/java/com/vaadin/data/provider/BackEndDataProvider.java
server/src/main/java/com/vaadin/data/provider/CallbackDataProvider.java
server/src/main/java/com/vaadin/data/provider/DataChangeEvent.java
server/src/main/java/com/vaadin/data/provider/DataCommunicator.java
server/src/main/java/com/vaadin/data/provider/DataGenerator.java
server/src/main/java/com/vaadin/data/provider/DataKeyMapper.java
server/src/main/java/com/vaadin/data/provider/DataProvider.java
server/src/main/java/com/vaadin/data/provider/DataProviderListener.java
server/src/main/java/com/vaadin/data/provider/DataProviderWrapper.java
server/src/main/java/com/vaadin/server/KeyMapper.java
server/src/main/java/com/vaadin/ui/AbstractMultiSelect.java
server/src/main/java/com/vaadin/ui/components/grid/MultiSelectionModelImpl.java
server/src/main/java/com/vaadin/ui/components/grid/SingleSelectionModelImpl.java
server/src/test/java/com/vaadin/data/provider/AbstractDataProviderTest.java
server/src/test/java/com/vaadin/data/provider/DataCommunicatorTest.java
server/src/test/java/com/vaadin/data/provider/ReplaceListDataProvider.java [new file with mode: 0644]
server/src/test/java/com/vaadin/data/provider/ReplaceListDataProviderTest.java [new file with mode: 0644]
server/src/test/java/com/vaadin/data/provider/StrBean.java
server/src/test/java/com/vaadin/tests/data/selection/AbstractStaleSelectionTest.java [new file with mode: 0644]
server/src/test/java/com/vaadin/tests/data/selection/GridStaleElementTest.java [new file with mode: 0644]
server/src/test/java/com/vaadin/tests/data/selection/StaleMultiSelectionTest.java [new file with mode: 0644]
server/src/test/java/com/vaadin/tests/data/selection/StaleSingleSelectionTest.java [new file with mode: 0644]
uitest/src/main/java/com/vaadin/tests/data/ReplaceDataProvider.java

index 6b5fea2275ada876c42e8ea6db63b9bf160870be..80b9fb9cb48b3faad6b3cd0d0ddb7c81ab9c33e7 100644 (file)
@@ -181,7 +181,7 @@ Button modifyPersonButton = new Button("Modify person",
 
     personToChange.setName("Changed person");
 
-    dataProvider.refreshAll();
+    dataProvider.refreshItem(personToChange);
 });
 ----
 
@@ -572,3 +572,48 @@ public class PersonDataProvider
   }
 }
 ----
+
+[[lazy-refresh]]
+=== Refreshing
+
+When your application makes changes to the data that is in your backend, you might need to make sure all parts of the application are aware of these changes.
+All data providers have the `refreshAll`and `refreshItem` methods. 
+These methods can be used when data in the backend has been updated.
+
+For example Spring Data gives you new instances with every request, and making changes to the repository will make old instances of the same object "stale".
+In these cases you should inform any interested component by calling `dataProvider.refreshItem(newInstance)`. 
+This can work out of the box, if your beans have equals and hashCode implementations that check if the objects represent the same data.
+Since that is not always the case, the user of a `CallbackDataProvider` can give it a `ValueProvider` that will provide a stable ID for the data objects. 
+This is usually a method reference, eg. `Person::getId`.
+
+As an example, our service interface has an update method that returns a new instance of the item. 
+Other functionality has been omitted to keep focus on the updating.
+
+[source, java]
+----
+public interface PersonService {
+  Person save(Person person);
+}
+----
+
+Part of the application code wants to update a persons name and save it to the backend.
+
+[source, java]
+----
+PersonService service;
+DataProvider<Person, String> allPersonsWithId = new CallbackDataProvider<>(
+  fetchCallback, sizeCallback, Person::getId);
+
+NativeSelect<Person> persons = new NativeSelect<>();
+persons.setDataProvider(allPersonsWithId);
+
+Button modifyPersonButton = new Button("Modify person",
+  clickEvent -> {
+    Person personToChange = persons.getValue();
+
+    personToChange.setName("Changed person");
+
+    Person newInstance = service.save(personToChange);
+    dataProvider.refreshItem(newInstance);
+});
+----
index 3319bfb382cfad29661e262d2fbb692eb25ef393..c87e18e1c29edb5f1786d560706613b5c76e0ed2 100644 (file)
@@ -18,6 +18,7 @@ package com.vaadin.data.provider;
 import java.lang.reflect.Method;
 import java.util.EventObject;
 
+import com.vaadin.data.provider.DataChangeEvent.DataRefreshEvent;
 import com.vaadin.event.EventRouter;
 import com.vaadin.shared.Registration;
 
@@ -39,14 +40,20 @@ public abstract class AbstractDataProvider<T, F> implements DataProvider<T, F> {
     private EventRouter eventRouter;
 
     @Override
-    public Registration addDataProviderListener(DataProviderListener listener) {
+    public Registration addDataProviderListener(
+            DataProviderListener<T> listener) {
         return addListener(DataChangeEvent.class, listener,
                 DataProviderListener.class.getMethods()[0]);
     }
 
     @Override
     public void refreshAll() {
-        fireEvent(new DataChangeEvent(this));
+        fireEvent(new DataChangeEvent<>(this));
+    }
+
+    @Override
+    public void refreshItem(T item) {
+        fireEvent(new DataRefreshEvent<>(this, item));
     }
 
     /**
@@ -65,7 +72,7 @@ public abstract class AbstractDataProvider<T, F> implements DataProvider<T, F> {
      * @return a registration for the listener
      */
     protected Registration addListener(Class<?> eventType,
-            DataProviderListener listener, Method method) {
+            DataProviderListener<T> listener, Method method) {
         if (eventRouter == null) {
             eventRouter = new EventRouter();
         }
index 36d875a21afb61bc3f36b2abc2d2a5ec3cc986a5..b50b5c7fc86ac895077ef579ffe881148d825edd 100644 (file)
@@ -71,5 +71,4 @@ public interface BackEndDataProvider<T, F> extends DataProvider<T, F> {
     default boolean isInMemory() {
         return false;
     }
-
 }
index d83c1f1934cec6b596f935983efe1b054bc42208..2f68e57e06eecaa4cc0a5eba538ca92c34cd7f6d 100644 (file)
@@ -18,6 +18,7 @@ package com.vaadin.data.provider;
 import java.util.Objects;
 import java.util.stream.Stream;
 
+import com.vaadin.data.ValueProvider;
 import com.vaadin.server.SerializableFunction;
 import com.vaadin.server.SerializableToIntFunction;
 
@@ -36,6 +37,7 @@ public class CallbackDataProvider<T, F>
         extends AbstractBackEndDataProvider<T, F> {
     private final SerializableFunction<Query<T, F>, Stream<T>> fetchCallback;
     private final SerializableToIntFunction<Query<T, F>> sizeCallback;
+    private final ValueProvider<T, Object> idGetter;
 
     /**
      * Constructs a new DataProvider to request data using callbacks for
@@ -45,16 +47,40 @@ public class CallbackDataProvider<T, F>
      *            function that returns a stream of items from the back end for
      *            a query
      * @param sizeCallback
-     *            function that returns the number of items in the back end for
-     *            a query
+     *            function that return the number of items in the back end for a
+     *            query
+     *
+     * @see #CallbackDataProvider(SerializableFunction,
+     *      SerializableToIntFunction, ValueProvider)
      */
     public CallbackDataProvider(
             SerializableFunction<Query<T, F>, Stream<T>> fetchCallback,
             SerializableToIntFunction<Query<T, F>> sizeCallback) {
-        Objects.requireNonNull(fetchCallback, "Request function can't be null");
+        this(fetchCallback, sizeCallback, t -> t);
+    }
+
+    /**
+     * Constructs a new DataProvider to request data using callbacks for
+     * fetching and counting items in the back end.
+     *
+     * @param fetchCallBack
+     *            function that requests data from back end based on query
+     * @param sizeCallback
+     *            function that returns the amount of data in back end for query
+     * @param identifierGetter
+     *            function that returns the identifier for a given item
+     */
+    public CallbackDataProvider(
+            SerializableFunction<Query<T, F>, Stream<T>> fetchCallBack,
+            SerializableToIntFunction<Query<T, F>> sizeCallback,
+            ValueProvider<T, Object> identifierGetter) {
+        Objects.requireNonNull(fetchCallBack, "Request function can't be null");
         Objects.requireNonNull(sizeCallback, "Size callback can't be null");
-        this.fetchCallback = fetchCallback;
+        Objects.requireNonNull(identifierGetter,
+                "Identifier getter function can't be null");
+        this.fetchCallback = fetchCallBack;
         this.sizeCallback = sizeCallback;
+        this.idGetter = identifierGetter;
     }
 
     @Override
@@ -66,4 +92,12 @@ public class CallbackDataProvider<T, F>
     protected int sizeInBackEnd(Query<T, F> query) {
         return sizeCallback.applyAsInt(query);
     }
+
+    @Override
+    public Object getId(T item) {
+        Object itemId = idGetter.apply(item);
+        assert itemId != null : "CallbackDataProvider got null as an id for item: "
+                + item;
+        return itemId;
+    }
 }
index 86ad9235a6f100c98c49014c818ba66f5b88e2f9..93def4f810edd7a5260958f8ef0ea66025041711 100644 (file)
 package com.vaadin.data.provider;
 
 import java.util.EventObject;
+import java.util.Objects;
 
 /**
  * An event fired when the data of a {@code DataProvider} changes.
  *
- *
  * @see DataProviderListener
  *
  * @author Vaadin Ltd
  * @since 8.0
  *
+ *
+ * @param <T>
+ *            the data type
  */
-public class DataChangeEvent extends EventObject {
+public class DataChangeEvent<T> extends EventObject {
+
+    /**
+     * An event fired when a single item of a {@code DataProvider} has been
+     * updated.
+     *
+     * @param <T>
+     *            the data type
+     */
+    public static class DataRefreshEvent<T> extends DataChangeEvent<T> {
+
+        private final T item;
+
+        /**
+         * Creates a new data refresh event originating from the given data
+         * provider.
+         *
+         * @param source
+         *            the data provider, not null
+         * @param item
+         *            the updated item, not null
+         */
+        public DataRefreshEvent(DataProvider<T, ?> source, T item) {
+            super(source);
+            Objects.requireNonNull(item, "Refreshed item can't be null");
+            this.item = item;
+        }
+
+        /**
+         * Gets the refreshed item.
+         *
+         * @return the refreshed item
+         */
+        public T getItem() {
+            return item;
+        }
+    }
 
     /**
      * Creates a new {@code DataChangeEvent} event originating from the given
@@ -36,13 +75,12 @@ public class DataChangeEvent extends EventObject {
      * @param source
      *            the data provider, not null
      */
-    public DataChangeEvent(DataProvider<?, ?> source) {
+    public DataChangeEvent(DataProvider<T, ?> source) {
         super(source);
     }
 
     @Override
-    public DataProvider<?, ?> getSource() {
-        return (DataProvider<?, ?>) super.getSource();
+    public DataProvider<T, ?> getSource() {
+        return (DataProvider<T, ?>) super.getSource();
     }
-
 }
index 021623555e623603b5d27f47e677bd44e68f7987..9080ad34427d0dbcda2a428918b5497b7667ccc2 100644 (file)
@@ -27,6 +27,7 @@ import java.util.Set;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
+import com.vaadin.data.provider.DataChangeEvent.DataRefreshEvent;
 import com.vaadin.server.AbstractExtension;
 import com.vaadin.server.KeyMapper;
 import com.vaadin.server.SerializableConsumer;
@@ -465,17 +466,20 @@ public class DataCommunicator<T> extends AbstractExtension {
      * @param initialFilter
      *            the initial filter value to use, or <code>null</code> to not
      *            use any initial filter value
+     *
+     * @param <F>
+     *            the filter type
+     *
      * @return a consumer that accepts a new filter value to use
      */
     public <F> SerializableConsumer<F> setDataProvider(
             DataProvider<T, F> dataProvider, F initialFilter) {
         Objects.requireNonNull(dataProvider, "data provider cannot be null");
-
         filter = initialFilter;
-        this.dataProvider = dataProvider;
-
         detachDataProviderListener();
         dropAllData();
+        this.dataProvider = dataProvider;
+
         /*
          * This introduces behavior which influence on the client-server
          * communication: now the very first response to the client will always
@@ -556,8 +560,18 @@ public class DataCommunicator<T> extends AbstractExtension {
 
     private void attachDataProviderListener() {
         dataProviderUpdateRegistration = getDataProvider()
-                .addDataProviderListener(
-                        event -> getUI().access(() -> reset()));
+                .addDataProviderListener(event -> {
+                    getUI().access(() -> {
+                        if (event instanceof DataRefreshEvent) {
+                            T item = ((DataRefreshEvent<T>) event).getItem();
+                            generators.forEach(g -> g.refreshData(item));
+                            keyMapper.refresh(item, dataProvider::getId);
+                            refresh(item);
+                        } else {
+                            reset();
+                        }
+                    });
+                });
     }
 
     private void detachDataProviderListener() {
index 5e88f7ff66e5c393c5c45f8030a7807ef1042c10..c5a5a6b9c97ce021cf3d2aee6158337b5aa24f6d 100644 (file)
@@ -62,4 +62,14 @@ public interface DataGenerator<T> extends Serializable {
      */
     public default void destroyAllData() {
     }
+
+    /**
+     * Informs the {@code DataGenerator} that a data object has been updated.
+     * This method should update any unneeded information stored for given item.
+     *
+     * @param item
+     *            the updated item
+     */
+    public default void refreshData(T item) {
+    }
 }
index 63bd2d92f80b8941748beea591688c9fa4d29b56..0ec6748a85c2eab0136998c80019cba6df8befc6 100644 (file)
@@ -17,6 +17,8 @@ package com.vaadin.data.provider;
 
 import java.io.Serializable;
 
+import com.vaadin.data.ValueProvider;
+
 /**
  * DataKeyMapper to map data objects to key strings.
  *
@@ -59,4 +61,16 @@ public interface DataKeyMapper<T> extends Serializable {
      * Dropped keys are not reused.
      */
     void removeAll();
+
+    /**
+     * Updates any existing mappings of given data object. The equality of two
+     * data objects is determined by the equality of their identifiers provided
+     * by the given value provider.
+     *
+     * @param dataObject
+     *            the data object to update
+     * @param identifierGetter
+     *            the function to get an identifier from a data object
+     */
+    void refresh(T dataObject, ValueProvider<T, Object> identifierGetter);
 }
index 380a1f48097deed836daa44151823ccf039e1456..f9c3c80146ca7ede7f22ef608a7e25c88b2d177f 100644 (file)
@@ -87,12 +87,39 @@ public interface DataProvider<T, F> extends Serializable {
      */
     Stream<T> fetch(Query<T, F> query);
 
+    /**
+     * Refreshes the given item. This method should be used to inform all
+     * {@link DataProviderListener DataProviderListeners} that an item has been
+     * updated or replaced with a new instance.
+     *
+     * @param item
+     *            the item to refresh
+     */
+    void refreshItem(T item);
+
     /**
      * Refreshes all data based on currently available data in the underlying
      * provider.
      */
     void refreshAll();
 
+    /**
+     * Gets an identifier for the given item. This identifier is used by the
+     * framework to determine equality between two items.
+     * <p>
+     * Default is to use item itself as its own identifier. If the item has
+     * {@link Object#equals(Object)} and {@link Object#hashCode()} implemented
+     * in a way that it can be compared to other items, no changes are required.
+     *
+     * @param item
+     *            the item to get identifier for; not {@code null}
+     * @return the identifier for given item; not {@code null}
+     */
+    public default Object getId(T item) {
+        Objects.requireNonNull(item, "Cannot provide an id for a null item.");
+        return item;
+    }
+
     /**
      * Adds a data provider listener. The listener is called when some piece of
      * data is updated.
@@ -106,7 +133,7 @@ public interface DataProvider<T, F> extends Serializable {
      *            the data change listener, not null
      * @return a registration for the listener
      */
-    Registration addDataProviderListener(DataProviderListener listener);
+    Registration addDataProviderListener(DataProviderListener<T> listener);
 
     /**
      * Wraps this data provider to create a data provider that uses a different
index 582bdc1d86fe400888d5ecb633f371c0ac68ae2b..da97e34c803efa92b97fcd21d8b8d192638ba3f8 100644 (file)
@@ -23,9 +23,12 @@ import java.io.Serializable;
  *
  * @author Vaadin Ltd
  * @since 8.0
+ *
+ * @param <T>
+ *            the data type
  */
 @FunctionalInterface
-public interface DataProviderListener extends Serializable {
+public interface DataProviderListener<T> extends Serializable {
 
     /**
      * Invoked when this listener receives a data change event from a data
@@ -39,5 +42,5 @@ public interface DataProviderListener extends Serializable {
      * @param event
      *            the received event, not null
      */
-    void onDataChange(DataChangeEvent event);
+    void onDataChange(DataChangeEvent<T> event);
 }
index 1eabb67abe79dc825d9f7268c35b710d916d3755..5195aa7837b7f1ae64e714a8e84ad119d6d64ec9 100644 (file)
@@ -65,7 +65,18 @@ public abstract class DataProviderWrapper<T, F, M>
     }
 
     @Override
-    public Registration addDataProviderListener(DataProviderListener listener) {
+    public void refreshItem(T item) {
+        dataProvider.refreshItem(item);
+    }
+
+    @Override
+    public Object getId(T item) {
+        return dataProvider.getId(item);
+    }
+
+    @Override
+    public Registration addDataProviderListener(
+            DataProviderListener<T> listener) {
         return dataProvider.addDataProviderListener(listener);
     }
 
index 471fea3e69856bce31fd1469a055e8d48baa6fdc..7ed723be66b5a81707e745e150c89fdc69be40fd 100644 (file)
@@ -19,6 +19,7 @@ package com.vaadin.server;
 import java.io.Serializable;
 import java.util.HashMap;
 
+import com.vaadin.data.ValueProvider;
 import com.vaadin.data.provider.DataKeyMapper;
 
 /**
@@ -113,4 +114,17 @@ public class KeyMapper<V> implements DataKeyMapper<V>, Serializable {
     public boolean containsKey(String key) {
         return keyObjectMap.containsKey(key);
     }
+
+    @Override
+    public void refresh(V dataObject,
+            ValueProvider<V, Object> identifierGetter) {
+        Object id = identifierGetter.apply(dataObject);
+        objectKeyMap.entrySet().stream()
+                .filter(e -> identifierGetter.apply(e.getKey()).equals(id))
+                .findAny().ifPresent(e -> {
+                    String key = objectKeyMap.remove(e.getKey());
+                    objectKeyMap.put(dataObject, key);
+                    keyObjectMap.put(key, dataObject);
+                });
+    }
 }
index 22df69da5cf8a3f0e1a2e02418081e4a29a3297a..9947067288750f8eda15c85ba4a276c262624a16 100644 (file)
@@ -15,6 +15,7 @@
  */
 package com.vaadin.ui;
 
+import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashSet;
@@ -31,6 +32,7 @@ import com.vaadin.data.HasValue;
 import com.vaadin.data.SelectionModel;
 import com.vaadin.data.SelectionModel.Multi;
 import com.vaadin.data.provider.DataGenerator;
+import com.vaadin.data.provider.DataProvider;
 import com.vaadin.event.selection.MultiSelectionEvent;
 import com.vaadin.event.selection.MultiSelectionListener;
 import com.vaadin.server.Resource;
@@ -59,7 +61,7 @@ import elemental.json.JsonObject;
 public abstract class AbstractMultiSelect<T> extends AbstractListing<T>
         implements MultiSelect<T> {
 
-    private Set<T> selection = new LinkedHashSet<>();
+    private List<T> selection = new ArrayList<>();
 
     private class MultiSelectServerRpcImpl implements MultiSelectServerRpc {
         @Override
@@ -122,6 +124,11 @@ public abstract class AbstractMultiSelect<T> extends AbstractListing<T>
         public void destroyAllData() {
             AbstractMultiSelect.this.deselectAll();
         }
+
+        @Override
+        public void refreshData(T item) {
+            refreshSelectedItem(item);
+        }
     }
 
     /**
@@ -228,9 +235,9 @@ public abstract class AbstractMultiSelect<T> extends AbstractListing<T>
     @Override
     public Registration addValueChangeListener(
             HasValue.ValueChangeListener<Set<T>> listener) {
-        return addSelectionListener(event -> listener.valueChange(
-                new ValueChangeEvent<>(this, event.getOldValue(),
-                        event.isUserOriginated())));
+        return addSelectionListener(
+                event -> listener.valueChange(new ValueChangeEvent<>(this,
+                        event.getOldValue(), event.isUserOriginated())));
     }
 
     /**
@@ -346,12 +353,15 @@ public abstract class AbstractMultiSelect<T> extends AbstractListing<T>
             return;
         }
 
-        updateSelection(Set::clear, false);
+        updateSelection(Collection::clear, false);
     }
 
     @Override
     public boolean isSelected(T item) {
-        return selection.contains(item);
+        DataProvider<T, ?> dataProvider = internalGetDataProvider();
+        Object id = dataProvider.getId(item);
+        return selection.stream().map(dataProvider::getId).anyMatch(id::equals);
+
     }
 
     /**
@@ -469,7 +479,7 @@ public abstract class AbstractMultiSelect<T> extends AbstractListing<T>
         return item;
     }
 
-    private void updateSelection(SerializableConsumer<Set<T>> handler,
+    private void updateSelection(SerializableConsumer<Collection<T>> handler,
             boolean userOriginated) {
         LinkedHashSet<T> oldSelection = new LinkedHashSet<>(selection);
         handler.accept(selection);
@@ -479,4 +489,15 @@ public abstract class AbstractMultiSelect<T> extends AbstractListing<T>
 
         getDataCommunicator().reset();
     }
+
+    private final void refreshSelectedItem(T item) {
+        DataProvider<T, ?> dataProvider = internalGetDataProvider();
+        Object id = dataProvider.getId(item);
+        for (int i = 0; i < selection.size(); ++i) {
+            if (id.equals(dataProvider.getId(selection.get(i)))) {
+                selection.set(i, item);
+                return;
+            }
+        }
+    }
 }
index 551918ef85a03fbc5324c20da9a7756372477350..97f79a4655f68552a8cd7bddc9997659ed35ab84 100644 (file)
  */
 package com.vaadin.ui.components.grid;
 
+import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.LinkedHashSet;
+import java.util.List;
 import java.util.Objects;
 import java.util.Set;
 import java.util.function.Consumer;
@@ -119,7 +122,7 @@ public class MultiSelectionModelImpl<T> extends AbstractSelectionModel<T>
         }
     }
 
-    private Set<T> selection = new LinkedHashSet<>();
+    private List<T> selection = new ArrayList<>();
 
     private SelectAllCheckBoxVisibility selectAllCheckBoxVisibility = SelectAllCheckBoxVisibility.DEFAULT;
 
@@ -199,8 +202,20 @@ public class MultiSelectionModelImpl<T> extends AbstractSelectionModel<T>
     @Override
     public boolean isSelected(T item) {
         return isAllSelected()
-                || com.vaadin.ui.components.grid.MultiSelectionModel.super.isSelected(
-                        item);
+                || selectionContainsId(getGrid().getDataProvider().getId(item));
+    }
+
+    /**
+     * Returns if the given id belongs to one of the selected items.
+     *
+     * @param id
+     *            the id to check for
+     * @return {@code true} if id is selected, {@code false} if not
+     */
+    protected boolean selectionContainsId(Object id) {
+        DataProvider<T, ?> dataProvider = getGrid().getDataProvider();
+        return selection.stream().map(dataProvider::getId)
+                .anyMatch(i -> id.equals(i));
     }
 
     @Override
@@ -447,7 +462,7 @@ public class MultiSelectionModelImpl<T> extends AbstractSelectionModel<T>
         return getState(false).selectionAllowed;
     }
 
-    private void doUpdateSelection(Consumer<Set<T>> handler,
+    private void doUpdateSelection(Consumer<Collection<T>> handler,
             boolean userOriginated) {
         if (getParent() == null) {
             throw new IllegalStateException(
@@ -460,4 +475,16 @@ public class MultiSelectionModelImpl<T> extends AbstractSelectionModel<T>
         fireEvent(new MultiSelectionEvent<>(getGrid(), asMultiSelect(),
                 oldSelection, userOriginated));
     }
+
+    @Override
+    public void refreshData(T item) {
+        DataProvider<T, ?> dataProvider = getGrid().getDataProvider();
+        Object refreshId = dataProvider.getId(item);
+        for (int i = 0; i < selection.size(); ++i) {
+            if (dataProvider.getId(selection.get(i)).equals(refreshId)) {
+                selection.set(i, item);
+                return;
+            }
+        }
+    }
 }
index 42ca337e65ae0cbafed6b55efa0d1c0be4f3a64b..cd985f473a01f0cb8d3566d8e2c3c3ea29333bd8 100644 (file)
@@ -284,4 +284,18 @@ public class SingleSelectionModelImpl<T> extends AbstractSelectionModel<T>
             }
         };
     }
+
+    @Override
+    public void refreshData(T item) {
+        if (isSelected(item)) {
+            selectedItem = item;
+        }
+    }
+
+    @Override
+    public boolean isSelected(T item) {
+        return item != null && selectedItem != null
+                && getGrid().getDataProvider().getId(selectedItem)
+                        .equals(getGrid().getDataProvider().getId(item));
+    }
 }
index 5fbaaaeb8ab2f5d02092983769312c1b808c34af..3cd368944d23288d961302c923d3de3c441b1b31 100644 (file)
@@ -31,6 +31,7 @@ public class AbstractDataProviderTest {
 
     private static class TestDataProvider
             extends AbstractDataProvider<Object, Object> {
+
         @Override
         public Stream<Object> fetch(Query<Object, Object> t) {
             return null;
@@ -50,7 +51,7 @@ public class AbstractDataProviderTest {
     @Test
     public void refreshAll_notifyListeners() {
         TestDataProvider dataProvider = new TestDataProvider();
-        AtomicReference<DataChangeEvent> event = new AtomicReference<>();
+        AtomicReference<DataChangeEvent<Object>> event = new AtomicReference<>();
         dataProvider.addDataProviderListener(ev -> {
             Assert.assertNull(event.get());
             event.set(ev);
@@ -63,7 +64,7 @@ public class AbstractDataProviderTest {
     @Test
     public void removeListener_listenerIsNotNotified() {
         TestDataProvider dataProvider = new TestDataProvider();
-        AtomicReference<DataChangeEvent> event = new AtomicReference<>();
+        AtomicReference<DataChangeEvent<Object>> event = new AtomicReference<>();
         Registration registration = dataProvider
                 .addDataProviderListener(ev -> event.set(ev));
         registration.remove();
index 50a3ebc3372557cdd854777272e4ad2a857339b8..1e75d5726368672a339de5c2b9df2188e2faf4cf 100644 (file)
@@ -16,6 +16,7 @@
 package com.vaadin.data.provider;
 
 import java.util.Collections;
+import java.util.concurrent.Future;
 
 import org.junit.Assert;
 import org.junit.Test;
@@ -28,12 +29,16 @@ import com.vaadin.server.VaadinSession;
 import com.vaadin.shared.Registration;
 import com.vaadin.ui.UI;
 
+import elemental.json.JsonObject;
+
 /**
  * @author Vaadin Ltd
  *
  */
 public class DataCommunicatorTest {
 
+    private static final Object TEST_OBJECT = new Object();
+
     private static class TestUI extends UI {
 
         private final VaadinSession session;
@@ -50,6 +55,12 @@ public class DataCommunicatorTest {
         public VaadinSession getSession() {
             return session;
         }
+
+        @Override
+        public Future<Void> access(Runnable runnable) {
+            runnable.run();
+            return null;
+        }
     }
 
     private static class TestDataProvider extends ListDataProvider<Object>
@@ -58,12 +69,12 @@ public class DataCommunicatorTest {
         private Registration registration;
 
         public TestDataProvider() {
-            super(Collections.singleton(new Object()));
+            super(Collections.singleton(TEST_OBJECT));
         }
 
         @Override
         public Registration addDataProviderListener(
-                DataProviderListener listener) {
+                DataProviderListener<Object> listener) {
             registration = super.addDataProviderListener(listener);
             return this;
         }
@@ -86,6 +97,21 @@ public class DataCommunicatorTest {
         }
     }
 
+    private static class TestDataGenerator implements DataGenerator<Object> {
+        Object refreshed = null;
+        Object generated = null;
+
+        @Override
+        public void generateData(Object item, JsonObject jsonObject) {
+            generated = item;
+        }
+
+        @Override
+        public void refreshData(Object item) {
+            refreshed = item;
+        }
+    }
+
     private final MockVaadinSession session = new MockVaadinSession(
             Mockito.mock(VaadinService.class));
 
@@ -127,4 +153,42 @@ public class DataCommunicatorTest {
         Assert.assertFalse(dataProvider.isListenerAdded());
     }
 
+    @Test
+    public void refresh_dataProviderListenerCallsRefreshInDataGeneartors() {
+        session.lock();
+
+        UI ui = new TestUI(session);
+
+        TestDataCommunicator communicator = new TestDataCommunicator();
+        communicator.extend(ui);
+
+        TestDataProvider dataProvider = new TestDataProvider();
+        communicator.setDataProvider(dataProvider, null);
+
+        TestDataGenerator generator = new TestDataGenerator();
+        communicator.addDataGenerator(generator);
+
+        // Generate initial data.
+        communicator.beforeClientResponse(true);
+        Assert.assertEquals("DataGenerator generate was not called",
+                TEST_OBJECT, generator.generated);
+        generator.generated = null;
+
+        // Make sure data does not get re-generated
+        communicator.beforeClientResponse(false);
+        Assert.assertEquals("DataGenerator generate was called again", null,
+                generator.generated);
+
+        // Refresh a data object to trigger an update.
+        dataProvider.refreshItem(TEST_OBJECT);
+
+        Assert.assertEquals("DataGenerator refresh was not called", TEST_OBJECT,
+                generator.refreshed);
+
+        // Test refreshed data generation
+        communicator.beforeClientResponse(false);
+        Assert.assertEquals("DataGenerator generate was not called",
+                TEST_OBJECT, generator.generated);
+    }
+
 }
diff --git a/server/src/test/java/com/vaadin/data/provider/ReplaceListDataProvider.java b/server/src/test/java/com/vaadin/data/provider/ReplaceListDataProvider.java
new file mode 100644 (file)
index 0000000..8d204fb
--- /dev/null
@@ -0,0 +1,66 @@
+package com.vaadin.data.provider;
+
+import java.util.List;
+import java.util.stream.Stream;
+
+/**
+ * A dummy data provider for testing item replacement and stale elements.
+ */
+public class ReplaceListDataProvider
+        extends AbstractDataProvider<StrBean, Void> {
+
+    private final List<StrBean> backend;
+
+    public ReplaceListDataProvider(List<StrBean> items) {
+        backend = items;
+    }
+
+    @Override
+    public void refreshItem(StrBean item) {
+        if (replaceItem(item)) {
+            super.refreshItem(item);
+        }
+    }
+
+    private boolean replaceItem(StrBean item) {
+        for (int i = 0; i < backend.size(); ++i) {
+            if (getId(backend.get(i)).equals(getId(item))) {
+                if (backend.get(i).equals(item)) {
+                    return false;
+                }
+                backend.remove(i);
+                backend.add(i, item);
+                return true;
+            }
+        }
+        return false;
+    }
+
+    @Override
+    public boolean isInMemory() {
+        return true;
+    }
+
+    @Override
+    public int size(Query<StrBean, Void> t) {
+        return backend.size();
+    }
+
+    @Override
+    public Stream<StrBean> fetch(Query<StrBean, Void> query) {
+        return backend.stream().skip(query.getOffset()).limit(query.getLimit());
+    }
+
+    public boolean isStale(StrBean item) {
+        Object id = getId(item);
+        boolean itemExistsInBackEnd = backend.contains(item);
+        boolean backEndHasInstanceWithSameId = backend.stream().map(this::getId)
+                .filter(i -> id.equals(i)).count() == 1;
+        return !itemExistsInBackEnd && backEndHasInstanceWithSameId;
+    }
+
+    @Override
+    public Object getId(StrBean item) {
+        return item.getId();
+    }
+}
diff --git a/server/src/test/java/com/vaadin/data/provider/ReplaceListDataProviderTest.java b/server/src/test/java/com/vaadin/data/provider/ReplaceListDataProviderTest.java
new file mode 100644 (file)
index 0000000..7552691
--- /dev/null
@@ -0,0 +1,49 @@
+package com.vaadin.data.provider;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+/**
+ * Test class that verifies that ReplaceListDataProvider functions the way it's
+ * meant to.
+ *
+ */
+public class ReplaceListDataProviderTest {
+
+    private static final StrBean TEST_OBJECT = new StrBean("Foo", 10, -1);
+    private ReplaceListDataProvider dataProvider = new ReplaceListDataProvider(
+            new ArrayList<>(Arrays.asList(TEST_OBJECT)));
+
+    @Test
+    public void testGetIdOfItem() {
+        Object id = dataProvider.fetch(new Query<>()).findFirst()
+                .map(dataProvider::getId).get();
+        Assert.assertEquals("DataProvider not using correct identifier getter",
+                TEST_OBJECT.getId(), id);
+    }
+
+    @Test
+    public void testGetIdOfReplacementItem() {
+        Assert.assertFalse("Test object was stale before making any changes.",
+                dataProvider.isStale(TEST_OBJECT));
+
+        dataProvider.refreshItem(new StrBean("Replacement TestObject", 10, -2));
+
+        StrBean fromDataProvider = dataProvider.fetch(new Query<>()).findFirst()
+                .get();
+        Object id = dataProvider.getId(fromDataProvider);
+
+        Assert.assertNotEquals("DataProvider did not return the replacement",
+                TEST_OBJECT, fromDataProvider);
+
+        Assert.assertEquals("DataProvider not using correct identifier getter",
+                TEST_OBJECT.getId(), id);
+
+        Assert.assertTrue("Old test object should be stale",
+                dataProvider.isStale(TEST_OBJECT));
+    }
+
+}
index b596ccdde569ceee0c0894360a55141547855af5..6deb1c1af67fc46221e849b0f9efe314cb9b54f6 100644 (file)
@@ -6,7 +6,7 @@ import java.util.List;
 import java.util.Objects;
 import java.util.Random;
 
-class StrBean implements Serializable {
+public class StrBean implements Serializable {
 
     private static final String[] values = new String[] { "Foo", "Bar", "Baz" };
 
diff --git a/server/src/test/java/com/vaadin/tests/data/selection/AbstractStaleSelectionTest.java b/server/src/test/java/com/vaadin/tests/data/selection/AbstractStaleSelectionTest.java
new file mode 100644 (file)
index 0000000..45e25cc
--- /dev/null
@@ -0,0 +1,68 @@
+package com.vaadin.tests.data.selection;
+
+import java.util.List;
+import java.util.concurrent.Future;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+
+import com.vaadin.data.provider.ReplaceListDataProvider;
+import com.vaadin.data.provider.StrBean;
+import com.vaadin.server.VaadinRequest;
+import com.vaadin.server.VaadinSession;
+import com.vaadin.tests.util.AlwaysLockedVaadinSession;
+import com.vaadin.ui.AbstractListing;
+import com.vaadin.ui.UI;
+
+@RunWith(Parameterized.class)
+public abstract class AbstractStaleSelectionTest<S extends AbstractListing<StrBean>> {
+
+    protected ReplaceListDataProvider dataProvider;
+    protected final List<StrBean> data = StrBean.generateRandomBeans(2);
+
+    @Parameter(0)
+    public String name;
+
+    @Parameter(1)
+    public S select;
+
+    @Before
+    public void setUp() {
+        dataProvider = new ReplaceListDataProvider(data);
+
+        final VaadinSession application = new AlwaysLockedVaadinSession(null);
+        final UI uI = new UI() {
+            @Override
+            protected void init(VaadinRequest request) {
+            }
+
+            @Override
+            public VaadinSession getSession() {
+                return application;
+            }
+
+            @Override
+            public Future<Void> access(Runnable runnable) {
+                runnable.run();
+                return null;
+            }
+        };
+        uI.setContent(select);
+        uI.attach();
+        select.getDataCommunicator().setDataProvider(dataProvider, null);
+    }
+
+    protected final void assertIsStale(StrBean bean) {
+        Assert.assertTrue("Bean with id " + bean.getId() + " should be stale.",
+                dataProvider.isStale(bean));
+    }
+
+    protected final void assertNotStale(StrBean bean) {
+        Assert.assertFalse(
+                "Bean with id " + bean.getId() + " should not be stale.",
+                dataProvider.isStale(bean));
+    }
+}
diff --git a/server/src/test/java/com/vaadin/tests/data/selection/GridStaleElementTest.java b/server/src/test/java/com/vaadin/tests/data/selection/GridStaleElementTest.java
new file mode 100644 (file)
index 0000000..64b7ecb
--- /dev/null
@@ -0,0 +1,116 @@
+package com.vaadin.tests.data.selection;
+
+import java.util.List;
+import java.util.concurrent.Future;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.vaadin.data.provider.ReplaceListDataProvider;
+import com.vaadin.data.provider.StrBean;
+import com.vaadin.server.VaadinRequest;
+import com.vaadin.server.VaadinSession;
+import com.vaadin.tests.util.AlwaysLockedVaadinSession;
+import com.vaadin.ui.Grid;
+import com.vaadin.ui.Grid.SelectionMode;
+import com.vaadin.ui.UI;
+import com.vaadin.ui.components.grid.GridSelectionModel;
+
+public class GridStaleElementTest {
+
+    private Grid<StrBean> grid = new Grid<>();
+    private ReplaceListDataProvider dataProvider;
+    private List<StrBean> data = StrBean.generateRandomBeans(2);
+
+    @Before
+    public void setUp() {
+        // Make Grid attached to UI to make DataCommunicator do it's magic.
+        final VaadinSession application = new AlwaysLockedVaadinSession(null);
+        final UI uI = new UI() {
+            @Override
+            protected void init(VaadinRequest request) {
+            }
+
+            @Override
+            public VaadinSession getSession() {
+                return application;
+            }
+
+            @Override
+            public Future<Void> access(Runnable runnable) {
+                runnable.run();
+                return null;
+            }
+        };
+        uI.setContent(grid);
+        uI.attach();
+        dataProvider = new ReplaceListDataProvider(data);
+        grid.setDataProvider(dataProvider);
+    }
+
+    @Test
+    public void testGridMultiSelectionUpdateOnRefreshItem() {
+        StrBean toReplace = data.get(0);
+        assertNotStale(toReplace);
+
+        GridSelectionModel<StrBean> model = grid
+                .setSelectionMode(SelectionMode.MULTI);
+        model.select(toReplace);
+
+        StrBean replacement = new StrBean("Replacement bean", toReplace.getId(),
+                -1);
+        dataProvider.refreshItem(replacement);
+
+        assertStale(toReplace);
+        model.getSelectedItems()
+                .forEach(item -> Assert.assertFalse(
+                        "Selection should not contain stale values",
+                        dataProvider.isStale(item)));
+
+        Object oldId = dataProvider.getId(toReplace);
+        Assert.assertTrue("Selection did not contain an item with matching Id.",
+                model.getSelectedItems().stream().map(dataProvider::getId)
+                        .anyMatch(oldId::equals));
+        Assert.assertTrue("Stale element is not considered selected.",
+                model.isSelected(toReplace));
+    }
+
+    @Test
+    public void testGridSingleSelectionUpdateOnRefreshItem() {
+        StrBean toReplace = data.get(0);
+        assertNotStale(toReplace);
+
+        GridSelectionModel<StrBean> model = grid
+                .setSelectionMode(SelectionMode.SINGLE);
+        model.select(toReplace);
+
+        StrBean replacement = new StrBean("Replacement bean", toReplace.getId(),
+                -1);
+        dataProvider.refreshItem(replacement);
+
+        assertStale(toReplace);
+        model.getSelectedItems()
+                .forEach(i -> Assert.assertFalse(
+                        "Selection should not contain stale values",
+                        dataProvider.isStale(i)));
+
+        Assert.assertTrue("Selection did not contain an item with matching Id.",
+                model.getSelectedItems().stream().map(dataProvider::getId)
+                        .filter(i -> dataProvider.getId(toReplace).equals(i))
+                        .findFirst().isPresent());
+        Assert.assertTrue("Stale element is not considered selected.",
+                model.isSelected(toReplace));
+    }
+
+    private void assertNotStale(StrBean bean) {
+        Assert.assertFalse(
+                "Bean with id " + bean.getId() + " should not be stale.",
+                dataProvider.isStale(bean));
+    }
+
+    private void assertStale(StrBean bean) {
+        Assert.assertTrue("Bean with id " + bean.getId() + " should be stale.",
+                dataProvider.isStale(bean));
+    }
+}
diff --git a/server/src/test/java/com/vaadin/tests/data/selection/StaleMultiSelectionTest.java b/server/src/test/java/com/vaadin/tests/data/selection/StaleMultiSelectionTest.java
new file mode 100644 (file)
index 0000000..9769ec3
--- /dev/null
@@ -0,0 +1,54 @@
+package com.vaadin.tests.data.selection;
+
+import java.util.Collection;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runners.Parameterized.Parameters;
+
+import com.vaadin.data.provider.StrBean;
+import com.vaadin.ui.AbstractMultiSelect;
+import com.vaadin.ui.CheckBoxGroup;
+import com.vaadin.ui.ListSelect;
+import com.vaadin.ui.TwinColSelect;
+
+public class StaleMultiSelectionTest
+        extends AbstractStaleSelectionTest<AbstractMultiSelect<StrBean>> {
+
+    @Test
+    public void testSelectionUpdateOnRefreshItem() {
+        StrBean toReplace = data.get(0);
+        assertNotStale(toReplace);
+
+        select.select(toReplace);
+
+        StrBean replacement = new StrBean("Replacement bean", toReplace.getId(),
+                -1);
+        dataProvider.refreshItem(replacement);
+
+        assertIsStale(toReplace);
+        select.getSelectedItems()
+                .forEach(item -> Assert.assertFalse(
+                        "Selection should not contain stale values",
+                        dataProvider.isStale(item)));
+
+        Object oldId = dataProvider.getId(toReplace);
+        Assert.assertTrue("Selection did not contain an item with matching Id.",
+                select.getSelectedItems().stream().map(dataProvider::getId)
+                        .anyMatch(oldId::equals));
+        Assert.assertTrue("Stale element is not considered selected.",
+                select.isSelected(toReplace));
+    }
+
+    @Parameters(name = "{0}")
+    public static Collection<Object[]> getParams() {
+        return Stream
+                .of(new ListSelect<>(), new TwinColSelect<>(),
+                        new CheckBoxGroup<>())
+                .map(component -> new Object[] {
+                        component.getClass().getSimpleName(), component })
+                .collect(Collectors.toList());
+    }
+}
diff --git a/server/src/test/java/com/vaadin/tests/data/selection/StaleSingleSelectionTest.java b/server/src/test/java/com/vaadin/tests/data/selection/StaleSingleSelectionTest.java
new file mode 100644 (file)
index 0000000..e8b354d
--- /dev/null
@@ -0,0 +1,48 @@
+package com.vaadin.tests.data.selection;
+
+import java.util.Collection;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runners.Parameterized.Parameters;
+
+import com.vaadin.data.provider.StrBean;
+import com.vaadin.ui.AbstractSingleSelect;
+import com.vaadin.ui.ComboBox;
+import com.vaadin.ui.NativeSelect;
+import com.vaadin.ui.RadioButtonGroup;
+
+public class StaleSingleSelectionTest
+        extends AbstractStaleSelectionTest<AbstractSingleSelect<StrBean>> {
+
+    @Test
+    public void testGridSingleSelectionUpdateOnRefreshItem() {
+        StrBean toReplace = data.get(0);
+        assertNotStale(toReplace);
+
+        select.setValue(toReplace);
+
+        StrBean replacement = new StrBean("Replacement bean", toReplace.getId(),
+                -1);
+        dataProvider.refreshItem(replacement);
+
+        assertIsStale(toReplace);
+        Assert.assertFalse("Selection should not contain stale values",
+                dataProvider.isStale(select.getValue()));
+
+        Assert.assertEquals("Selected item id did not match original.",
+                toReplace.getId(), dataProvider.getId(select.getValue()));
+    }
+
+    @Parameters(name = "{0}")
+    public static Collection<Object[]> getParams() {
+        return Stream
+                .of(new NativeSelect<>(), new ComboBox<>(),
+                        new RadioButtonGroup<>())
+                .map(c -> new Object[] { c.getClass().getSimpleName(), c })
+                .collect(Collectors.toList());
+    }
+
+}
index 8a33cae95ddba4b55383bdcc063c210477d060fa..8cc8013618b84e4d833d86bb03870de02460e3a5 100644 (file)
@@ -19,16 +19,6 @@ public class ReplaceDataProvider extends AbstractTestUI {
             this.hash = hash;
             someField = "a";
         }
-
-        @Override
-        public int hashCode() {
-            return hash;
-        }
-
-        @Override
-        public boolean equals(Object obj) {
-            return true;
-        }
     }
 
     @Override