There are two features that are very cool in the recent CXF 2.3 release that deserves its mention in the release notes/features document as they prove to be quite useful and powerful in certain use cases. Advanced search capabilities and Atom Logging features make CXF a compelling choice for developers looking for this support in JAX-RS frameworks. I first read about these features when Sergey introduced their availability in 2.3 snapshots.

In this post, I am going to explore FIQL based advanced search query support in CXF. I have experimented Atom logger before and it deserves a separate blog post and when used with FIQL, they are more powerful.

Introduction to FIQL Expressions in CXF

The Feed Item Query Language (FIQL, pronounced “fickle”) is a draft specification by Mark Nottingham that proposes URL-friendly syntax for expressing filters that operate on the feed entries. An FIQL expression is composed of one or more constraints, related to each other with Boolean operators. FIQL expressions yield Boolean values: True or False.

Let us look at each of the operators defined in the draft spec with an example FIQL expression and the equivalent SQL query produced by CXF.

Two boolean operators:
“;” is the Boolean AND operator; it yields True for a particular entry if both operands evaluate to True, otherwise False.

/sakila/searchActors?_s=firstname==PENELOPE;lastname==GUINESS
SELECT * FROM actor WHERE lastname = 'GUINESS' AND firstname = 'PENELOPE'

“,” is the Boolean OR operator; it yields True if either operand evaluates to True, otherwise False.

/sakila/searchActors?_s=lastname==MONROE,lastname==GUINESS
SELECT * FROM actor WHERE (lastname = 'MONROE') OR (lastname = 'GUINESS')

Two comparison operators are applicable to simple text comparisons:
“==” yields True if the string value (as per XPath) of any selected node matches the argument; otherwise False.

/sakila/searchActors?_s=lastname==MONROE
SELECT * FROM actor WHERE lastname = 'MONROE'

“!=” yields True if the string value of every selected node does not match the argument; otherwise False.

/sakila/searchActors?_s=lastname!=MONROE
SELECT * FROM actor WHERE lastname <> 'MONROE'

If the argument string begins or ends with an asterisk character (“*”), it acts as a wild card, matching any characters preceding or following (respectively) that position.

/sakila/searchActors?_s=lastname==PEN*
SELECT * FROM actor WHERE firstname LIKE 'PEN%'

[Note: The spec mentions that text comparisons should allow case insensitive textual content. CXF text comparison is case sensitive.]

Four operators are relevant to date comparisons:
“==” yields True if the point in time specified in the argument matches that indicated by the string-value of the selected node; otherwise False.

/sakila/searchRentals?_s=returndate==2005-08-31T20:00:25.000%2B06:00
SELECT * FROM rental WHERE returndate = 'Wed Aug 31 08:00:25 MDT 2005'

[Note: The database contains the following timestamp for the returndate ‘2005-08-31T20:00:25-06:00′, but when I query this using the above FIQL expression, it returned no matching entry. I tried another similar expression with no success: /sakila/searchRentals?_s=returndate==2005-08-31T20:00:25Z]

“!=” yields True if the point in time specified in the argument does not match that indicated by the string-value of the selected node; otherwise False.

/sakila/searchRentals?_s=returndate!=2005-08-31T20:00:25.000%2B06:00
SELECT * FROM rental WHERE returndate <> 'Wed Aug 31 08:00:25 MDT 2005'

“=lt=” yields True if the point in time specified in the argument follows that indicated by the string-value of the selected node; otherwise False.

/sakila/searchRentals?_s=rentaldate=lt=2005-05-27T00:00:00.000%2B00:00
SELECT * FROM rental WHERE rentaldate < 'Thu May 26 18:00:00 MDT 2005'

“=le=” yields True if the point in time specified in the argument follows that indicated by the string-value of the selected node, or is equal to it; otherwise False.

/sakila/searchRentals?_s=returndate=le=2005-05-27T00:00:00.000%2B00:00
SELECT * FROM rental WHERE returndate <= 'Thu May 26 18:00:00 MDT 2005'

“=gt=” yields True if the point in time specified in the argument precedes that indicated by the string-value of the selected node; otherwise False.

/sakila/searchRentals?_s=returndate=gt=2005-08-30T00:00:00.000%2B00:00
SELECT * FROM rental WHERE returndate > 'Mon Aug 29 18:00:00 MDT 2005'

“=ge=” yields True if the point in time specified in the argument precedes that indicated by the string-value of the selected node, or is equal to it; otherwise False.

/sakila/searchRentals?_s=returndate=ge=2005-09-01T00:00:00.000%2B00:00
SELECT * FROM rental WHERE returndate >= 'Wed Aug 31 18:00:00 MDT 2005'

