diff --git a/vaadin-combo-box-flow-parent/vaadin-combo-box-flow/pom.xml b/vaadin-combo-box-flow-parent/vaadin-combo-box-flow/pom.xml index e1cdf58be1f..2048653789c 100644 --- a/vaadin-combo-box-flow-parent/vaadin-combo-box-flow/pom.xml +++ b/vaadin-combo-box-flow-parent/vaadin-combo-box-flow/pom.xml @@ -44,6 +44,12 @@ vaadin-renderer-flow ${project.version} + + com.vaadin + vaadin-spring + ${flow.version} + true + jakarta.platform jakarta.jakartaee-web-api @@ -63,6 +69,13 @@ slf4j-simple test + + + org.springframework.data + spring-data-commons + ${spring-data-commons.version} + true + diff --git a/vaadin-combo-box-flow-parent/vaadin-combo-box-flow/src/main/java/com/vaadin/flow/component/combobox/ComboBoxBase.java b/vaadin-combo-box-flow-parent/vaadin-combo-box-flow/src/main/java/com/vaadin/flow/component/combobox/ComboBoxBase.java index ff6b6c476aa..543ed094d9d 100644 --- a/vaadin-combo-box-flow-parent/vaadin-combo-box-flow/src/main/java/com/vaadin/flow/component/combobox/ComboBoxBase.java +++ b/vaadin-combo-box-flow-parent/vaadin-combo-box-flow/src/main/java/com/vaadin/flow/component/combobox/ComboBoxBase.java @@ -15,11 +15,17 @@ */ package com.vaadin.flow.component.combobox; +import java.io.Serializable; import java.util.Collection; +import java.util.List; import java.util.Locale; import java.util.Objects; import java.util.Optional; import java.util.function.Function; +import java.util.stream.Stream; + +import org.slf4j.LoggerFactory; +import org.springframework.data.domain.Pageable; import com.vaadin.flow.component.AbstractField; import com.vaadin.flow.component.AbstractSinglePropertyField; @@ -65,6 +71,7 @@ import com.vaadin.flow.data.provider.InMemoryDataProvider; import com.vaadin.flow.data.provider.ListDataProvider; import com.vaadin.flow.data.provider.ListDataView; +import com.vaadin.flow.data.provider.Query; import com.vaadin.flow.data.renderer.Renderer; import com.vaadin.flow.function.SerializableBiFunction; import com.vaadin.flow.function.SerializableConsumer; @@ -72,6 +79,7 @@ import com.vaadin.flow.function.SerializablePredicate; import com.vaadin.flow.function.SerializableSupplier; import com.vaadin.flow.shared.Registration; +import com.vaadin.flow.spring.data.VaadinSpringDataHelpers; /** * Provides base functionality for combo box related components, such as @@ -958,6 +966,138 @@ public ComboBoxLazyDataView setItems( return HasLazyDataView.super.setItems(fetchCallback, countCallback); } + public interface SpringData extends Serializable { + /** + * Callback interface for fetching a list of items from a backend based + * on a Spring Data Pageable and a filter string. + * + * @param + * the type of the items to fetch + */ + @FunctionalInterface + public interface FetchCallback extends Serializable { + + /** + * Fetches a list of items based on a pageable and a filter string. + * The pageable defines the paging of the items to fetch and the + * sorting. + * + * @param pageable + * the pageable that defines which items to fetch and the + * sort order + * @param filterString + * the filter string provided by the ComboBox + * @return a list of items + */ + List fetch(PAGEABLE pageable, String filterString); + } + + /** + * Callback interface for counting the number of items in a backend + * based on a Spring Data Pageable and a filter string. + */ + @FunctionalInterface + public interface CountCallback extends Serializable { + /** + * Counts the number of available items based on a pageable and a + * filter string. The pageable defines the paging of the items to + * fetch and the sorting and is provided although it is generally + * not needed for determining the number of items. + * + * @param pageable + * the pageable that defines which items to fetch and the + * sort order + * @param filterString + * the filter string provided by the ComboBox + * @return the number of available items + */ + long count(PAGEABLE pageable, String filterString); + } + } + + /** + * Supply items lazily with a callback from a backend based on a Spring Data + * Pageable. The component will automatically fetch more items and adjust + * its size until the backend runs out of items. Usage example: + *

+ * {@code comboBox.setItemsPageable((pageable, filterString) -> orderService.getOrders(pageable, filterString));} + *

+ * The returned data view object can be used for further configuration, or + * later on fetched with {@link #getLazyDataView()}. For using in-memory + * data, like {@link java.util.Collection}, use + * {@link HasListDataView#setItems(Collection)} instead. + * + * @param fetchCallback + * a function that returns a sorted list of items from the + * backend based on the given pageable + * @return a data view for further configuration + */ + public ComboBoxLazyDataView setItemsPageable( + SpringData.FetchCallback fetchCallback) { + return setItems( + query -> handleSpringFetchCallback(query, fetchCallback)); + } + + /** + * Supply items lazily with callbacks: the first one fetches a list of items + * from a backend based on a Spring Data Pageable, the second provides the + * exact count of items in the backend. Use this in case getting the count + * is cheap and the user benefits from the component showing immediately the + * exact size. Usage example: + *

+ * {@code component.setItemsPageable( + * (pageable, filterString) -> orderService.getOrders(pageable, filterString), + * (pageable, filterString) -> orderService.countOrders(filterString));} + *

+ * The returned data view object can be used for further configuration, or + * later on fetched with {@link #getLazyDataView()}. For using in-memory + * data, like {@link java.util.Collection}, use + * {@link HasListDataView#setItems(Collection)} instead. + * + * @param fetchCallback + * a function that returns a sorted list of items from the + * backend based on the given pageable and filter string + * @param countCallback + * a function that returns the number of items in the back end + * based on the filter string + * @return LazyDataView instance for further configuration + */ + public ComboBoxLazyDataView setItemsPageable( + SpringData.FetchCallback fetchCallback, + SpringData.CountCallback countCallback) { + return setItems( + query -> handleSpringFetchCallback(query, fetchCallback), + query -> handleSpringCountCallback(query, countCallback)); + } + + @SuppressWarnings("unchecked") + private static Stream handleSpringFetchCallback( + Query query, + SpringData.FetchCallback fetchCallback) { + PAGEABLE pageable = (PAGEABLE) VaadinSpringDataHelpers + .toSpringPageRequest(query); + List itemList = fetchCallback.fetch(pageable, + query.getFilter().orElse("")); + return itemList.stream(); + } + + @SuppressWarnings("unchecked") + private static int handleSpringCountCallback( + Query query, + SpringData.CountCallback countCallback) { + PAGEABLE pageable = (PAGEABLE) VaadinSpringDataHelpers + .toSpringPageRequest(query); + long count = countCallback.count(pageable, + query.getFilter().orElse("")); + if (count > Integer.MAX_VALUE) { + LoggerFactory.getLogger(ComboBoxBase.class).warn( + "The count of items in the backend ({}) exceeds the maximum supported by the ComboBox.", + count); + return Integer.MAX_VALUE; + } + return (int) count; + } + @Override public ComboBoxLazyDataView setItems( BackEndDataProvider dataProvider) { diff --git a/vaadin-combo-box-flow-parent/vaadin-combo-box-flow/src/test/java/com/vaadin/flow/component/combobox/ComboBoxSpringDataTest.java b/vaadin-combo-box-flow-parent/vaadin-combo-box-flow/src/test/java/com/vaadin/flow/component/combobox/ComboBoxSpringDataTest.java new file mode 100644 index 00000000000..8be7f513111 --- /dev/null +++ b/vaadin-combo-box-flow-parent/vaadin-combo-box-flow/src/test/java/com/vaadin/flow/component/combobox/ComboBoxSpringDataTest.java @@ -0,0 +1,128 @@ +/* + * Copyright 2000-2025 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.flow.component.combobox; + +import java.io.Serializable; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.Assert; +import org.junit.Test; + +public class ComboBoxSpringDataTest { + public static class Person implements Serializable { + private String name; + private final int born; + + public Person(String name, int born) { + this.name = name; + this.born = born; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int getBorn() { + return born; + } + } + + private static final List data = List.of(new Person("John", 1293), + new Person("Jane", 1923), new Person("Homer", 1956)); + + @Test + public void setItemsPageableNoCountNoFilter() { + AtomicInteger pageSize = new AtomicInteger(-1); + AtomicInteger pageNumber = new AtomicInteger(-1); + ComboBox comboBox = new ComboBox<>(); + comboBox.setItemsPageable((pageable, filterString) -> { + if (pageSize.get() != -1) { + throw new IllegalStateException( + "There should be only one call to the data provider"); + } + pageSize.set(pageable.getPageSize()); + pageNumber.set(pageable.getPageNumber()); + + return filteredData(filterString); + }); + + List items = comboBox.getLazyDataView().getItems().toList(); + Assert.assertEquals(3, items.size()); + Assert.assertEquals(0, pageNumber.get()); + Assert.assertTrue(pageSize.get() > 0); + Assert.assertEquals("Homer", items.get(2).getName()); + } + + @Test + public void setItemsPageableNoCountFilter() { + AtomicInteger pageSize = new AtomicInteger(-1); + AtomicInteger pageNumber = new AtomicInteger(-1); + ComboBox comboBox = new ComboBox<>(); + comboBox.setItemsPageable((pageable, filterString) -> { + if (pageSize.get() != -1) { + throw new IllegalStateException( + "There should be only one call to the data provider"); + } + pageSize.set(pageable.getPageSize()); + pageNumber.set(pageable.getPageNumber()); + + return filteredData(filterString); + }); + comboBox.getDataController().setRequestedRange(0, 50, "J"); + + List items = comboBox.getLazyDataView().getItems().toList(); + Assert.assertEquals(2, items.size()); + Assert.assertEquals(0, pageNumber.get()); + Assert.assertTrue(pageSize.get() > 0); + Assert.assertEquals("Jane", items.get(1).getName()); + } + + @Test + public void setItemsPageableWithCount() { + AtomicInteger pageSize = new AtomicInteger(-1); + AtomicInteger pageNumber = new AtomicInteger(-1); + ComboBox comboBox = new ComboBox<>(); + comboBox.setItemsPageable((pageable, filterString) -> { + if (pageSize.get() != -1) { + throw new IllegalStateException( + "There should be only one call to the data provider"); + } + pageSize.set(pageable.getPageSize()); + pageNumber.set(pageable.getPageNumber()); + + return filteredData(filterString); + }, (pageable, filterString) -> 3L); + + List items = comboBox.getLazyDataView().getItems().toList(); + + Assert.assertEquals(3, items.size()); + Assert.assertEquals(0, pageNumber.get()); + Assert.assertTrue(pageSize.get() > 0); + Assert.assertEquals("Jane", items.get(1).getName()); + } + + private static List filteredData(String filterString) { + return data.stream() + .filter(person -> person.getName().contains(filterString)) + .toList(); + } + +} diff --git a/vaadin-combo-box-flow-parent/vaadin-combo-box-flow/src/test/java/com/vaadin/flow/component/combobox/ComboboxSerializableTest.java b/vaadin-combo-box-flow-parent/vaadin-combo-box-flow/src/test/java/com/vaadin/flow/component/combobox/ComboboxSerializableTest.java index 5054f07b7b9..53415a86c4f 100644 --- a/vaadin-combo-box-flow-parent/vaadin-combo-box-flow/src/test/java/com/vaadin/flow/component/combobox/ComboboxSerializableTest.java +++ b/vaadin-combo-box-flow-parent/vaadin-combo-box-flow/src/test/java/com/vaadin/flow/component/combobox/ComboboxSerializableTest.java @@ -18,12 +18,20 @@ import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.List; +import java.util.stream.Stream; import org.junit.Test; import com.vaadin.flow.testutil.ClassesSerializableTest; public class ComboboxSerializableTest extends ClassesSerializableTest { + + @Override + protected Stream getExcludedPatterns() { + return Stream.concat(super.getExcludedPatterns(), + Stream.of("com\\.vaadin\\.flow\\.spring\\..*")); + } + @Test public void setItems_callSetRequestedRange_comboBoxSerializable() throws Throwable {