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.
<dependency>
<groupId>com.turkraft</groupId>
<artifactId>spring-filter</artifactId>
<version>2.1.1</version>
</dependency>
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 thePageable
argument and return aList
if pagination and sorting are not needed.
Requires javax.persistence-api, spring-data-jpa, spring-web
Specification<Entity> spec = new FilterSpecification<Entity>(query);
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
/* 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);
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
. Others such as strings, enums, dates, should be quoted. For example: status : 'active'
Literal | 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 | 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 withFilterParameters.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(a, b), sum(scores) |
diff | returns the difference | diff(a, b) |
prod | returns the product | prod(a, b) |
quot | returns the quotient | quot(a, b) |
mod | returns the modulus | mod(a, b) |
sqrt | returns the square root | sqrt(a) |
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) |
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 |
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.LocalTime | HH:mm:ss | turkraft.springfilter.localtimeformatter.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
⚠️ Functions are currently not supported with MongoDB, and the~
operator actually uses the regex operator.
@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);
// ...
The codec registry can be customized using the BsonGeneratorParameters.setCodecRegistry
method. You may also use the CodecRegistryProvider
as follows:
@Configuration
public class MongoDBCodecConfiguration {
public MongoDBCodecConfiguration(CodecRegistryProvider codecRegistryProvider) {
BsonGeneratorParameters.setCodecRegistry(codecRegistryProvider.getCodecRegistry());
}
}
Instead of manually writing string queries in your frontend applications, you may use the JavaScript query builder which is similar to the Java FilterBuilder
class.
import { SpringFilterQueryBuilder as builder } from 'https://cdn.jsdelivr.net/npm/[email protected]/src/index.min.js';
const filter =
builder.or(
builder.and(
builder.equal("test.test1", "testvalue1"),
builder.isNotNull("test.test2")
),
builder.notEqual("test.test2", "testvalue2")
);
const req = await fetch('http://api/person?filter=' + filter.toString());
Please see documentation.
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.
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.
- Thanks to @sisimomo for creating the JavaScript query builder.
- Thanks to @68ociredef for creating the Angular query builder.
Distributed under the MIT license.