Some advanced FIQL expressions prove to be very powerful when dealing with date range and relative comparison:

/sakila/searchRentals?_s=returndate=gt=%2DP5Y2M
SELECT * FROM rental WHERE returndate > 'Wed Aug 17 14:53:44 MDT 2005'
 (assuming processing on Oct 17th, 2010)

/sakila/searchRentals?_s=returndate=ge=2005-05-27T00:00:00.000%2B00:00;rentaldate=le=2005-06-27T00:00:00.000%2B00:00
SELECT * FROM rental WHERE rentaldate <= 'Sun Jun 26 18:00:00 MDT 2005' AND returndate >= 'Thu May 26 18:00:00 MDT 2005'

Four operators are relevant to numeric comparisons:

“==” yields True if the string-value of the selected node is numerically equal to the argument; otherwise False.

/sakila/searchFilms?_s=filmid==1;rentalduration!=0
SELECT * FROM film WHERE filmid = '1' AND rentalduration <> '0'

“!=” yields True if the string-value of the selected node is not numerically equal to the argument; otherwise False.

/sakila/searchFilms?_s=filmid!=1;rentalduration!=0
SELECT * FROM film WHERE filmid <> '1' AND rentalduration <> '0'

“=lt=” yields True if the string-value of the selected node evaluates as numerically less than the argument; otherwise, False.

/sakila/searchFilms?_s=filmid=lt=2;rentalduration!=0
SELECT * FROM film WHERE filmid < '2' AND rentalduration <> '0'

“=le=” yields True if the string-value of the selected node evaluates as numerically less than the argument, or as equal to it; otherwise, False.

/sakila/searchFilms?_s=filmid=le=2;rentalduration!=0
SELECT * FROM film WHERE filmid <= '2' AND rentalduration <> '0'

“=gt=” yields True if the string-value of the selected node evaluates as numerically greater than the argument; otherwise, False.

/sakila/searchFilms?_s=filmid=gt=995;rentalduration!=0
SELECT * FROM film WHERE filmid > '995' AND rentalduration <> '0'

“=ge=” yields True if the string-value of the selected node evaluates as numerically greater than the argument, or as equal to it; otherwise, False.

/sakila/searchFilms?_s=filmid=ge=995;rentalduration!=0
SELECT * FROM film WHERE filmid >= '995' AND rentalduration <> '0'

I believe this covers majority of the FIQL operators used in various proportions as documented in the spec and these FIQL expressions are not just examples, all of these searches can be tested with our sample Sakila database.

Sakila Database

Sakila is a MySQL sample database that represents a DVD rental store and comes with some useful data. This is handy for demonstrations and the schema is well designed. I will be using this sample database to demonstrate the advanced search capabilities using CXF. Java IDEs provide a decent support to reverse engineer JPA entities from a database schema. Here is one which walks through creating JPA entities using a NetBeans IDE from the Sakila database. I will be using these generated entities in this example with Hibernate as the JPA provider. The entire example is available as a maven project in Git, so feel free to check this out.

The meat of this example lies in the SakilaResource implemented as a CXF JAX-RS endpoint. In this code, a SearchContext is injected to the resource which will be used in the resource methods to get the SearchCondition specified in the query. FiqlParser does the magic for you in parsing an FIQL expression to construct these search conditions.

@Path("/sakila")
public class SakilaResource {

    @Context
    private SearchContext searchContext;

    List<Actor> actors = new ArrayList<Actor>();
    List<Film> films = new ArrayList<Film>();
    List<Rental> rentals = new ArrayList<Rental>();

    public SakilaResource() {
       // JPA Plumbing omitted for clarity.
    }

    @GET
    @Produces("application/xml")
    @Path("searchActors")
    public List<Actor> searchActors() {
        SearchCondition<Actor> sc = searchContext.getCondition(Actor.class);

        if (sc == null) {
            throw new NotFoundException("Invalid search query.");
        }
        System.out.println(sc.toSQL("actor"));

        List<Actor> found = sc.findAll(actors);
        if (found.size() == 0) {
            throw new NotFoundException("No matching actor found.");
        }
        return found;
    }

    @GET
    @Produces("application/xml")
    @Path("searchFilms")
    public List<Film> searchFilms() {
        SearchCondition<Film> sc = searchContext.getCondition(Film.class);

        if (sc == null) {
            throw new NotFoundException("Invalid search query.");
        }
        System.out.println(sc.toSQL("film"));

        List<Film> found = sc.findAll(films);
        if (found.size() == 0) {
            throw new NotFoundException("No matching film found.");
        }
        return found;
    }

