diff options
author | Teemu Suo-Anttila <tsuoanttila@users.noreply.github.com> | 2017-01-25 12:27:44 +0200 |
---|---|---|
committer | Denis <denis@vaadin.com> | 2017-01-25 12:27:44 +0200 |
commit | 294ca0a2f5c29b65f9c4dc5887bfa0abc1cb7c7f (patch) | |
tree | 0a4630122dadc632e6508c85e4e79c717508de85 /server/src/main | |
parent | be694984fb35262b32c89be075e6d4a059931b62 (diff) | |
download | vaadin-framework-294ca0a2f5c29b65f9c4dc5887bfa0abc1cb7c7f.tar.gz vaadin-framework-294ca0a2f5c29b65f9c4dc5887bfa0abc1cb7c7f.zip |
Add APIs to inform components of stale objects in DataProvider (#8271)
* 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
Diffstat (limited to 'server/src/main')
14 files changed, 267 insertions, 34 deletions
diff --git a/server/src/main/java/com/vaadin/data/provider/AbstractDataProvider.java b/server/src/main/java/com/vaadin/data/provider/AbstractDataProvider.java index 3319bfb382..c87e18e1c2 100644 --- a/server/src/main/java/com/vaadin/data/provider/AbstractDataProvider.java +++ b/server/src/main/java/com/vaadin/data/provider/AbstractDataProvider.java @@ -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(); } diff --git a/server/src/main/java/com/vaadin/data/provider/BackEndDataProvider.java b/server/src/main/java/com/vaadin/data/provider/BackEndDataProvider.java index 36d875a21a..b50b5c7fc8 100644 --- a/server/src/main/java/com/vaadin/data/provider/BackEndDataProvider.java +++ b/server/src/main/java/com/vaadin/data/provider/BackEndDataProvider.java @@ -71,5 +71,4 @@ public interface BackEndDataProvider<T, F> extends DataProvider<T, F> { default boolean isInMemory() { return false; } - } diff --git a/server/src/main/java/com/vaadin/data/provider/CallbackDataProvider.java b/server/src/main/java/com/vaadin/data/provider/CallbackDataProvider.java index d83c1f1934..2f68e57e06 100644 --- a/server/src/main/java/com/vaadin/data/provider/CallbackDataProvider.java +++ b/server/src/main/java/com/vaadin/data/provider/CallbackDataProvider.java @@ -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; + } } diff --git a/server/src/main/java/com/vaadin/data/provider/DataChangeEvent.java b/server/src/main/java/com/vaadin/data/provider/DataChangeEvent.java index 86ad9235a6..93def4f810 100644 --- a/server/src/main/java/com/vaadin/data/provider/DataChangeEvent.java +++ b/server/src/main/java/com/vaadin/data/provider/DataChangeEvent.java @@ -16,18 +16,57 @@ 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(); } - } diff --git a/server/src/main/java/com/vaadin/data/provider/DataCommunicator.java b/server/src/main/java/com/vaadin/data/provider/DataCommunicator.java index 021623555e..9080ad3442 100644 --- a/server/src/main/java/com/vaadin/data/provider/DataCommunicator.java +++ b/server/src/main/java/com/vaadin/data/provider/DataCommunicator.java @@ -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() { diff --git a/server/src/main/java/com/vaadin/data/provider/DataGenerator.java b/server/src/main/java/com/vaadin/data/provider/DataGenerator.java index 5e88f7ff66..c5a5a6b9c9 100644 --- a/server/src/main/java/com/vaadin/data/provider/DataGenerator.java +++ b/server/src/main/java/com/vaadin/data/provider/DataGenerator.java @@ -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) { + } } diff --git a/server/src/main/java/com/vaadin/data/provider/DataKeyMapper.java b/server/src/main/java/com/vaadin/data/provider/DataKeyMapper.java index 63bd2d92f8..0ec6748a85 100644 --- a/server/src/main/java/com/vaadin/data/provider/DataKeyMapper.java +++ b/server/src/main/java/com/vaadin/data/provider/DataKeyMapper.java @@ -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); } diff --git a/server/src/main/java/com/vaadin/data/provider/DataProvider.java b/server/src/main/java/com/vaadin/data/provider/DataProvider.java index 380a1f4809..f9c3c80146 100644 --- a/server/src/main/java/com/vaadin/data/provider/DataProvider.java +++ b/server/src/main/java/com/vaadin/data/provider/DataProvider.java @@ -88,12 +88,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. * <p> @@ -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 diff --git a/server/src/main/java/com/vaadin/data/provider/DataProviderListener.java b/server/src/main/java/com/vaadin/data/provider/DataProviderListener.java index 582bdc1d86..da97e34c80 100644 --- a/server/src/main/java/com/vaadin/data/provider/DataProviderListener.java +++ b/server/src/main/java/com/vaadin/data/provider/DataProviderListener.java @@ -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); } diff --git a/server/src/main/java/com/vaadin/data/provider/DataProviderWrapper.java b/server/src/main/java/com/vaadin/data/provider/DataProviderWrapper.java index 1eabb67abe..5195aa7837 100644 --- a/server/src/main/java/com/vaadin/data/provider/DataProviderWrapper.java +++ b/server/src/main/java/com/vaadin/data/provider/DataProviderWrapper.java @@ -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); } diff --git a/server/src/main/java/com/vaadin/server/KeyMapper.java b/server/src/main/java/com/vaadin/server/KeyMapper.java index 471fea3e69..7ed723be66 100644 --- a/server/src/main/java/com/vaadin/server/KeyMapper.java +++ b/server/src/main/java/com/vaadin/server/KeyMapper.java @@ -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); + }); + } } diff --git a/server/src/main/java/com/vaadin/ui/AbstractMultiSelect.java b/server/src/main/java/com/vaadin/ui/AbstractMultiSelect.java index 22df69da5c..9947067288 100644 --- a/server/src/main/java/com/vaadin/ui/AbstractMultiSelect.java +++ b/server/src/main/java/com/vaadin/ui/AbstractMultiSelect.java @@ -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; + } + } + } } diff --git a/server/src/main/java/com/vaadin/ui/components/grid/MultiSelectionModelImpl.java b/server/src/main/java/com/vaadin/ui/components/grid/MultiSelectionModelImpl.java index 551918ef85..97f79a4655 100644 --- a/server/src/main/java/com/vaadin/ui/components/grid/MultiSelectionModelImpl.java +++ b/server/src/main/java/com/vaadin/ui/components/grid/MultiSelectionModelImpl.java @@ -15,9 +15,12 @@ */ 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; + } + } + } } diff --git a/server/src/main/java/com/vaadin/ui/components/grid/SingleSelectionModelImpl.java b/server/src/main/java/com/vaadin/ui/components/grid/SingleSelectionModelImpl.java index 42ca337e65..cd985f473a 100644 --- a/server/src/main/java/com/vaadin/ui/components/grid/SingleSelectionModelImpl.java +++ b/server/src/main/java/com/vaadin/ui/components/grid/SingleSelectionModelImpl.java @@ -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)); + } } |