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. From a technical point of view, I compile a simple syntax to JPA predicates.
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.
<dependency>
<groupId>com.turkraft</groupId>
<artifactId>spring-filter</artifactId>
<version>1.0.2</version>
</dependency>
Requires javax.persistence-api, spring-data-jpa, spring-web and spring-webmvc
@GetMapping(value = "/search")
public List<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 thePageable
argument if pagination is not needed.
Requires javax.persistence-api, spring-data-jpa, spring-web
Specification<Entity> spec = new FilterSpecification<Entity>(query);
Requires javax.persistence-api, spring-data-jpa
Predicate predicate = ExpressionGenerator.run(String query, Root<?> r, CriteriaQuery<?> q, CriteriaBuilder cb);
⚠️ If you need to search over relations, you also require hibernate-core
/* Using static methods */
import static com.turkraft.springfilter.FilterBuilder.*;
Filter filter = filter(like("name", "%jose%"));
String query = filter.generate(); // name ~ '%jose%'
// Predicate predicate = ExpressionGenerator.run(filter, Root<?> r, CriteriaQuery<?> cq, CriteriaBuilder cb);
// Specification<Entity> spec = new FilterSpecification<Entity>(filter);
Field names should be directly given without any extra literals. Dots indicate nested fields. For example: category.updatedAt
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'
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)
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 withExpressionGeneratorParameters.CASE_SENSITIVE_LIKE_OPERATOR
.
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 two given strings | concat(firstName, concat(' ', lastName)) |
You may want to customize the behavior of the different processes taking place. For now, you can only change the date format but advanced customization will be soon available in order to let you completely personalize the tokenizer, the parser, the query builder, with the possibility of adding custom functions and much more.
You are able to change the date format by setting the static formatters inside the SpringFilterParameters
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 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
@GetMapping(value = "/search")
public List<Entity> search(@Filter Document doc, Pageable page) {
// your repo may implement DocumentExecutor for easy usage
return repo.findAll(doc, page);
}
Bson bson = BsonGenerator.run(filter);
Document doc = BsonUtils.getDocumentFromBson(bson);
Query query = BsonUtils.getQueryFromDocument(doc);
// ...
⚠️ Functions are currently not supported, and field types are limited to strings/enums, numbers, booleans, and collections
Ideas and pull requests are always welcome. Google's Java Style is used for formatting.
Thanks to @marcopag90 and @glodepa for adding support to MongoDB.
Distributed under the MIT license.