The new Certification Class of REST With Spring is out:

>> CHECK OUT THE COURSE

1. Overview

In this fifth article of the series we’ll illustrate building the REST API Query language with the help of a cool library – rsql-parser.

RSQL is a super-set of the Feed Item Query Language (FIQL) – a clean and simple filter syntax for feeds; so it fits quite naturally into a REST API.

New Guide: Microservices with

Spring Boot and Spring Cloud

2. Preparations

First, let’s add a maven dependency to the library:

<dependency>
    <groupId>cz.jirutka.rsql</groupId>
    <artifactId>rsql-parser</artifactId>
    <version>2.0.0</version>
</dependency>

And also define the main entity we’re going to be working with throughout the examples – User:

@Entity
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
 
    private String firstName;
    private String lastName;
    private String email;
 
    private int age;
}

3. Parse the Request

The way RSQL expressions are represented internally is in the form of nodes and the visitor pattern is used parse out the input.

With that in mind, we’re going to implement the RSQLVisitor interface and create our own visitor implementation – CustomRsqlVisitor:

public class CustomRsqlVisitor<T> implements RSQLVisitor<Specification<T>, Void> {

    private GenericRsqlSpecBuilder<T> builder;

    public CustomRsqlVisitor() {
        builder = new GenericRsqlSpecBuilder<T>();
    }

    @Override
    public Specification<T> visit(AndNode node, Void param) {
        return builder.createSpecification(node);
    }

    @Override
    public Specification<T> visit(OrNode node, Void param) {
        return builder.createSpecification(node);
    }

    @Override
    public Specification<T> visit(ComparisonNode node, Void params) {
        return builder.createSecification(node);
    }
}

Now we need to deal with persistence and construct our query out of each of these nodes.

We’re going to use the Spring Data JPA Specifications we used before – and we’re going to implement a Specification builder to construct Specifications out of each of these nodes we visit:

public class GenericRsqlSpecBuilder<T> {

    public Specifications<T> createSpecification(Node node) {
        if (node instanceof LogicalNode) {
            return createSpecification((LogicalNode) node);
        }
        if (node instanceof ComparisonNode) {
            return createSpecification((ComparisonNode) node);
        }
        return null;
    }

    public Specifications<T> createSpecification(LogicalNode logicalNode) {
        List<Specifications<T>> specs = new ArrayList<Specifications<T>>();
        Specifications<T> temp;
        for (Node node : logicalNode.getChildren()) {
            temp = createSpecification(node);
            if (temp != null) {
                specs.add(temp);
            }
        }

        Specifications<T> 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<T> createSpecification(ComparisonNode comparisonNode) {
        Specifications<T> result = Specifications.where(
          new GenericRsqlSpecification<T>(
            comparisonNode.getSelector(), 
            comparisonNode.getOperator(), 
            comparisonNode.getArguments()
          )
        );
        return result;
    }
}

Note how:

  • LogicalNode is an AND/OR Node and has multiple children
  • ComparisonNode has no children and it hold the Selector, Operator and the Arguments

For example, for a query “name==john” – we have:

  1. Selector: “name”
  2. Operator: “==”
  3. Arguments:[john]

4. Create Custom Specification

When constructing the query we made use of a Specification:

public class GenericRsqlSpecification<T> implements Specification<T> {
    private String property;
    private ComparisonOperator operator;
    private List<String> arguments;

    public UserRsqlSpecification(
      String property, ComparisonOperator operator, List<String> arguments) {
        super();
        this.property = property;
        this.operator = operator;
        this.arguments = arguments;
    }

