I just announced the new Spring 5 modules in REST With Spring:

>> 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.

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 a Maven-based project, so it should be easy to import and run as it is.

I just announced the new Spring 5 modules in REST With Spring:

>> CHECK OUT THE LESSONS

Sort by:   newest | oldest | most voted
Andriy Redko
Guest

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!

Eugen Paraschiv
Guest

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

Bernard Farrell
Guest

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

Eugen Paraschiv
Guest

Hey Bernard – here you go – hope that’s what you were looking for. Cheers,
Eugen.

Siwel
Guest

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!

Eugen Paraschiv
Guest

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
Guest

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

Eugen Paraschiv
Guest

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
Guest

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

Eugen Paraschiv
Guest

Sounds good – it’s in the content calendar, but it might be a few months before I personally get to it. I am looking for authors, so if things pick up, it may be sooner than that 🙂
Cheers,
Eugen.

Kisna
Guest

Apparently, there is some support to provide only the fields required by the client like described here: http://wiki.fasterxml.com/JacksonFeatureJsonFilter and also supported since Spring 4.2 here: https://jira.spring.io/browse/SPR-12586

Paul Rutledge
Guest

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

Eugen Paraschiv
Guest

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.

wpDiscuz