diff --git a/spring-security-rest-full/pom.xml b/spring-security-rest-full/pom.xml index f26ef6df37..e8971b6fa3 100644 --- a/spring-security-rest-full/pom.xml +++ b/spring-security-rest-full/pom.xml @@ -107,7 +107,15 @@ querydsl-jpa 3.6.2 - + + + + + cz.jirutka.rsql + rsql-parser + 2.0.0 + + diff --git a/spring-security-rest-full/src/main/java/org/baeldung/persistence/dao/rsql/CustomRsqlVisitor.java b/spring-security-rest-full/src/main/java/org/baeldung/persistence/dao/rsql/CustomRsqlVisitor.java new file mode 100644 index 0000000000..64c84678af --- /dev/null +++ b/spring-security-rest-full/src/main/java/org/baeldung/persistence/dao/rsql/CustomRsqlVisitor.java @@ -0,0 +1,34 @@ +package org.baeldung.persistence.dao.rsql; + +import org.baeldung.persistence.model.User; +import org.springframework.data.jpa.domain.Specification; + +import cz.jirutka.rsql.parser.ast.AndNode; +import cz.jirutka.rsql.parser.ast.ComparisonNode; +import cz.jirutka.rsql.parser.ast.OrNode; +import cz.jirutka.rsql.parser.ast.RSQLVisitor; + +public class CustomRsqlVisitor implements RSQLVisitor, Void> { + + private UserRsqlSpecBuilder builder; + + public CustomRsqlVisitor() { + builder = new UserRsqlSpecBuilder(); + } + + @Override + public Specification visit(final AndNode node, final Void param) { + return builder.createSpecification(node); + } + + @Override + public Specification visit(final OrNode node, final Void param) { + return builder.createSpecification(node); + } + + @Override + public Specification visit(final ComparisonNode node, final Void params) { + return builder.createSpecification(node); + } + +} diff --git a/spring-security-rest-full/src/main/java/org/baeldung/persistence/dao/rsql/RsqlSearchOperation.java b/spring-security-rest-full/src/main/java/org/baeldung/persistence/dao/rsql/RsqlSearchOperation.java new file mode 100644 index 0000000000..769063cbee --- /dev/null +++ b/spring-security-rest-full/src/main/java/org/baeldung/persistence/dao/rsql/RsqlSearchOperation.java @@ -0,0 +1,28 @@ +package org.baeldung.persistence.dao.rsql; + +import cz.jirutka.rsql.parser.ast.ComparisonOperator; +import cz.jirutka.rsql.parser.ast.RSQLOperators; + +public enum RsqlSearchOperation { + EQUAL(RSQLOperators.EQUAL), NOT_EQUAL(RSQLOperators.NOT_EQUAL), GREATER_THAN(RSQLOperators.GREATER_THAN), GREATER_THAN_OR_EQUAL(RSQLOperators.GREATER_THAN_OR_EQUAL), LESS_THAN(RSQLOperators.LESS_THAN), LESS_THAN_OR_EQUAL(RSQLOperators.LESS_THAN_OR_EQUAL), IN( + RSQLOperators.IN), NOT_IN(RSQLOperators.NOT_IN); + + private ComparisonOperator operator; + + private RsqlSearchOperation(final ComparisonOperator operator) { + this.operator = operator; + } + + public static RsqlSearchOperation getSimpleOperator(final ComparisonOperator operator) { + for (final RsqlSearchOperation operation : values()) { + if (operation.getOperator() == operator) { + return operation; + } + } + return null; + } + + public ComparisonOperator getOperator() { + return operator; + } +} diff --git a/spring-security-rest-full/src/main/java/org/baeldung/persistence/dao/rsql/UserRsqlSpecBuilder.java b/spring-security-rest-full/src/main/java/org/baeldung/persistence/dao/rsql/UserRsqlSpecBuilder.java new file mode 100644 index 0000000000..202370a64b --- /dev/null +++ b/spring-security-rest-full/src/main/java/org/baeldung/persistence/dao/rsql/UserRsqlSpecBuilder.java @@ -0,0 +1,58 @@ +package org.baeldung.persistence.dao.rsql; + +import java.util.ArrayList; +import java.util.List; + +import org.baeldung.persistence.model.User; +import org.springframework.data.jpa.domain.Specifications; + +import cz.jirutka.rsql.parser.ast.ComparisonNode; +import cz.jirutka.rsql.parser.ast.LogicalNode; +import cz.jirutka.rsql.parser.ast.LogicalOperator; +import cz.jirutka.rsql.parser.ast.Node; + +public class UserRsqlSpecBuilder { + + public Specifications createSpecification(final Node node) { + if (node instanceof LogicalNode) { + return createSpecification((LogicalNode) node); + } + if (node instanceof ComparisonNode) { + return createSpecification((ComparisonNode) node); + } + return null; + } + + public Specifications createSpecification(final LogicalNode logicalNode) { + final List> specs = new ArrayList>(); + Specifications temp; + for (final Node node : logicalNode.getChildren()) { + temp = createSpecification(node); + if (temp != null) { + specs.add(temp); + } + } + + Specifications result = specs.get(0); + + if (logicalNode.getOperator() == LogicalOperator.AND) { + for (int i = 1; i < specs.size(); i++) { + result = Specifications.where(result).and(specs.get(i)); + } + } + + else if (logicalNode.getOperator() == LogicalOperator.OR) { + for (int i = 1; i < specs.size(); i++) { + result = Specifications.where(result).or(specs.get(i)); + } + } + + return result; + } + + public Specifications createSpecification(final ComparisonNode comparisonNode) { + final Specifications result = Specifications.where(new UserRsqlSpecification(comparisonNode.getSelector(), comparisonNode.getOperator(), comparisonNode.getArguments())); + return result; + } + +} diff --git a/spring-security-rest-full/src/main/java/org/baeldung/persistence/dao/rsql/UserRsqlSpecification.java b/spring-security-rest-full/src/main/java/org/baeldung/persistence/dao/rsql/UserRsqlSpecification.java new file mode 100644 index 0000000000..77b491babe --- /dev/null +++ b/spring-security-rest-full/src/main/java/org/baeldung/persistence/dao/rsql/UserRsqlSpecification.java @@ -0,0 +1,94 @@ +package org.baeldung.persistence.dao.rsql; + +import java.util.ArrayList; +import java.util.List; + +import javax.persistence.criteria.CriteriaBuilder; +import javax.persistence.criteria.CriteriaQuery; +import javax.persistence.criteria.Predicate; +import javax.persistence.criteria.Root; + +import org.baeldung.persistence.model.User; +import org.springframework.data.jpa.domain.Specification; + +import cz.jirutka.rsql.parser.ast.ComparisonOperator; + +public class UserRsqlSpecification implements Specification { + + private String property; + private ComparisonOperator operator; + private List arguments; + + public UserRsqlSpecification(final String property, final ComparisonOperator operator, final List arguments) { + super(); + this.property = property; + this.operator = operator; + this.arguments = arguments; + } + + + @Override + public Predicate toPredicate(final Root root, final CriteriaQuery query, final CriteriaBuilder builder) { + final List args = castArguments(root); + final Object argument = args.get(0); + switch (RsqlSearchOperation.getSimpleOperator(operator)) { + + case EQUAL: { + if (argument instanceof String) { + return builder.like(root. get(property), argument.toString().replace('*', '%')); + } else if (argument == null) { + return builder.isNull(root.get(property)); + } else { + return builder.equal(root.get(property), argument); + } + } + case NOT_EQUAL: { + if (argument instanceof String) { + return builder.notLike(root. get(property), argument.toString().replace('*', '%')); + } else if (argument == null) { + return builder.isNotNull(root.get(property)); + } else { + return builder.notEqual(root.get(property), argument); + } + } + case GREATER_THAN: { + return builder.greaterThan(root. get(property), argument.toString()); + } + case GREATER_THAN_OR_EQUAL: { + return builder.greaterThanOrEqualTo(root. get(property), argument.toString()); + } + case LESS_THAN: { + return builder.lessThan(root. get(property), argument.toString()); + } + case LESS_THAN_OR_EQUAL: { + return builder.lessThanOrEqualTo(root. get(property), argument.toString()); + } + case IN: + return root.get(property).in(args); + case NOT_IN: + return builder.not(root.get(property).in(args)); + } + + return null; + } + + // === private + + private List castArguments(final Root root) { + final List args = new ArrayList(); + final Class type = root.get(property).getJavaType(); + + for (final String argument : arguments) { + if (type.equals(Integer.class)) { + args.add(Integer.parseInt(argument)); + } else if (type.equals(Long.class)) { + args.add(Long.parseLong(argument)); + } else { + args.add(argument); + } + } + + return args; + } + +} diff --git a/spring-security-rest-full/src/main/java/org/baeldung/web/controller/UserController.java b/spring-security-rest-full/src/main/java/org/baeldung/web/controller/UserController.java index e3674f3e03..08b2042182 100644 --- a/spring-security-rest-full/src/main/java/org/baeldung/web/controller/UserController.java +++ b/spring-security-rest-full/src/main/java/org/baeldung/web/controller/UserController.java @@ -10,6 +10,7 @@ import org.baeldung.persistence.dao.MyUserPredicatesBuilder; import org.baeldung.persistence.dao.MyUserRepository; import org.baeldung.persistence.dao.UserRepository; import org.baeldung.persistence.dao.UserSpecificationsBuilder; +import org.baeldung.persistence.dao.rsql.CustomRsqlVisitor; import org.baeldung.persistence.model.MyUser; import org.baeldung.persistence.model.User; import org.baeldung.web.util.SearchCriteria; @@ -29,6 +30,9 @@ import com.google.common.base.Joiner; import com.google.common.base.Preconditions; import com.mysema.query.types.expr.BooleanExpression; +import cz.jirutka.rsql.parser.RSQLParser; +import cz.jirutka.rsql.parser.ast.Node; + @Controller public class UserController { @@ -91,6 +95,14 @@ public class UserController { return mydao.findAll(exp); } + @RequestMapping(method = RequestMethod.GET, value = "/users/rsql") + @ResponseBody + public List findAllByRsql(@RequestParam(value = "search") final String search) { + final Node rootNode = new RSQLParser().parse(search); + final Specification spec = rootNode.accept(new CustomRsqlVisitor()); + return dao.findAll(spec); + } + // API - WRITE @RequestMapping(method = RequestMethod.POST, value = "/users") diff --git a/spring-security-rest-full/src/test/java/org/baeldung/persistence/query/RsqlTest.java b/spring-security-rest-full/src/test/java/org/baeldung/persistence/query/RsqlTest.java new file mode 100644 index 0000000000..0b02f533e8 --- /dev/null +++ b/spring-security-rest-full/src/test/java/org/baeldung/persistence/query/RsqlTest.java @@ -0,0 +1,105 @@ +package org.baeldung.persistence.query; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.collection.IsIn.isIn; +import static org.hamcrest.core.IsNot.not; + +import java.util.List; + +import org.baeldung.persistence.dao.UserRepository; +import org.baeldung.persistence.dao.rsql.CustomRsqlVisitor; +import org.baeldung.persistence.model.User; +import org.baeldung.spring.PersistenceConfig; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.transaction.TransactionConfiguration; +import org.springframework.transaction.annotation.Transactional; + +import cz.jirutka.rsql.parser.RSQLParser; +import cz.jirutka.rsql.parser.ast.Node; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = { PersistenceConfig.class }) +@Transactional +@TransactionConfiguration +public class RsqlTest { + + @Autowired + private UserRepository repository; + + private User userJohn; + + private User userTom; + + @Before + public void init() { + userJohn = new User(); + userJohn.setFirstName("john"); + userJohn.setLastName("doe"); + userJohn.setEmail("john@doe.com"); + userJohn.setAge(22); + repository.save(userJohn); + + userTom = new User(); + userTom.setFirstName("tom"); + userTom.setLastName("doe"); + userTom.setEmail("tom@doe.com"); + userTom.setAge(26); + repository.save(userTom); + } + + @Test + public void givenFirstAndLastName_whenGettingListOfUsers_thenCorrect() { + final Node rootNode = new RSQLParser().parse("firstName==john;lastName==doe"); + final Specification spec = rootNode.accept(new CustomRsqlVisitor()); + final List results = repository.findAll(spec); + + assertThat(userJohn, isIn(results)); + assertThat(userTom, not(isIn(results))); + } + + @Test + public void givenFirstNameInverse_whenGettingListOfUsers_thenCorrect() { + final Node rootNode = new RSQLParser().parse("firstName!=john"); + final Specification spec = rootNode.accept(new CustomRsqlVisitor()); + final List results = repository.findAll(spec); + + assertThat(userTom, isIn(results)); + assertThat(userJohn, not(isIn(results))); + } + + @Test + public void givenMinAge_whenGettingListOfUsers_thenCorrect() { + final Node rootNode = new RSQLParser().parse("age>25"); + final Specification spec = rootNode.accept(new CustomRsqlVisitor()); + final List results = repository.findAll(spec); + + assertThat(userTom, isIn(results)); + assertThat(userJohn, not(isIn(results))); + } + + @Test + public void givenFirstNamePrefix_whenGettingListOfUsers_thenCorrect() { + final Node rootNode = new RSQLParser().parse("firstName==jo*"); + final Specification spec = rootNode.accept(new CustomRsqlVisitor()); + final List results = repository.findAll(spec); + + assertThat(userJohn, isIn(results)); + assertThat(userTom, not(isIn(results))); + } + + @Test + public void givenListOfFirstName_whenGettingListOfUsers_thenCorrect() { + final Node rootNode = new RSQLParser().parse("firstName=in=(john,jack)"); + final Specification spec = rootNode.accept(new CustomRsqlVisitor()); + final List results = repository.findAll(spec); + + assertThat(userJohn, isIn(results)); + assertThat(userTom, not(isIn(results))); + } +}