Skip to content

Commit

Permalink
feat: Spring Data API for ComboBox (#7020)
Browse files Browse the repository at this point in the history
* feat: Spring Data API for ComboBox

* code cleanup

* cleanup tests

---------

Co-authored-by: Sascha Ißbrücker <[email protected]>
  • Loading branch information
Artur- and sissbruecker authored Jan 21, 2025
1 parent 9a34804 commit 0894923
Show file tree
Hide file tree
Showing 4 changed files with 289 additions and 0 deletions.
13 changes: 13 additions & 0 deletions vaadin-combo-box-flow-parent/vaadin-combo-box-flow/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,12 @@
<artifactId>vaadin-renderer-flow</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.vaadin</groupId>
<artifactId>vaadin-spring</artifactId>
<version>${flow.version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>jakarta.platform</groupId>
<artifactId>jakarta.jakartaee-web-api</artifactId>
Expand All @@ -63,6 +69,13 @@
<artifactId>slf4j-simple</artifactId>
<scope>test</scope>
</dependency>
<!-- Optional Spring dependencies to be able to provide Spring Data API-->
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-commons</artifactId>
<version>${spring-data-commons.version}</version>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<plugins>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -65,13 +71,15 @@
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;
import com.vaadin.flow.function.SerializableFunction;
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
Expand Down Expand Up @@ -958,6 +966,138 @@ public ComboBoxLazyDataView<TItem> 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 <T>
* the type of the items to fetch
*/
@FunctionalInterface
public interface FetchCallback<PAGEABLE, T> 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<T> 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<PAGEABLE> 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:
* <p>
* {@code comboBox.setItemsPageable((pageable, filterString) -> orderService.getOrders(pageable, filterString));}
* <p>
* 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<TItem> setItemsPageable(
SpringData.FetchCallback<Pageable, TItem> 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:
* <p>
* {@code component.setItemsPageable(
* (pageable, filterString) -> orderService.getOrders(pageable, filterString),
* (pageable, filterString) -> orderService.countOrders(filterString));}
* <p>
* 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<TItem> setItemsPageable(
SpringData.FetchCallback<Pageable, TItem> fetchCallback,
SpringData.CountCallback<Pageable> countCallback) {
return setItems(
query -> handleSpringFetchCallback(query, fetchCallback),
query -> handleSpringCountCallback(query, countCallback));
}

@SuppressWarnings("unchecked")
private static <PAGEABLE, T> Stream<T> handleSpringFetchCallback(
Query<T, String> query,
SpringData.FetchCallback<PAGEABLE, T> fetchCallback) {
PAGEABLE pageable = (PAGEABLE) VaadinSpringDataHelpers
.toSpringPageRequest(query);
List<T> itemList = fetchCallback.fetch(pageable,
query.getFilter().orElse(""));
return itemList.stream();
}

@SuppressWarnings("unchecked")
private static <PAGEABLE> int handleSpringCountCallback(
Query<?, String> query,
SpringData.CountCallback<PAGEABLE> 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<TItem> setItems(
BackEndDataProvider<TItem, String> dataProvider) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Person> 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<Person> 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<Person> 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<Person> 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<Person> 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<Person> 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<Person> 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<Person> filteredData(String filterString) {
return data.stream()
.filter(person -> person.getName().contains(filterString))
.toList();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> getExcludedPatterns() {
return Stream.concat(super.getExcludedPatterns(),
Stream.of("com\\.vaadin\\.flow\\.spring\\..*"));
}

@Test
public void setItems_callSetRequestedRange_comboBoxSerializable()
throws Throwable {
Expand Down

0 comments on commit 0894923

Please sign in to comment.