    @Override
    public Predicate toPredicate(
      Root<T> root, CriteriaQuery<?> query, CriteriaBuilder builder) {
        
        List<Object> args = castArguments(root);
        Object argument = args.get(0);
        switch (RsqlSearchOperation.getSimpleOperator(operator)) {

        case EQUAL: {
            if (argument instanceof String) {
                return builder.like(
                  root.<String> 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.<String> 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.<String> get(property), argument.toString());
        }
        case GREATER_THAN_OR_EQUAL: {
            return builder.greaterThanOrEqualTo(
              root.<String> get(property), argument.toString());
        }
        case LESS_THAN: {
            return builder.lessThan(root.<String> get(property), argument.toString());
        }
        case LESS_THAN_OR_EQUAL: {
            return builder.lessThanOrEqualTo(
              root.<String> 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 List<Object> castArguments(Root<T> root) {
        List<Object> args = new ArrayList<Object>();
        Class<? extends Object> type = root.get(property).getJavaType();

        for (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;
    }
}

Notice how the spec is using generics and isn’t tied to any specific Entity (such as the User).

Next – here’s our enum “RsqlSearchOperation which holds default rsql-parser operators:

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(ComparisonOperator operator) {
        this.operator = operator;
    }

    public static RsqlSearchOperation getSimpleOperator(ComparisonOperator operator) {
        for (RsqlSearchOperation operation : values()) {
            if (operation.getOperator() == operator) {
                return operation;
            }
        }
        return null;
    }
}

5. Test Search Queries

Let’s now start testing our new and flexible operations through some real-world scenarios:

First – let’s initialize the data:

@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("[email protected]");
        userJohn.setAge(22);
        repository.save(userJohn);

        userTom = new User();
        userTom.setFirstName("tom");
        userTom.setLastName("doe");
        userTom.setEmail("[email protected]");
        userTom.setAge(26);
        repository.save(userTom);
    }
}

Now let’s test the different operations:

5.1. Test Equality

In the following example – we’ll search for users by their first and last name:

@Test
public void givenFirstAndLastName_whenGettingListOfUsers_thenCorrect() {
    Node rootNode = new RSQLParser().parse("firstName==john;lastName==doe");
    Specification<User> spec = rootNode.accept(new CustomRsqlVisitor<User>());
    List<User> results = repository.findAll(spec);

    assertThat(userJohn, isIn(results));
    assertThat(userTom, not(isIn(results)));
}

5.2. Test Negation

Next, let’s search for users that by the their first name not “john”:

@Test
public void givenFirstNameInverse_whenGettingListOfUsers_thenCorrect() {
    Node rootNode = new RSQLParser().parse("firstName!=john");
    Specification<User> spec = rootNode.accept(new CustomRsqlVisitor<User>());
    List<User> results = repository.findAll(spec);

    assertThat(userTom, isIn(results));
    assertThat(userJohn, not(isIn(results)));
}

5.3. Test Greater Than

Next – we will search for users with age greater than “25”:

@Test
public void givenMinAge_whenGettingListOfUsers_thenCorrect() {
    Node rootNode = new RSQLParser().parse("age>25");
    Specification<User> spec = rootNode.accept(new CustomRsqlVisitor<User>());
    List<User> results = repository.findAll(spec);

    assertThat(userTom, isIn(results));
    assertThat(userJohn, not(isIn(results)));
}

5.4. Test Like

Next – we will search for users with their first name starting with “jo”:

@Test
public void givenFirstNamePrefix_whenGettingListOfUsers_thenCorrect() {
    Node rootNode = new RSQLParser().parse("firstName==jo*");
    Specification<User> spec = rootNode.accept(new CustomRsqlVisitor<User>());
    List<User> results = repository.findAll(spec);

    assertThat(userJohn, isIn(results));
    assertThat(userTom, not(isIn(results)));
}

5.5. Test IN

Next – we will search for users their first name is “john” or “jack“:

@Test
public void givenListOfFirstName_whenGettingListOfUsers_thenCorrect() {
    Node rootNode = new RSQLParser().parse("firstName=in=(john,jack)");
    Specification<User> spec = rootNode.accept(new CustomRsqlVisitor<User>());
    List<User> results = repository.findAll(spec);

    assertThat(userJohn, isIn(results));
    assertThat(userTom, not(isIn(results)));
}

6. UserController

Finally – let’s tie it all in with the controller:

@RequestMapping(method = RequestMethod.GET, value = "/users")
@ResponseBody
public List<User> findAllByRsql(@RequestParam(value = "search") String search) {
    Node rootNode = new RSQLParser().parse(search);
    Specification<User> spec = rootNode.accept(new CustomRsqlVisitor<User>());
    return dao.findAll(spec);
}

Here’s a sample URL:

http://localhost:8080/users?search=firstName==jo*;age<25

And the response:

[{
    "id":1,
    "firstName":"john",
    "lastName":"doe",
    "email":"[email protected]",
    "age":24
}]

7. Conclusion

This tutorial illustrated how to build out a Query/Search Language for a REST API without having to re-invent the syntax and instead using FIQL / RSQL.

The full implementation of this article can be found in the github project – this is an Eclipse based project, so it should be easy to import and run as it is.

Go deeper into building a REST API with Spring:

>> CHECK OUT THE CLASSES

  • Andriy Redko

    Hi!

    Thanks a lot for this post. In you are looking for something like that in JAX-RS world (with FIQL), you may take a look on Apache CXF search extension: http://cxf.apache.org/docs/jax-rs-search.html

    Thanks!

    • Looks quite interesting – thanks for the suggestion, I’ll definitely take a look. Cheers,
      Eugen.

  • Would you post a link to the entire series? I’m having difficulty finding one. Many thanks.

  • Siwel

    Thanks for the post! Do you perhaps have an example of building a QueryDsl predicate from the RSQL instead of a Specification? That way, we could re-use the syntax and use it with e.g. MongoDB repositories too!

    • Hey Siwel – I don’t – not exactly. I picked Specifications for this particular implementation, but one of the previous articles in this series does use QueryDSL, so you can pretty much use that implementation, with minimal work to make it match RSQL. Hope it helps. Cheers,
      Eugen.

  • Kisna

    Did we miss querying for specific properties like
    http://localhost:8080/users?search=firstName==jo*;age<25
    &fields=firstName,lastName in this series?

    • Hey Kisna – you’re right, that’s not part of this Query Language POC. The main reason is that it’s not really specific to the query language and more towards the fetch graph direction. Cheers,
      Eugen.

      • Kisna

        Great, looking forward to a mini series on dynamic fetch graph to complete this REST query language features 🙂

  • Paul Rutledge

    Great blog. I ended up implementing an RSQL -> mongodb converter and since I couldn’t find a good builder for RSQL I wrote one of those as well. I’m going to link them here in case others find them helpful:

    The rsql -> mongo query library:
    https://github.com/RutledgePaulV/rsql-mongodb

    The query builders:
    https://github.com/RutledgePaulV/q-builders

    • Hey Paul – that’s definitely an interesting usecase, going to Mongo instead of what I focused on here (JPA). If you’d like to explore it into an article, have a look at the “Write for Baeldung” page – it’s a cool subject and I’d be happy to publish it. Cheers,
      Eugen.

  • MuellerMichael

    Hey, great blog thanks :).
    I have a question to the query language with lists.

    Example we have a class group with a list of users.
    public class Group{
    private List users;
    }
    Now how can I filter for all groups which contains a user with the name “xyz”.
    Is this possible?

    • Hey Michael – that’s an interesting question.
      In terms of URL design, there are a few ways of doing that – for instance:
      /api/groups?userName=xyz
      Now, at the persistence level, that depends of course on what you’re using there, but you can have a look at this series to get some ideas of potential solutions, or you can simply craft a lower level type of query yourself.
      Hope that helps. Cheers,
      Eugen.

      • MuellerMichael

        In URL i would expected somethink like that, because users are a list
        api/groups?users.userName=in=(xyz)

        I will use CriteriaBuilder an hibernate.

        • Sounds good – it looks like you have everything you need to open up the new operation in the API. Cheers, and best of luck with the implementation.
          Eugen.

  • tkaczmarzyk

    Hi, cool article, I think Specifications are powerful in regard to rest
    query language. Some time ago I wrote annotation-based extension to
    Spring which simplifies it a bit, you might find it useful:
    https://github.com/tkaczmarzyk/specification-arg-resolver

    • Hey Tomasz – that does look interesting. I don’t usually run articles focused on the smaller libraries, but if you’d like to do a writeup on using the micro-library – I’d be happy to publish it. Cheers,
      Eugen.

      • tkaczmarzyk

        Cool, I’ll write it on the weekend then, thanks 🙂

        • Sounds good – just note that there is a bit of a process when publishing on the site.
          Have a look at the “Write for Baeldung” page and you’ll get a general idea about that. Cheers,
          Eugen.

  • Hernan Diaz

    Hello Eugen, thank you for the article.
    I downloaded and run the project and after testing some cases I found some strange behavior.
    So basically using Chrome with this URL: http://localhost:8080/users?search=firstName==jo*;age<26 I'm getting Tom and John.
    However if I do something like this: http://localhost:8080/users?search=firstName:jo,age<26 it seems to work better and I'm getting only John's data.
    I'm using for Tom and John the same data that you have in the tests:
    [{"id":2,"firstName":"john","lastName":"doe","email":"[email protected]","age":22},{"id":3,"firstName":"tom","lastName":"doe","email":"[email protected]","age":26}]
    Am I missing something?

    I will answer myself: it seems that the URL to use with the project in the repository is a bit different:
    http://localhost:8080/users/rsql?search=firstName==jo*;age<26

  • Hernan Diaz

    Hello Eugen.
    I have one more question. Would it be possible instead of using a controller, to integrate the rsql search in a @RepositoryRestResource in a way that you send to the server a request like
    https://localhost:8080/users{?page,limit,sort, search},

    {

    “page”: “”,

    “limit”: “”,

    “sort”: “”

    “search”:””

    }

    And this is automatically processed for each entity that has a RepositoryRestResource?
    Thanks

    • Hey Hernan, sorry for the late answer, I missed the email so I just noticed the comment now.
      So – as far as I’m aware, that’s not easy to do unless you’re willing to go lower level and maybe extend the framework.
      That being said, I didn’t do a lot of research on this aspect, so you should have another look at the reference.
      Cheers,
      Eugen.

    • ram

      Hey Hernan Diaz , you had some luck with integrating RSQL and @ RepositoryRestResource?

  • Benjamin Runnels

    Hello Eugen,

    Thank you for the valuable resourses you provide. I’ve utilized them more than a few times.

    I developed a generic implementation for this based on your work so that it’s not necessary to create additional classes for each repository.

    Gist is here https://gist.github.com/brunnels/ab980094e0f7c5a5d5f65eff0b5f5613

    • Hey Benjamin – that’s a good point – there’s no reason to tie that implementation to the User, as it doesn’t actually use any User specific API. Updating the article now – thanks for the solid suggestion (and clean presentation of that gist). Cheers
      Eugen.

  • Rafael Anaice

    Hello Eugen,

    nice article! One question: if I have an @OneToOne relation like User->Address

    Is it possibe to execute this query?
    http://localhost:8080/users?search=firstName=='Rafael‘;address.country==’Brazil’

    thanks!

    • Hey Rafael,
      You should be able to build that into the query language, sure.
      The series here covers a simple QL and a few interesting examples, but you can take the syntax a lot further of course. So yes – you should be fine doing that, technically.
      Now, from the perspective of design, it’s a good idea to ask yourself is that’s a good idea or not. If I were to give a general answer – I would say it’s not. If you need to do complex queries via the API, that’s usually an indication of the system needing some domain design work that would make that not needed.

      Of course that’s just a general observation and it may or may not apply.
      Best of luck with the implementation.
      Cheers,
      Eugen.