Skip to content

Commit

Permalink
fix: Make JpaFilterConverter work without the EntityManager
Browse files Browse the repository at this point in the history
Fixes #2569
  • Loading branch information
Artur- committed Dec 18, 2024
1 parent aa99331 commit fdaae27
Show file tree
Hide file tree
Showing 6 changed files with 35 additions and 80 deletions.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,36 +1,28 @@
package com.vaadin.hilla.crud;

import jakarta.persistence.EntityManager;
import org.springframework.data.jpa.domain.Specification;

import com.vaadin.hilla.crud.filter.AndFilter;
import com.vaadin.hilla.crud.filter.Filter;
import com.vaadin.hilla.crud.filter.OrFilter;
import com.vaadin.hilla.crud.filter.PropertyStringFilter;
import jakarta.persistence.criteria.Path;
import jakarta.persistence.criteria.Root;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Component;

/**
* 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.
* <p>
* This class requires an EntityManager to be available as a Spring bean and
* thus should be injected into the bean that wants to use the converter.
* Manually creating new instances of this class will not work.
* custom {@link com.vaadin.flow.spring.data.ListService} or
* {@link com.vaadin.flow.spring.data.CrudService} implementations that use JPA
* as the data source.
*/
@Component
public class JpaFilterConverter {
public final class JpaFilterConverter {

@Autowired
private EntityManager em;
private JpaFilterConverter() {
// Utils only
}

/**
* Converts the given Hilla 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.
* <p>
* If the filter contains {@link PropertyStringFilter} instances, their
* properties, or nested property paths, need to match the structure of the
Expand All @@ -45,7 +37,8 @@ public class JpaFilterConverter {
* the entity class
* @return a JPA filter specification for the given filter
*/
public <T> Specification<T> toSpec(Filter rawFilter, Class<T> entity) {
public static <T> Specification<T> toSpec(Filter rawFilter,
Class<T> entity) {
if (rawFilter == null) {
return Specification.anyOf();
}
Expand All @@ -56,35 +49,10 @@ public <T> Specification<T> toSpec(Filter rawFilter, Class<T> 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 {
if (rawFilter != null) {
throw new IllegalArgumentException("Unknown filter type "
+ rawFilter.getClass().getName());
}
return Specification.anyOf();
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 = em.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 em.getMetamodel().entity(entity).getAttribute(propertyId)
.getJavaType();
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,6 @@
public class ListRepositoryService<T, ID, R extends CrudRepository<T, ID> & JpaSpecificationExecutor<T>>
implements ListService<T>, GetService<T, ID>, CountService {

@Autowired
private JpaFilterConverter jpaFilterConverter;

@Autowired
private ApplicationContext applicationContext;

Expand Down Expand Up @@ -105,7 +102,7 @@ public long count(@Nullable Filter filter) {
* @return a JPA specification
*/
protected Specification<T> toSpec(@Nullable Filter filter) {
return jpaFilterConverter.toSpec(filter, entityClass);
return JpaFilterConverter.toSpec(filter, entityClass);
}

@SuppressWarnings("unchecked")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,16 @@
public class PropertyStringFilterSpecification<T> implements Specification<T> {

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<T> root, CriteriaQuery<?> query,
CriteriaBuilder criteriaBuilder) {
String value = filter.getFilterValue();
Class<?> javaType = getJavaType(filter.getPropertyId(), root);
Path<String> propertyPath = getPath(filter.getPropertyId(), root);
if (javaType == String.class) {
Expression<String> expr = criteriaBuilder.lower(propertyPath);
Expand Down Expand Up @@ -171,6 +169,21 @@ private Path<String> getPath(String propertyId, Root<T> root) {
return path;
}

private Class<?> getJavaType(String propertyId, Root<T> 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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,6 @@ public class FilterTest {
@Autowired
private TestRepository repository;

@Autowired
private JpaFilterConverter jpaFilterConverter;

@Test
public void filterStringPropertyUsingContains() {
setupNames("Jack", "John", "Johnny", "Polly", "Josh");
Expand Down Expand Up @@ -425,7 +422,7 @@ private void assertFilterResult(Filter filter, List<TestObject> result) {
}

private List<TestObject> executeFilter(Filter filter) {
Specification<TestObject> spec = jpaFilterConverter.toSpec(filter,
Specification<TestObject> spec = JpaFilterConverter.toSpec(filter,
TestObject.class);
return repository.findAll(spec);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,6 @@ public class FilterTransformerTest {
@Autowired
private TestRepository repository;

@Autowired
private JpaFilterConverter jpaFilterConverter;

@Test
public void testRemap() {
var testObject = new TestObject();
Expand Down Expand Up @@ -88,7 +85,7 @@ public void testRemap() {
});

var filter = transformer.apply(andFilter);
var spec = jpaFilterConverter.toSpec(filter, TestObject.class);
var spec = JpaFilterConverter.toSpec(filter, TestObject.class);
var result = repository.findAll(spec);
assertEquals(2, result.size());

Expand Down

0 comments on commit fdaae27

Please sign in to comment.