Over a million developers have joined DZone.

Beyond the JAX-RS Spec: Apache CXF Search Extension

· Java Zone

What every Java engineer should know about microservices: Reactive Microservices Architecture.  Brought to you in partnership with Lightbend.

In today's post we are going to look beyond the JAX-RS 2.0 specification and explore the useful extensions which Apache CXF, one of the popular JAX-RS 2.0 implementations, is offering to the developers of RESTservices and APIs. In particular, we are going to talk about search extension using subset of the OData 2.0query filters.

In the nutshell, search extension just maps some kind of the filter expression to a set of matching typed entities (instances of Java classes). The OData 2.0 query filters may be very complex however at the moment Apache CXF supports only subset of them:

Operator Description Example
eq Equal city eq ‘Redmond’
ne Not equal city ne ‘London’
gt Greater than price gt 20
ge Greater than or equal price ge 10
lt Less than price lt 20
le Less than or equal price le 100
and Logical and price le 200 and price gt 3.5
or Logical or price le 3.5 or price gt 200

Basically, to configure and activate the search extension for your JAX-RS services it is enough to define two properties, search.query.parameter.name and search.parser, plus one additional provider,SearchContextProvider:

@Configuration
public class AppConfig {    
    @Bean( destroyMethod = "shutdown" )
    public SpringBus cxf() {
        return new SpringBus();
    }
    
    @Bean @DependsOn( "cxf" )
    public Server jaxRsServer() {
        final Map< String, Object > properties = new HashMap< String, Object >();        
        properties.put( "search.query.parameter.name", "$filter" );
        properties.put( "search.parser", new ODataParser< Person >( Person.class ) );

        final JAXRSServerFactoryBean factory = 
            RuntimeDelegate.getInstance().createEndpoint( 
                jaxRsApiApplication(), 
                JAXRSServerFactoryBean.class 
            );
        factory.setProvider( new SearchContextProvider() );
        factory.setProvider( new JacksonJsonProvider() );
        factory.setServiceBeans( Arrays.< Object >asList( peopleRestService() ) );
        factory.setAddress( factory.getAddress() );      
        factory.setProperties( properties );

        return factory.create();
    }
    
    @Bean 
    public JaxRsApiApplication jaxRsApiApplication() {
        return new JaxRsApiApplication();
    }
    
    @Bean 
    public PeopleRestService peopleRestService() {
        return new PeopleRestService();
    }   
}

The search.query.parameter.name defines what would be the name of query string parameter used as a filter (we set it to be $filter), while search.parser defines the parser to be used to parse the filter expression (we set it to be ODataParser parametrized with Person class). The ODataParser is built on top of excellentApache Olingo project which currently implements OData 2.0 protocol (the support for OData 4.0 is on the way).

Once the configuration is done, any JAX-RS 2.0 service is able to benefit from search capabilities by injecting the contextual parameter SearchContext. Let us take a look on that in action by defining the REST service to manage people represented by following class Person:

public class Person {
    private String firstName;
    private String lastName;
    private int age;

    // Setters and getters here
}

The PeopleRestService would just allow to create new persons using HTTP POST and perform the search using HTTP GET, listed under /search endpoint:

package com.example.rs;

import java.util.ArrayList;
import java.util.Collection;

import javax.ws.rs.FormParam;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;

import org.apache.cxf.jaxrs.ext.search.SearchCondition;
import org.apache.cxf.jaxrs.ext.search.SearchContext;

import com.example.model.Person;

@Path( "/people" ) 
public class PeopleRestService {
    private final Collection< Person > people = new ArrayList<>();
       
    @Produces( { MediaType.APPLICATION_JSON  } )
    @POST
    public Response addPerson( @Context final UriInfo uriInfo,
            @FormParam( "firstName" ) final String firstName, 
            @FormParam( "lastName" ) final String lastName,
            @FormParam( "age" ) final int age ) {      
        
        final Person person = new Person( firstName, lastName, age );
        people.add( person );
        
        return Response
            .created( uriInfo.getRequestUriBuilder().path( "/search" )
            .queryParam( "$filter=firstName eq '{firstName}' and lastName eq '{lastName}' and age eq {age}" )
            .build( firstName, lastName, age ) )
            .entity( person ).build();
    }
    
