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();

        UserSearchQueryCriteriaConsumer searchConsumer = 
          new UserSearchQueryCriteriaConsumer(predicate, builder, r);
        params.stream().forEach(searchConsumer);
        predicate = searchConsumer.getPredicate();
        query.where(predicate);

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

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

Let’s have a look at the UserSearchQueryCriteriaConsumer class:

public class UserSearchQueryCriteriaConsumer implements Consumer<SearchCriteria>{

    private Predicate predicate;
    private CriteriaBuilder builder;
    private Root r;

    @Override
    public void accept(SearchCriteria param) {
        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()));
            }
        }
    }

    // standard constructor, getter, setter
}

As you can see, the searchUser API takes a list of very simple constraints, composes a query based on these constraints, 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.