personToChange.setName("Changed person");
- dataProvider.refreshAll();
+ dataProvider.refreshItem(personToChange);
});
----
}
}
----
+
+[[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);
+});
+----
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;
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));
}
/**
* @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();
}
default boolean isInMemory() {
return false;
}
-
}
import java.util.Objects;
import java.util.stream.Stream;
+import com.vaadin.data.ValueProvider;
import com.vaadin.server.SerializableFunction;
import com.vaadin.server.SerializableToIntFunction;
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
* 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
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;
+ }
}
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
* @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();
}
-
}
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;
* @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
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() {
*/
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) {
+ }
}
import java.io.Serializable;
+import com.vaadin.data.ValueProvider;
+
/**
* DataKeyMapper to map data objects to key strings.
*
* 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);
}
*/
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.
* 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
*
* @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
* @param event
* the received event, not null
*/
- void onDataChange(DataChangeEvent event);
+ void onDataChange(DataChangeEvent<T> event);
}
}
@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);
}
import java.io.Serializable;
import java.util.HashMap;
+import com.vaadin.data.ValueProvider;
import com.vaadin.data.provider.DataKeyMapper;
/**
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);
+ });
+ }
}
*/
package com.vaadin.ui;
+import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
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;
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
public void destroyAllData() {
AbstractMultiSelect.this.deselectAll();
}
+
+ @Override
+ public void refreshData(T item) {
+ refreshSelectedItem(item);
+ }
}
/**
@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())));
}
/**
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);
+
}
/**
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);
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;
+ }
+ }
+ }
}
*/
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;
}
}
- private Set<T> selection = new LinkedHashSet<>();
+ private List<T> selection = new ArrayList<>();
private SelectAllCheckBoxVisibility selectAllCheckBoxVisibility = SelectAllCheckBoxVisibility.DEFAULT;
@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
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(
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;
+ }
+ }
+ }
}
}
};
}
+
+ @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));
+ }
}
private static class TestDataProvider
extends AbstractDataProvider<Object, Object> {
+
@Override
public Stream<Object> fetch(Query<Object, Object> t) {
return null;
@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);
@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();
package com.vaadin.data.provider;
import java.util.Collections;
+import java.util.concurrent.Future;
import org.junit.Assert;
import org.junit.Test;
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;
public VaadinSession getSession() {
return session;
}
+
+ @Override
+ public Future<Void> access(Runnable runnable) {
+ runnable.run();
+ return null;
+ }
}
private static class TestDataProvider extends ListDataProvider<Object>
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;
}
}
}
+ 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));
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);
+ }
+
}
--- /dev/null
+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();
+ }
+}
--- /dev/null
+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));
+ }
+
+}
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" };
--- /dev/null
+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));
+ }
+}
--- /dev/null
+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));
+ }
+}
--- /dev/null
+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());
+ }
+}
--- /dev/null
+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());
+ }
+
+}
this.hash = hash;
someField = "a";
}
-
- @Override
- public int hashCode() {
- return hash;
- }
-
- @Override
- public boolean equals(Object obj) {
- return true;
- }
}
@Override