The new Certification Class of REST With Spring is out:

>> CHECK OUT THE COURSE

1. Overview

In this first article of this new series, we’ll explore a simple query language for a REST API. We’ll make good use of Spring for the REST API and JPA 2 Criteria for the persistence aspects.

Why a query language? Because – for any complex enough API – searching/filtering your resources by very simple fields is simply not enough. A query language is more flexible, and allows you to filter down to exactly the resources you need.

2. User Entity

First – let’s put forward the simple entity that we’re going to use for our filter/search API – a basic 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. Filter using CriteriaBuilder

Now – let’s get into the meat of the problem – the query in the persistence layer.

Building a query abstraction is a matter of balance. We need a good amount of flexibility on the one hand, and we need to keep complexity manageable on the other. High level, the functionality is simple – you pass in some constraints and you get back some results.

Let’s see how that works:

@Repository
public class UserDAO implements IUserDAO {

    @PersistenceContext
    private EntityManager entityManager;

    @Override
    public List<User> searchUser(List<SearchCriteria> params) {
        CriteriaBuilder builder = entityManager.getCriteriaBuilder();
        CriteriaQuery<User> query = builder.createQuery(User.class);
        Root r = query.from(User.class);

        Predicate predicate = builder.conjunction();

        for (SearchCriteria param : params) {
            if (param.getOperation().equalsIgnoreCase(">")) {
                predicate = builder.and(predicate, 
                  builder.greaterThanOrEqualTo(r.get(param.getKey()), 
                  param.getValue().toString()));
            } else if (param.getOperation().equalsIgnoreCase("<")) {
                predicate = builder.and(predicate, 
                  builder.lessThanOrEqualTo(r.get(param.getKey()), 
                  param.getValue().toString()));
            } else if (param.getOperation().equalsIgnoreCase(":")) {
                if (r.get(param.getKey()).getJavaType() == String.class) {
                    predicate = builder.and(predicate, 
                      builder.like(r.get(param.getKey()), 
                      "%" + param.getValue() + "%"));
                } else {
                    predicate = builder.and(predicate, 
                      builder.equal(r.get(param.getKey()), param.getValue()));
                }
            }
        }
        query.where(predicate);

        List<User> result = entityManager.createQuery(query).getResultList();
        return result;
    }

    @Override
    public void save(User entity) {
        entityManager.persist(entity);
    }
}

As you can see, the searchUser API takes a list of very simple constraints, composes a query based on these constrains, does the search and returns the results.

The constraint class is quite simple as well:

public class SearchCriteria {
    private String key;
    private String operation;
    private Object value;
}

The SearchCriteria implementation holds our Query parameters:

  • key: used to hold field name – for example: firstName, age, … etc.
  • operation: used to hold the operation – for example: Equality, less than, … etc.
  • value: used to hold the field value – for example: john, 25, … etc.

4. Test the Search Queries

Now – let’s test our search mechanism to make sure it holds water.

First – let’s initialize our database for testing by adding two users – as in the following example:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = { PersistenceConfig.class })
@Transactional
@TransactionConfiguration
public class JPACriteriaQueryTest {

    @Autowired
    private IUserDAO userApi;

    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);
        userApi.save(userJohn);

        userTom = new User();
        userTom.setFirstName("Tom");
        userTom.setLastName("Doe");
        userTom.setEmail("[email protected]");
        userTom.setAge(26);
        userApi.save(userTom);
    }
}

Now, let’s get a User with specific firstName and lastName – as in the following example:

@Test
public void givenFirstAndLastName_whenGettingListOfUsers_thenCorrect() {
    List<SearchCriteria> params = new ArrayList<SearchCriteria>();
    params.add(new SearchCriteria("firstName", ":", "John"));
    params.add(new SearchCriteria("lastName", ":", "Doe"));

    List<User> results = userApi.searchUser(params);

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

Next, let’s get a List of User with the same lastName:

@Test
public void givenLast_whenGettingListOfUsers_thenCorrect() {
    List<SearchCriteria> params = new ArrayList<SearchCriteria>();
    params.add(new SearchCriteria("lastName", ":", "Doe"));

    List<User> results = userApi.searchUser(params);
    assertThat(userJohn, isIn(results));
    assertThat(userTom, isIn(results));
}

Next, let’s get users with age greater than or equal 25:

@Test
public void givenLastAndAge_whenGettingListOfUsers_thenCorrect() {
    List<SearchCriteria> params = new ArrayList<SearchCriteria>();
    params.add(new SearchCriteria("lastName", ":", "Doe"));
    params.add(new SearchCriteria("age", ">", "25"));

    List<User> results = userApi.searchUser(params);

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

Next, let’s search for users that don’t actually exist:

@Test
public void givenWrongFirstAndLast_whenGettingListOfUsers_thenCorrect() {
    List<SearchCriteria> params = new ArrayList<SearchCriteria>();
    params.add(new SearchCriteria("firstName", ":", "Adam"));
    params.add(new SearchCriteria("lastName", ":", "Fox"));

    List<User> results = userApi.searchUser(params);
    assertThat(userJohn, not(isIn(results)));
    assertThat(userTom, not(isIn(results)));
}

Finally, let’s search for users given only partial firstName:

@Test
public void givenPartialFirst_whenGettingListOfUsers_thenCorrect() {
    List<SearchCriteria> params = new ArrayList<SearchCriteria>();
    params.add(new SearchCriteria("firstName", ":", "jo"));

    List<User> results = userApi.searchUser(params);

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

6. The UserController

Finally, let’s now wire in the persistence support for this flexible search to our REST API.

We’re going to be setting up a simple UserController – with a findAll() using the “search” to pass in the entire search/filter expression:

@Controller
public class UserController {

    @Autowired
    private IUserDao api;

    @RequestMapping(method = RequestMethod.GET, value = "/users")
    @ResponseBody
    public List<User> findAll(@RequestParam(value = "search", required = false) String search) {
        List<SearchCriteria> params = new ArrayList<SearchCriteria>();
        if (search != null) {
            Pattern pattern = Pattern.compile("(\w+?)(:|<|>)(\w+?),");
            Matcher matcher = pattern.matcher(search + ",");
            while (matcher.find()) {
                params.add(new SearchCriteria(matcher.group(1), 
                  matcher.group(2), matcher.group(3)));
            }
        }
        return api.searchUser(params);
    }
}

Note how we’re simply creating our search criteria objects out of the search expression.

We’re now at the point where we can start playing with the API and make sure everything is working correctly:

http://localhost:8080/users?search=lastName:doe,age>25

And here is its response:

[{
    "id":2,
    "firstName":"tom",
    "lastName":"doe",
    "email":"[email protected]",
    "age":26
}]

7. Conclusion

This simple yet powerful implementation enables quite a bit of smart filtering on a REST API. Yes – it’s still rough around the edges and can be improved (and will be improved in the next article) – but it’s a solid starting point to implement this kind of filtering functionality on your APIs.

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

  • Brandon G.

    Great article! Took a glance through your code on GH and your “API – write” stuff stood out like a sore thumb though. Why in the world is “dao.save”-logic called via GET instead of PUT?

    Otherwise nice piece though, and possibly very applicable to my next project, so thanks! 🙂

    • Hmm – that’s a temporary piece of code that shouldn’t have made it in there 🙂 I’m going to get it fixed soon – thanks for pointing it out.

      • Brandon G.

        Oh, sorry for the misunderstanding. 🙂

  • Rob Mitchell

    Nice job of “and-ing” together search criteria. Perhaps it’d be cool to enhance to include “or-ing” of search terms as well? From the URI you could /users?searchAnd=lastName:doe,age>25&searchOr=lastName:foo

    • Hey Rob,
      Yeah, I’ve been thinking about that as well – I’m adding it to the content calendar. Thanks for the suggestion. Cheers,
      Eugen.

  • Srinivas Pogiri

    What is your opinion on using Apache FIQL (or RSQL) as search criteria for REST API?

  • Wim Deblauwe

    I believe there is a typo:

    We’re not at the point …

    Should probably be

    We’re now at the point …

    regards,

    Wim

    • Hey Wim – yes it should. Nice catch 🙂
      Thanks.
      Eugen.

  • Nice article.
    A while ago I worked with OData4J – http://odata4j.org/ – which tries to standardize REST query language similar to the way explained here. OData4j may be interesting to you.