    @GET
    @Path("/search")
    @Produces( { MediaType.APPLICATION_JSON  } )
    public Collection< Person > findPeople( @Context SearchContext searchContext ) {        
        final SearchCondition< Person > filter = searchContext.getCondition( Person.class );
        return filter.findAll( people );
    }
}

The findPeople method is the one we are looking for. Thanks to all hard lifting which Apache CXF does, the method looks very simple: the SearchContext is injected and the filter expression is automatically picked up from $filter query string parameter. The last part is to apply the filter to the data, which in our case is just a collection named people. Very clean and straightforward.

Let us build the project and run it:

mvn clean package
java -jar target/cxf-search-extension-0.0.1-SNAPSHOT.jar

Using awesome curl tool, let us issue a couple of HTTP POST requests to generate some data to run the filter queries against:

> curl http://localhost:8080/rest/api/people -X POST -d "firstName=Tom&lastName=Knocker&age=16"
{
    "firstName": "Tom",
    "lastName": "Knocker",
    "age": 16
}

> curl http://localhost:8080/rest/api/people -X POST -d "firstName=Bob&lastName=Bobber&age=23"
{
    "firstName": "Bob",
    "lastName": "Bobber",
    "age": 23
}

> curl http://localhost:8080/rest/api/people -X POST -d "firstName=Tim&lastName=Smith&age=50"
{
    "firstName": "Tim",
    "lastName": "Smith",
    "age": 50
}

With sample data in place, let us go ahead and come up with a couple of different search criteria, complicated enough to show off the power of OData 2.0 query filters:

  • find all persons whose first name is Bob ($filter="firstName eq 'Bob'")
  • > curl -G -X GET http://localhost:8080/rest/api/people/search --data-urlencode 
      $filter="firstName eq 'Bob'" 
    [
        {
            "firstName": "Bob",
            "lastName": "Bobber",
            "age": 23
        }
    ]
    
  • find all persons whose last name is Bobber or last name is Smith and firstName is not Bob($filter="lastName eq 'Bobber' or (lastName eq 'Smith' and firstName ne 'Bob')")
  • > curl -G -X GET http://localhost:8080/rest/api/people/search --data-urlencode 
      $filter="lastName eq 'Bobber' or (lastName eq 'Smith' and firstName ne 'Bob')" 
    [
        {
            "firstName": "Bob",
            "lastName": "Bobber",
            "age": 23
        },
        {    
            "firstName": "Tim",
            "lastName": "Smith",
            "age": 50
        }
    ]
    
  • find all persons whose first name starts from letter T and who are 16 or older ($filter="firstName eq 'T*' and age ge 16")
  • > curl -G -X GET http://localhost:8080/rest/api/people/search --data-urlencode 
      $filter="firstName eq 'T*' and age ge 16"
    [
        {
            "firstName": "Tom",
            "lastName": "Knocker",
            "age": 16
        },
        {
            "firstName": "Tim",
            "lastName": "Smith",
            "age": 50
        }
    ]
    

Note: if you run this commands on Linux-like environment, you may need to escape the $ sign using \$instead, for example: curl -X GET -G http://localhost:8080/rest/api/people/search --data-urlencode \$filter="firstName eq 'Bob'"

At the moment, Apache CXF offers just basic support of OData 2.0 query filters, with many powerful expressions left aside. However, there is a commitment to push it forward once the community expresses enough interest in using this feature.

It is worth mentioning that OData 2.0 query filters is not the only option available. Search extension also supports FIQL (The Feed Item Query Language) and this great article from one of the core Apache CXFdevelopers is a great introduction into it.

I think this quite useful feature of Apache CXF can save a lot of your time and efforts by providing simple (and not so simple) search capabilities to your JAX-RS 2.0 services. Please give it a try if it fits your application needs.

The complete project source code is available on Github.

Microservices for Java, explained. Revitalize your legacy systems (and your career) with Reactive Microservices Architecture, a free O'Reilly book. Brought to you in partnership with Lightbend.

Topics:

Published at DZone with permission of Andriy Redko, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

The best of DZone straight to your inbox.

SEE AN EXAMPLE
Please provide a valid email address.

Thanks for subscribing!

Awesome! Check your inbox to verify your email so you can start receiving the latest in tech news and resources.
Subscribe

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}