diff options
author | Teemu Suo-Anttila <teemusa@vaadin.com> | 2016-11-22 13:07:32 +0200 |
---|---|---|
committer | Vaadin Code Review <review@vaadin.com> | 2016-11-22 14:49:44 +0000 |
commit | 97062bad914ea27934f3928b07f74f60cb26fd8c (patch) | |
tree | 0e6497a45210476e1d1dc74c92f1f7e0c76e34d2 /server | |
parent | 5708366ebf49e1c5c800b4684a735d1cd1868772 (diff) | |
download | vaadin-framework-97062bad914ea27934f3928b07f74f60cb26fd8c.tar.gz vaadin-framework-97062bad914ea27934f3928b07f74f60cb26fd8c.zip |
Implement chaining of filters for data providers
Change-Id: I7b7dced73e19b9b4e4358b95878eb31fe5c87346
Diffstat (limited to 'server')
5 files changed, 292 insertions, 42 deletions
diff --git a/server/src/main/java/com/vaadin/server/data/AppendableFilterDataProvider.java b/server/src/main/java/com/vaadin/server/data/AppendableFilterDataProvider.java new file mode 100644 index 0000000000..b6c57f03da --- /dev/null +++ b/server/src/main/java/com/vaadin/server/data/AppendableFilterDataProvider.java @@ -0,0 +1,75 @@ +/* + * Copyright 2000-2016 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.server.data; + +import java.util.Objects; +import java.util.Optional; + +/** + * Interface for DataProviders that support chaining filters. + * + * @author Vaadin Ltd + * @since + * + * @param <T> + * the data provider data type + * @param <F> + * the data provider filter type + */ +public interface AppendableFilterDataProvider<T, F> extends DataProvider<T, F> { + + /** + * Applies a filter to the current chain of filters in this data provider. + * + * @param filter + * the applied filter; not {@code null} + * @return new data provider with the filter applied + */ + public default AppendableFilterDataProvider<T, F> applyFilter(F filter) { + Objects.requireNonNull(filter, "The applied filter can't be null"); + return FilteringDataProviderWrapper.chain(this, filter); + } + + /** + * Combines two filters into one. Default implementation returns the base + * filter if the optional is not present. + * <p> + * Actual implementation of the combination is done in the method + * {@link #combineFilters(Object, Object)}. + * + * @param filter1 + * the base filter; not {@code null} + * @param filter2 + * an optional filter + * @return combined filter; not {@code null} + */ + public default F combineFilters(F filter1, Optional<F> filter2) { + Objects.requireNonNull(filter1, "The base filter can't be null"); + return filter2.map(f -> combineFilters(filter1, f)).orElse(filter1); + } + + /** + * Combines two filters into one. + * + * @param filter1 + * the base filter; not {@code null} + * @param filter2 + * the filter to merge to the base filter; not {@code null} + * @return combined filter; not {@code null} + */ + public F combineFilters(F filter1, F filter2); + +} diff --git a/server/src/main/java/com/vaadin/server/data/DataProvider.java b/server/src/main/java/com/vaadin/server/data/DataProvider.java index 129fe08713..0975a02c94 100644 --- a/server/src/main/java/com/vaadin/server/data/DataProvider.java +++ b/server/src/main/java/com/vaadin/server/data/DataProvider.java @@ -18,6 +18,7 @@ package com.vaadin.server.data; import java.io.Serializable; import java.util.Arrays; import java.util.Collection; +import java.util.Objects; import java.util.stream.Stream; import com.vaadin.server.SerializableFunction; @@ -99,7 +100,8 @@ public interface DataProvider<T, F> extends Serializable { * @return wrapped data provider with provided filter */ public default DataProvider<T, Void> setFilter(F filter) { - return new FilteringDataProviderWrapper<>(this, filter); + Objects.requireNonNull(filter, "Filter can't be null"); + return FilteringDataProviderWrapper.filter(this, filter); } /** @@ -114,8 +116,8 @@ public interface DataProvider<T, F> extends Serializable { * DataProvider<Person, Predicate<Person>> dataProvider; * // ComboBox uses String as the filter type * DataProvider<Person, String> wrappedProvider = dataProvider - * .convertFilter(filterText -> { - * Predicate<Person> predicate = person -> person.getName() + * .convertFilter(filterText -> { + * Predicate<Person> predicate = person -> person.getName() * .startsWith(filterText); * return predicate; * }); @@ -123,7 +125,7 @@ public interface DataProvider<T, F> extends Serializable { * </pre> * * @param mapper - * the mapper from new filter type to old filter type + * the mapper from new filter type to old filter type; not null * * @param <M> * the filter type to map from; typically provided by a Component @@ -132,7 +134,8 @@ public interface DataProvider<T, F> extends Serializable { */ public default <M> DataProvider<T, M> convertFilter( SerializableFunction<M, F> mapper) { - return new FilteringDataProviderWrapper<>(this, mapper); + Objects.requireNonNull(mapper, "Filter mapper can't be null"); + return FilteringDataProviderWrapper.convert(this, mapper); } /** diff --git a/server/src/main/java/com/vaadin/server/data/FilteringDataProviderWrapper.java b/server/src/main/java/com/vaadin/server/data/FilteringDataProviderWrapper.java index a6807cfb26..08a76d4904 100644 --- a/server/src/main/java/com/vaadin/server/data/FilteringDataProviderWrapper.java +++ b/server/src/main/java/com/vaadin/server/data/FilteringDataProviderWrapper.java @@ -21,10 +21,12 @@ import com.vaadin.server.SerializableFunction; import com.vaadin.shared.Registration; /** - * Wrapper class for modifying filters in a query. Used to create a suitable - * {@link Query} for the underlying data provider. + * Wrapper class for modifying, chaining and replacing filters in a query. Used + * to create a suitable {@link Query} for the underlying data provider with + * correct filters. * * @author Vaadin Ltd. + * @since * * @param <T> * data provider data type @@ -33,41 +35,53 @@ import com.vaadin.shared.Registration; * @param <M> * underlying data provider filter type */ -public class FilteringDataProviderWrapper<T, F, M> +public abstract class FilteringDataProviderWrapper<T, F, M> implements DataProvider<T, F> { - private DataProvider<T, M> dataProvider; - private SerializableFunction<F, M> mapper; - private M staticFilter = null; - /** - * Constructs a filtering wrapper for a data provider with always applied - * static filter. + * Variant of data provider wrapper that supports chaining filters. * - * @param dataProvider - * the wrapped data provider - * @param filter - * the static filter + * @param <T> + * the data provider data type + * @param <F> + * the data provider filter type */ - public FilteringDataProviderWrapper(DataProvider<T, M> dataProvider, - M filter) { - this.dataProvider = dataProvider; - this.staticFilter = filter; + protected abstract static class AppendableFilterDataProviderWrapper<T, F> + extends FilteringDataProviderWrapper<T, F, F> + implements AppendableFilterDataProvider<T, F> { + + /** + * Constructs a filtering wrapper for a data provider with filter + * chaining. + * + * @param dataProvider + * the wrapped data provider + */ + protected AppendableFilterDataProviderWrapper( + AppendableFilterDataProvider<T, F> dataProvider) { + super(dataProvider); + } + + @Override + public F combineFilters(F filter1, F filter2) { + return ((AppendableFilterDataProvider<T, F>) dataProvider) + .combineFilters(filter1, filter2); + } } /** - * Constructs a filtering wrapper for a data provider with a mapping from - * one filter type to another. + * The actual data provider behind this wrapper. + */ + protected DataProvider<T, M> dataProvider; + + /** + * Constructs a filtering wrapper for a data provider. * * @param dataProvider * the wrapped data provider - * @param mapper - * the filter mapping function */ - public FilteringDataProviderWrapper(DataProvider<T, M> dataProvider, - SerializableFunction<F, M> mapper) { + protected FilteringDataProviderWrapper(DataProvider<T, M> dataProvider) { this.dataProvider = dataProvider; - this.mapper = mapper; } @Override @@ -97,10 +111,101 @@ public class FilteringDataProviderWrapper<T, F, M> t.getSortOrders(), getFilter(t))); } - private M getFilter(Query<F> query) { - if (staticFilter != null) { - return staticFilter; - } - return query.getFilter().map(mapper).orElse(null); + /** + * Gets the filter that should be used in the modified Query. + * + * @param query + * the current query + * @return filter for the modified Query + */ + protected abstract M getFilter(Query<F> query); + + /** + * Creates a data provider wrapper with a static filter set to each Query. + * This {@code DataProvider} will deliberately ignore any possible filters + * from the Query. + * + * @see DataProvider#setFilter(Object) + * + * @param dataProvider + * the underlying data provider + * @param filter + * the static filter for each query + * + * @param <T> + * data provider data type + * @param <F> + * query filter type + * + * @return wrapped data provider with static filter + */ + public static <T, F> DataProvider<T, Void> filter( + DataProvider<T, F> dataProvider, F filter) { + return new FilteringDataProviderWrapper<T, Void, F>(dataProvider) { + + @Override + protected F getFilter(Query<Void> query) { + return filter; + } + }; + } + + /** + * Creates a data provider wrapper with filter type mapping. The mapper + * function will be applied to a query filter if it is present. + * + * @see DataProvider#convertFilter(SerializableFunction) + * + * @param dataProvider + * the underlying data provider + * @param mapper + * the function to map from one filter type to another + * + * @param <T> + * data provider data type + * @param <F> + * wrapper query filter type + * @param <M> + * underlying data provider filter type + * + * @return wrapped data provider with filter conversion + */ + public static <T, F, M> DataProvider<T, F> convert( + DataProvider<T, M> dataProvider, + SerializableFunction<F, M> mapper) { + return new FilteringDataProviderWrapper<T, F, M>(dataProvider) { + + @Override + protected M getFilter(Query<F> query) { + return query.getFilter().map(mapper).orElse(null); + } + }; + } + + /** + * Creates a data provider wrapper with a chained filter. The filter will be + * combined to existing filters using + * {@link AppendableFilterDataProvider#combineFilters(Object, java.util.Optional)}. + * + * @param dataProvider + * the underlying data provider + * @param filter + * the chained filter + * + * @param <T> + * data provider data type + * @param <F> + * query filter type + * @return wrapped data provider with chained filter + */ + public static <T, F> AppendableFilterDataProvider<T, F> chain( + AppendableFilterDataProvider<T, F> dataProvider, F filter) { + return new AppendableFilterDataProviderWrapper<T, F>(dataProvider) { + + @Override + protected F getFilter(Query<F> query) { + return combineFilters(filter, query.getFilter()); + } + }; } } diff --git a/server/src/main/java/com/vaadin/server/data/ListDataProvider.java b/server/src/main/java/com/vaadin/server/data/ListDataProvider.java index 0184a2e37b..7d2fa5c1f3 100644 --- a/server/src/main/java/com/vaadin/server/data/ListDataProvider.java +++ b/server/src/main/java/com/vaadin/server/data/ListDataProvider.java @@ -31,7 +31,8 @@ import com.vaadin.server.SerializablePredicate; * data type */ public class ListDataProvider<T> - extends AbstractDataProvider<T, SerializablePredicate<T>> { + extends AbstractDataProvider<T, SerializablePredicate<T>> + implements AppendableFilterDataProvider<T, SerializablePredicate<T>> { private Comparator<T> sortOrder; private final Collection<T> backend; @@ -121,4 +122,10 @@ public class ListDataProvider<T> .count(); } + @Override + public SerializablePredicate<T> combineFilters( + SerializablePredicate<T> filter1, + SerializablePredicate<T> filter2) { + return t -> filter1.test(t) && filter2.test(t); + } } diff --git a/server/src/test/java/com/vaadin/server/data/provider/ListDataProviderTest.java b/server/src/test/java/com/vaadin/server/data/provider/ListDataProviderTest.java index 29a234061f..ec0f054da3 100644 --- a/server/src/test/java/com/vaadin/server/data/provider/ListDataProviderTest.java +++ b/server/src/test/java/com/vaadin/server/data/provider/ListDataProviderTest.java @@ -20,6 +20,10 @@ public class ListDataProviderTest { private ListDataProvider<StrBean> dataProvider; private List<StrBean> data; + private SerializablePredicate<StrBean> fooFilter = s -> s.getValue() + .equals("Foo"); + private SerializablePredicate<StrBean> gt5Filter = s -> s + .getRandomNumber() > 5; @Before public void setUp() { @@ -214,23 +218,79 @@ public class ListDataProviderTest { Assert.assertEquals("No item should match 'Zyx'", 0, dataProvider.size( new Query<>(strBean -> strBean.getValue().contains("Zyx")))); Assert.assertEquals("Unexpected number of matches for 'Foo'", 36, - dataProvider.size(new Query<>( - strBean -> strBean.getValue().contains("Foo")))); + dataProvider.size(new Query<>(fooFilter))); } @Test public void filteringListDataProvider_defaultFilter() { - SerializablePredicate<StrBean> filter = s -> s.getRandomNumber() > 4; // Intentionally lost filter type. Not actually filterable anymore. - DataProvider<StrBean, ?> filtered = dataProvider.setFilter(filter); + DataProvider<StrBean, ?> filtered = dataProvider.setFilter(gt5Filter); Assert.assertEquals("Filter not applied, unexpected item count", - dataProvider.size(new Query<>(filter)), + dataProvider.size(new Query<>(gt5Filter)), filtered.size(new Query<>())); Assert.assertEquals("Further filtering succeeded", filtered.size(new Query<>()), - filtered.size((Query) new Query<SerializablePredicate<StrBean>>( - s -> s.getValue().equals("Foo")))); + filtered.size((Query) new Query<>(fooFilter))); + } + + public void filteringListDataProvider_appliedFilters() { + Assert.assertEquals("Filtering result differ", + data.stream().filter(fooFilter).count(), + dataProvider.applyFilter(fooFilter).size(new Query<>())); + + Assert.assertEquals("Chained filtering result differ", + data.stream().filter(fooFilter.and(gt5Filter)).count(), + dataProvider.applyFilter(fooFilter) + .size(new Query<>(gt5Filter))); + } + + @Test + public void filteringListDataProvider_chainedFilters() { + Assert.assertEquals("Chained filtering result differ", + data.stream().filter(fooFilter.and(gt5Filter)).count(), + dataProvider.applyFilter(fooFilter).applyFilter(gt5Filter) + .size(new Query<>())); + } + + @Test + public void filteringListDataProvider_chainedFiltersWithOrInsteadOfAnd() { + ListDataProvider<StrBean> orFilteredDataProvider = new ListDataProvider<StrBean>( + data) { + + @Override + public SerializablePredicate<StrBean> combineFilters( + SerializablePredicate<StrBean> filter1, + SerializablePredicate<StrBean> filter2) { + return t -> filter1.test(t) || filter2.test(t); + } + }; + + Assert.assertEquals("Chained filtering result differ", + data.stream().filter(fooFilter.or(gt5Filter)).count(), + orFilteredDataProvider.applyFilter(fooFilter) + .applyFilter(gt5Filter).size(new Query<>())); } + + @Test + public void filteringListDataProvider_appliedFilterAndConverter() { + Assert.assertEquals("Filtering result differ with 'Foo'", + data.stream().filter(gt5Filter.and(fooFilter)).count(), + dataProvider.applyFilter(gt5Filter).convertFilter( + text -> strBean -> strBean.getValue().equals(text)) + .size(new Query<>("Foo"))); + + Assert.assertEquals("Filtering result differ with 'Xyz'", data.stream() + .filter(gt5Filter.and(s -> s.getValue().equals("Xyz"))).count(), + dataProvider.applyFilter(gt5Filter).convertFilter( + text -> strBean -> strBean.getValue().equals(text)) + .size(new Query<>("Xyz"))); + + Assert.assertEquals("No results should've been found", 0, + dataProvider.applyFilter(gt5Filter).convertFilter( + text -> strBean -> strBean.getValue().equals(text)) + .size(new Query<>("Zyx"))); + } + } |