    @GET
    @Produces("application/xml")
    @Path("searchRentals")
    public List<Rental> searchRentals() {
        SearchCondition<Rental> sc = searchContext.getCondition(Rental.class);

        if (sc == null) {
            throw new NotFoundException("Invalid search query.");
        }
        System.out.println(sc.toSQL("rental"));

        List<Rental> found = sc.findAll(rentals);
        if (found.size() == 0) {
            throw new NotFoundException("No matching rental found.");
        }
        return found;
    }
}

One can think of FIQL based search support in CXF is somewhat similar to using Hibernate Search to query JPA entities. I think Hibernate Search is a heavy weight solution which uses Lucene as the search provider and it requires annotating your entities. FIQL support in CXF is light weight and implementing advanced search capabilities in resources is a child’s play. Unlike Hibernate Search, CXF FIQL cannot be used to perform joins on multiple entities as the FIQL spec was designed for syndication feed data model.

In CXF, there are couple options to test these search conditions and they are easy to develop. One approach is to use the search extension API. You can find an example in the javadoc of SimpleSearchCondition.

The other approach is the traditional CXF way of using HTTP centric WebClient.

public class SakilaSearchTest {

    static Server server;

    @BeforeClass
    public static void setUp() {
        JAXRSServerFactoryBean sf = new JAXRSServerFactoryBean();
        sf.setResourceClasses(SakilaResource.class);
        sf.getInInterceptors().add(new LoggingInInterceptor());
        sf.getOutInterceptors().add(new LoggingOutInterceptor());
        sf.setResourceProvider(SakilaResource.class, new SingletonResourceProvider(new SakilaResource()));
        sf.setAddress("http://localhost:9000");
        server = sf.create();
    }

    @Test
    public void searchActors() {
        //http://localhost:9000/sakila/searchActors?_s=firstname==PENELOPE
        WebClient wc = WebClient.create("http://localhost:9000/sakila/searchActors?_s=firstname%3D%3DPENELOPE");
        Collection<? extends Actor> actors = wc.getCollection(Actor.class);
        assertEquals(4, actors.size());
    }

    @Test
    public void searchFilms() {
        //http://localhost:9000/sakila/searchFilms?_s=rating==PG;rentalduration!=0;title==SANTA*
        WebClient wc = WebClient.create("http://localhost:9000/sakila/searchFilms?_s=rating%3D%3DPG;rentalduration%21%3D0;title%3D%3DSANTA*");
        Collection<? extends Film> films = wc.getCollection(Film.class);
        assertEquals(1, films.size());
    }

    @Test
    public void searchRentals() {
        //http://localhost:9000/sakila/searchRentals?_s=rentaldate=lt=2005-05-27T00:00:00.000%2B00:00
        WebClient wc = WebClient.create("http://localhost:9000/sakila/searchRentals?_s=rentaldate%3Dlt%3D2005-05-27T00:00:00.000%2B00:00");
        Collection<? extends Rental> rentals = wc.getCollection(Rental.class);
        assertEquals(278, rentals.size());
    }

    @AfterClass
    public static void tearDown() {
        server.destroy();
    }
}

One thing I noticed with WebClient is that I need to encode certain operators specified in the search condition and this is inconsistent when testing with a web browser. I would expect the java API should take care of this for you instead of providing partially encoded URLs. Also, the date comparison can be tricky, although it works for most cases discussed above.

Possibly Related Posts:


2 Responses to “Sakila Restful Search using CXF FIQL”

  • Sergey Beryozkin

    Hi Arul

    Can you please let me know which operators you had to encode explicitly for WebClient to work ?
    thanks for a great post
    Sergey

  • Arul

    Hi Sergey,

    No problem!

    I was actually referring to using “==” in the URI as shown below.

    WebClient wc = WebClient.create("http://localhost:9000/sakila/searchActors?_s=firstname==PENELOPE");
    

    This would return a 404, but if I encode “==” as shown below, it works just fine.

     
    WebClient wc = WebClient.create("http://localhost:9000/sakila/searchActors?_s=firstname%3D%3DPENELOPE");
    

    It can also be reproduced when using other operators in search query such as “!=”.

    I am not certain if this is the correct way to construct WebClient instance which takes the baseUri including query parameters. I believe another option is to use it as shown below, which works fine.

    WebClient wc = WebClient.create("http://localhost:9000/sakila/searchActors");
    wc.query("_s", "firstname==PENELOPE");
    

    Btw, thanks for implementing such an useful feature in CXF JAX-RS.

    -Arul