/spring-filter

Painless filtering library for JPA entities and MongoDB collections. Smoothly integrates with Spring APIs.

Primary LanguageJavaMIT LicenseMIT

Spring Filter

Spring Filter Logo

You need a way to dynamically filter entities without any effort? Just add me to your pom.xml. Your API will gain a full featured search functionality. You don't work with APIs? No problem, you may still not want to mess with SQL, JPA predicates, security, and all of that I guess.

Example (try it live)

/search?filter= average(ratings) > 4.5 and brand.name in ('audi', 'land rover') and (year > 2018 or km < 50000) and color : 'white' and accidents is empty

/* Entity used in the query above */
@Entity public class Car {
  @Id long id;
      int year;
      int km;
  @Enumerated Color color;
  @ManyToOne Brand brand;
  @OneToMany List<Accident> accidents;
  @ElementCollection List<Integer> ratings;
  // ...
}

🚀 Yes we support booleans, dates, enums, functions, and even relations! Need something else? Tell us here.

Installation

<dependency>
    <groupId>com.turkraft</groupId>
    <artifactId>spring-filter</artifactId>
    <version>2.0.2</version>
</dependency>

Usages

a. Controller

Requires javax.persistence-api, spring-data-jpa, spring-web and spring-webmvc

@GetMapping(value = "/search")
public Page<Entity> search(@Filter Specification<Entity> spec, Pageable page) {
  return repo.findAll(spec, page);
}

The repository should implement JpaSpecificationExecutor in order to execute Spring's Specification, SimpleJpaRepository is a well known implementation. You can remove the Pageable argument and return a List if pagination is not needed.

b. Specification

Requires javax.persistence-api, spring-data-jpa, spring-web

Specification<Entity> spec = new FilterSpecification<Entity>(query);

c. Predicate

Requires javax.persistence-api

Predicate predicate = ExpressionGenerator.run(String query, Root<?> r, CriteriaQuery<?> q, CriteriaBuilder cb);

⚠️ If you need to search over relations, you also require hibernate-core

d. Builder

/* Using static methods */
import static com.turkraft.springfilter.FilterBuilder.*;
Filter filter = like("name", "%jose%");
String query = filter.generate(); // name ~ '%jose%'
// filter = Filter.from(query);
// Predicate predicate = ExpressionGenerator.run(filter, Root<?> r, CriteriaQuery<?> cq, CriteriaBuilder cb);
// Specification<Entity> spec = new FilterSpecification<Entity>(filter);

Syntax

Fields

Field names should be directly given without any extra literals. Dots indicate nested fields. For example: category.updatedAt

Inputs

Numbers should be directly given. Booleans should also directly be given, valid values are true and false (case insensitive). Others such as strings, enums, dates, should be quoted. For example: status : 'active'

Operators

Literal (case insensitive) Description Example
and and's two expressions status : 'active' and createdAt > '1-1-2000'
or or's two expressions value ~ '%hello%' or name ~ '%world%'
not not's an expression not (id > 100 or category.order is null)

You may prioritize operators using parentheses, for example: x and (y or z)

Comparators

Literal (case insensitive) Description Example
~ checks if the left (string) expression is similar to the right (string) expression catalog.name ~ 'electronic%'
: checks if the left expression is equal to the right expression id : 5
! checks if the left expression is not equal to the right expression username ! 'torshid'
> checks if the left expression is greater than the right expression distance > 100
>: checks if the left expression is greater or equal to the right expression distance >: 100
< checks if the left expression is smaller than the right expression distance < 100
<: checks if the left expression is smaller or equal to the right expression distance <: 100
is null checks if an expression is null status is null
is not null checks if an expression is not null status is not null
is empty checks if the (collection) expression is empty children is empty
is not empty checks if the (collection) expression is not empty children is not empty
in checks if an expression is present in the right expressions status in ('initialized', 'active')

Note that the * character can also be used instead of % when using the ~ comparator. By default, this comparator is case insensitive, the behavior can be changed with FilterParameters.CASE_SENSITIVE_LIKE_OPERATOR.

Functions

A function is characterized by its name (case insensitive) followed by parentheses. For example: currentTime(). Some functions might also take arguments, arguments are seperated with commas. For example: min(ratings) > 3

Name Description Example
absolute returns the absolute absolute(x)
average returns the average average(ratings)
min returns the minimum min(ratings)
max returns the maximum max(ratings)
sum returns the sum sum(scores)
currentDate returns the current date currentDate()
currentTime returns the current time currentTime()
currentTimestamp returns the current time stamp currentTimestamp()
size returns the collection's size size(accidents)
length returns the string's length length(name)
trim returns the trimmed string trim(name)
upper returns the uppercased string upper(name)
lower returns the lowercased string lower(name)
concat returns the concatenation of given strings concat(firstName, ' ', lastName)

Subqueries

Name Description Example Explanation
exists returns the existence of a subquery result exists(employees.age > 60) returns true if at least one employee's age is greater than 60

Configuration

Date format

You are able to change the date format by setting the static formatters inside the FilterParameters class. You may see below the default patterns and how you can set them with properties:

Type Default Pattern Property Name
java.util.Date dd-MM-yyyy turkraft.springfilter.dateformatter.pattern
java.time.LocalDate dd-MM-yyyy turkraft.springfilter.localdateformatter.pattern
java.time.LocalDateTime dd-MM-yyyy'T'HH:mm:ss turkraft.springfilter.localdatetimeformatter.pattern
java.time.OffsetDateTime dd-MM-yyyy'T'HH:mm:ss.SSSXXX turkraft.springfilter.offsetdatetimeformatter.pattern
java.time.Instant dd-MM-yyyy'T'HH:mm:ss.SSSXXX Parses using DateTimeFormatter.ISO_INSTANT

MongoDB

MongoDB is also partially supported as an alternative to JPA. The query input is compiled to a Bson/Document filter. You can then use it as you wish with MongoTemplate or MongoOperations for example.

Requires spring-data-mongodb

Usage

@GetMapping(value = "/search")
public Page<Entity> search(@Filter(entityClass = Entity.class) Document doc, Pageable page) {
  // your repo may implement DocumentExecutor for easy usage
  return repo.findAll(doc, page); 
}
Bson bson = BsonGenerator.run(Filter.from(query), Entity.class);
Document doc = BsonUtils.getDocumentFromBson(bson);
Query query = BsonUtils.getQueryFromDocument(doc);
// ...

⚠️ Functions are currently not supported with MongoDB, and the ~ operator actually uses the regex operator

Customization

If you need to customize the behavior of the filter, the way to go is to extend the FilterBaseVisitor class, by taking QueryGenerator or ExpressionGenerator as examples. In order to also modify the query syntax, you should start by cloning the repository and editing the Filter.g4 file.

Articles

Contributing

Ideas and pull requests are always welcome. Google's Java Style is used for formatting.

Contributors

Thanks to @marcopag90 and @glodepa for adding support to MongoDB.

License

Distributed under the MIT license.