From 9f5c5ec7ff9500c1cbabc8924903410760f33a25 Mon Sep 17 00:00:00 2001 From: Artur Signell Date: Wed, 18 Dec 2024 11:58:41 +0200 Subject: [PATCH 01/29] feat: Add interfaces for CRUD services These were previously in Hilla but are equally useful in Flow applications --- .../main/java/com/vaadin/flow/Nonnull.java | 36 ++++ .../main/java/com/vaadin/flow/Nullable.java | 36 ++++ vaadin-spring/pom.xml | 20 ++ .../vaadin/flow/spring/data/CountService.java | 21 ++ .../spring/data/CrudRepositoryService.java | 63 ++++++ .../vaadin/flow/spring/data/CrudService.java | 7 + .../vaadin/flow/spring/data/FormService.java | 30 +++ .../vaadin/flow/spring/data/GetService.java | 30 +++ .../flow/spring/data/JpaFilterConverter.java | 87 ++++++++ .../spring/data/ListRepositoryService.java | 104 ++++++++++ .../vaadin/flow/spring/data/ListService.java | 28 +++ .../PropertyStringFilterSpecification.java | 189 ++++++++++++++++++ .../flow/spring/data/filter/AndFilter.java | 27 +++ .../flow/spring/data/filter/Filter.java | 27 +++ .../spring/data/filter/FilterTransformer.java | 125 ++++++++++++ .../flow/spring/data/filter/OrFilter.java | 28 +++ .../data/filter/PropertyStringFilter.java | 98 +++++++++ .../flow/spring/data/filter/package-info.java | 2 + .../vaadin/flow/spring/data/package-info.java | 2 + 19 files changed, 960 insertions(+) create mode 100644 flow-server/src/main/java/com/vaadin/flow/Nonnull.java create mode 100644 flow-server/src/main/java/com/vaadin/flow/Nullable.java create mode 100644 vaadin-spring/src/main/java/com/vaadin/flow/spring/data/CountService.java create mode 100644 vaadin-spring/src/main/java/com/vaadin/flow/spring/data/CrudRepositoryService.java create mode 100644 vaadin-spring/src/main/java/com/vaadin/flow/spring/data/CrudService.java create mode 100644 vaadin-spring/src/main/java/com/vaadin/flow/spring/data/FormService.java create mode 100644 vaadin-spring/src/main/java/com/vaadin/flow/spring/data/GetService.java create mode 100644 vaadin-spring/src/main/java/com/vaadin/flow/spring/data/JpaFilterConverter.java create mode 100644 vaadin-spring/src/main/java/com/vaadin/flow/spring/data/ListRepositoryService.java create mode 100644 vaadin-spring/src/main/java/com/vaadin/flow/spring/data/ListService.java create mode 100644 vaadin-spring/src/main/java/com/vaadin/flow/spring/data/PropertyStringFilterSpecification.java create mode 100644 vaadin-spring/src/main/java/com/vaadin/flow/spring/data/filter/AndFilter.java create mode 100644 vaadin-spring/src/main/java/com/vaadin/flow/spring/data/filter/Filter.java create mode 100644 vaadin-spring/src/main/java/com/vaadin/flow/spring/data/filter/FilterTransformer.java create mode 100644 vaadin-spring/src/main/java/com/vaadin/flow/spring/data/filter/OrFilter.java create mode 100644 vaadin-spring/src/main/java/com/vaadin/flow/spring/data/filter/PropertyStringFilter.java create mode 100644 vaadin-spring/src/main/java/com/vaadin/flow/spring/data/filter/package-info.java create mode 100644 vaadin-spring/src/main/java/com/vaadin/flow/spring/data/package-info.java diff --git a/flow-server/src/main/java/com/vaadin/flow/Nonnull.java b/flow-server/src/main/java/com/vaadin/flow/Nonnull.java new file mode 100644 index 00000000000..a08af410cc6 --- /dev/null +++ b/flow-server/src/main/java/com/vaadin/flow/Nonnull.java @@ -0,0 +1,36 @@ +/* + * Copyright 2000-2022 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; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation to mark either field, method, parameter or type parameter as + * non-nullable. It is used by Typescript Generator as a source of type + * nullability information. + * + * This annotation exists because the traditional `jakarta.annotation.Nonnull` + * annotation is not applicable to type parameters. + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.TYPE_USE }) +public @interface Nonnull { +} diff --git a/flow-server/src/main/java/com/vaadin/flow/Nullable.java b/flow-server/src/main/java/com/vaadin/flow/Nullable.java new file mode 100644 index 00000000000..39f13ca04ca --- /dev/null +++ b/flow-server/src/main/java/com/vaadin/flow/Nullable.java @@ -0,0 +1,36 @@ +/* + * Copyright 2000-2022 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; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation to mark either field, method, parameter or type parameter as + * nullable. It is used by Typescript Generator as a source of type nullability + * information. + * + * This annotation exists because the traditional `jakarta.annotation.Nullable` + * annotation is not applicable to type parameters. + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.TYPE_USE }) +public @interface Nullable { +} diff --git a/vaadin-spring/pom.xml b/vaadin-spring/pom.xml index 54583311dae..5a214b00dd7 100644 --- a/vaadin-spring/pom.xml +++ b/vaadin-spring/pom.xml @@ -97,6 +97,26 @@ spring-data-commons true + + org.springframework.data + spring-data-jpa + provided + + + aspectjrt + org.aspectj + + + jcl-over-slf4j + org.slf4j + + + + + jakarta.persistence + jakarta.persistence-api + provided + com.vaadin flow-data diff --git a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/CountService.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/CountService.java new file mode 100644 index 00000000000..22687e4bafb --- /dev/null +++ b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/CountService.java @@ -0,0 +1,21 @@ +package com.vaadin.flow.spring.data; + +import com.vaadin.flow.Nullable; + +import com.vaadin.flow.spring.data.filter.Filter; + +/** + * A service that can count the given type of objects with a given filter. + */ +public interface CountService { + + /** + * Counts the number of entities that match the given filter. + * + * @param filter + * the filter, or {@code null} to use no filter + * @return + */ + public long count(@Nullable Filter filter); + +} diff --git a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/CrudRepositoryService.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/CrudRepositoryService.java new file mode 100644 index 00000000000..cd587b78df2 --- /dev/null +++ b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/CrudRepositoryService.java @@ -0,0 +1,63 @@ +package com.vaadin.flow.spring.data; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.repository.CrudRepository; + +import com.vaadin.flow.Nullable; + +/** + * A service that delegates crud operations to a JPA repository. + */ +public class CrudRepositoryService & JpaSpecificationExecutor> + extends ListRepositoryService implements CrudService { + + /** + * Creates the service using the given repository. + * + * @param repository + * the JPA repository + */ + public CrudRepositoryService(R repository, + JpaFilterConverter jpaFilterConverter) { + super(repository, jpaFilterConverter); + } + + @Override + public @Nullable T save(T value) { + return getRepository().save(value); + } + + /** + * Saves the given objects and returns the (potentially) updated objects. + *

+ * The returned objects might have new ids or updated consistency versions. + * + * @param values + * the objects to save + * @return the fresh objects + */ + public List saveAll(Iterable values) { + List saved = new ArrayList<>(); + getRepository().saveAll(values).forEach(saved::add); + return saved; + } + + @Override + public void delete(ID id) { + getRepository().deleteById(id); + } + + /** + * Deletes the objects with the given ids. + * + * @param ids + * the ids of the objects to delete + */ + public void deleteAll(Iterable ids) { + getRepository().deleteAllById(ids); + } + +} diff --git a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/CrudService.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/CrudService.java new file mode 100644 index 00000000000..24a28c62469 --- /dev/null +++ b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/CrudService.java @@ -0,0 +1,7 @@ +package com.vaadin.flow.spring.data; + +/** + * A service that can create, read, update, and delete a given type of object. + */ +public interface CrudService extends ListService, FormService { +} diff --git a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/FormService.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/FormService.java new file mode 100644 index 00000000000..93360e70ecc --- /dev/null +++ b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/FormService.java @@ -0,0 +1,30 @@ +package com.vaadin.flow.spring.data; + +import com.vaadin.flow.Nullable; + +/** + * A service that can create, update, and delete a given type of object. + */ +public interface FormService { + + /** + * Saves the given object and returns the (potentially) updated object. + *

+ * If you store the object in a SQL database, the returned object might have + * a new id or updated consistency version. + * + * @param value + * the object to save + * @return the fresh object or {@code null} if no object was found to update + */ + @Nullable + T save(T value); + + /** + * Deletes the object with the given id. + * + * @param id + * the id of the object to delete + */ + void delete(ID id); +} diff --git a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/GetService.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/GetService.java new file mode 100644 index 00000000000..1f7cf3eaca1 --- /dev/null +++ b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/GetService.java @@ -0,0 +1,30 @@ +package com.vaadin.flow.spring.data; + +import java.util.Optional; + +/** + * A service that can fetch the given type of object. + */ +public interface GetService { + + /** + * Gets the object with the given id. + * + * @param id + * the id of the object + * @return the object, or an empty optional if no object with the given id + */ + Optional get(ID id); + + /** + * Checks if an object with the given id exists. + * + * @param id + * the id of the object + * @return {@code true} if the object exists, {@code false} otherwise + */ + default boolean exists(ID id) { + return get(id).isPresent(); + } + +} diff --git a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/JpaFilterConverter.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/JpaFilterConverter.java new file mode 100644 index 00000000000..e852c64af60 --- /dev/null +++ b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/JpaFilterConverter.java @@ -0,0 +1,87 @@ +package com.vaadin.flow.spring.data; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.criteria.Path; +import jakarta.persistence.criteria.Root; + +import org.springframework.data.jpa.domain.Specification; + +import com.vaadin.flow.spring.data.filter.AndFilter; +import com.vaadin.flow.spring.data.filter.Filter; +import com.vaadin.flow.spring.data.filter.OrFilter; +import com.vaadin.flow.spring.data.filter.PropertyStringFilter; + +/** + * Utility class for converting Hilla {@link Filter} specifications into JPA + * filter specifications. This class can be used to implement filtering for + * custom {@link ListService} or {@link CrudService} implementations that use + * JPA as the data source. + */ +public class JpaFilterConverter { + + private EntityManager entityManager; + + /** + * Creates a new converter using the given entity manager. + */ + public JpaFilterConverter(EntityManager entityManager) { + this.entityManager = entityManager; + } + + /** + * Converts the given filter specification into a JPA filter + * specification for the specified entity class. + *

+ * If the filter contains {@link PropertyStringFilter} instances, their + * properties, or nested property paths, need to match the structure of the + * entity class. Likewise, their filter values should be in a format that + * can be parsed into the type that the property is of. + * + * @param + * the type of the entity + * @param rawFilter + * the filter to convert + * @param entity + * the entity class + * @return a JPA filter specification for the given filter + */ + public Specification toSpec(Filter rawFilter, Class entity) { + if (rawFilter == null) { + return Specification.anyOf(); + } + if (rawFilter instanceof AndFilter filter) { + return Specification.allOf(filter.getChildren().stream() + .map(f -> toSpec(f, entity)).toList()); + } else if (rawFilter instanceof OrFilter filter) { + return Specification.anyOf(filter.getChildren().stream() + .map(f -> toSpec(f, entity)).toList()); + } else if (rawFilter instanceof PropertyStringFilter filter) { + Class javaType = extractPropertyJavaType(entity, + filter.getPropertyId()); + return new PropertyStringFilterSpecification<>(filter, javaType); + } else { + throw new IllegalArgumentException("Unknown filter type " + + rawFilter.getClass().getName()); + } + } + + private Class extractPropertyJavaType(Class entity, + String propertyId) { + if (propertyId.contains(".")) { + String[] parts = propertyId.split("\\."); + Root root = entityManager.getCriteriaBuilder().createQuery(entity) + .from(entity); + Path path = root.get(parts[0]); + int i = 1; + while (i < parts.length) { + path = path.get(parts[i]); + i++; + } + return path.getJavaType(); + } else { + return entityManager.getMetamodel().entity(entity).getAttribute(propertyId) + .getJavaType(); + } + } + +} diff --git a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/ListRepositoryService.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/ListRepositoryService.java new file mode 100644 index 00000000000..e9dec64303b --- /dev/null +++ b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/ListRepositoryService.java @@ -0,0 +1,104 @@ +package com.vaadin.flow.spring.data; + +import java.lang.reflect.Type; +import java.util.List; +import java.util.Optional; + +import com.googlecode.gentyref.GenericTypeReflector; +import org.springframework.context.ApplicationContext; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.repository.CrudRepository; + +import com.vaadin.flow.Nullable; +import com.vaadin.flow.spring.data.filter.Filter; + +/** + * A service that delegates list operations to a JPA repository. + */ +public class ListRepositoryService & JpaSpecificationExecutor> + implements ListService, GetService, CountService { + + private final JpaFilterConverter jpaFilterConverter; + + private R repository; + + private final Class entityClass; + + /** + * Creates the service using the given repository. + * + * @param repository + * the JPA repository + */ + public ListRepositoryService(R repository, + JpaFilterConverter jpaFilterConverter) { + this.jpaFilterConverter = jpaFilterConverter; + this.repository = repository; + this.entityClass = resolveEntityClass(); + } + + /** + * Accessor for the repository instance. + * + * @return the repository instance + */ + protected R getRepository() { + return repository; + } + + @Override + public List list(Pageable pageable, @Nullable Filter filter) { + Specification spec = toSpec(filter); + return getRepository().findAll(spec, pageable).getContent(); + } + + @Override + public Optional get(ID id) { + return getRepository().findById(id); + } + + @Override + public boolean exists(ID id) { + return getRepository().existsById(id); + } + + /** + * Counts the number of entities that match the given filter. + * + * @param filter + * the filter, or {@code null} to use no filter + * @return + */ + @Override + public long count(@Nullable Filter filter) { + return getRepository().count(toSpec(filter)); + } + + /** + * Converts the given filter to a JPA specification. + * + * @param filter + * the filter to convert + * @return a JPA specification + */ + protected Specification toSpec(@Nullable Filter filter) { + return jpaFilterConverter.toSpec(filter, entityClass); + } + + @SuppressWarnings("unchecked") + protected Class resolveEntityClass() { + var entityTypeParam = ListRepositoryService.class + .getTypeParameters()[0]; + Type entityType = GenericTypeReflector.getTypeParameter(getClass(), + entityTypeParam); + if (entityType == null) { + throw new IllegalStateException(String.format( + "Unable to detect the type for the class '%s' in the " + + "class '%s'.", + entityTypeParam, getClass())); + } + return (Class) GenericTypeReflector.erase(entityType); + } +} diff --git a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/ListService.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/ListService.java new file mode 100644 index 00000000000..3d0855aee17 --- /dev/null +++ b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/ListService.java @@ -0,0 +1,28 @@ +package com.vaadin.flow.spring.data; + +import java.util.List; + +import com.vaadin.flow.Nullable; +import com.vaadin.flow.Nonnull; + +import com.vaadin.flow.spring.data.filter.Filter; +import org.springframework.data.domain.Pageable; + +/** + * A service that can list the given type of object. + */ +public interface ListService { + /** + * Lists objects of the given type using the paging, sorting and filtering + * options provided in the parameters. + * + * @param pageable + * contains information about paging and sorting + * @param filter + * the filter to apply or {@code null} to not filter + * @return a list of objects or an empty list if no objects were found + */ + @Nonnull + List<@Nonnull T> list(Pageable pageable, @Nullable Filter filter); + +} diff --git a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/PropertyStringFilterSpecification.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/PropertyStringFilterSpecification.java new file mode 100644 index 00000000000..5c3618760b2 --- /dev/null +++ b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/PropertyStringFilterSpecification.java @@ -0,0 +1,189 @@ +package com.vaadin.flow.spring.data; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; + +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.Expression; +import jakarta.persistence.criteria.Path; +import jakarta.persistence.criteria.Predicate; +import jakarta.persistence.criteria.Root; + +import com.vaadin.flow.spring.data.filter.PropertyStringFilter; +import org.springframework.data.jpa.domain.Specification; + +public class PropertyStringFilterSpecification implements Specification { + + private final PropertyStringFilter filter; + private final Class javaType; + + public PropertyStringFilterSpecification(PropertyStringFilter filter, + Class javaType) { + this.filter = filter; + this.javaType = javaType; + } + + @Override + public Predicate toPredicate(Root root, CriteriaQuery query, + CriteriaBuilder criteriaBuilder) { + String value = filter.getFilterValue(); + Path propertyPath = getPath(filter.getPropertyId(), root); + if (javaType == String.class) { + Expression expr = criteriaBuilder.lower(propertyPath); + switch (filter.getMatcher()) { + case EQUALS: + return criteriaBuilder.equal(expr, value.toLowerCase()); + case CONTAINS: + return criteriaBuilder.like(expr, + "%" + value.toLowerCase() + "%"); + case GREATER_THAN: + throw new IllegalArgumentException( + "A string cannot be filtered using greater than"); + case LESS_THAN: + throw new IllegalArgumentException( + "A string cannot be filtered using less than"); + default: + break; + } + + } else if (isNumber(javaType)) { + switch (filter.getMatcher()) { + case EQUALS: + return criteriaBuilder.equal(propertyPath, value); + case CONTAINS: + throw new IllegalArgumentException( + "A number cannot be filtered using contains"); + case GREATER_THAN: + return criteriaBuilder.greaterThan(propertyPath, value); + case LESS_THAN: + return criteriaBuilder.lessThan(propertyPath, value); + default: + break; + } + } else if (isBoolean(javaType)) { + Boolean booleanValue = Boolean.valueOf(value); + switch (filter.getMatcher()) { + case EQUALS: + return criteriaBuilder.equal(propertyPath, booleanValue); + case CONTAINS: + throw new IllegalArgumentException( + "A boolean cannot be filtered using contains"); + case GREATER_THAN: + throw new IllegalArgumentException( + "A boolean cannot be filtered using greater than"); + case LESS_THAN: + throw new IllegalArgumentException( + "A boolean cannot be filtered using less than"); + default: + break; + } + } else if (isLocalDate(javaType)) { + var path = root. get(filter.getPropertyId()); + var dateValue = LocalDate.parse(value); + switch (filter.getMatcher()) { + case EQUALS: + return criteriaBuilder.equal(path, dateValue); + case CONTAINS: + throw new IllegalArgumentException( + "A date cannot be filtered using contains"); + case GREATER_THAN: + return criteriaBuilder.greaterThan(path, dateValue); + case LESS_THAN: + return criteriaBuilder.lessThan(path, dateValue); + default: + break; + } + } else if (isLocalTime(javaType)) { + var path = root. get(filter.getPropertyId()); + var timeValue = LocalTime.parse(value); + switch (filter.getMatcher()) { + case EQUALS: + return criteriaBuilder.equal(path, timeValue); + case CONTAINS: + throw new IllegalArgumentException( + "A time cannot be filtered using contains"); + case GREATER_THAN: + return criteriaBuilder.greaterThan(path, timeValue); + case LESS_THAN: + return criteriaBuilder.lessThan(path, timeValue); + default: + break; + } + } else if (isLocalDateTime(javaType)) { + var path = root. get(filter.getPropertyId()); + var dateValue = LocalDate.parse(value); + var minValue = LocalDateTime.of(dateValue, LocalTime.MIN); + var maxValue = LocalDateTime.of(dateValue, LocalTime.MAX); + switch (filter.getMatcher()) { + case EQUALS: + return criteriaBuilder.between(path, minValue, maxValue); + case CONTAINS: + throw new IllegalArgumentException( + "A datetime cannot be filtered using contains"); + case GREATER_THAN: + return criteriaBuilder.greaterThan(path, maxValue); + case LESS_THAN: + return criteriaBuilder.lessThan(path, minValue); + default: + break; + } + } else if (javaType.isEnum()) { + var enumValue = Enum.valueOf(javaType.asSubclass(Enum.class), + value); + + switch (filter.getMatcher()) { + case EQUALS: + return criteriaBuilder.equal(propertyPath, enumValue); + case CONTAINS: + throw new IllegalArgumentException( + "An enum cannot be filtered using contains"); + case GREATER_THAN: + throw new IllegalArgumentException( + "An enum cannot be filtered using greater than"); + case LESS_THAN: + throw new IllegalArgumentException( + "An enum cannot be filtered using less than"); + default: + break; + } + } + throw new IllegalArgumentException("No implementation for " + javaType + + " using " + filter.getMatcher() + "."); + } + + private boolean isNumber(Class javaType) { + return javaType == int.class || javaType == Integer.class + || javaType == long.class || javaType == Long.class + || javaType == float.class || javaType == Float.class + || javaType == double.class || javaType == Double.class; + } + + private Path getPath(String propertyId, Root root) { + String[] parts = propertyId.split("\\."); + Path path = root.get(parts[0]); + int i = 1; + while (i < parts.length) { + path = path.get(parts[i]); + i++; + } + return path; + } + + private boolean isBoolean(Class javaType) { + return javaType == boolean.class || javaType == Boolean.class; + } + + private boolean isLocalDate(Class javaType) { + return javaType == java.time.LocalDate.class; + } + + private boolean isLocalTime(Class javaType) { + return javaType == java.time.LocalTime.class; + } + + private boolean isLocalDateTime(Class javaType) { + return javaType == java.time.LocalDateTime.class; + } +} diff --git a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/filter/AndFilter.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/filter/AndFilter.java new file mode 100644 index 00000000000..a5b73fdcca0 --- /dev/null +++ b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/filter/AndFilter.java @@ -0,0 +1,27 @@ +package com.vaadin.flow.spring.data.filter; + +import java.util.List; + +/** + * A filter that requires all children to pass. + *

+ * Custom filter implementations need to handle this filter by running all child + * filters and verifying that all of them pass. + */ +public class AndFilter extends Filter { + private List children; + + public List getChildren() { + return children; + } + + public void setChildren(List children) { + this.children = children; + } + + @Override + public String toString() { + return "AndFilter [children=" + children + "]"; + } + +} diff --git a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/filter/Filter.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/filter/Filter.java new file mode 100644 index 00000000000..a4b98e715e8 --- /dev/null +++ b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/filter/Filter.java @@ -0,0 +1,27 @@ +package com.vaadin.flow.spring.data.filter; + +import java.io.Serializable; + +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonSubTypes.Type; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +/** + * Superclass for all filters to be used with CRUD services. This specific class + * is never used, instead a filter instance will be one of the following types: + *

    + *
  • {@link AndFilter} - Contains a list of nested filters, all of which need + * to pass.
  • + *
  • {@link OrFilter} - Contains a list of nested filters, of which at least + * one needs to pass.
  • + *
  • {@link PropertyStringFilter} - Matches a specific property, or nested + * property path, against a filter value, using a specific operator.
  • + *
+ */ +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY) +@JsonSubTypes({ @Type(value = OrFilter.class, name = "or"), + @Type(value = AndFilter.class, name = "and"), + @Type(value = PropertyStringFilter.class, name = "propertyString") }) +public class Filter implements Serializable { + +} diff --git a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/filter/FilterTransformer.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/filter/FilterTransformer.java new file mode 100644 index 00000000000..56581410a38 --- /dev/null +++ b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/filter/FilterTransformer.java @@ -0,0 +1,125 @@ +package com.vaadin.flow.spring.data.filter; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; + +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; + +/** + * Utility class for transforming property names in filters and pageable + * objects. + */ +public class FilterTransformer { + + private final Map mappings = new HashMap<>(); + private Function filterTransformation; + + /** + * Declares a mapping from one property name to another. If a filter or + * pageable is transformed, all occurrences of the property name will be + * replaced with the new name. + * + * @param from + * The original property name. + * @param to + * The new property name. + * @return This instance. + */ + public FilterTransformer withMapping(String from, String to) { + mappings.put(from, to); + return this; + } + + /** + * Declares a function that will be applied to all + * {@link PropertyStringFilter} instances, allowing any kind of + * customization, including a replacement of the filter itself. This can be + * used to modify the filter value in a more complex way than a simple + * mapping. + *

+ * Note: The passed in {@code filterTransformation} function is applied + * after all other mappings are applied. + * + * @param filterTransformation + * The function to apply. + * @return This instance. + */ + public FilterTransformer withFilterTransformation( + Function filterTransformation) { + this.filterTransformation = filterTransformation; + return this; + } + + /** + * Applies registered property mappings and transformation function on the + * provided filter instance. + * + * @param filter + * The filter instance to transform. + * @return The transformed filter. + */ + public Filter apply(Filter filter) { + if (filter == null) { + return null; + } + + if (filter instanceof AndFilter andFilter) { + var newAndFilter = new AndFilter(); + newAndFilter.setChildren( + andFilter.getChildren().stream().map(this::apply).toList()); + return newAndFilter; + } else if (filter instanceof OrFilter orFilter) { + var newOrFilter = new OrFilter(); + newOrFilter.setChildren( + orFilter.getChildren().stream().map(this::apply).toList()); + return newOrFilter; + } else if (filter instanceof PropertyStringFilter propertyStringFilter) { + var property = propertyStringFilter.getPropertyId(); + var mappedProperty = mappings.get(property); + + var newFilter = new PropertyStringFilter(); + newFilter.setPropertyId( + mappedProperty == null ? property : mappedProperty); + newFilter.setFilterValue(propertyStringFilter.getFilterValue()); + newFilter.setMatcher(propertyStringFilter.getMatcher()); + + if (filterTransformation != null) { + newFilter = filterTransformation.apply(newFilter); + } + + return newFilter; + } + + // unknown filters are returned as-is: they are supposed to be already + // customized according to the use case + return filter; + } + + /** + * Applies registered property mappings on the provided pageable instance. + *

+ * Note: The passed in {@code filterTransformation} function is not applied + * on pageables. + * + * @param pageable + * The pageable instance to transform. + * @return The transformed pageable. + */ + public Pageable apply(Pageable pageable) { + if (pageable == null) { + return null; + } + + var orders = pageable.getSort().stream().map(order -> { + var mappedProperty = mappings.get(order.getProperty()); + return mappedProperty == null ? order + : new Sort.Order(order.getDirection(), mappedProperty); + }).toList(); + + return PageRequest.of(pageable.getPageNumber(), pageable.getPageSize(), + Sort.by(orders)); + } +} diff --git a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/filter/OrFilter.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/filter/OrFilter.java new file mode 100644 index 00000000000..83544f81919 --- /dev/null +++ b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/filter/OrFilter.java @@ -0,0 +1,28 @@ +package com.vaadin.flow.spring.data.filter; + +import java.util.List; + +/** + * A filter that requires at least one of its children to pass. + *

+ * Custom filter implementations need to handle this filter by running all child + * filters and verifying that at least one of them passes. + */ +public class OrFilter extends Filter { + + private List children; + + public List getChildren() { + return children; + } + + public void setChildren(List children) { + this.children = children; + } + + @Override + public String toString() { + return "OrFilter [children=" + children + "]"; + } + +} diff --git a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/filter/PropertyStringFilter.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/filter/PropertyStringFilter.java new file mode 100644 index 00000000000..8111a245a76 --- /dev/null +++ b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/filter/PropertyStringFilter.java @@ -0,0 +1,98 @@ +package com.vaadin.flow.spring.data.filter; + +/** + * A filter that matches a given property, or nested property path, against a + * filter value using the specified matcher. + *

+ * Custom filter implementations need to handle this filter by: + *

    + *
  • Extracting the property value from the object being filtered using + * {@link #getPropertyId()}.
  • + *
  • Convert the string representation of the filter value from + * {@link #getFilterValue()} into a type that can be used for implementing a + * comparison.
  • + *
  • Do the actual comparison using the matcher / operator provided by + * {@link #getMatcher()}
  • + *
+ */ +public class PropertyStringFilter extends Filter { + public enum Matcher { + EQUALS, CONTAINS, LESS_THAN, GREATER_THAN; + } + + private String propertyId; + private String filterValue; + private Matcher matcher; + + /** + * Gets the property, or nested property path, to filter by. For example + * {@code "name"} or {@code "address.city"}. + * + * @return the property name + */ + public String getPropertyId() { + return propertyId; + } + + /** + * Sets the property, or nested property path, to filter by. + * + * @param propertyId + * the property name + */ + public void setPropertyId(String propertyId) { + this.propertyId = propertyId; + } + + /** + * Gets the filter value to compare against. The filter value is always + * stored as a string, but can represent multiple types of values using + * specific formats. For example, when filtering a property of type + * {@code LocalDate}, the filter value could be {@code "2020-01-01"}. The + * actual filter implementation is responsible for parsing the filter value + * into the correct type to use for querying the underlying data layer. + * + * @return the filter value + */ + public String getFilterValue() { + return filterValue; + } + + /** + * Sets the filter value to compare against. + * + * @param filterValue + * the filter value + */ + public void setFilterValue(String filterValue) { + this.filterValue = filterValue; + } + + /** + * The matcher, or operator, to use when comparing the property value to the + * filter value. + * + * @return the matcher + */ + public Matcher getMatcher() { + return matcher; + } + + /** + * Sets the matcher, or operator, to use when comparing the property value + * to the filter value. + * + * @param type + * the matcher + */ + public void setMatcher(Matcher type) { + this.matcher = type; + } + + @Override + public String toString() { + return "PropertyStringFilter [propertyId=" + propertyId + ", matcher=" + + matcher + ", filterValue=" + filterValue + "]"; + } + +} diff --git a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/filter/package-info.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/filter/package-info.java new file mode 100644 index 00000000000..e93d497fe45 --- /dev/null +++ b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/filter/package-info.java @@ -0,0 +1,2 @@ +@org.springframework.lang.NonNullApi +package com.vaadin.flow.spring.data.filter; diff --git a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/package-info.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/package-info.java new file mode 100644 index 00000000000..e3aa13fcc5e --- /dev/null +++ b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/package-info.java @@ -0,0 +1,2 @@ +@org.springframework.lang.NonNullApi +package com.vaadin.flow.spring.data; From 8ebca2539c477f4382582717a80699f624bf6c15 Mon Sep 17 00:00:00 2001 From: Artur Signell Date: Wed, 18 Dec 2024 14:19:07 +0200 Subject: [PATCH 02/29] Move jpa related classes into separate package --- .../flow/spring/data/{ => jpa}/CrudRepositoryService.java | 0 .../flow/spring/data/{ => jpa}/JpaFilterConverter.java | 0 .../flow/spring/data/{ => jpa}/ListRepositoryService.java | 6 ++++-- .../data/{ => jpa}/PropertyStringFilterSpecification.java | 0 .../java/com/vaadin/flow/spring/data/jpa/package-info.java | 2 ++ 5 files changed, 6 insertions(+), 2 deletions(-) rename vaadin-spring/src/main/java/com/vaadin/flow/spring/data/{ => jpa}/CrudRepositoryService.java (100%) rename vaadin-spring/src/main/java/com/vaadin/flow/spring/data/{ => jpa}/JpaFilterConverter.java (100%) rename vaadin-spring/src/main/java/com/vaadin/flow/spring/data/{ => jpa}/ListRepositoryService.java (94%) rename vaadin-spring/src/main/java/com/vaadin/flow/spring/data/{ => jpa}/PropertyStringFilterSpecification.java (100%) create mode 100644 vaadin-spring/src/main/java/com/vaadin/flow/spring/data/jpa/package-info.java diff --git a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/CrudRepositoryService.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/jpa/CrudRepositoryService.java similarity index 100% rename from vaadin-spring/src/main/java/com/vaadin/flow/spring/data/CrudRepositoryService.java rename to vaadin-spring/src/main/java/com/vaadin/flow/spring/data/jpa/CrudRepositoryService.java diff --git a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/JpaFilterConverter.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/jpa/JpaFilterConverter.java similarity index 100% rename from vaadin-spring/src/main/java/com/vaadin/flow/spring/data/JpaFilterConverter.java rename to vaadin-spring/src/main/java/com/vaadin/flow/spring/data/jpa/JpaFilterConverter.java diff --git a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/ListRepositoryService.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/jpa/ListRepositoryService.java similarity index 94% rename from vaadin-spring/src/main/java/com/vaadin/flow/spring/data/ListRepositoryService.java rename to vaadin-spring/src/main/java/com/vaadin/flow/spring/data/jpa/ListRepositoryService.java index e9dec64303b..63579718125 100644 --- a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/ListRepositoryService.java +++ b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/jpa/ListRepositoryService.java @@ -1,17 +1,19 @@ -package com.vaadin.flow.spring.data; +package com.vaadin.flow.spring.data.jpa; import java.lang.reflect.Type; import java.util.List; import java.util.Optional; import com.googlecode.gentyref.GenericTypeReflector; -import org.springframework.context.ApplicationContext; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.domain.Specification; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; import org.springframework.data.repository.CrudRepository; import com.vaadin.flow.Nullable; +import com.vaadin.flow.spring.data.CountService; +import com.vaadin.flow.spring.data.GetService; +import com.vaadin.flow.spring.data.ListService; import com.vaadin.flow.spring.data.filter.Filter; /** diff --git a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/PropertyStringFilterSpecification.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/jpa/PropertyStringFilterSpecification.java similarity index 100% rename from vaadin-spring/src/main/java/com/vaadin/flow/spring/data/PropertyStringFilterSpecification.java rename to vaadin-spring/src/main/java/com/vaadin/flow/spring/data/jpa/PropertyStringFilterSpecification.java diff --git a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/jpa/package-info.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/jpa/package-info.java new file mode 100644 index 00000000000..741ae7120fb --- /dev/null +++ b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/jpa/package-info.java @@ -0,0 +1,2 @@ +@org.springframework.lang.NonNullApi +package com.vaadin.flow.spring.data.jpa; From ffbdff0ec1367e75a6aecb452379e4f80041c77e Mon Sep 17 00:00:00 2001 From: Artur Signell Date: Wed, 18 Dec 2024 14:26:12 +0200 Subject: [PATCH 03/29] format --- .../flow/spring/data/jpa/JpaFilterConverter.java | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/jpa/JpaFilterConverter.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/jpa/JpaFilterConverter.java index e852c64af60..6acf45153aa 100644 --- a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/jpa/JpaFilterConverter.java +++ b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/jpa/JpaFilterConverter.java @@ -29,8 +29,8 @@ public JpaFilterConverter(EntityManager entityManager) { } /** - * Converts the given filter specification into a JPA filter - * specification for the specified entity class. + * Converts the given filter specification into a JPA filter specification + * for the specified entity class. *

* If the filter contains {@link PropertyStringFilter} instances, their * properties, or nested property paths, need to match the structure of the @@ -60,8 +60,8 @@ public Specification toSpec(Filter rawFilter, Class entity) { filter.getPropertyId()); return new PropertyStringFilterSpecification<>(filter, javaType); } else { - throw new IllegalArgumentException("Unknown filter type " - + rawFilter.getClass().getName()); + throw new IllegalArgumentException( + "Unknown filter type " + rawFilter.getClass().getName()); } } @@ -69,8 +69,8 @@ private Class extractPropertyJavaType(Class entity, String propertyId) { if (propertyId.contains(".")) { String[] parts = propertyId.split("\\."); - Root root = entityManager.getCriteriaBuilder().createQuery(entity) - .from(entity); + Root root = entityManager.getCriteriaBuilder() + .createQuery(entity).from(entity); Path path = root.get(parts[0]); int i = 1; while (i < parts.length) { @@ -79,8 +79,8 @@ private Class extractPropertyJavaType(Class entity, } return path.getJavaType(); } else { - return entityManager.getMetamodel().entity(entity).getAttribute(propertyId) - .getJavaType(); + return entityManager.getMetamodel().entity(entity) + .getAttribute(propertyId).getJavaType(); } } From e840b565990f0ca427af7bbe4543fc7af48db5dd Mon Sep 17 00:00:00 2001 From: Artur Signell Date: Wed, 18 Dec 2024 14:34:27 +0200 Subject: [PATCH 04/29] Changes VS Code left unsaved --- .../com/vaadin/flow/spring/data/jpa/CrudRepositoryService.java | 3 ++- .../com/vaadin/flow/spring/data/jpa/JpaFilterConverter.java | 2 +- .../spring/data/jpa/PropertyStringFilterSpecification.java | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/jpa/CrudRepositoryService.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/jpa/CrudRepositoryService.java index cd587b78df2..63ebcf0e8ab 100644 --- a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/jpa/CrudRepositoryService.java +++ b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/jpa/CrudRepositoryService.java @@ -1,4 +1,4 @@ -package com.vaadin.flow.spring.data; +package com.vaadin.flow.spring.data.jpa; import java.util.ArrayList; import java.util.List; @@ -7,6 +7,7 @@ import org.springframework.data.repository.CrudRepository; import com.vaadin.flow.Nullable; +import com.vaadin.flow.spring.data.CrudService; /** * A service that delegates crud operations to a JPA repository. diff --git a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/jpa/JpaFilterConverter.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/jpa/JpaFilterConverter.java index 6acf45153aa..80b7db5b491 100644 --- a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/jpa/JpaFilterConverter.java +++ b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/jpa/JpaFilterConverter.java @@ -1,4 +1,4 @@ -package com.vaadin.flow.spring.data; +package com.vaadin.flow.spring.data.jpa; import jakarta.persistence.EntityManager; import jakarta.persistence.criteria.Path; diff --git a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/jpa/PropertyStringFilterSpecification.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/jpa/PropertyStringFilterSpecification.java index 5c3618760b2..c46a0a6cd75 100644 --- a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/jpa/PropertyStringFilterSpecification.java +++ b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/jpa/PropertyStringFilterSpecification.java @@ -1,4 +1,4 @@ -package com.vaadin.flow.spring.data; +package com.vaadin.flow.spring.data.jpa; import java.time.LocalDate; import java.time.LocalDateTime; From a32c1ffddf7c93cd623e72aed3f9b6b988e62324 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petter=20Holmstr=C3=B6m?= Date: Wed, 18 Dec 2024 14:47:09 +0200 Subject: [PATCH 05/29] Make JpaFilterConverter work without the EntityManager. --- .../data/jpa/CrudRepositoryService.java | 14 ++++++- .../spring/data/jpa/JpaFilterConverter.java | 42 ++----------------- .../data/jpa/ListRepositoryService.java | 16 ++++++- .../PropertyStringFilterSpecification.java | 21 ++++++++-- 4 files changed, 48 insertions(+), 45 deletions(-) diff --git a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/jpa/CrudRepositoryService.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/jpa/CrudRepositoryService.java index 63ebcf0e8ab..901074f62e4 100644 --- a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/jpa/CrudRepositoryService.java +++ b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/jpa/CrudRepositoryService.java @@ -16,16 +16,28 @@ public class CrudRepositoryService & JpaS extends ListRepositoryService implements CrudService { /** - * Creates the service using the given repository. + * Creates the service using the given repository and filter converter. * * @param repository * the JPA repository + * @param jpaFilterConverter + * the JPA filter converter */ public CrudRepositoryService(R repository, JpaFilterConverter jpaFilterConverter) { super(repository, jpaFilterConverter); } + /** + * Creates the service using the given repository. + * + * @param repository + * the JPA repository + */ + public CrudRepositoryService(R repository) { + super(repository); + } + @Override public @Nullable T save(T value) { return getRepository().save(value); diff --git a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/jpa/JpaFilterConverter.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/jpa/JpaFilterConverter.java index 80b7db5b491..c4c0ad9f9d6 100644 --- a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/jpa/JpaFilterConverter.java +++ b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/jpa/JpaFilterConverter.java @@ -1,9 +1,5 @@ package com.vaadin.flow.spring.data.jpa; -import jakarta.persistence.EntityManager; -import jakarta.persistence.criteria.Path; -import jakarta.persistence.criteria.Root; - import org.springframework.data.jpa.domain.Specification; import com.vaadin.flow.spring.data.filter.AndFilter; @@ -14,20 +10,12 @@ /** * Utility class for converting Hilla {@link Filter} specifications into JPA * filter specifications. This class can be used to implement filtering for - * custom {@link ListService} or {@link CrudService} implementations that use - * JPA as the data source. + * custom {@link com.vaadin.flow.spring.data.ListService} or + * {@link com.vaadin.flow.spring.data.CrudService} implementations that use JPA + * as the data source. */ public class JpaFilterConverter { - private EntityManager entityManager; - - /** - * Creates a new converter using the given entity manager. - */ - public JpaFilterConverter(EntityManager entityManager) { - this.entityManager = entityManager; - } - /** * Converts the given filter specification into a JPA filter specification * for the specified entity class. @@ -56,32 +44,10 @@ public Specification toSpec(Filter rawFilter, Class entity) { return Specification.anyOf(filter.getChildren().stream() .map(f -> toSpec(f, entity)).toList()); } else if (rawFilter instanceof PropertyStringFilter filter) { - Class javaType = extractPropertyJavaType(entity, - filter.getPropertyId()); - return new PropertyStringFilterSpecification<>(filter, javaType); + return new PropertyStringFilterSpecification<>(filter); } else { throw new IllegalArgumentException( "Unknown filter type " + rawFilter.getClass().getName()); } } - - private Class extractPropertyJavaType(Class entity, - String propertyId) { - if (propertyId.contains(".")) { - String[] parts = propertyId.split("\\."); - Root root = entityManager.getCriteriaBuilder() - .createQuery(entity).from(entity); - Path path = root.get(parts[0]); - int i = 1; - while (i < parts.length) { - path = path.get(parts[i]); - i++; - } - return path.getJavaType(); - } else { - return entityManager.getMetamodel().entity(entity) - .getAttribute(propertyId).getJavaType(); - } - } - } diff --git a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/jpa/ListRepositoryService.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/jpa/ListRepositoryService.java index 63579718125..fa48eeb2dd9 100644 --- a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/jpa/ListRepositoryService.java +++ b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/jpa/ListRepositoryService.java @@ -24,15 +24,17 @@ public class ListRepositoryService & JpaS private final JpaFilterConverter jpaFilterConverter; - private R repository; + private final R repository; private final Class entityClass; /** - * Creates the service using the given repository. + * Creates the service using the given repository and filter converter. * * @param repository * the JPA repository + * @param jpaFilterConverter + * the JPA filter converter */ public ListRepositoryService(R repository, JpaFilterConverter jpaFilterConverter) { @@ -41,6 +43,16 @@ public ListRepositoryService(R repository, this.entityClass = resolveEntityClass(); } + /** + * Creates the service using the given repository. + * + * @param repository + * the JPA repository + */ + public ListRepositoryService(R repository) { + this(repository, new JpaFilterConverter()); + } + /** * Accessor for the repository instance. * diff --git a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/jpa/PropertyStringFilterSpecification.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/jpa/PropertyStringFilterSpecification.java index c46a0a6cd75..ef58b452162 100644 --- a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/jpa/PropertyStringFilterSpecification.java +++ b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/jpa/PropertyStringFilterSpecification.java @@ -17,18 +17,16 @@ public class PropertyStringFilterSpecification implements Specification { private final PropertyStringFilter filter; - private final Class javaType; - public PropertyStringFilterSpecification(PropertyStringFilter filter, - Class javaType) { + public PropertyStringFilterSpecification(PropertyStringFilter filter) { this.filter = filter; - this.javaType = javaType; } @Override public Predicate toPredicate(Root root, CriteriaQuery query, CriteriaBuilder criteriaBuilder) { String value = filter.getFilterValue(); + Class javaType = getJavaType(filter.getPropertyId(), root); Path propertyPath = getPath(filter.getPropertyId(), root); if (javaType == String.class) { Expression expr = criteriaBuilder.lower(propertyPath); @@ -171,6 +169,21 @@ private Path getPath(String propertyId, Root root) { return path; } + private Class getJavaType(String propertyId, Root root) { + if (propertyId.contains(".")) { + String[] parts = propertyId.split("\\."); + Path path = root.get(parts[0]); + int i = 1; + while (i < parts.length) { + path = path.get(parts[i]); + i++; + } + return path.getJavaType(); + } else { + return root.get(propertyId).getJavaType(); + } + } + private boolean isBoolean(Class javaType) { return javaType == boolean.class || javaType == Boolean.class; } From 7fd759017eda807a8e8f010151de4f6d414b9309 Mon Sep 17 00:00:00 2001 From: Artur Signell Date: Wed, 18 Dec 2024 14:47:17 +0200 Subject: [PATCH 06/29] Leave out FilterTransformer as it is not used anywhere --- .../spring/data/filter/FilterTransformer.java | 125 ------------------ 1 file changed, 125 deletions(-) delete mode 100644 vaadin-spring/src/main/java/com/vaadin/flow/spring/data/filter/FilterTransformer.java diff --git a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/filter/FilterTransformer.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/filter/FilterTransformer.java deleted file mode 100644 index 56581410a38..00000000000 --- a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/filter/FilterTransformer.java +++ /dev/null @@ -1,125 +0,0 @@ -package com.vaadin.flow.spring.data.filter; - -import java.util.HashMap; -import java.util.Map; -import java.util.function.Function; - -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Sort; - -/** - * Utility class for transforming property names in filters and pageable - * objects. - */ -public class FilterTransformer { - - private final Map mappings = new HashMap<>(); - private Function filterTransformation; - - /** - * Declares a mapping from one property name to another. If a filter or - * pageable is transformed, all occurrences of the property name will be - * replaced with the new name. - * - * @param from - * The original property name. - * @param to - * The new property name. - * @return This instance. - */ - public FilterTransformer withMapping(String from, String to) { - mappings.put(from, to); - return this; - } - - /** - * Declares a function that will be applied to all - * {@link PropertyStringFilter} instances, allowing any kind of - * customization, including a replacement of the filter itself. This can be - * used to modify the filter value in a more complex way than a simple - * mapping. - *

- * Note: The passed in {@code filterTransformation} function is applied - * after all other mappings are applied. - * - * @param filterTransformation - * The function to apply. - * @return This instance. - */ - public FilterTransformer withFilterTransformation( - Function filterTransformation) { - this.filterTransformation = filterTransformation; - return this; - } - - /** - * Applies registered property mappings and transformation function on the - * provided filter instance. - * - * @param filter - * The filter instance to transform. - * @return The transformed filter. - */ - public Filter apply(Filter filter) { - if (filter == null) { - return null; - } - - if (filter instanceof AndFilter andFilter) { - var newAndFilter = new AndFilter(); - newAndFilter.setChildren( - andFilter.getChildren().stream().map(this::apply).toList()); - return newAndFilter; - } else if (filter instanceof OrFilter orFilter) { - var newOrFilter = new OrFilter(); - newOrFilter.setChildren( - orFilter.getChildren().stream().map(this::apply).toList()); - return newOrFilter; - } else if (filter instanceof PropertyStringFilter propertyStringFilter) { - var property = propertyStringFilter.getPropertyId(); - var mappedProperty = mappings.get(property); - - var newFilter = new PropertyStringFilter(); - newFilter.setPropertyId( - mappedProperty == null ? property : mappedProperty); - newFilter.setFilterValue(propertyStringFilter.getFilterValue()); - newFilter.setMatcher(propertyStringFilter.getMatcher()); - - if (filterTransformation != null) { - newFilter = filterTransformation.apply(newFilter); - } - - return newFilter; - } - - // unknown filters are returned as-is: they are supposed to be already - // customized according to the use case - return filter; - } - - /** - * Applies registered property mappings on the provided pageable instance. - *

- * Note: The passed in {@code filterTransformation} function is not applied - * on pageables. - * - * @param pageable - * The pageable instance to transform. - * @return The transformed pageable. - */ - public Pageable apply(Pageable pageable) { - if (pageable == null) { - return null; - } - - var orders = pageable.getSort().stream().map(order -> { - var mappedProperty = mappings.get(order.getProperty()); - return mappedProperty == null ? order - : new Sort.Order(order.getDirection(), mappedProperty); - }).toList(); - - return PageRequest.of(pageable.getPageNumber(), pageable.getPageSize(), - Sort.by(orders)); - } -} From 2f7a5db4c23580f6a60d4ceb7a27e35bcde31052 Mon Sep 17 00:00:00 2001 From: Artur Signell Date: Wed, 18 Dec 2024 15:02:37 +0200 Subject: [PATCH 07/29] Simplify as JpaFilterConverter now only has one helper method --- .../data/jpa/CrudRepositoryService.java | 13 ------------ .../spring/data/jpa/JpaFilterConverter.java | 4 ++-- .../data/jpa/ListRepositoryService.java | 20 ++----------------- 3 files changed, 4 insertions(+), 33 deletions(-) diff --git a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/jpa/CrudRepositoryService.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/jpa/CrudRepositoryService.java index 901074f62e4..b5803d50d12 100644 --- a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/jpa/CrudRepositoryService.java +++ b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/jpa/CrudRepositoryService.java @@ -15,19 +15,6 @@ public class CrudRepositoryService & JpaSpecificationExecutor> extends ListRepositoryService implements CrudService { - /** - * Creates the service using the given repository and filter converter. - * - * @param repository - * the JPA repository - * @param jpaFilterConverter - * the JPA filter converter - */ - public CrudRepositoryService(R repository, - JpaFilterConverter jpaFilterConverter) { - super(repository, jpaFilterConverter); - } - /** * Creates the service using the given repository. * diff --git a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/jpa/JpaFilterConverter.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/jpa/JpaFilterConverter.java index c4c0ad9f9d6..49cff5ccc08 100644 --- a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/jpa/JpaFilterConverter.java +++ b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/jpa/JpaFilterConverter.java @@ -8,7 +8,7 @@ import com.vaadin.flow.spring.data.filter.PropertyStringFilter; /** - * Utility class for converting Hilla {@link Filter} specifications into JPA + * Utility class for converting {@link Filter} specifications into JPA * filter specifications. This class can be used to implement filtering for * custom {@link com.vaadin.flow.spring.data.ListService} or * {@link com.vaadin.flow.spring.data.CrudService} implementations that use JPA @@ -33,7 +33,7 @@ public class JpaFilterConverter { * the entity class * @return a JPA filter specification for the given filter */ - public Specification toSpec(Filter rawFilter, Class entity) { + public static Specification toSpec(Filter rawFilter, Class entity) { if (rawFilter == null) { return Specification.anyOf(); } diff --git a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/jpa/ListRepositoryService.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/jpa/ListRepositoryService.java index fa48eeb2dd9..251e90fd9be 100644 --- a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/jpa/ListRepositoryService.java +++ b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/jpa/ListRepositoryService.java @@ -22,8 +22,6 @@ public class ListRepositoryService & JpaSpecificationExecutor> implements ListService, GetService, CountService { - private final JpaFilterConverter jpaFilterConverter; - private final R repository; private final Class entityClass; @@ -33,26 +31,12 @@ public class ListRepositoryService & JpaS * * @param repository * the JPA repository - * @param jpaFilterConverter - * the JPA filter converter */ - public ListRepositoryService(R repository, - JpaFilterConverter jpaFilterConverter) { - this.jpaFilterConverter = jpaFilterConverter; + public ListRepositoryService(R repository) { this.repository = repository; this.entityClass = resolveEntityClass(); } - /** - * Creates the service using the given repository. - * - * @param repository - * the JPA repository - */ - public ListRepositoryService(R repository) { - this(repository, new JpaFilterConverter()); - } - /** * Accessor for the repository instance. * @@ -98,7 +82,7 @@ public long count(@Nullable Filter filter) { * @return a JPA specification */ protected Specification toSpec(@Nullable Filter filter) { - return jpaFilterConverter.toSpec(filter, entityClass); + return JpaFilterConverter.toSpec(filter, entityClass); } @SuppressWarnings("unchecked") From fcaf97499cc0361ffd70e8997db5afa2d90dd69c Mon Sep 17 00:00:00 2001 From: Artur Signell Date: Wed, 18 Dec 2024 15:38:22 +0200 Subject: [PATCH 08/29] Tests from Hilla --- .../spring/data/jpa/JpaFilterConverter.java | 3 + .../flow/spring/data/TestApplication.java | 7 + .../flow/spring/data/filter/FilterTest.java | 512 ++++++++++++++++++ .../data/filter/SecondLevelNestedObject.java | 42 ++ .../jpa/CrudRepositoryServiceJpaTest.java | 96 ++++ .../data/jpa/CrudRepositoryServiceTest.java | 393 ++++++++++++++ .../flow/spring/data/jpa/NestedObject.java | 57 ++ .../data/jpa/TestCrudRepositoryService.java | 14 + .../vaadin/flow/spring/data/jpa/TestEnum.java | 5 + .../flow/spring/data/jpa/TestObject.java | 164 ++++++ .../flow/spring/data/jpa/TestRepository.java | 9 + 11 files changed, 1302 insertions(+) create mode 100644 vaadin-spring/src/test/java/com/vaadin/flow/spring/data/TestApplication.java create mode 100644 vaadin-spring/src/test/java/com/vaadin/flow/spring/data/filter/FilterTest.java create mode 100644 vaadin-spring/src/test/java/com/vaadin/flow/spring/data/filter/SecondLevelNestedObject.java create mode 100644 vaadin-spring/src/test/java/com/vaadin/flow/spring/data/jpa/CrudRepositoryServiceJpaTest.java create mode 100644 vaadin-spring/src/test/java/com/vaadin/flow/spring/data/jpa/CrudRepositoryServiceTest.java create mode 100644 vaadin-spring/src/test/java/com/vaadin/flow/spring/data/jpa/NestedObject.java create mode 100644 vaadin-spring/src/test/java/com/vaadin/flow/spring/data/jpa/TestCrudRepositoryService.java create mode 100644 vaadin-spring/src/test/java/com/vaadin/flow/spring/data/jpa/TestEnum.java create mode 100644 vaadin-spring/src/test/java/com/vaadin/flow/spring/data/jpa/TestObject.java create mode 100644 vaadin-spring/src/test/java/com/vaadin/flow/spring/data/jpa/TestRepository.java diff --git a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/jpa/JpaFilterConverter.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/jpa/JpaFilterConverter.java index 49cff5ccc08..dda14a8733b 100644 --- a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/jpa/JpaFilterConverter.java +++ b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/jpa/JpaFilterConverter.java @@ -16,6 +16,9 @@ */ public class JpaFilterConverter { + private JpaFilterConverter() { + // Utilities only + } /** * Converts the given filter specification into a JPA filter specification * for the specified entity class. diff --git a/vaadin-spring/src/test/java/com/vaadin/flow/spring/data/TestApplication.java b/vaadin-spring/src/test/java/com/vaadin/flow/spring/data/TestApplication.java new file mode 100644 index 00000000000..56bff5fe674 --- /dev/null +++ b/vaadin-spring/src/test/java/com/vaadin/flow/spring/data/TestApplication.java @@ -0,0 +1,7 @@ +package com.vaadin.flow.spring.data; + +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class TestApplication { +} diff --git a/vaadin-spring/src/test/java/com/vaadin/flow/spring/data/filter/FilterTest.java b/vaadin-spring/src/test/java/com/vaadin/flow/spring/data/filter/FilterTest.java new file mode 100644 index 00000000000..d5cc0a9e4a5 --- /dev/null +++ b/vaadin-spring/src/test/java/com/vaadin/flow/spring/data/filter/FilterTest.java @@ -0,0 +1,512 @@ +package com.vaadin.flow.spring.data.filter; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager; +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.test.context.junit4.SpringRunner; + +import com.vaadin.flow.spring.data.filter.PropertyStringFilter.Matcher; +import com.vaadin.flow.spring.data.jpa.JpaFilterConverter; +import com.vaadin.flow.spring.data.jpa.NestedObject; +import com.vaadin.flow.spring.data.jpa.TestEnum; +import com.vaadin.flow.spring.data.jpa.TestObject; +import com.vaadin.flow.spring.data.jpa.TestRepository; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; + +@RunWith(SpringRunner.class) +@DataJpaTest() +public class FilterTest { + + @Autowired + private TestEntityManager entityManager; + + @Autowired + private TestRepository repository; + + @Test + public void filterStringPropertyUsingContains() { + setupNames("Jack", "John", "Johnny", "Polly", "Josh"); + PropertyStringFilter filter = createFilter("name", Matcher.CONTAINS, + "Jo"); + assertFilteredNames(filter, "John", "Johnny", "Josh"); + } + + @Test + public void filterStringPropertyUsingEquals() { + setupNames("Jack", "John", "Johnny", "Polly", "Josh"); + PropertyStringFilter filter = createFilter("name", Matcher.EQUALS, + "John"); + assertFilteredNames(filter, "John"); + } + + @Test(expected = InvalidDataAccessApiUsageException.class) + public void filterStringPropertyUsingLessThan() { + setupNames("Jack", "John", "Johnny", "Polly", "Josh"); + PropertyStringFilter filter = createFilter("name", Matcher.LESS_THAN, + "John"); + executeFilter(filter); + } + + @Test(expected = InvalidDataAccessApiUsageException.class) + public void filterStringPropertyUsingGreaterThan() { + setupNames("Jack", "John", "Johnny", "Polly", "Josh"); + PropertyStringFilter filter = createFilter("name", Matcher.GREATER_THAN, + "John"); + executeFilter(filter); + } + + @Test + public void filterNumberPropertyUsingContains() { + setupNumbers(); + + assertThrows(InvalidDataAccessApiUsageException.class, () -> { + PropertyStringFilter filter = createFilter("intValue", + Matcher.CONTAINS, "2"); + executeFilter(filter); + }); + + assertThrows(InvalidDataAccessApiUsageException.class, () -> { + PropertyStringFilter filter = createFilter("nullableIntValue", + Matcher.CONTAINS, "2"); + executeFilter(filter); + }); + + assertThrows(InvalidDataAccessApiUsageException.class, () -> { + PropertyStringFilter filter = createFilter("longValue", + Matcher.CONTAINS, "2"); + executeFilter(filter); + }); + + assertThrows(InvalidDataAccessApiUsageException.class, () -> { + PropertyStringFilter filter = createFilter("nullableLongValue", + Matcher.CONTAINS, "2"); + executeFilter(filter); + }); + + assertThrows(InvalidDataAccessApiUsageException.class, () -> { + PropertyStringFilter filter = createFilter("floatValue", + Matcher.CONTAINS, "2"); + executeFilter(filter); + }); + + assertThrows(InvalidDataAccessApiUsageException.class, () -> { + PropertyStringFilter filter = createFilter("nullableFloatValue", + Matcher.CONTAINS, "2"); + executeFilter(filter); + }); + + assertThrows(InvalidDataAccessApiUsageException.class, () -> { + PropertyStringFilter filter = createFilter("doubleValue", + Matcher.CONTAINS, "2"); + executeFilter(filter); + }); + + assertThrows(InvalidDataAccessApiUsageException.class, () -> { + PropertyStringFilter filter = createFilter("nullableDoubleValue", + Matcher.CONTAINS, "2"); + executeFilter(filter); + }); + } + + @Test + public void filterNumberPropertyUsingEquals() { + List created = setupNumbers(); + + PropertyStringFilter filter = createFilter("intValue", Matcher.EQUALS, + "4"); + assertFilterResult(filter, List.of(created.get(4))); + + filter = createFilter("nullableIntValue", Matcher.EQUALS, "4"); + assertFilterResult(filter, List.of(created.get(4))); + + filter = createFilter("longValue", Matcher.EQUALS, "4"); + assertFilterResult(filter, List.of(created.get(4))); + + filter = createFilter("nullableLongValue", Matcher.EQUALS, "4"); + assertFilterResult(filter, List.of(created.get(4))); + + filter = createFilter("floatValue", Matcher.EQUALS, "0.4"); + assertFilterResult(filter, List.of(created.get(4))); + + filter = createFilter("nullableFloatValue", Matcher.EQUALS, "0.4"); + assertFilterResult(filter, List.of(created.get(4))); + + filter = createFilter("doubleValue", Matcher.EQUALS, "0.4"); + assertFilterResult(filter, List.of(created.get(4))); + + filter = createFilter("nullableDoubleValue", Matcher.EQUALS, "0.4"); + assertFilterResult(filter, List.of(created.get(4))); + } + + @Test + public void filterNumberPropertyUsingLessThan() { + List created = setupNumbers(); + + PropertyStringFilter filter = createFilter("intValue", + Matcher.LESS_THAN, "4"); + assertFilterResult(filter, created.subList(0, 4)); + + filter = createFilter("nullableIntValue", Matcher.LESS_THAN, "4"); + assertFilterResult(filter, created.subList(0, 4)); + + filter = createFilter("longValue", Matcher.LESS_THAN, "4"); + assertFilterResult(filter, created.subList(0, 4)); + + filter = createFilter("nullableLongValue", Matcher.LESS_THAN, "4"); + assertFilterResult(filter, created.subList(0, 4)); + + filter = createFilter("floatValue", Matcher.LESS_THAN, "0.4"); + assertFilterResult(filter, created.subList(0, 4)); + + filter = createFilter("nullableFloatValue", Matcher.LESS_THAN, "0.4"); + assertFilterResult(filter, created.subList(0, 4)); + + filter = createFilter("doubleValue", Matcher.LESS_THAN, "0.4"); + assertFilterResult(filter, created.subList(0, 4)); + + filter = createFilter("nullableDoubleValue", Matcher.LESS_THAN, "0.4"); + assertFilterResult(filter, created.subList(0, 4)); + } + + @Test + public void filterNumberPropertyUsingGreaterThan() { + List created = setupNumbers(); + + PropertyStringFilter filter = createFilter("intValue", + Matcher.GREATER_THAN, "4"); + assertFilterResult(filter, created.subList(5, 10)); + + filter = createFilter("nullableIntValue", Matcher.GREATER_THAN, "4"); + assertFilterResult(filter, created.subList(5, 10)); + + filter = createFilter("longValue", Matcher.GREATER_THAN, "4"); + assertFilterResult(filter, created.subList(5, 10)); + + filter = createFilter("nullableLongValue", Matcher.GREATER_THAN, "4"); + assertFilterResult(filter, created.subList(5, 10)); + + filter = createFilter("floatValue", Matcher.GREATER_THAN, "0.4"); + assertFilterResult(filter, created.subList(5, 10)); + + filter = createFilter("nullableFloatValue", Matcher.GREATER_THAN, + "0.4"); + assertFilterResult(filter, created.subList(5, 10)); + + filter = createFilter("doubleValue", Matcher.GREATER_THAN, "0.4"); + assertFilterResult(filter, created.subList(5, 10)); + + filter = createFilter("nullableDoubleValue", Matcher.GREATER_THAN, + "0.4"); + assertFilterResult(filter, created.subList(5, 10)); + } + + @Test(expected = InvalidDataAccessApiUsageException.class) + public void filterBooleanPropertyUsingContains() { + setupBooleans(); + PropertyStringFilter filter = createFilter("booleanValue", + Matcher.CONTAINS, "True"); + executeFilter(filter); + } + + @Test + public void filterBooleanPropertyUsingEquals() { + setupBooleans(); + + PropertyStringFilter filter = createFilter("booleanValue", + Matcher.EQUALS, "True"); + List testObjects = executeFilter(filter); + + assertEquals(1, testObjects.size()); + Assert.assertTrue(testObjects.get(0).getBooleanValue()); + + filter = createFilter("booleanValue", Matcher.EQUALS, "False"); + testObjects = executeFilter(filter); + + assertEquals(1, testObjects.size()); + Assert.assertFalse(testObjects.get(0).getBooleanValue()); + } + + @Test(expected = InvalidDataAccessApiUsageException.class) + public void filterBooleanPropertyUsingLessThan() { + setupBooleans(); + PropertyStringFilter filter = createFilter("booleanValue", + Matcher.LESS_THAN, "True"); + executeFilter(filter); + } + + @Test(expected = InvalidDataAccessApiUsageException.class) + public void filterBooleanPropertyUsingGreaterThan() { + setupBooleans(); + PropertyStringFilter filter = createFilter("booleanValue", + Matcher.GREATER_THAN, "True"); + executeFilter(filter); + } + + @Test(expected = InvalidDataAccessApiUsageException.class) + public void filterEnumPropertyUsingContains() { + setupEnums(); + PropertyStringFilter filter = createFilter("enumValue", + Matcher.CONTAINS, TestEnum.TEST1.name()); + executeFilter(filter); + } + + @Test + public void filterEnumPropertyUsingEquals() { + setupEnums(); + + PropertyStringFilter filter = createFilter("enumValue", Matcher.EQUALS, + TestEnum.TEST1.name()); + List testObjects = executeFilter(filter); + + assertEquals(1, testObjects.size()); + Assert.assertEquals(TestEnum.TEST1, testObjects.get(0).getEnumValue()); + + filter = createFilter("enumValue", Matcher.EQUALS, + TestEnum.TEST2.name()); + testObjects = executeFilter(filter); + + assertEquals(1, testObjects.size()); + Assert.assertEquals(TestEnum.TEST2, testObjects.get(0).getEnumValue()); + } + + @Test(expected = InvalidDataAccessApiUsageException.class) + public void filterEnumPropertyUsingLessThan() { + setupBooleans(); + PropertyStringFilter filter = createFilter("enumValue", + Matcher.LESS_THAN, TestEnum.TEST1.name()); + executeFilter(filter); + } + + @Test(expected = InvalidDataAccessApiUsageException.class) + public void filterEnumPropertyUsingGreaterThan() { + setupBooleans(); + PropertyStringFilter filter = createFilter("enumValue", + Matcher.GREATER_THAN, TestEnum.TEST1.name()); + executeFilter(filter); + } + + @Test(expected = InvalidDataAccessApiUsageException.class) + public void filterUnknownEnumValue() { + setupBooleans(); + PropertyStringFilter filter = createFilter("enumValue", Matcher.EQUALS, + "FOO"); + executeFilter(filter); + } + + @Test(expected = IllegalArgumentException.class) + public void filterNonExistingProperty() { + setupNames("Jack", "John", "Johnny", "Polly", "Josh"); + PropertyStringFilter filter = createFilter("foo", Matcher.EQUALS, + "John"); + executeFilter(filter); + } + + @Test + public void basicOrFilter() { + setupNames("Jack", "John", "Johnny", "Polly", "Josh"); + PropertyStringFilter filter1 = createFilter("name", Matcher.EQUALS, + "John"); + PropertyStringFilter filter2 = createFilter("name", Matcher.EQUALS, + "Polly"); + OrFilter filter = new OrFilter(); + filter.setChildren(List.of(filter1, filter2)); + assertFilteredNames(filter, "John", "Polly"); + } + + @Test + public void basicAndFilter() { + setupNames("Jack", "John", "Johnny", "Polly", "Josh"); + PropertyStringFilter filter1 = createFilter("name", Matcher.CONTAINS, + "Joh"); + PropertyStringFilter filter2 = createFilter("name", Matcher.CONTAINS, + "nny"); + AndFilter filter = new AndFilter(); + filter.setChildren(List.of(filter1, filter2)); + assertFilteredNames(filter, "Johnny"); + } + + @Test + public void nestedPropertyFilterString() { + setupNestedObjects(); + PropertyStringFilter filter = createFilter("nestedObject.name", + Matcher.CONTAINS, "42"); + List result = executeFilter(filter); + assertEquals(1, result.size()); + TestObject testObject = result.get(0); + assertEquals("some name 1", testObject.getName()); + assertEquals(42, testObject.getNestedObject().getLuckyNumber()); + } + + @Test + public void nestedPropertyFilterNumber() { + setupNestedObjects(); + PropertyStringFilter filter = createFilter("nestedObject.luckyNumber", + Matcher.EQUALS, "84"); + List result = executeFilter(filter); + assertEquals(1, result.size()); + TestObject testObject = result.get(0); + assertEquals("some name 2", testObject.getName()); + assertEquals(84, testObject.getNestedObject().getLuckyNumber()); + } + + @Test + public void nestedPropertyFilterNumberNoResult() { + setupNestedObjects(); + PropertyStringFilter filter = createFilter("nestedObject.luckyNumber", + Matcher.EQUALS, "85"); + assertEquals(0, executeFilter(filter).size()); + } + + @Test + public void secondLevelNestedPropertyFilterString() { + setupNestedObjects(); + PropertyStringFilter filter = createFilter( + "nestedObject.secondLevelNestedObject.name", Matcher.CONTAINS, + "second level nested object 1"); + List result = executeFilter(filter); + assertEquals(1, result.size()); + TestObject testObject = result.get(0); + assertEquals("some name 1", testObject.getName()); + assertEquals(42, testObject.getNestedObject().getLuckyNumber()); + } + + @Test + public void secondLevelNestedPropertyFilterNumber() { + setupNestedObjects(); + PropertyStringFilter filter = createFilter( + "nestedObject.secondLevelNestedObject.luckyNumber", + Matcher.EQUALS, "2"); + List result = executeFilter(filter); + assertEquals(1, result.size()); + TestObject testObject = result.get(0); + assertEquals("some name 2", testObject.getName()); + assertEquals(84, testObject.getNestedObject().getLuckyNumber()); + } + + @Test + public void secondLevelNestedPropertyFilterNumberNoResult() { + setupNestedObjects(); + PropertyStringFilter filter = createFilter( + "nestedObject.secondLevelNestedObject.luckyNumber", + Matcher.EQUALS, "3"); + assertEquals(0, executeFilter(filter).size()); + } + + private PropertyStringFilter createFilter(String propertyPath, + Matcher matcher, String filterValue) { + PropertyStringFilter filter = new PropertyStringFilter(); + filter.setPropertyId(propertyPath); + filter.setFilterValue(filterValue); + filter.setMatcher(matcher); + return filter; + } + + private void assertFilteredNames(Filter filter, String... expectedNames) { + List result = executeFilter(filter); + assertEquals(expectedNames.length, result.size()); + Object[] actual = result.stream().map(TestObject::getName).toArray(); + Assert.assertArrayEquals(expectedNames, actual); + } + + private void assertFilterResult(Filter filter, List result) { + List actual = executeFilter(filter); + assertEquals(result, actual); + } + + private List executeFilter(Filter filter) { + Specification spec = JpaFilterConverter.toSpec(filter, + TestObject.class); + return repository.findAll(spec); + } + + private List setupNames(String... names) { + List created = new ArrayList<>(); + for (String name : names) { + TestObject testObject = new TestObject(); + testObject.setName(name); + created.add(entityManager.persist(testObject)); + } + entityManager.flush(); + return created; + } + + private void setupBooleans() { + TestObject testObject = new TestObject(); + testObject.setBooleanValue(true); + entityManager.persist(testObject); + testObject = new TestObject(); + testObject.setBooleanValue(false); + entityManager.persist(testObject); + entityManager.flush(); + } + + private List setupNumbers() { + List created = new ArrayList<>(); + for (int i = 0; i < 10; i++) { + TestObject testObject = new TestObject(); + testObject.setIntValue(i); + testObject.setNullableIntValue(i); + testObject.setLongValue(i); + testObject.setNullableLongValue((long) i); + testObject.setFloatValue((float) i / 10); + testObject.setNullableFloatValue((float) i / 10); + testObject.setDoubleValue((double) i / 10); + testObject.setNullableDoubleValue((double) i / 10); + entityManager.persist(testObject); + created.add(testObject); + } + entityManager.flush(); + return created; + } + + private void setupEnums() { + TestObject testObject = new TestObject(); + testObject.setEnumValue(TestEnum.TEST1); + entityManager.persist(testObject); + testObject = new TestObject(); + testObject.setEnumValue(TestEnum.TEST2); + entityManager.persist(testObject); + entityManager.flush(); + } + + private void setupNestedObjects() { + SecondLevelNestedObject secondLevelNestedObject1 = new SecondLevelNestedObject(); + secondLevelNestedObject1.setName("second level nested object 1"); + secondLevelNestedObject1.setLuckyNumber(1); + entityManager.persist(secondLevelNestedObject1); + SecondLevelNestedObject secondLevelNestedObject2 = new SecondLevelNestedObject(); + secondLevelNestedObject2.setName("second level nested object 2"); + secondLevelNestedObject2.setLuckyNumber(2); + entityManager.persist(secondLevelNestedObject2); + NestedObject nestedObject1 = new NestedObject(); + nestedObject1.setName("nested object 42"); + nestedObject1.setLuckyNumber(42); + nestedObject1.setSecondLevelNestedObject(secondLevelNestedObject1); + entityManager.persist(nestedObject1); + NestedObject nestedObject2 = new NestedObject(); + nestedObject2.setName("nested object 84"); + nestedObject2.setLuckyNumber(84); + nestedObject2.setSecondLevelNestedObject(secondLevelNestedObject2); + entityManager.persist(nestedObject2); + TestObject testObject1 = new TestObject(); + testObject1.setName("some name 1"); + testObject1.setNestedObject(nestedObject1); + entityManager.persist(testObject1); + TestObject testObject2 = new TestObject(); + testObject2.setName("some name 2"); + testObject2.setNestedObject(nestedObject2); + entityManager.persist(testObject2); + entityManager.flush(); + } + +} diff --git a/vaadin-spring/src/test/java/com/vaadin/flow/spring/data/filter/SecondLevelNestedObject.java b/vaadin-spring/src/test/java/com/vaadin/flow/spring/data/filter/SecondLevelNestedObject.java new file mode 100644 index 00000000000..901457ae31b --- /dev/null +++ b/vaadin-spring/src/test/java/com/vaadin/flow/spring/data/filter/SecondLevelNestedObject.java @@ -0,0 +1,42 @@ +package com.vaadin.flow.spring.data.filter; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; + +@Entity +public class SecondLevelNestedObject { + + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + private Integer id; + + private String name; + + private long luckyNumber; + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public long getLuckyNumber() { + return luckyNumber; + } + + public void setLuckyNumber(long luckyNumber) { + this.luckyNumber = luckyNumber; + } +} diff --git a/vaadin-spring/src/test/java/com/vaadin/flow/spring/data/jpa/CrudRepositoryServiceJpaTest.java b/vaadin-spring/src/test/java/com/vaadin/flow/spring/data/jpa/CrudRepositoryServiceJpaTest.java new file mode 100644 index 00000000000..17fbc359593 --- /dev/null +++ b/vaadin-spring/src/test/java/com/vaadin/flow/spring/data/jpa/CrudRepositoryServiceJpaTest.java @@ -0,0 +1,96 @@ +package com.vaadin.flow.spring.data.jpa; + +import java.util.ArrayList; +import java.util.List; + +import com.vaadin.flow.spring.data.filter.PropertyStringFilter; +import com.vaadin.flow.spring.data.filter.PropertyStringFilter.Matcher; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager; +import org.springframework.context.annotation.Import; +import org.springframework.data.domain.Pageable; +import org.springframework.test.context.junit4.SpringRunner; + +@RunWith(SpringRunner.class) +@DataJpaTest() +@Import(TestCrudRepositoryService.class) +public class CrudRepositoryServiceJpaTest { + + @Autowired + TestRepository jpaRepository; + @Autowired + private TestEntityManager entityManager; + @Autowired + TestCrudRepositoryService testCrudRepositoryService; + + private List testObjects; + + @Before + public void setupDB() { + String[] names = new String[] { "John", "Jeff", "Michael", "Michelle", + "Dana", "Lady" }; + testObjects = new ArrayList<>(); + for (String name : names) { + TestObject testObject = new TestObject(); + testObject.setName(name); + testObjects.add(entityManager.persist(testObject)); + } + entityManager.flush(); + } + + @Test + public void count() { + Assert.assertEquals(6, testCrudRepositoryService.count(null)); + + PropertyStringFilter filter = new PropertyStringFilter(); + filter.setPropertyId("name"); + filter.setMatcher(Matcher.CONTAINS); + filter.setFilterValue("Mich"); + Assert.assertEquals(2, testCrudRepositoryService.count(filter)); + } + + @Test + public void get() { + TestObject object = testObjects.get(2); + Assert.assertEquals(object.getName(), testCrudRepositoryService + .get(object.getId()).orElseThrow().getName()); + Assert.assertFalse( + testCrudRepositoryService.get(object.getId() + 10).isPresent()); + } + + @Test + public void exists() { + TestObject object = testObjects.get(3); + Assert.assertTrue(testCrudRepositoryService.exists(object.getId())); + Assert.assertFalse( + testCrudRepositoryService.exists(object.getId() + 10)); + } + + @Test + public void saveAll() { + TestObject o1 = new TestObject(); + o1.setName("Hello"); + TestObject o2 = new TestObject(); + o2.setName("World"); + + List saved = testCrudRepositoryService + .saveAll(List.of(o1, o2)); + Assert.assertEquals("World", saved.get(1).getName()); + Assert.assertTrue( + testCrudRepositoryService.exists(saved.get(1).getId())); + } + + @Test + public void deleteAll() { + testCrudRepositoryService.deleteAll(List.of(testObjects.get(3).getId(), + testObjects.get(4).getId())); + Assert.assertEquals(List.of("John", "Jeff", "Michael", "Lady"), + testCrudRepositoryService.list(Pageable.unpaged(), null) + .stream().map(o -> o.getName()).toList()); + } +} diff --git a/vaadin-spring/src/test/java/com/vaadin/flow/spring/data/jpa/CrudRepositoryServiceTest.java b/vaadin-spring/src/test/java/com/vaadin/flow/spring/data/jpa/CrudRepositoryServiceTest.java new file mode 100644 index 00000000000..0eb98bc56b5 --- /dev/null +++ b/vaadin-spring/src/test/java/com/vaadin/flow/spring/data/jpa/CrudRepositoryServiceTest.java @@ -0,0 +1,393 @@ +package com.vaadin.flow.spring.data.jpa; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.domain.Example; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.query.FluentQuery; +import org.springframework.stereotype.Repository; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringRunner; + +import com.vaadin.flow.spring.data.jpa.CrudRepositoryService; + +import java.util.List; +import java.util.Optional; +import java.util.function.Function; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = { + CrudRepositoryServiceTest.DefaultJpaRepositoryService.class, + CrudRepositoryServiceTest.CustomCrudRepository.class, + CrudRepositoryServiceTest.CustomCrudRepositoryService.class, + CrudRepositoryServiceTest.CustomJpaRepository.class, + CrudRepositoryServiceTest.CustomJpaRepositoryService.class }) +@EnableAutoConfiguration +public class CrudRepositoryServiceTest { + + @Repository + static class CustomCrudRepository + implements CrudRepository, + JpaSpecificationExecutor { + @Override + public Optional findOne(Specification spec) { + return Optional.empty(); + } + + @Override + public List findAll(Specification spec) { + return null; + } + + @Override + public Page findAll(Specification spec, + Pageable pageable) { + return null; + } + + @Override + public List findAll(Specification spec, + Sort sort) { + return null; + } + + @Override + public long count(Specification spec) { + return 0; + } + + @Override + public boolean exists(Specification spec) { + return false; + } + + @Override + public long delete(Specification spec) { + return 0; + } + + @Override + public R findBy( + Specification spec, + Function, R> queryFunction) { + return null; + } + + @Override + public S save(S entity) { + return null; + } + + @Override + public Iterable saveAll( + Iterable entities) { + return null; + } + + @Override + public Optional findById(Integer integer) { + return Optional.empty(); + } + + @Override + public boolean existsById(Integer integer) { + return false; + } + + @Override + public Iterable findAll() { + return null; + } + + @Override + public Iterable findAllById(Iterable integers) { + return null; + } + + @Override + public long count() { + return 0; + } + + @Override + public void deleteById(Integer integer) { + } + + @Override + public void delete(TestObject entity) { + } + + @Override + public void deleteAllById(Iterable integers) { + } + + @Override + public void deleteAll(Iterable entities) { + } + + @Override + public void deleteAll() { + } + } + + @Repository + static class CustomJpaRepository + implements JpaRepository, + JpaSpecificationExecutor { + @Override + public void flush() { + } + + @Override + public S saveAndFlush(S entity) { + return null; + } + + @Override + public List saveAllAndFlush( + Iterable entities) { + return null; + } + + @Override + public void deleteAllInBatch(Iterable entities) { + } + + @Override + public void deleteAllByIdInBatch(Iterable integers) { + } + + @Override + public void deleteAllInBatch() { + } + + @Override + public TestObject getOne(Integer integer) { + return null; + } + + @Override + public TestObject getById(Integer integer) { + return null; + } + + @Override + public TestObject getReferenceById(Integer integer) { + return null; + } + + @Override + public List findAll(Example example) { + return null; + } + + @Override + public List findAll(Example example, + Sort sort) { + return null; + } + + @Override + public Optional findOne(Specification spec) { + return Optional.empty(); + } + + @Override + public List findAll(Specification spec) { + return null; + } + + @Override + public Page findAll(Specification spec, + Pageable pageable) { + return null; + } + + @Override + public List findAll(Specification spec, + Sort sort) { + return null; + } + + @Override + public long count(Specification spec) { + return 0; + } + + @Override + public boolean exists(Specification spec) { + return false; + } + + @Override + public long delete(Specification spec) { + return 0; + } + + @Override + public R findBy( + Specification spec, + Function, R> queryFunction) { + return null; + } + + @Override + public List saveAll(Iterable entities) { + return null; + } + + @Override + public List findAll() { + return null; + } + + @Override + public List findAllById(Iterable integers) { + return null; + } + + @Override + public S save(S entity) { + return null; + } + + @Override + public Optional findById(Integer integer) { + return Optional.empty(); + } + + @Override + public boolean existsById(Integer integer) { + return false; + } + + @Override + public long count() { + return 0; + } + + @Override + public void deleteById(Integer integer) { + } + + @Override + public void delete(TestObject entity) { + } + + @Override + public void deleteAllById(Iterable integers) { + } + + @Override + public void deleteAll(Iterable entities) { + } + + @Override + public void deleteAll() { + } + + @Override + public List findAll(Sort sort) { + return null; + } + + @Override + public Page findAll(Pageable pageable) { + return null; + } + + @Override + public Optional findOne(Example example) { + return Optional.empty(); + } + + @Override + public Page findAll(Example example, + Pageable pageable) { + return null; + } + + @Override + public long count(Example example) { + return 0; + } + + @Override + public boolean exists(Example example) { + return false; + } + + @Override + public R findBy(Example example, + Function, R> queryFunction) { + return null; + } + } + + static class DefaultJpaRepositoryService + extends CrudRepositoryService { + DefaultJpaRepositoryService(TestRepository repository) { + super(repository); + } + } + + static class CustomCrudRepositoryService extends + CrudRepositoryService { + public CustomCrudRepositoryService(CustomCrudRepository repository) { + super(repository); + } + } + + static class CustomJpaRepositoryService extends + CrudRepositoryService { + public CustomJpaRepositoryService(CustomJpaRepository repository) { + super(repository); + } + } + + @Autowired + private DefaultJpaRepositoryService defaultJpaRepositoryService; + + @Autowired + private CustomCrudRepositoryService customCrudRepositoryService; + + @Autowired + private CustomJpaRepositoryService customJpaRepositoryService; + + @Test + public void when_serviceHasNoExplicitConstructor_then_getRepositoryResolvesRepositoryFromContext() { + assertNotNull(defaultJpaRepositoryService.getRepository()); + } + + @Test + public void when_serviceHasExplicitConstructor_then_getRepositoryReturnsTheProvidedRepositoryInstance() { + assertNotNull(customCrudRepositoryService.getRepository()); + assertEquals(CustomCrudRepository.class, customCrudRepositoryService + .getRepository().getClass().getSuperclass()); + } + + @Test + public void JpaRepository_Is_CompatibleWith_CrudRepositoryService() { + assertNotNull(defaultJpaRepositoryService.getRepository()); + } + + @Test + public void customJpaRepository_Is_CompatibleWith_CrudRepositoryService() { + assertNotNull(customJpaRepositoryService.getRepository()); + assertEquals(CustomJpaRepository.class, customJpaRepositoryService + .getRepository().getClass().getSuperclass()); + } + +} diff --git a/vaadin-spring/src/test/java/com/vaadin/flow/spring/data/jpa/NestedObject.java b/vaadin-spring/src/test/java/com/vaadin/flow/spring/data/jpa/NestedObject.java new file mode 100644 index 00000000000..b869275ccbf --- /dev/null +++ b/vaadin-spring/src/test/java/com/vaadin/flow/spring/data/jpa/NestedObject.java @@ -0,0 +1,57 @@ +package com.vaadin.flow.spring.data.jpa; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.OneToOne; + +import com.vaadin.flow.spring.data.filter.SecondLevelNestedObject; + +@Entity +public class NestedObject { + + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + private Integer id; + + private String name; + + private long luckyNumber; + + @OneToOne + private SecondLevelNestedObject secondLevelNestedObject; + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public long getLuckyNumber() { + return luckyNumber; + } + + public void setLuckyNumber(long luckyNumber) { + this.luckyNumber = luckyNumber; + } + + public SecondLevelNestedObject getSecondLevelNestedObject() { + return secondLevelNestedObject; + } + + public void setSecondLevelNestedObject( + SecondLevelNestedObject secondLevelNestedObject) { + this.secondLevelNestedObject = secondLevelNestedObject; + } +} diff --git a/vaadin-spring/src/test/java/com/vaadin/flow/spring/data/jpa/TestCrudRepositoryService.java b/vaadin-spring/src/test/java/com/vaadin/flow/spring/data/jpa/TestCrudRepositoryService.java new file mode 100644 index 00000000000..fd71a17d1f3 --- /dev/null +++ b/vaadin-spring/src/test/java/com/vaadin/flow/spring/data/jpa/TestCrudRepositoryService.java @@ -0,0 +1,14 @@ +package com.vaadin.flow.spring.data.jpa; + +import org.springframework.stereotype.Service; + +import com.vaadin.flow.spring.data.jpa.CrudRepositoryService; + +@Service +public class TestCrudRepositoryService + extends CrudRepositoryService { + TestCrudRepositoryService(TestRepository repository) { + super(repository); + } + +} diff --git a/vaadin-spring/src/test/java/com/vaadin/flow/spring/data/jpa/TestEnum.java b/vaadin-spring/src/test/java/com/vaadin/flow/spring/data/jpa/TestEnum.java new file mode 100644 index 00000000000..10af48572bb --- /dev/null +++ b/vaadin-spring/src/test/java/com/vaadin/flow/spring/data/jpa/TestEnum.java @@ -0,0 +1,5 @@ +package com.vaadin.flow.spring.data.jpa; + +public enum TestEnum { + TEST1, TEST2, TEST3 +} diff --git a/vaadin-spring/src/test/java/com/vaadin/flow/spring/data/jpa/TestObject.java b/vaadin-spring/src/test/java/com/vaadin/flow/spring/data/jpa/TestObject.java new file mode 100644 index 00000000000..daf23712150 --- /dev/null +++ b/vaadin-spring/src/test/java/com/vaadin/flow/spring/data/jpa/TestObject.java @@ -0,0 +1,164 @@ +package com.vaadin.flow.spring.data.jpa; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.OneToOne; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; + +@Entity +public class TestObject { + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + private Integer id; + + private String name; + private LocalDate localDate; + private LocalTime localTime; + private LocalDateTime localDateTime; + private Boolean booleanValue; + private int intValue; + private Integer nullableIntValue; + private long longValue; + private Long nullableLongValue; + private float floatValue; + private Float nullableFloatValue; + private double doubleValue; + private Double nullableDoubleValue; + private TestEnum enumValue; + + @OneToOne + private NestedObject nestedObject; + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public LocalDate getLocalDate() { + return localDate; + } + + public void setLocalDate(LocalDate localDate) { + this.localDate = localDate; + } + + public LocalDateTime getLocalDateTime() { + return localDateTime; + } + + public void setLocalDateTime(LocalDateTime localDateTime) { + this.localDateTime = localDateTime; + } + + public LocalTime getLocalTime() { + return localTime; + } + + public void setLocalTime(LocalTime localTime) { + this.localTime = localTime; + } + + public Boolean getBooleanValue() { + return booleanValue; + } + + public void setBooleanValue(Boolean aBoolean) { + this.booleanValue = aBoolean; + } + + public int getIntValue() { + return intValue; + } + + public void setIntValue(int intValue) { + this.intValue = intValue; + } + + public Integer getNullableIntValue() { + return nullableIntValue; + } + + public void setNullableIntValue(Integer nullableIntValue) { + this.nullableIntValue = nullableIntValue; + } + + public long getLongValue() { + return longValue; + } + + public void setLongValue(long longValue) { + this.longValue = longValue; + } + + public Long getNullableLongValue() { + return nullableLongValue; + } + + public void setNullableLongValue(Long nullableLongValue) { + this.nullableLongValue = nullableLongValue; + } + + public float getFloatValue() { + return floatValue; + } + + public void setFloatValue(float floatValue) { + this.floatValue = floatValue; + } + + public Float getNullableFloatValue() { + return nullableFloatValue; + } + + public void setNullableFloatValue(Float nullableFloatValue) { + this.nullableFloatValue = nullableFloatValue; + } + + public double getDoubleValue() { + return doubleValue; + } + + public void setDoubleValue(double doubleValue) { + this.doubleValue = doubleValue; + } + + public Double getNullableDoubleValue() { + return nullableDoubleValue; + } + + public void setNullableDoubleValue(Double nullableDoubleValue) { + this.nullableDoubleValue = nullableDoubleValue; + } + + public TestEnum getEnumValue() { + return enumValue; + } + + public void setEnumValue(TestEnum testEnum) { + this.enumValue = testEnum; + } + + public NestedObject getNestedObject() { + return nestedObject; + } + + public void setNestedObject(NestedObject nestedObject) { + this.nestedObject = nestedObject; + } +} diff --git a/vaadin-spring/src/test/java/com/vaadin/flow/spring/data/jpa/TestRepository.java b/vaadin-spring/src/test/java/com/vaadin/flow/spring/data/jpa/TestRepository.java new file mode 100644 index 00000000000..1c6107eb6ab --- /dev/null +++ b/vaadin-spring/src/test/java/com/vaadin/flow/spring/data/jpa/TestRepository.java @@ -0,0 +1,9 @@ +package com.vaadin.flow.spring.data.jpa; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; + +public interface TestRepository extends JpaRepository, + JpaSpecificationExecutor { + +} From 5a2d557ff2e45460985979fb1c52fbbd739898ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petter=20Holmstr=C3=B6m?= Date: Wed, 18 Dec 2024 15:52:53 +0200 Subject: [PATCH 09/29] Add missing dependencies to make Spring Data tests work. --- vaadin-spring/pom.xml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/vaadin-spring/pom.xml b/vaadin-spring/pom.xml index 5a214b00dd7..88cc5a3f21c 100644 --- a/vaadin-spring/pom.xml +++ b/vaadin-spring/pom.xml @@ -140,6 +140,22 @@ test + + com.h2database + h2 + test + + + org.springframework.boot + spring-boot-test-autoconfigure + test + + + org.springframework.boot + spring-boot-starter-data-jpa + test + + org.springframework.boot spring-boot-starter-test From 14d7d8774ea88ee03ebea0a6eea888afb76a30bb Mon Sep 17 00:00:00 2001 From: Artur Signell Date: Wed, 18 Dec 2024 16:13:04 +0200 Subject: [PATCH 10/29] Change expected exception --- .../java/com/vaadin/flow/spring/data/filter/FilterTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vaadin-spring/src/test/java/com/vaadin/flow/spring/data/filter/FilterTest.java b/vaadin-spring/src/test/java/com/vaadin/flow/spring/data/filter/FilterTest.java index d5cc0a9e4a5..5a985fe96ab 100644 --- a/vaadin-spring/src/test/java/com/vaadin/flow/spring/data/filter/FilterTest.java +++ b/vaadin-spring/src/test/java/com/vaadin/flow/spring/data/filter/FilterTest.java @@ -303,7 +303,7 @@ public void filterUnknownEnumValue() { executeFilter(filter); } - @Test(expected = IllegalArgumentException.class) + @Test(expected = InvalidDataAccessApiUsageException.class) public void filterNonExistingProperty() { setupNames("Jack", "John", "Johnny", "Polly", "Josh"); PropertyStringFilter filter = createFilter("foo", Matcher.EQUALS, From 94436bfcf53eadc2d6d7a90ab4a24f9e14ddea22 Mon Sep 17 00:00:00 2001 From: Artur Signell Date: Wed, 18 Dec 2024 17:25:33 +0200 Subject: [PATCH 11/29] fix test --- .../vaadin/flow/spring/data/filter/FilterTest.java | 1 + .../spring/data/jpa/CrudRepositoryServiceTest.java | 11 ++++------- .../com/vaadin/flow/spring/data/jpa/NestedObject.java | 2 -- .../data/{filter => jpa}/SecondLevelNestedObject.java | 2 +- 4 files changed, 6 insertions(+), 10 deletions(-) rename vaadin-spring/src/test/java/com/vaadin/flow/spring/data/{filter => jpa}/SecondLevelNestedObject.java (94%) diff --git a/vaadin-spring/src/test/java/com/vaadin/flow/spring/data/filter/FilterTest.java b/vaadin-spring/src/test/java/com/vaadin/flow/spring/data/filter/FilterTest.java index 5a985fe96ab..b944b9df8b6 100644 --- a/vaadin-spring/src/test/java/com/vaadin/flow/spring/data/filter/FilterTest.java +++ b/vaadin-spring/src/test/java/com/vaadin/flow/spring/data/filter/FilterTest.java @@ -16,6 +16,7 @@ import com.vaadin.flow.spring.data.filter.PropertyStringFilter.Matcher; import com.vaadin.flow.spring.data.jpa.JpaFilterConverter; import com.vaadin.flow.spring.data.jpa.NestedObject; +import com.vaadin.flow.spring.data.jpa.SecondLevelNestedObject; import com.vaadin.flow.spring.data.jpa.TestEnum; import com.vaadin.flow.spring.data.jpa.TestObject; import com.vaadin.flow.spring.data.jpa.TestRepository; diff --git a/vaadin-spring/src/test/java/com/vaadin/flow/spring/data/jpa/CrudRepositoryServiceTest.java b/vaadin-spring/src/test/java/com/vaadin/flow/spring/data/jpa/CrudRepositoryServiceTest.java index 0eb98bc56b5..db85139f0f8 100644 --- a/vaadin-spring/src/test/java/com/vaadin/flow/spring/data/jpa/CrudRepositoryServiceTest.java +++ b/vaadin-spring/src/test/java/com/vaadin/flow/spring/data/jpa/CrudRepositoryServiceTest.java @@ -1,5 +1,9 @@ package com.vaadin.flow.spring.data.jpa; +import java.util.List; +import java.util.Optional; +import java.util.function.Function; + import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; @@ -15,15 +19,8 @@ import org.springframework.data.repository.CrudRepository; import org.springframework.data.repository.query.FluentQuery; import org.springframework.stereotype.Repository; -import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringRunner; -import com.vaadin.flow.spring.data.jpa.CrudRepositoryService; - -import java.util.List; -import java.util.Optional; -import java.util.function.Function; - import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; diff --git a/vaadin-spring/src/test/java/com/vaadin/flow/spring/data/jpa/NestedObject.java b/vaadin-spring/src/test/java/com/vaadin/flow/spring/data/jpa/NestedObject.java index b869275ccbf..aa3ecc995df 100644 --- a/vaadin-spring/src/test/java/com/vaadin/flow/spring/data/jpa/NestedObject.java +++ b/vaadin-spring/src/test/java/com/vaadin/flow/spring/data/jpa/NestedObject.java @@ -6,8 +6,6 @@ import jakarta.persistence.Id; import jakarta.persistence.OneToOne; -import com.vaadin.flow.spring.data.filter.SecondLevelNestedObject; - @Entity public class NestedObject { diff --git a/vaadin-spring/src/test/java/com/vaadin/flow/spring/data/filter/SecondLevelNestedObject.java b/vaadin-spring/src/test/java/com/vaadin/flow/spring/data/jpa/SecondLevelNestedObject.java similarity index 94% rename from vaadin-spring/src/test/java/com/vaadin/flow/spring/data/filter/SecondLevelNestedObject.java rename to vaadin-spring/src/test/java/com/vaadin/flow/spring/data/jpa/SecondLevelNestedObject.java index 901457ae31b..75aa2ae2ddb 100644 --- a/vaadin-spring/src/test/java/com/vaadin/flow/spring/data/filter/SecondLevelNestedObject.java +++ b/vaadin-spring/src/test/java/com/vaadin/flow/spring/data/jpa/SecondLevelNestedObject.java @@ -1,4 +1,4 @@ -package com.vaadin.flow.spring.data.filter; +package com.vaadin.flow.spring.data.jpa; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; From c1a906247e75774056f0277e0eba2e99d7382f07 Mon Sep 17 00:00:00 2001 From: Artur Signell Date: Wed, 18 Dec 2024 17:26:02 +0200 Subject: [PATCH 12/29] final --- .../com/vaadin/flow/spring/data/jpa/JpaFilterConverter.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/jpa/JpaFilterConverter.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/jpa/JpaFilterConverter.java index dda14a8733b..67c26cb83a7 100644 --- a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/jpa/JpaFilterConverter.java +++ b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/jpa/JpaFilterConverter.java @@ -14,7 +14,7 @@ * {@link com.vaadin.flow.spring.data.CrudService} implementations that use JPA * as the data source. */ -public class JpaFilterConverter { +public final class JpaFilterConverter { private JpaFilterConverter() { // Utilities only From b00f39409b2d2407b6e00fb29b62133082082752 Mon Sep 17 00:00:00 2001 From: Artur Signell Date: Wed, 18 Dec 2024 17:29:03 +0200 Subject: [PATCH 13/29] Use jspecify instead of custom annotations --- .../main/java/com/vaadin/flow/Nonnull.java | 36 ------------------- .../main/java/com/vaadin/flow/Nullable.java | 36 ------------------- vaadin-spring/pom.xml | 20 +++++++---- .../vaadin/flow/spring/data/CountService.java | 2 +- .../vaadin/flow/spring/data/FormService.java | 2 +- .../vaadin/flow/spring/data/ListService.java | 10 +++--- .../data/jpa/CrudRepositoryService.java | 2 +- .../data/jpa/ListRepositoryService.java | 2 +- 8 files changed, 22 insertions(+), 88 deletions(-) delete mode 100644 flow-server/src/main/java/com/vaadin/flow/Nonnull.java delete mode 100644 flow-server/src/main/java/com/vaadin/flow/Nullable.java diff --git a/flow-server/src/main/java/com/vaadin/flow/Nonnull.java b/flow-server/src/main/java/com/vaadin/flow/Nonnull.java deleted file mode 100644 index a08af410cc6..00000000000 --- a/flow-server/src/main/java/com/vaadin/flow/Nonnull.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright 2000-2022 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; - -import java.lang.annotation.Documented; -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * Annotation to mark either field, method, parameter or type parameter as - * non-nullable. It is used by Typescript Generator as a source of type - * nullability information. - * - * This annotation exists because the traditional `jakarta.annotation.Nonnull` - * annotation is not applicable to type parameters. - */ -@Documented -@Retention(RetentionPolicy.RUNTIME) -@Target({ ElementType.TYPE_USE }) -public @interface Nonnull { -} diff --git a/flow-server/src/main/java/com/vaadin/flow/Nullable.java b/flow-server/src/main/java/com/vaadin/flow/Nullable.java deleted file mode 100644 index 39f13ca04ca..00000000000 --- a/flow-server/src/main/java/com/vaadin/flow/Nullable.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright 2000-2022 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; - -import java.lang.annotation.Documented; -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * Annotation to mark either field, method, parameter or type parameter as - * nullable. It is used by Typescript Generator as a source of type nullability - * information. - * - * This annotation exists because the traditional `jakarta.annotation.Nullable` - * annotation is not applicable to type parameters. - */ -@Documented -@Retention(RetentionPolicy.RUNTIME) -@Target({ ElementType.TYPE_USE }) -public @interface Nullable { -} diff --git a/vaadin-spring/pom.xml b/vaadin-spring/pom.xml index 88cc5a3f21c..ce207540a81 100644 --- a/vaadin-spring/pom.xml +++ b/vaadin-spring/pom.xml @@ -43,6 +43,12 @@ jakarta.servlet-api provided + + org.jspecify + jspecify + 1.0.0 + + com.vaadin @@ -225,13 +231,13 @@ - - org.springframework.boot - spring-boot-dependencies - ${spring.boot.version} - pom - import - + + org.springframework.boot + spring-boot-dependencies + ${spring.boot.version} + pom + import + diff --git a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/CountService.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/CountService.java index 22687e4bafb..5e90a385b7f 100644 --- a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/CountService.java +++ b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/CountService.java @@ -1,6 +1,6 @@ package com.vaadin.flow.spring.data; -import com.vaadin.flow.Nullable; +import org.jspecify.annotations.Nullable; import com.vaadin.flow.spring.data.filter.Filter; diff --git a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/FormService.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/FormService.java index 93360e70ecc..63b8a144969 100644 --- a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/FormService.java +++ b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/FormService.java @@ -1,6 +1,6 @@ package com.vaadin.flow.spring.data; -import com.vaadin.flow.Nullable; +import org.jspecify.annotations.Nullable; /** * A service that can create, update, and delete a given type of object. diff --git a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/ListService.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/ListService.java index 3d0855aee17..cdbe9519273 100644 --- a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/ListService.java +++ b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/ListService.java @@ -2,11 +2,11 @@ import java.util.List; -import com.vaadin.flow.Nullable; -import com.vaadin.flow.Nonnull; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; +import org.springframework.data.domain.Pageable; import com.vaadin.flow.spring.data.filter.Filter; -import org.springframework.data.domain.Pageable; /** * A service that can list the given type of object. @@ -22,7 +22,7 @@ public interface ListService { * the filter to apply or {@code null} to not filter * @return a list of objects or an empty list if no objects were found */ - @Nonnull - List<@Nonnull T> list(Pageable pageable, @Nullable Filter filter); + @NonNull + List<@NonNull T> list(Pageable pageable, @Nullable Filter filter); } diff --git a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/jpa/CrudRepositoryService.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/jpa/CrudRepositoryService.java index b5803d50d12..d2bcf87e6f4 100644 --- a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/jpa/CrudRepositoryService.java +++ b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/jpa/CrudRepositoryService.java @@ -3,10 +3,10 @@ import java.util.ArrayList; import java.util.List; +import org.jspecify.annotations.Nullable; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; import org.springframework.data.repository.CrudRepository; -import com.vaadin.flow.Nullable; import com.vaadin.flow.spring.data.CrudService; /** diff --git a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/jpa/ListRepositoryService.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/jpa/ListRepositoryService.java index 251e90fd9be..02b3d6385b7 100644 --- a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/jpa/ListRepositoryService.java +++ b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/jpa/ListRepositoryService.java @@ -5,12 +5,12 @@ import java.util.Optional; import com.googlecode.gentyref.GenericTypeReflector; +import org.jspecify.annotations.Nullable; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.domain.Specification; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; import org.springframework.data.repository.CrudRepository; -import com.vaadin.flow.Nullable; import com.vaadin.flow.spring.data.CountService; import com.vaadin.flow.spring.data.GetService; import com.vaadin.flow.spring.data.ListService; From 5163a3c3f91a27f0e7b180fc2c53f303c927226d Mon Sep 17 00:00:00 2001 From: Artur Signell Date: Wed, 18 Dec 2024 18:16:55 +0200 Subject: [PATCH 14/29] Util class not serializable --- .../com/vaadin/flow/spring/SpringClassesSerializableTest.java | 1 + 1 file changed, 1 insertion(+) diff --git a/vaadin-spring/src/test/java/com/vaadin/flow/spring/SpringClassesSerializableTest.java b/vaadin-spring/src/test/java/com/vaadin/flow/spring/SpringClassesSerializableTest.java index 886073b9e75..59644442b0c 100644 --- a/vaadin-spring/src/test/java/com/vaadin/flow/spring/SpringClassesSerializableTest.java +++ b/vaadin-spring/src/test/java/com/vaadin/flow/spring/SpringClassesSerializableTest.java @@ -93,6 +93,7 @@ protected Stream getExcludedPatterns() { "com\\.vaadin\\.flow\\.spring\\.SpringLookupInitializer(\\$.*)?", "com\\.vaadin\\.flow\\.spring\\.VaadinConfigurationProperties", "com\\.vaadin\\.flow\\.spring\\.SpringDevToolsPortHandler", + "com\\.vaadin\\.flow\\.spring\\.data\\.jpa\\.JpaFilterConverter", "com\\.vaadin\\.flow\\.spring\\.springnative\\.AtmosphereHintsRegistrar", "com\\.vaadin\\.flow\\.spring\\.springnative\\.VaadinBeanFactoryInitializationAotProcessor", "com\\.vaadin\\.flow\\.spring\\.springnative\\.VaadinBeanFactoryInitializationAotProcessor\\$Marker", From 7645905ab1b249054df17e3c38ec9bc90ae64004 Mon Sep 17 00:00:00 2001 From: Artur Signell Date: Wed, 18 Dec 2024 18:17:14 +0200 Subject: [PATCH 15/29] format --- .../flow/spring/data/jpa/JpaFilterConverter.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/jpa/JpaFilterConverter.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/jpa/JpaFilterConverter.java index 67c26cb83a7..06e16d660c0 100644 --- a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/jpa/JpaFilterConverter.java +++ b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/jpa/JpaFilterConverter.java @@ -8,9 +8,9 @@ import com.vaadin.flow.spring.data.filter.PropertyStringFilter; /** - * Utility class for converting {@link Filter} specifications into JPA - * filter specifications. This class can be used to implement filtering for - * custom {@link com.vaadin.flow.spring.data.ListService} or + * Utility class for converting {@link Filter} specifications into JPA filter + * specifications. This class can be used to implement filtering for custom + * {@link com.vaadin.flow.spring.data.ListService} or * {@link com.vaadin.flow.spring.data.CrudService} implementations that use JPA * as the data source. */ @@ -19,6 +19,7 @@ public final class JpaFilterConverter { private JpaFilterConverter() { // Utilities only } + /** * Converts the given filter specification into a JPA filter specification * for the specified entity class. @@ -36,7 +37,8 @@ private JpaFilterConverter() { * the entity class * @return a JPA filter specification for the given filter */ - public static Specification toSpec(Filter rawFilter, Class entity) { + public static Specification toSpec(Filter rawFilter, + Class entity) { if (rawFilter == null) { return Specification.anyOf(); } From 2b8b1507516627ed347a9828e70cc8977da4e4d0 Mon Sep 17 00:00:00 2001 From: Artur Signell Date: Wed, 18 Dec 2024 18:17:14 +0200 Subject: [PATCH 16/29] For Hilla compat --- .../flow/spring/data/jpa/ListRepositoryService.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/jpa/ListRepositoryService.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/jpa/ListRepositoryService.java index 02b3d6385b7..2ce7c4f9718 100644 --- a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/jpa/ListRepositoryService.java +++ b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/jpa/ListRepositoryService.java @@ -22,7 +22,7 @@ public class ListRepositoryService & JpaSpecificationExecutor> implements ListService, GetService, CountService { - private final R repository; + private R repository; private final Class entityClass; @@ -46,6 +46,12 @@ protected R getRepository() { return repository; } + @Deprecated + protected void internalSetRepository(R repository) { + // Only for Hilla backwards compatibility + this.repository = repository; + } + @Override public List list(Pageable pageable, @Nullable Filter filter) { Specification spec = toSpec(filter); From 8a9f6b61a360ca51c2d940798da3aec8521a9f97 Mon Sep 17 00:00:00 2001 From: Artur Signell Date: Wed, 18 Dec 2024 19:00:10 +0200 Subject: [PATCH 17/29] fix test --- .../vaadin/flow/spring/SpringClassesSerializableTest.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/vaadin-spring/src/test/java/com/vaadin/flow/spring/SpringClassesSerializableTest.java b/vaadin-spring/src/test/java/com/vaadin/flow/spring/SpringClassesSerializableTest.java index 59644442b0c..e2e917e7f3f 100644 --- a/vaadin-spring/src/test/java/com/vaadin/flow/spring/SpringClassesSerializableTest.java +++ b/vaadin-spring/src/test/java/com/vaadin/flow/spring/SpringClassesSerializableTest.java @@ -94,6 +94,13 @@ protected Stream getExcludedPatterns() { "com\\.vaadin\\.flow\\.spring\\.VaadinConfigurationProperties", "com\\.vaadin\\.flow\\.spring\\.SpringDevToolsPortHandler", "com\\.vaadin\\.flow\\.spring\\.data\\.jpa\\.JpaFilterConverter", + "com\\.vaadin\\.flow\\.spring\\.data\\.jpa\\.ListRepositoryService", + "com\\.vaadin\\.flow\\.spring\\.data\\.jpa\\.CrudRepositoryService", + "com\\.vaadin\\.flow\\.spring\\.data\\.ListService", + "com\\.vaadin\\.flow\\.spring\\.data\\.CountService", + "com\\.vaadin\\.flow\\.spring\\.data\\.CrudService", + "com\\.vaadin\\.flow\\.spring\\.data\\.FormService", + "com\\.vaadin\\.flow\\.spring\\.data\\.GetService", "com\\.vaadin\\.flow\\.spring\\.springnative\\.AtmosphereHintsRegistrar", "com\\.vaadin\\.flow\\.spring\\.springnative\\.VaadinBeanFactoryInitializationAotProcessor", "com\\.vaadin\\.flow\\.spring\\.springnative\\.VaadinBeanFactoryInitializationAotProcessor\\$Marker", From 230bbc53be7baeefb3b4bff8f9715f428610bc04 Mon Sep 17 00:00:00 2001 From: Artur Signell Date: Thu, 19 Dec 2024 12:53:21 +0200 Subject: [PATCH 18/29] Tweak --- .../main/java/com/vaadin/flow/spring/data/FormService.java | 2 +- .../main/java/com/vaadin/flow/spring/data/ListService.java | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/FormService.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/FormService.java index 63b8a144969..42225116f81 100644 --- a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/FormService.java +++ b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/FormService.java @@ -15,7 +15,7 @@ public interface FormService { * * @param value * the object to save - * @return the fresh object or {@code null} if no object was found to update + * @return the fresh object; will never be {@literal null}. */ @Nullable T save(T value); diff --git a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/ListService.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/ListService.java index cdbe9519273..7ee731a9069 100644 --- a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/ListService.java +++ b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/ListService.java @@ -2,7 +2,6 @@ import java.util.List; -import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; import org.springframework.data.domain.Pageable; @@ -22,7 +21,6 @@ public interface ListService { * the filter to apply or {@code null} to not filter * @return a list of objects or an empty list if no objects were found */ - @NonNull - List<@NonNull T> list(Pageable pageable, @Nullable Filter filter); + List list(Pageable pageable, @Nullable Filter filter); } From 10e9eadc96d5ddc1fa5c374b26725602795f47b7 Mon Sep 17 00:00:00 2001 From: Artur Signell Date: Thu, 19 Dec 2024 15:08:48 +0200 Subject: [PATCH 19/29] Remove nullable --- .../src/main/java/com/vaadin/flow/spring/data/FormService.java | 1 - 1 file changed, 1 deletion(-) diff --git a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/FormService.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/FormService.java index 42225116f81..1c99d11d348 100644 --- a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/FormService.java +++ b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/FormService.java @@ -17,7 +17,6 @@ public interface FormService { * the object to save * @return the fresh object; will never be {@literal null}. */ - @Nullable T save(T value); /** From 57ec343dd3d28b21733ebee3685f2bd467e22d4c Mon Sep 17 00:00:00 2001 From: Artur Signell Date: Fri, 20 Dec 2024 15:56:38 +0200 Subject: [PATCH 20/29] Refactor to be more understandable and overrideable/extendable --- .../spring/data/jpa/JpaFilterConverter.java | 17 +- .../data/jpa/ListRepositoryService.java | 3 +- .../PropertyStringFilterSpecification.java | 343 ++++++++++-------- 3 files changed, 207 insertions(+), 156 deletions(-) diff --git a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/jpa/JpaFilterConverter.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/jpa/JpaFilterConverter.java index 06e16d660c0..8ead95920a4 100644 --- a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/jpa/JpaFilterConverter.java +++ b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/jpa/JpaFilterConverter.java @@ -1,5 +1,8 @@ package com.vaadin.flow.spring.data.jpa; +import java.util.function.Function; +import java.util.function.Supplier; + import org.springframework.data.jpa.domain.Specification; import com.vaadin.flow.spring.data.filter.AndFilter; @@ -35,21 +38,27 @@ private JpaFilterConverter() { * the filter to convert * @param entity * the entity class + * @param propertyStringFilterSupplier + * a function that can convert a PropertyStringFilter into a JPA + * filter specification * @return a JPA filter specification for the given filter */ public static Specification toSpec(Filter rawFilter, - Class entity) { + Class entity, + Function> propertyStringFilterSupplier) { if (rawFilter == null) { return Specification.anyOf(); } if (rawFilter instanceof AndFilter filter) { return Specification.allOf(filter.getChildren().stream() - .map(f -> toSpec(f, entity)).toList()); + .map(f -> toSpec(f, entity, propertyStringFilterSupplier)) + .toList()); } else if (rawFilter instanceof OrFilter filter) { return Specification.anyOf(filter.getChildren().stream() - .map(f -> toSpec(f, entity)).toList()); + .map(f -> toSpec(f, entity, propertyStringFilterSupplier)) + .toList()); } else if (rawFilter instanceof PropertyStringFilter filter) { - return new PropertyStringFilterSpecification<>(filter); + return propertyStringFilterSupplier.apply(filter); } else { throw new IllegalArgumentException( "Unknown filter type " + rawFilter.getClass().getName()); diff --git a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/jpa/ListRepositoryService.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/jpa/ListRepositoryService.java index 2ce7c4f9718..8fb5fa36f0b 100644 --- a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/jpa/ListRepositoryService.java +++ b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/jpa/ListRepositoryService.java @@ -88,7 +88,8 @@ public long count(@Nullable Filter filter) { * @return a JPA specification */ protected Specification toSpec(@Nullable Filter filter) { - return JpaFilterConverter.toSpec(filter, entityClass); + return JpaFilterConverter.toSpec(filter, entityClass, + PropertyStringFilterSpecification::new); } @SuppressWarnings("unchecked") diff --git a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/jpa/PropertyStringFilterSpecification.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/jpa/PropertyStringFilterSpecification.java index ef58b452162..a788ddc5dc8 100644 --- a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/jpa/PropertyStringFilterSpecification.java +++ b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/jpa/PropertyStringFilterSpecification.java @@ -1,9 +1,5 @@ package com.vaadin.flow.spring.data.jpa; -import java.time.LocalDate; -import java.time.LocalDateTime; -import java.time.LocalTime; - import jakarta.persistence.criteria.CriteriaBuilder; import jakarta.persistence.criteria.CriteriaQuery; import jakarta.persistence.criteria.Expression; @@ -11,9 +7,14 @@ import jakarta.persistence.criteria.Predicate; import jakarta.persistence.criteria.Root; -import com.vaadin.flow.spring.data.filter.PropertyStringFilter; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; + import org.springframework.data.jpa.domain.Specification; +import com.vaadin.flow.spring.data.filter.PropertyStringFilter; + public class PropertyStringFilterSpecification implements Specification { private final PropertyStringFilter filter; @@ -25,137 +26,196 @@ public PropertyStringFilterSpecification(PropertyStringFilter filter) { @Override public Predicate toPredicate(Root root, CriteriaQuery query, CriteriaBuilder criteriaBuilder) { - String value = filter.getFilterValue(); - Class javaType = getJavaType(filter.getPropertyId(), root); - Path propertyPath = getPath(filter.getPropertyId(), root); - if (javaType == String.class) { - Expression expr = criteriaBuilder.lower(propertyPath); - switch (filter.getMatcher()) { - case EQUALS: - return criteriaBuilder.equal(expr, value.toLowerCase()); - case CONTAINS: - return criteriaBuilder.like(expr, - "%" + value.toLowerCase() + "%"); - case GREATER_THAN: - throw new IllegalArgumentException( - "A string cannot be filtered using greater than"); - case LESS_THAN: - throw new IllegalArgumentException( - "A string cannot be filtered using less than"); - default: - break; - } + Path propertyPath = getPath(filter.getPropertyId(), root); + return toPredicate(root, criteriaBuilder, propertyPath); + } + + /** + * Converts a filter to a JPA predicate. + * + * @param root + * The root entity + * @param criteriaBuilder + * The criteria builder + * @param propertyPath + * The property path + * + */ + protected Predicate toPredicate(Root root, + CriteriaBuilder criteriaBuilder, Path propertyPath) { + Class javaType = propertyPath.getJavaType(); + if (javaType == String.class) { + return stringToPredicate(root, criteriaBuilder, filter, + (Path) propertyPath); } else if (isNumber(javaType)) { - switch (filter.getMatcher()) { - case EQUALS: - return criteriaBuilder.equal(propertyPath, value); - case CONTAINS: - throw new IllegalArgumentException( - "A number cannot be filtered using contains"); - case GREATER_THAN: - return criteriaBuilder.greaterThan(propertyPath, value); - case LESS_THAN: - return criteriaBuilder.lessThan(propertyPath, value); - default: - break; - } + return numberToPredicate(root, criteriaBuilder, filter, + (Path) propertyPath); } else if (isBoolean(javaType)) { - Boolean booleanValue = Boolean.valueOf(value); - switch (filter.getMatcher()) { - case EQUALS: - return criteriaBuilder.equal(propertyPath, booleanValue); - case CONTAINS: - throw new IllegalArgumentException( - "A boolean cannot be filtered using contains"); - case GREATER_THAN: - throw new IllegalArgumentException( - "A boolean cannot be filtered using greater than"); - case LESS_THAN: - throw new IllegalArgumentException( - "A boolean cannot be filtered using less than"); - default: - break; - } - } else if (isLocalDate(javaType)) { - var path = root. get(filter.getPropertyId()); - var dateValue = LocalDate.parse(value); - switch (filter.getMatcher()) { - case EQUALS: - return criteriaBuilder.equal(path, dateValue); - case CONTAINS: - throw new IllegalArgumentException( - "A date cannot be filtered using contains"); - case GREATER_THAN: - return criteriaBuilder.greaterThan(path, dateValue); - case LESS_THAN: - return criteriaBuilder.lessThan(path, dateValue); - default: - break; - } - } else if (isLocalTime(javaType)) { - var path = root. get(filter.getPropertyId()); - var timeValue = LocalTime.parse(value); - switch (filter.getMatcher()) { - case EQUALS: - return criteriaBuilder.equal(path, timeValue); - case CONTAINS: - throw new IllegalArgumentException( - "A time cannot be filtered using contains"); - case GREATER_THAN: - return criteriaBuilder.greaterThan(path, timeValue); - case LESS_THAN: - return criteriaBuilder.lessThan(path, timeValue); - default: - break; - } - } else if (isLocalDateTime(javaType)) { - var path = root. get(filter.getPropertyId()); - var dateValue = LocalDate.parse(value); - var minValue = LocalDateTime.of(dateValue, LocalTime.MIN); - var maxValue = LocalDateTime.of(dateValue, LocalTime.MAX); - switch (filter.getMatcher()) { - case EQUALS: - return criteriaBuilder.between(path, minValue, maxValue); - case CONTAINS: - throw new IllegalArgumentException( - "A datetime cannot be filtered using contains"); - case GREATER_THAN: - return criteriaBuilder.greaterThan(path, maxValue); - case LESS_THAN: - return criteriaBuilder.lessThan(path, minValue); - default: - break; - } + return booleanToPredicate(root, criteriaBuilder, filter, + (Path) propertyPath); + } else if (javaType == java.time.LocalDate.class) { + return localDateToPredicate(root, criteriaBuilder, filter, + (Path) propertyPath); + } else if (javaType == LocalTime.class) { + return localTimeToPredicate(root, criteriaBuilder, filter, + (Path) propertyPath); + } else if (javaType == java.time.LocalDateTime.class) { + return localDateTimeToPredicate(root, criteriaBuilder, filter, + (Path) propertyPath); } else if (javaType.isEnum()) { - var enumValue = Enum.valueOf(javaType.asSubclass(Enum.class), - value); - - switch (filter.getMatcher()) { - case EQUALS: - return criteriaBuilder.equal(propertyPath, enumValue); - case CONTAINS: - throw new IllegalArgumentException( - "An enum cannot be filtered using contains"); - case GREATER_THAN: - throw new IllegalArgumentException( - "An enum cannot be filtered using greater than"); - case LESS_THAN: - throw new IllegalArgumentException( - "An enum cannot be filtered using less than"); - default: - break; - } + return enumToPredicate(root, criteriaBuilder, filter, propertyPath); + } throw new IllegalArgumentException("No implementation for " + javaType + " using " + filter.getMatcher() + "."); } - private boolean isNumber(Class javaType) { - return javaType == int.class || javaType == Integer.class - || javaType == long.class || javaType == Long.class - || javaType == float.class || javaType == Float.class - || javaType == double.class || javaType == Double.class; + private static Predicate enumToPredicate(Root root, + CriteriaBuilder criteriaBuilder, PropertyStringFilter filter, + Path propertyPath) { + var enumValue = Enum.valueOf( + propertyPath.getJavaType().asSubclass(Enum.class), + filter.getFilterValue()); + + switch (filter.getMatcher()) { + case EQUALS: + return criteriaBuilder.equal(propertyPath, enumValue); + case CONTAINS: + throw new IllegalArgumentException( + "An enum cannot be filtered using contains"); + case GREATER_THAN: + throw new IllegalArgumentException( + "An enum cannot be filtered using greater than"); + case LESS_THAN: + throw new IllegalArgumentException( + "An enum cannot be filtered using less than"); + } + throw new IllegalArgumentException( + "No implementation for " + filter.getMatcher() + "."); + } + + private static Predicate localTimeToPredicate(Root root, + CriteriaBuilder criteriaBuilder, PropertyStringFilter filter, + Path propertyPath) { + var timeValue = LocalTime.parse(filter.getFilterValue()); + switch (filter.getMatcher()) { + case EQUALS: + return criteriaBuilder.equal(propertyPath, timeValue); + case CONTAINS: + throw new IllegalArgumentException( + "A time cannot be filtered using contains"); + case GREATER_THAN: + return criteriaBuilder.greaterThan(propertyPath, timeValue); + case LESS_THAN: + return criteriaBuilder.lessThan(propertyPath, timeValue); + } + throw new IllegalArgumentException( + "No implementation for " + filter.getMatcher() + "."); + } + + private static Predicate localDateTimeToPredicate(Root root, + CriteriaBuilder criteriaBuilder, PropertyStringFilter filter, + Path propertyPath) { + var dateValue = LocalDate.parse(filter.getFilterValue()); + var minValue = LocalDateTime.of(dateValue, LocalTime.MIN); + var maxValue = LocalDateTime.of(dateValue, LocalTime.MAX); + switch (filter.getMatcher()) { + case EQUALS: + return criteriaBuilder.between(propertyPath, minValue, maxValue); + case CONTAINS: + throw new IllegalArgumentException( + "A datetime cannot be filtered using contains"); + case GREATER_THAN: + return criteriaBuilder.greaterThan(propertyPath, maxValue); + case LESS_THAN: + return criteriaBuilder.lessThan(propertyPath, minValue); + } + throw new IllegalArgumentException( + "No implementation for " + filter.getMatcher() + "."); + } + + private static Predicate localDateToPredicate(Root root, + CriteriaBuilder criteriaBuilder, PropertyStringFilter filter, + Path propertyPath) { + var dateValue = LocalDate.parse(filter.getFilterValue()); + + switch (filter.getMatcher()) { + case EQUALS: + return criteriaBuilder.equal(propertyPath, dateValue); + case CONTAINS: + throw new IllegalArgumentException( + "A date cannot be filtered using contains"); + case GREATER_THAN: + return criteriaBuilder.greaterThan(propertyPath, dateValue); + case LESS_THAN: + return criteriaBuilder.lessThan(propertyPath, dateValue); + } + throw new IllegalArgumentException( + "No implementation for " + filter.getMatcher() + "."); + + } + + private static Predicate booleanToPredicate(Root root, + CriteriaBuilder criteriaBuilder, PropertyStringFilter filter, + Path propertyPath) { + Boolean booleanValue = Boolean.valueOf(filter.getFilterValue()); + switch (filter.getMatcher()) { + case EQUALS: + return criteriaBuilder.equal(propertyPath, booleanValue); + case CONTAINS: + throw new IllegalArgumentException( + "A boolean cannot be filtered using contains"); + case GREATER_THAN: + throw new IllegalArgumentException( + "A boolean cannot be filtered using greater than"); + case LESS_THAN: + throw new IllegalArgumentException( + "A boolean cannot be filtered using less than"); + } + throw new IllegalArgumentException( + "No implementation for " + filter.getMatcher() + "."); + } + + private static Predicate numberToPredicate(Root root, + CriteriaBuilder criteriaBuilder, PropertyStringFilter filter, + Path propertyPath) { + String value = filter.getFilterValue(); + switch (filter.getMatcher()) { + case EQUALS: + return criteriaBuilder.equal(propertyPath, value); + case CONTAINS: + throw new IllegalArgumentException( + "A number cannot be filtered using contains"); + case GREATER_THAN: + return criteriaBuilder.greaterThan(propertyPath, value); + case LESS_THAN: + return criteriaBuilder.lessThan(propertyPath, value); + } + throw new IllegalArgumentException( + "No implementation for " + filter.getMatcher() + "."); + } + + private static Predicate stringToPredicate(Root root, + CriteriaBuilder criteriaBuilder, PropertyStringFilter filter, + Path propertyPath) { + Expression expr = criteriaBuilder + .lower((Path) propertyPath); + var filterValueLowerCase = filter.getFilterValue().toLowerCase(); + switch (filter.getMatcher()) { + case EQUALS: + return criteriaBuilder.equal(expr, filterValueLowerCase); + case CONTAINS: + return criteriaBuilder.like(expr, "%" + filterValueLowerCase + "%"); + case GREATER_THAN: + throw new IllegalArgumentException( + "A string cannot be filtered using greater than"); + case LESS_THAN: + throw new IllegalArgumentException( + "A string cannot be filtered using less than"); + } + throw new IllegalArgumentException( + "Unknown matcher type: " + filter.getMatcher()); } private Path getPath(String propertyId, Root root) { @@ -169,34 +229,15 @@ private Path getPath(String propertyId, Root root) { return path; } - private Class getJavaType(String propertyId, Root root) { - if (propertyId.contains(".")) { - String[] parts = propertyId.split("\\."); - Path path = root.get(parts[0]); - int i = 1; - while (i < parts.length) { - path = path.get(parts[i]); - i++; - } - return path.getJavaType(); - } else { - return root.get(propertyId).getJavaType(); - } + private boolean isNumber(Class javaType) { + return javaType == int.class || javaType == Integer.class + || javaType == long.class || javaType == Long.class + || javaType == float.class || javaType == Float.class + || javaType == double.class || javaType == Double.class; } private boolean isBoolean(Class javaType) { return javaType == boolean.class || javaType == Boolean.class; } - private boolean isLocalDate(Class javaType) { - return javaType == java.time.LocalDate.class; - } - - private boolean isLocalTime(Class javaType) { - return javaType == java.time.LocalTime.class; - } - - private boolean isLocalDateTime(Class javaType) { - return javaType == java.time.LocalDateTime.class; - } } From 2cb96767d6d2783e43e393db8d012879578e47ef Mon Sep 17 00:00:00 2001 From: Artur Signell Date: Fri, 20 Dec 2024 16:07:34 +0200 Subject: [PATCH 21/29] Fix test --- .../java/com/vaadin/flow/spring/data/filter/FilterTest.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/vaadin-spring/src/test/java/com/vaadin/flow/spring/data/filter/FilterTest.java b/vaadin-spring/src/test/java/com/vaadin/flow/spring/data/filter/FilterTest.java index b944b9df8b6..e88f8caf998 100644 --- a/vaadin-spring/src/test/java/com/vaadin/flow/spring/data/filter/FilterTest.java +++ b/vaadin-spring/src/test/java/com/vaadin/flow/spring/data/filter/FilterTest.java @@ -16,6 +16,7 @@ import com.vaadin.flow.spring.data.filter.PropertyStringFilter.Matcher; import com.vaadin.flow.spring.data.jpa.JpaFilterConverter; import com.vaadin.flow.spring.data.jpa.NestedObject; +import com.vaadin.flow.spring.data.jpa.PropertyStringFilterSpecification; import com.vaadin.flow.spring.data.jpa.SecondLevelNestedObject; import com.vaadin.flow.spring.data.jpa.TestEnum; import com.vaadin.flow.spring.data.jpa.TestObject; @@ -426,7 +427,7 @@ private void assertFilterResult(Filter filter, List result) { private List executeFilter(Filter filter) { Specification spec = JpaFilterConverter.toSpec(filter, - TestObject.class); + TestObject.class, PropertyStringFilterSpecification::new); return repository.findAll(spec); } From 3f9f7fe6e3e7967368dee11e7a9f6162b7bdaf40 Mon Sep 17 00:00:00 2001 From: Artur Signell Date: Fri, 20 Dec 2024 16:10:28 +0200 Subject: [PATCH 22/29] format --- .../com/vaadin/flow/spring/data/jpa/JpaFilterConverter.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/jpa/JpaFilterConverter.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/jpa/JpaFilterConverter.java index 8ead95920a4..f9990a64d0a 100644 --- a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/jpa/JpaFilterConverter.java +++ b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/jpa/JpaFilterConverter.java @@ -43,8 +43,7 @@ private JpaFilterConverter() { * filter specification * @return a JPA filter specification for the given filter */ - public static Specification toSpec(Filter rawFilter, - Class entity, + public static Specification toSpec(Filter rawFilter, Class entity, Function> propertyStringFilterSupplier) { if (rawFilter == null) { return Specification.anyOf(); From c272a4355a7d16ce36068afdffea43a40e05a8bf Mon Sep 17 00:00:00 2001 From: Artur Signell Date: Mon, 13 Jan 2025 11:00:14 +0200 Subject: [PATCH 23/29] Add convenience constructors --- .../flow/spring/data/filter/AndFilter.java | 20 ++++++++++++- .../flow/spring/data/filter/OrFilter.java | 19 +++++++++++- .../data/filter/PropertyStringFilter.java | 30 +++++++++++++++++-- 3 files changed, 65 insertions(+), 4 deletions(-) diff --git a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/filter/AndFilter.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/filter/AndFilter.java index a5b73fdcca0..320147171a2 100644 --- a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/filter/AndFilter.java +++ b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/filter/AndFilter.java @@ -9,8 +9,26 @@ * filters and verifying that all of them pass. */ public class AndFilter extends Filter { + private List children; + /** + * Create an empty filter. + */ + public AndFilter() { + // Empty constructor is needed for serialization + } + + /** + * Create a filter with the given children. + * + * @param children + * the children of the filter + */ + public AndFilter(Filter... children) { + setChildren(List.of(children)); + } + public List getChildren() { return children; } @@ -21,7 +39,7 @@ public void setChildren(List children) { @Override public String toString() { - return "AndFilter [children=" + children + "]"; + return getClass().getSimpleName()+" [children=" + children + "]"; } } diff --git a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/filter/OrFilter.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/filter/OrFilter.java index 83544f81919..c8eba1c0adc 100644 --- a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/filter/OrFilter.java +++ b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/filter/OrFilter.java @@ -12,6 +12,23 @@ public class OrFilter extends Filter { private List children; + /** + * Create an empty filter. + */ + public OrFilter() { + // Empty constructor is needed for serialization + } + + /** + * Create a filter with the given children. + * + * @param children + * the children of the filter + */ + public OrFilter(Filter... children) { + setChildren(List.of(children)); + } + public List getChildren() { return children; } @@ -22,7 +39,7 @@ public void setChildren(List children) { @Override public String toString() { - return "OrFilter [children=" + children + "]"; + return getClass().getSimpleName() + " [children=" + children + "]"; } } diff --git a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/filter/PropertyStringFilter.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/filter/PropertyStringFilter.java index 8111a245a76..77d4a6e5831 100644 --- a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/filter/PropertyStringFilter.java +++ b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/filter/PropertyStringFilter.java @@ -24,6 +24,32 @@ public enum Matcher { private String filterValue; private Matcher matcher; + /** + * Create an empty filter. + */ + public PropertyStringFilter() { + // Empty constructor is needed for serialization + } + + /** + * Create a filter with the given property, matcher and filter value. + * + * @param propertyId + * the property id, or a nested property path where each + * property is separated by a dot + * @param matcher + * the matcher to use when comparing the property value to the + * filter value + * @param filterValue + * the filter value to compare against + */ + public PropertyStringFilter(String propertyId, Matcher matcher, + String filterValue) { + this.propertyId = propertyId; + this.matcher = matcher; + this.filterValue = filterValue; + } + /** * Gets the property, or nested property path, to filter by. For example * {@code "name"} or {@code "address.city"}. @@ -91,8 +117,8 @@ public void setMatcher(Matcher type) { @Override public String toString() { - return "PropertyStringFilter [propertyId=" + propertyId + ", matcher=" - + matcher + ", filterValue=" + filterValue + "]"; + return getClass().getSimpleName() + " [propertyId=" + propertyId + + ", matcher=" + matcher + ", filterValue=" + filterValue + "]"; } } From 72f6548d9c3a50c293434595dee5d27194c18739 Mon Sep 17 00:00:00 2001 From: Artur Signell Date: Mon, 13 Jan 2025 11:04:39 +0200 Subject: [PATCH 24/29] Improve javadoc --- .../java/com/vaadin/flow/spring/data/CountService.java | 4 ++-- .../java/com/vaadin/flow/spring/data/CrudService.java | 5 +++++ .../java/com/vaadin/flow/spring/data/FormService.java | 10 +++++++--- .../java/com/vaadin/flow/spring/data/ListService.java | 3 +++ 4 files changed, 17 insertions(+), 5 deletions(-) diff --git a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/CountService.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/CountService.java index 5e90a385b7f..71ff3e63691 100644 --- a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/CountService.java +++ b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/CountService.java @@ -5,12 +5,12 @@ import com.vaadin.flow.spring.data.filter.Filter; /** - * A service that can count the given type of objects with a given filter. + * A service that can count the number of items with a given filter. */ public interface CountService { /** - * Counts the number of entities that match the given filter. + * Counts the number of items that match the given filter. * * @param filter * the filter, or {@code null} to use no filter diff --git a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/CrudService.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/CrudService.java index 24a28c62469..9f2225935a0 100644 --- a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/CrudService.java +++ b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/CrudService.java @@ -2,6 +2,11 @@ /** * A service that can create, read, update, and delete a given type of object. + * + * @param + * the type of object to manage + * @param + * the type of the object's identifier */ public interface CrudService extends ListService, FormService { } diff --git a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/FormService.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/FormService.java index 1c99d11d348..7bc78c1fd5a 100644 --- a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/FormService.java +++ b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/FormService.java @@ -1,9 +1,13 @@ package com.vaadin.flow.spring.data; -import org.jspecify.annotations.Nullable; - /** - * A service that can create, update, and delete a given type of object. + * A service that can update and delete a given type of object. + * + * @param + * the type of object to manage + * @param + * the type of the object's identifier + * */ public interface FormService { diff --git a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/ListService.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/ListService.java index 7ee731a9069..c55a3db83ea 100644 --- a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/ListService.java +++ b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/ListService.java @@ -9,6 +9,9 @@ /** * A service that can list the given type of object. + * + * @param + * the type of object to list */ public interface ListService { /** From 9fa935e809fe9b65fa79b7293955240b32606f5d Mon Sep 17 00:00:00 2001 From: Artur Signell Date: Mon, 13 Jan 2025 11:04:39 +0200 Subject: [PATCH 25/29] Improve javadoc --- .../com/vaadin/flow/spring/data/filter/AndFilter.java | 4 ++-- .../com/vaadin/flow/spring/data/filter/OrFilter.java | 2 +- .../flow/spring/data/filter/PropertyStringFilter.java | 6 +++--- .../flow/spring/data/jpa/CrudRepositoryService.java | 8 ++++++++ .../flow/spring/data/jpa/ListRepositoryService.java | 7 +++++++ .../data/jpa/PropertyStringFilterSpecification.java | 10 ++++++++++ 6 files changed, 31 insertions(+), 6 deletions(-) diff --git a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/filter/AndFilter.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/filter/AndFilter.java index 320147171a2..9df1abea1aa 100644 --- a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/filter/AndFilter.java +++ b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/filter/AndFilter.java @@ -21,7 +21,7 @@ public AndFilter() { /** * Create a filter with the given children. - * + * * @param children * the children of the filter */ @@ -39,7 +39,7 @@ public void setChildren(List children) { @Override public String toString() { - return getClass().getSimpleName()+" [children=" + children + "]"; + return getClass().getSimpleName() + " [children=" + children + "]"; } } diff --git a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/filter/OrFilter.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/filter/OrFilter.java index c8eba1c0adc..4529658ff45 100644 --- a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/filter/OrFilter.java +++ b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/filter/OrFilter.java @@ -21,7 +21,7 @@ public OrFilter() { /** * Create a filter with the given children. - * + * * @param children * the children of the filter */ diff --git a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/filter/PropertyStringFilter.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/filter/PropertyStringFilter.java index 77d4a6e5831..ca7cfd9f4e7 100644 --- a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/filter/PropertyStringFilter.java +++ b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/filter/PropertyStringFilter.java @@ -33,10 +33,10 @@ public PropertyStringFilter() { /** * Create a filter with the given property, matcher and filter value. - * + * * @param propertyId - * the property id, or a nested property path where each - * property is separated by a dot + * the property id, or a nested property path where each property + * is separated by a dot * @param matcher * the matcher to use when comparing the property value to the * filter value diff --git a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/jpa/CrudRepositoryService.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/jpa/CrudRepositoryService.java index d2bcf87e6f4..c5ff6e269e6 100644 --- a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/jpa/CrudRepositoryService.java +++ b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/jpa/CrudRepositoryService.java @@ -11,6 +11,14 @@ /** * A service that delegates crud operations to a JPA repository. + * + * @param + * the type of object to manage + * @param + * the type of the object's identifier + * @param + * the type of the JPA repository + * */ public class CrudRepositoryService & JpaSpecificationExecutor> extends ListRepositoryService implements CrudService { diff --git a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/jpa/ListRepositoryService.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/jpa/ListRepositoryService.java index 8fb5fa36f0b..4f1709c28a2 100644 --- a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/jpa/ListRepositoryService.java +++ b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/jpa/ListRepositoryService.java @@ -18,6 +18,13 @@ /** * A service that delegates list operations to a JPA repository. + * + * @param + * the type of object to list + * @param + * the type of the object's identifier + * @param + * the type of the JPA repository */ public class ListRepositoryService & JpaSpecificationExecutor> implements ListService, GetService, CountService { diff --git a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/jpa/PropertyStringFilterSpecification.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/jpa/PropertyStringFilterSpecification.java index a788ddc5dc8..53e12d1c893 100644 --- a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/jpa/PropertyStringFilterSpecification.java +++ b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/jpa/PropertyStringFilterSpecification.java @@ -15,10 +15,20 @@ import com.vaadin.flow.spring.data.filter.PropertyStringFilter; +/** + * A specification that can be used to filter entities based on a + * {@link PropertyStringFilter}. + */ public class PropertyStringFilterSpecification implements Specification { private final PropertyStringFilter filter; + /** + * Creates a new specification based on the given filter. + * + * @param filter + * the filter to use + */ public PropertyStringFilterSpecification(PropertyStringFilter filter) { this.filter = filter; } From 98bee6660a98c059c50ce5ded00549f2aa404720 Mon Sep 17 00:00:00 2001 From: Artur Signell Date: Mon, 13 Jan 2025 11:14:21 +0200 Subject: [PATCH 26/29] javadoc --- .../src/main/java/com/vaadin/flow/spring/data/CountService.java | 1 + 1 file changed, 1 insertion(+) diff --git a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/CountService.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/CountService.java index 71ff3e63691..211189d20aa 100644 --- a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/CountService.java +++ b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/CountService.java @@ -15,6 +15,7 @@ public interface CountService { * @param filter * the filter, or {@code null} to use no filter * @return + * the number of items in the service that match the filter */ public long count(@Nullable Filter filter); From c16124fa2c205215384d63d75353a425a108196d Mon Sep 17 00:00:00 2001 From: Artur Signell Date: Mon, 13 Jan 2025 12:34:16 +0200 Subject: [PATCH 27/29] Add count() --- .../com/vaadin/flow/spring/data/CountService.java | 13 +++++++++++-- .../spring/data/jpa/ListRepositoryService.java | 14 ++++++-------- .../spring/data/jpa/TestCrudRepositoryService.java | 2 -- 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/CountService.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/CountService.java index 211189d20aa..697bfc19349 100644 --- a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/CountService.java +++ b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/CountService.java @@ -1,5 +1,6 @@ package com.vaadin.flow.spring.data; +import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; import com.vaadin.flow.spring.data.filter.Filter; @@ -9,14 +10,22 @@ */ public interface CountService { + /** + * Counts the number of items. + * + * @return + * the number of items in the service + */ + public long count(); + /** * Counts the number of items that match the given filter. * * @param filter - * the filter, or {@code null} to use no filter + * the filter, never {@code null} * @return * the number of items in the service that match the filter */ - public long count(@Nullable Filter filter); + public long count(Filter filter); } diff --git a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/jpa/ListRepositoryService.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/jpa/ListRepositoryService.java index 4f1709c28a2..2ce69b40ff9 100644 --- a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/jpa/ListRepositoryService.java +++ b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/jpa/ListRepositoryService.java @@ -75,15 +75,13 @@ public boolean exists(ID id) { return getRepository().existsById(id); } - /** - * Counts the number of entities that match the given filter. - * - * @param filter - * the filter, or {@code null} to use no filter - * @return - */ @Override - public long count(@Nullable Filter filter) { + public long count() { + return getRepository().count(); + } + + @Override + public long count(Filter filter) { return getRepository().count(toSpec(filter)); } diff --git a/vaadin-spring/src/test/java/com/vaadin/flow/spring/data/jpa/TestCrudRepositoryService.java b/vaadin-spring/src/test/java/com/vaadin/flow/spring/data/jpa/TestCrudRepositoryService.java index fd71a17d1f3..ee252c7be3e 100644 --- a/vaadin-spring/src/test/java/com/vaadin/flow/spring/data/jpa/TestCrudRepositoryService.java +++ b/vaadin-spring/src/test/java/com/vaadin/flow/spring/data/jpa/TestCrudRepositoryService.java @@ -2,8 +2,6 @@ import org.springframework.stereotype.Service; -import com.vaadin.flow.spring.data.jpa.CrudRepositoryService; - @Service public class TestCrudRepositoryService extends CrudRepositoryService { From ce450dfab5cf9abb16d7dc769a4001beaab8197b Mon Sep 17 00:00:00 2001 From: Artur Signell Date: Mon, 13 Jan 2025 12:52:45 +0200 Subject: [PATCH 28/29] Revert "Add count()" This reverts commit c16124fa2c205215384d63d75353a425a108196d. --- .../com/vaadin/flow/spring/data/CountService.java | 13 ++----------- .../spring/data/jpa/ListRepositoryService.java | 14 ++++++++------ .../spring/data/jpa/TestCrudRepositoryService.java | 2 ++ 3 files changed, 12 insertions(+), 17 deletions(-) diff --git a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/CountService.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/CountService.java index 697bfc19349..211189d20aa 100644 --- a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/CountService.java +++ b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/CountService.java @@ -1,6 +1,5 @@ package com.vaadin.flow.spring.data; -import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; import com.vaadin.flow.spring.data.filter.Filter; @@ -10,22 +9,14 @@ */ public interface CountService { - /** - * Counts the number of items. - * - * @return - * the number of items in the service - */ - public long count(); - /** * Counts the number of items that match the given filter. * * @param filter - * the filter, never {@code null} + * the filter, or {@code null} to use no filter * @return * the number of items in the service that match the filter */ - public long count(Filter filter); + public long count(@Nullable Filter filter); } diff --git a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/jpa/ListRepositoryService.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/jpa/ListRepositoryService.java index 2ce69b40ff9..4f1709c28a2 100644 --- a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/jpa/ListRepositoryService.java +++ b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/jpa/ListRepositoryService.java @@ -75,13 +75,15 @@ public boolean exists(ID id) { return getRepository().existsById(id); } + /** + * Counts the number of entities that match the given filter. + * + * @param filter + * the filter, or {@code null} to use no filter + * @return + */ @Override - public long count() { - return getRepository().count(); - } - - @Override - public long count(Filter filter) { + public long count(@Nullable Filter filter) { return getRepository().count(toSpec(filter)); } diff --git a/vaadin-spring/src/test/java/com/vaadin/flow/spring/data/jpa/TestCrudRepositoryService.java b/vaadin-spring/src/test/java/com/vaadin/flow/spring/data/jpa/TestCrudRepositoryService.java index ee252c7be3e..fd71a17d1f3 100644 --- a/vaadin-spring/src/test/java/com/vaadin/flow/spring/data/jpa/TestCrudRepositoryService.java +++ b/vaadin-spring/src/test/java/com/vaadin/flow/spring/data/jpa/TestCrudRepositoryService.java @@ -2,6 +2,8 @@ import org.springframework.stereotype.Service; +import com.vaadin.flow.spring.data.jpa.CrudRepositoryService; + @Service public class TestCrudRepositoryService extends CrudRepositoryService { From 961675c8de0751df1f5385bff8a81c312f59ff39 Mon Sep 17 00:00:00 2001 From: Artur Signell Date: Mon, 13 Jan 2025 13:21:36 +0200 Subject: [PATCH 29/29] format --- .../main/java/com/vaadin/flow/spring/data/CountService.java | 3 +-- .../main/java/com/vaadin/flow/spring/data/CrudService.java | 2 +- .../main/java/com/vaadin/flow/spring/data/FormService.java | 4 ++-- .../main/java/com/vaadin/flow/spring/data/ListService.java | 2 +- 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/CountService.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/CountService.java index 211189d20aa..8105ea82da5 100644 --- a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/CountService.java +++ b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/CountService.java @@ -14,8 +14,7 @@ public interface CountService { * * @param filter * the filter, or {@code null} to use no filter - * @return - * the number of items in the service that match the filter + * @return the number of items in the service that match the filter */ public long count(@Nullable Filter filter); diff --git a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/CrudService.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/CrudService.java index 9f2225935a0..72de511899a 100644 --- a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/CrudService.java +++ b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/CrudService.java @@ -2,7 +2,7 @@ /** * A service that can create, read, update, and delete a given type of object. - * + * * @param * the type of object to manage * @param diff --git a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/FormService.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/FormService.java index 7bc78c1fd5a..b6b74d9a36e 100644 --- a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/FormService.java +++ b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/FormService.java @@ -2,12 +2,12 @@ /** * A service that can update and delete a given type of object. - * + * * @param * the type of object to manage * @param * the type of the object's identifier - * + * */ public interface FormService { diff --git a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/ListService.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/ListService.java index c55a3db83ea..237efd7afb6 100644 --- a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/ListService.java +++ b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/ListService.java @@ -9,7 +9,7 @@ /** * A service that can list the given type of object. - * + * * @param * the type of object to list */