How to use CXF Search extension and JPA
CXF is well known for being a JAXWS and JAXRS implementation but it comes with a complete ecosystem. Some part of it are complete products like Fediz but others are just libraries making the development easier.
One of these libraries is cxf search. It allows to convert a http query parameter in a backend query.
Here is the example we'll use in this post: we have users, a user is an id, a name and an age. Here is a sample dataset:
[
{
age: 39,
id: 2,
name: "Matthew"
},
{
age: 30,
id: 1,
name: "Romain"
}
]
What cxf search will allows us is to convert automatically this request:
GET http://localhost:8080/find?_search=name==Romain
To
[
{
age: 30,
id: 1,
name: "Romain"
}
]
or
GET http://localhost:8080/find?_search=name==Romain;age==25
to
[
]
The syntax support is configurable, I'll not detail everything here but you can use any query parameter instead of the _search default one to build the backend query, you can control if you want to manage the encoding, which time format you use, define aliases for nested properties of your model graph etc... For this post I'll keep the default syntax (_search or _s query parameter but it is common to use q or query as alias).
Now we know what we want to do and the question is how?
Overall goal is:
@GET
@Produces(MediaType.APPLICATION_JSON)
public List<User> find(/*params to get the query*/) {
// parse the query and return the list of matching users
}
Side note: in real life you would wrap the list in a Page wrapper and handle pagination but once we'll be at the end of this post it would be as easy as adding the wrapper and calling another method so I'll not add it now to try to keep it simple.
Question is how to parse the query? Do I need to implement a language? Do I need to map this language on JPA then?
This is the purpose of cxf search (except it handles more than just JPA).
First let's add the right dependency to get cxf search:
<dependency>
<groupId>org.apache.cxf</groupId>
<artifactId>cxf-rt-rs-extension-search</artifactId>
<version>3.1.11</version>
</dependency>
Cxf search relies on a SearchContext which is just a ContextProvider so we need to register it as @Provider. You can either register org.apache.cxf.jaxrs.ext.search.SearchContextProvider or (like in meecrowave context) make it scanned:
@Provider
@Dependent
public class SearchProvider extends SearchContextProvider
implements ContextProvider<SearchContext> {
}
Now we can inject as a @Context the SearchContext. This class will allow us to build a Condition which represents the parsed query and then, using a visitor pattern, to build our query.
CXF being really nice it also provides a JPA visitor ready to build your query (actually 2, one being criteria builder oriented but we'll not detail this one here).
The only thing to take care is if there is or not a query, if not you would get null as condition and need to fallback on a custom query (plain find all probably).
Here is one potential implementation:
@GET
@Produces(MediaType.APPLICATION_JSON)
public List<User> find(@Context final SearchContext searchContext) {
final JPATypedQueryVisitor<User> builder = new JPATypedQueryVisitor<User>(em, User.class);
final SearchCondition<User> condition = searchContext.getCondition(User.class);
if (condition != null) {
condition.accept(builder);
return builder.getQuery().getResultList();
}
return em.createQuery("select u from User u order by u.name", User.class).getResultList();
}
Not that hard right? That is all we need (the fallback would likely need a named query to be more elegant but this is already functional).
Now we can hit that endpoint and previous sample requests will behave as expected.
I spoke earlier of using a Page wrapper, in this case we'll use the criteria builder visitor which will provide a count() method making the pagination quite easy to handle:
@GET
@Produces(MediaType.APPLICATION_JSON)
public List<User> find(@Context final SearchContext searchContext) {
final JPACriteriaQueryVisitor<User, Long> builder = new JPACriteriaQueryVisitor<User, Long>(em, User.class, Long.class);
final SearchCondition<User> condition = searchContext.getCondition(User.class);
if (condition != null) {
condition.accept(builder);
return new Page(builder.getTypedQuery().getResultList(), builder.count());
}
return new Page(...2 named queries: findAll and countAll...);
}
And here we are :).
Why is it nice to use this SearchContext as a standard? First because it already handles the "query language" for you, no need to invent a new one. Secondly because it is portable accross backends: CXF provides implementations for lucene, LDAP, HBase,... and allows to extend it to custom backends easily. This means if you provide several microservices you will get a unified API and your consumers will not need to learn a new syntax each time which is quite important for your service acceptance. A nice to have point is also that it speeds up prototyping a lot and allows to expose the full power of your condition classes (which don't need to be an entity BTW) without having to spend time doing it yourself.
From the same author:
In the same category: