Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Filtering for auto grid #1259

Merged
merged 12 commits into from
Sep 14, 2023
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

23 changes: 21 additions & 2 deletions packages/java/endpoint/pom.xml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>dev.hilla</groupId>
Expand Down Expand Up @@ -201,7 +202,12 @@
</exclusion>
</exclusions>
</dependency>

<dependency>
<groupId>jakarta.persistence</groupId>
<artifactId>jakarta.persistence-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>dev.hilla</groupId>
<artifactId>parser-jvm-utils</artifactId>
Expand Down Expand Up @@ -264,6 +270,19 @@
<artifactId>spring-boot-test-autoconfigure</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
<scope>test</scope>
</dependency>


<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>


</dependencies>

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package dev.hilla.crud;

import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.domain.Specification;

@Configuration
public class CrudConfiguration {

@Bean
@ConditionalOnClass(Specification.class)
JpaFilterConverter jpaFilterConverter() {
return new JpaFilterConverter();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,13 @@
import java.util.List;

import dev.hilla.EndpointExposed;
import dev.hilla.Nullable;
import dev.hilla.crud.filter.Filter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;

/**
* A browser-callable service that delegates crud operations to a JPA
Expand All @@ -13,21 +18,32 @@
@EndpointExposed
public class CrudRepositoryService<T, ID> implements CrudService<T> {

private JpaRepository<T, ID> repository;
@Autowired
private JpaFilterConverter jpaFilterConverter;

private JpaSpecificationExecutor<T> repository;
private Class<T> entityClass;

/**
* Creates the service using the given repository.
*
* @param repository
* the JPA repository
*/
public CrudRepositoryService(JpaRepository<T, ID> repository) {
public <R extends JpaRepository<T, ID> & JpaSpecificationExecutor<T>> CrudRepositoryService(
Class<T> entityClass, R repository) {
this.repository = repository;
this.entityClass = entityClass;
}

protected JpaSpecificationExecutor<T> getRepository() {
return repository;
}

@Override
public List<T> list(Pageable pageable) {
return repository.findAll(pageable).getContent();
public List<T> list(Pageable pageable, @Nullable Filter filter) {
Specification<T> spec = jpaFilterConverter.toSpec(filter, entityClass);
return repository.findAll(spec, pageable).getContent();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import java.util.List;

import dev.hilla.Nullable;
import dev.hilla.crud.filter.Filter;
import org.springframework.data.domain.Pageable;

/**
Expand All @@ -18,8 +20,10 @@ public interface CrudService<T> {
*
* @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
*/
List<T> list(Pageable pageable);
List<T> list(Pageable pageable, @Nullable Filter filter);

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package dev.hilla.crud;

import jakarta.persistence.EntityManager;

import dev.hilla.crud.filter.AndFilter;
import dev.hilla.crud.filter.Filter;
import dev.hilla.crud.filter.OrFilter;
import dev.hilla.crud.filter.PropertyStringFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Component;

@Component
public class JpaFilterConverter {

@Autowired
private EntityManager em;

/**
* Converts the given Hilla filter specification into a JPA filter
* specification.
*
* @param <T>
* 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 <T> Specification<T> toSpec(Filter rawFilter, Class<T> entity) {
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 = em.getMetamodel().entity(entity)
.getAttribute(filter.getPropertyId()).getJavaType();

return new PropertyStringFilterSpecification<>(filter, javaType);

} else {
if (rawFilter != null) {
throw new IllegalArgumentException("Unknown filter type "
+ rawFilter.getClass().getName());
}
return Specification.anyOf();
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package dev.hilla.crud;

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 dev.hilla.crud.filter.PropertyStringFilter;
import org.springframework.data.jpa.domain.Specification;

public class PropertyStringFilterSpecification<T> implements Specification<T> {

private PropertyStringFilter filter;
private Class<?> javaType;

public PropertyStringFilterSpecification(PropertyStringFilter filter,
Class<?> javaType) {
this.filter = filter;
this.javaType = javaType;
}

@Override
public Predicate toPredicate(Root<T> root, CriteriaQuery<?> query,
CriteriaBuilder criteriaBuilder) {
String value = filter.getFilterValue();
Path<String> propertyPath = root.get(filter.getPropertyId());
sissbruecker marked this conversation as resolved.
Show resolved Hide resolved
if (javaType == String.class) {
Expression<String> expr = criteriaBuilder.lower(propertyPath);
switch (filter.getMatcher()) {
case EQUALS:
return criteriaBuilder.equal(expr, value.toLowerCase());
case CONTAINS:
return criteriaBuilder.like(expr,
"%" + value.toLowerCase() + "%");
}

throw new IllegalArgumentException(
"Matcher of type " + filter.getMatcher() + " is unknown");
} else {
return criteriaBuilder.equal(propertyPath, value.toLowerCase());
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package dev.hilla.crud.filter;

import java.util.List;

/**
* A filter that requires all children to pass.
*/
public class AndFilter implements Filter {
private List<Filter> children;

public List<Filter> getChildren() {
return children;
}

public void setChildren(List<Filter> children) {
this.children = children;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package dev.hilla.crud.filter;

import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonSubTypes.Type;
import com.fasterxml.jackson.annotation.JsonTypeInfo;

@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "t")
@JsonSubTypes({ @Type(value = OrFilter.class, name = "or"),
@Type(value = AndFilter.class, name = "and"),
@Type(value = PropertyStringFilter.class, name = "propertyString") })
public interface Filter {
sissbruecker marked this conversation as resolved.
Show resolved Hide resolved

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package dev.hilla.crud.filter;

import java.util.List;

/**
* A filter that requires at least on of its children to pass.
Artur- marked this conversation as resolved.
Show resolved Hide resolved
*/
public class OrFilter implements Filter {

private List<Filter> children;

public List<Filter> getChildren() {
return children;
}

public void setChildren(List<Filter> children) {
this.children = children;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package dev.hilla.crud.filter;

/**
* A filter for a given property that matches a string value using the given
* matcher.
*/
public class PropertyStringFilter implements Filter {
public enum Matcher {
EQUALS, CONTAINS;
}

private String propertyId;
private String filterValue;
private Matcher matcher;

public String getPropertyId() {
return propertyId;
}

public void setPropertyId(String propertyId) {
this.propertyId = propertyId;
}

public String getFilterValue() {
return filterValue;
}

public void setFilterValue(String filterValue) {
this.filterValue = filterValue;
}

public Matcher getMatcher() {
return matcher;
}

public void setMatcher(Matcher type) {
this.matcher = type;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
@org.springframework.lang.NonNullApi
package dev.hilla.crud.filter;
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
dev.hilla.EndpointController
dev.hilla.push.PushConfigurer
dev.hilla.internal.hotswap.HotSwapConfiguration
dev.hilla.crud.CrudConfiguration
Loading
Loading