Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.

JAX-RS Search

 


Table of Contents

Advanced Search Queries

...

Operator

Description

"eq"

Equal

"ne"

Not Equal

"lt"

Less Than

"le"

Less or Equal

"gt"

Greater Than

"ge"

Greater or Equal

"and"

AND

"or"

OR

...


Please see the specification text for some examples.

...

The following dependency is required starting from CXF 2.6.0:

Code Block
xml
xml
   <dependency>
      <groupId>org.apache.cxf</groupId>
      <artifactId>cxf-rt-rs-extension-search</artifactId>
      <version>2.6.0</version>
   </dependency>

   <!-- If working with OData -->
   <!--
       <dependency>
            <groupId>org.apache.olingo</groupId>
            <artifactId>olingo-odata2-core-incubating</artifactId>
            <version>1.1.0</version> 
        </dependency>
   -->
 

Additionally, starting from CXF 2.6.0, SearchContextProvider needs to be registered as jaxrs:provider.

Working with the queries

SearchContext needs be injected into an application code and used to retrieve a SearchCondition representing the current FIQL/OData query. This SearchCondition can be used in a number of ways for finding the matching data.

...

Code Block
java
java
@Path("books")
public class Books {

    private Map<Long, Book> books;
    @Context
    private SearchContext context;

    @GET
    public List<Book> getBook() {

        SearchCondition<Book> sc = searchContext.getCondition(Book.class);
        // SearchCondition#isMet method can also be used to build a list of matching beans

        // iterate over all the values in the books map and return a collection of matching beans
        List<Book> found = sc.findAll(books.values());
        return found;
    }
}

Note that a searchContext.getCondition(Book. class) call may return an arbitrary complex SearchCondition, it can be a simple primitive
expression primitiveexpression or a more complex, composite one.

...

Code Block
java
java
public class Book {

    private int id;
    private OwnerInfo ownerinfo;
    //setters and getters omitted for brewitybrevity
}

@Embeddable
public class OwnerInfo {

    private Address address;
    private Name name;
    //setters and getters omitted for brewitybrevity
}

@Embeddable
public class Name {

    private String name;
    //setters and getters omitted for brewitybrevity
}

and the following map:

Code Block
xml
xml
<map>
 <!-- 'oname' is alias for the actual nested bean property -->
 <entry key="oname" value="ownerinfo.name.name"/>
</map>

...

Code Block
java
java
public class Name {

    private String name;
    public Name() {
    } 
    public Name(String name) {
        this.name = name;
    }
    //setters and getters omitted for brewitybrevity
}

the mapping between "oname" and "ownerinfo.name" will work too.

...

Code Block
xml
xml
<map>
 <!-- 'oname' and 'owner' are aliases for the 'ownerinfo.name.name' bean property -->
 <entry key="oname" value="ownerinfo.name.name"/>
 <entry key="owner" value="ownerinfo.name.name"/>
</map>

Dealing with mistyped property names

Consider a case where a documented search property is named as 'address' (lower case) and a query contains a mistyped 'Address' instead. In this case, unless a "search.lax.property.match" property is set, PropertyNotFoundException will be thrown.

Supporting case-insensitive property mapping is easy, register a "search.bean.property.map" (mentioned earlier) map as Java TreeMap

with a case-insensitive String.CASE_INSENSITIVE_ORDER Comparator.

However it will not help if the 'address' property was mistyped as 'adress'. In this case, "search.bean.property.map" might still be useful with having few more keys supporting some typical typos, example, 'adress' - 'address', 'addres' - 'address', etc.

Starting from  CXF 3.1.5, org.apache.cxf.jaxrs.ext.search.PropertyNameConverter  is available and might be used for a more sophisticated conversion of mistyped property names to correct names. 

The implementation can be registered as a "search.bean.property.converter" endpoint contextual property.

Parser properties

The parser properties are the ones which tell the parser how to treat the conversion of Date values and the unrecognized search property names.

...

Code Block
java
java
// ?_s="level=gt=10"
SearchCondition<SearchBean> sc = searchContext.getCondition(SearchBean.class);

Map\<, String\> fieldMap = new HashMap\<String, String\>();
fieldMap.put("level", "LEVEL_COLUMN");

SQLPrinterVisitor<SearchBean> visitor = new SQLPrinterVisitor<SearchBean>(fieldMap, "table", "LEVEL_COLUMN");
sc.accept(visitor);
assertEquals("SELECT LEVEL_COLUMN FROM table 
              WHERE LEVEL_COLUMN > '10'",
              visitor.getResultgetQuery());

Converting the queries

SearchCondition can also be used to convert the search requirements (originally expressed in FIQL/OData) into other query languages.
A custom SearchConditionVisitor implementation can be used to convert SearchCondition objects into custom expressions or typed objects. CXF ships visitors for converting expressions to SQL, JPA 2.0 CriteriaQuery or TypedQuery, Lucene Query.

...

Code Block
java
java
// ?_s="name==ami*;level=gt=10"
SearchCondition<Book> sc = searchContext.getCondition(Book.class);
SQLPrinterVisitor<Book> visitor = new SQLPrinterVisitor<Book>("table");
sc.accept(visitor);
assertEquals("SELECT * FROM table 
              WHERE 
              name LIKE 'ami%' 
              AND 
              level > '10'",
              visitor.getResultgetQuery());

Note that SQLPrinterVisitor can also be initialized with the names of columns and the field aliases map:

Code Block
java
java
// ?_s="level=gt=10"
SearchCondition<Book> sc = searchContext.getCondition(Book.class);

Map<String, String> fieldMap = new HashMap<String, String>();
fieldMap.put("level", "LEVEL_COLUMN");

SQLPrinterVisitor<Book> visitor = new SQLPrinterVisitor<Book>(fieldMap, "table", "LEVEL_COLUMN");
sc.accept(visitor);
assertEquals("SELECT LEVEL_COLUMN FROM table 
              WHERE LEVEL_COLUMN > '10'",
              visitor.getResultgetQuery());

The fields map can help hide the names of the actual table columns/record fields from the Web frontend. Example, the users will know that the 'level' property is available while internally it will be converted to a LEVEL_COLUMN name.

Warning: Using the SQLPrinterVisitor may leave your service open to SQL injection attacks. Please take appropriate steps to avoid these attacks (for example validating queries using a custom PropertyValidator, or manually escaping the input values).

JPA 2.0

CXF 2.6.4 and CXF 2.7.1 introduce org.apache.cxf.jaxrs.ext.search.jpa.JPATypedQueryVisitor and org.apache.cxf.jaxrs.ext.search.jpa.JPACriteriaQueryVisitor which can be used to capture FIQL/OData expressions into
javax.persistence.TypedQuery or javax.persistence.criteria.CriteriaQuery objects.

...

Code Block
java
java
public class Book {

    private String title;
    private Date date;
    private OwnerInfo ownerinfo;
    //setters and getters omitted for brewitybrevity
}

@Embeddable
public class OwnerInfo {

    private Address address;
    private Name name;
    //setters and getters omitted for brewitybrevity
}

@Embeddable
public class Name {

    private String name;
    //setters and getters omitted for brewitybrevity
}

@Embeddable
public class Address {

    private String street;
    //setters and getters omitted for brewitybrevity
}


the following code can be used:

...

Code Block
java
java
public static class BookInfo {
        private int id;
        private String title;

        public BookInfo() {
            
        }
        
        public BookInfo(Integer id, String title) {
            this.id = id;
            this.title = title;
        }
        //setters and getters omitted for brewitybrevity
 }

// actual application code:

SearchCondition<Book> sc = searchContext.getCondition(Book.class);
JPACriteriaQueryVisitor<Book, BookInfo> visitor = 
    new JPACriteriaQueryVisitor<Book, BookInfo>(entityManager, Book.class, BookInfo.class);
sc.accept(visitor);

List<SingularAttribute<Book, ?>> selections = new LinkedList<SingularAttribute<Book, ?>>();
// Book_ class is generated by JPA2 compiler
selections.add(Book_.id);
selections.add(Book_.title);

visitor.selectConstruct(selections);

TypedQuery<BookInfo> query = visitor.getQuery();

List<BookInfo> bookInfo = typedQuery.getResultList();
return bookInfo;

JPA2 typed converters also support join operations in cases when explicit collections are used, for example, given:

Code Block
java
java
@Entity(name = "Book")
public class Book {

    private List<BookReview> reviews = new LinkedList<BookReview>();
    private List<String> authors = new LinkedList<String>();
    // other properties omitted

    @OneToMany
    public List<BookReview> getReviews() {
        return reviews;
    }

    public void setReviews(List<BookReview> reviews) {
        this.reviews = reviews;
    }

    @ElementCollection
    public List<String> getAuthors() {
        return authors;
    }

    public void setAuthors(List<String> authors) {
        this.authors = authors;
    }
}

@Entity
public class BookReview {
    private Review review;
    private List<String> authors = new LinkedList<String>();
    private Book book;
    // other properties omitted    

    public Review getReview() {
        return review;
    }

    public void setReview(Review review) {
        this.review = review;
    }

    @OneToOne
    public Book getBook() {
        return book;
    }

    public void setBook(Book book) {
        this.book = book;
    }

    @ElementCollection
    public List<String> getAuthors() {
        return authors;
    }

    public void setAuthors(List<String> authors) {
        this.authors = authors;
    }

    public static enum Review {
        GOOD,
        BAD
    }
}

the following will find "all the books with good reviews written by Ted":

Code Block
java
java
SearchCondition<Book> filter = new FiqlParser<Book>(Book.class).parse("reviews.review==good;reviews.authors==Ted");
// in practice, map "reviews.review" to "review", "reviews.authors" to "reviewAuthor" 
// and have a simple query like "review==good;reviewAuthor==Ted" instead

SearchConditionVisitor<Book, TypedQuery<Book>> jpa = new JPATypedQueryVisitor<Book>(em, Book.class);
filter.accept(jpa);
TypedQuery<Book> query = jpa.getQuery();
return query.getResultList();

org.apache.cxf.jaxrs.ext.search.jpa.JPALanguageVisitor for converting FIQL/OData expressions into JPQL expressions have also been introduced.

...

Code Block
java
java
SearchCondition<Book> filter = new FiqlParser<Book>(Book.class).parse("reviews.review==good;reviews.authors==Ted");

JPACriteriaQueryVisitor<Book, Long> jpa = new JPACriteriaQueryVisitor<Book, Long>(em, Book.class, Long.class);
filter.accept(jpa);
long count = jpa.count();

...


Second, only when using FIQL, a count extension can be used. For example, one may want to find 'all the books written by at least two authors or all the books with no reviews'.
If a collection entity such as BookReview has a non primitive type, then typing "reviews==0" is all what is needed, otherwise a count extension needs to be used, for example: "count(authors)=ge=2"

...

Code Block
java
java
Analyzer analyzer = new StandardAnalyzer(Version.LUCENE_4_9);

// Lower-case filter and stop-words filter are part of the StandardAnalyzer
SearchCondition<SearchBean> filter = new FiqlParser<SearchBean>(SearchBean.class).parse("contents==pears and APPLES");
LuceneQueryVisitor<SearchBean> lucene = new LuceneQueryVisitor<SearchBean>("contents", analyzer);
lucene.visit(filter);

org.apache.lucene.search.Query query = lucene.getQuery();

...


LDAP

Mapping of FIQL/OData expressions to LDAP queries as defined by RFC-4515 is supported starting from CXF 2.7.1 with the help of org.apache.cxf.jaxrs.ext.search.ldap.LdapQueryVisitor. Use this visitor when working with LDAP or OSGI.

...

Code Block
java
java
// FIQL "oclass=Bar"

// map 'oclass' used in the FIQL query to the actual property name, 'objectClass'
LdapQueryVisitor<Condition> visitor = 
   new LdapQueryVisitor<Condition>(Collections.singletonMap("oclass", "objectClass"));

filter.accept(visitor.visitor());
String ldap = visitor.getQuery();

HBase

 

Note that since CXF 3.2.5 the query values are encoded by default, to prevent possible LDAP injection attacks. If you want to support wildcard searching with the LdapQueryVisitor from CXF 3.2.5 onwards, it is necessary to set the 'encodeQueryValues' property of LdapQueryVisitor to 'false'CXF 3.0.2 introduces an initial support for querying HBase databases. Please see this test for more information.

Custom visitors

In cases when a custom conversion has to be done, a converter for doing the untyped (example, SQL) or typed (example, JPA2 TypedQuery) conversions can be provided.

...

Code Block
java
java
public class CustomSQLVisitor<T> extends AbstractSearchConditionVisitor<T, String> {

    private String tableName;
    private StringBuilder sb = new StringBuilder();

    public void visit(SearchCondition<T> sc) {
        
        if (sb == null) {
            sb = new StringBuilder();
            // start the expression as needed, example
            // sb.append("Select from ").append(tableName);
        }
        
        PrimitiveStatement statement = sc.getStatement();
        if (statement != null) {
                // ex "a > b"
                // use statement.getValue()
                // use statement.getConditionType() such as greaterThan, lessThan
                // use statement.getProperty();
                // to convert "a > b" into SQL expression
                sb.append(toSQL(statement));         
        } else {
            // composite expression, ex "a > b;c < d"
            for (SearchCondition<T> condition : sc.getSearchConditions()) {
                // pre-process, example sb.append("(");
                condition.accept(this);
                // post-process, example sb.append(")");
            }
        }
    }

    public String getQuery() {
        return sb.toString();
    }
}

...

Code Block
java
java
public class CustomTypedVisitor<T> extends AbstractSearchConditionVisitor<T, Query> {

    private Stack<List<Query>> queryStack = new Stack<List<Query>>();

    public void visit(SearchCondition<T> sc) {
                
        PrimitiveStatement statement = sc.getStatement();
        if (statement != null) {
                // ex "a > b"
                // use statement.getValue()
                // use statement.getConditionType() such as greaterThan, lessThan
                // use statement.getProperty();
                // to convert "a > b" into Query object
                Query query = buildSimpleQuery(statement);
                queryStack.peek().add(query);                 

        } else {
            // composite expression, ex "a > b;c < d"
            queryStack.push(new ArrayList<Query>());

            for (SearchCondition<T> condition : sc.getSearchConditions()) {
                condition.accept(this);
            }

            boolean orCondition = sc.getConditionType() == ConditionType.OR;
            List<Query> queries = queryStack.pop();
            queryStack.peek().add(createCompositeQuery(queries, orCondition));
        }
    }

    public Query getResultgetQuery() {
        return queryStack.peek().get(0);
    }
}

...

Code Block
java
java
@Path("/search")
public class SearchEngine {
    @Context
    private UriInfo ui;

    @GET
    public List<Book> findBooks() {
        MultivaluedMap<String, String> params = ui.getQueryParameters();
        String query = params.getFirst("_s"); // or $filter, etc
        // delegate to your own custom handler 

        // note that the original search expression can also be retrieved 
        // using a SearchContext.getSearchExpression() method
}

Converting the queries with QueryContext

QueryContext is the helper context available from CXF 2.7.1 which makes it simpler for the application code to
get the converted query expression, with the actual converter/visitor registered as the jaxrs contextual property, for example:

Code Block
java
java
import java.util.ArrayList;
import java.util.List;
import org.apache.cxf.jaxrs.JAXRSServerFactoryBean;
import org.apache.cxf.jaxrs.ext.search.QueryContextProvider;
import org.apache.cxf.jaxrs.ext.search.SearchBean;
import org.apache.cxf.jaxrs.ext.search.visitor.SBThrealLocalVisitorState;
import org.apache.cxf.jaxrs.ext.search.sql.SQLPrinterVisitor;

import books.BookStore;

// Register the visitor:
JAXRSServerFactoryBean sf = new JAXRSServerFactoryBean();
List<Object> providers = new ArrayList<Object>();
providers.add(new QueryContextProvider());
sf.setProviders(providers);

SQLPrinterVisitor<SearchBean> sqlVisitor = new SQLPrinterVisitor<SearchBean>("books");
sqlVisitor.setVisitorState(new SBThrealLocalVisitorState());
sf.getProperties(true).put("search.visitor", sqlVisitor);


sf.setResourceClasses(BookStore.class);
server = sf.create();

...

Code Block
java
java
@Path("/")
public class BookStore { 
    @GET
    @Path("/books/{expression}")
    @Produces("application/xml")
    public List<Book> getBookQueryContext(@PathParam("expression") String expression, 
                                      @Context QueryContext searchContext) 
        throws BookNotFoundFault {
        String sqlExpression = searchContext.getConvertedExpression(expression, Book.class);
        // pass it to the SQL DB and return the list of Books
    }
}

...

Code Block
java
java
// GET /search?a=a1&a=v2
String exp = searchContext.getSearchExpression();
assertEquals("(a==a1,a==a2)", exp);

// GET /search?a=a1&b=b1
exp = searchContext.getSearchExpression();
assertEquals("(a==a1;b==b1)", exp);

Also, by default, if a query property name ends with "From" then "=ge=" (greater or equals to) will be used, and if ends with "Till" then "=lt=" will be used, for example:

...

First option is to have a bean capturing specific property values do a domain specific validation. For example, a Book.class may have its setName(String name) method validating the name value.
Another option is to inject a custom validator into a visitor which is used to build the untyped or typed query.

...

Building the queries

FIQL

CXF 2.4.0 introduces SearchConditionBuilder which makes it simpler to build FIQL queries. SearchConditionBuilder is an abstract class that returns a FIQL builder by default:

...

Code Block
java
java
// Connecting composite or() and and() expressions will add "()" implicitly:
String ret = b.is("foo").equalTo(20, 10).and("bar").lessThan(10).query();
assertEquals("(foo==20,foo==10);bar=lt=10", ret);

// wrap() method can be used to wrap explicitly:

String ret = b.is("foo").equalTo(10).and("bar").lessThan(10).wrap().or("bar").greaterThan(25).query();
assertEquals("(foo==20;bar=lt=10),bar=gt=25", ret);


...


Using dates in queries

By default, the date values have to have the following format: "yyyy-MM-dd", for example:

...

Code Block
java
java
Map<String, String> props = new HashMap<String, String>();
props.put("search.date-format", "yyyy-MM-dd'T'HH:mm:ss");
props.put("search.timezone.support", "false");

Date d = df.parse("2011-03-01 12:34:00");
        
FiqlSearchConditionBuilder bCustom = new FiqlSearchConditionBuilder(props);
        
String ret = bCustom.is("foo").equalTo(d).query();
assertEquals("foo==2011-03-01T12:34:00", ret);


Relative dates

Date value can be specified as a duration from the current date/time, as its string representation, "PnYnMnDTnHnMnS".
Resulted date will be calculated as a current date + specified duration. For example:

Code Block
java
java
?_search=date=ge=-P90D


This query will search for a date which is 90 days in the past or newer.

Alternative query languages

Custom org.apache.cxf.jaxrs.ext.search.SearchConditionParser implementations can be registered as a "search.parser" contextual property starting from CXF 3.0.0-milestone2.

OData

...


Please use a "search.query.parameter.name" contextual property to indicate to the runtime that an OData '$filter' query option needs to be checked for the query expression and a "search.parser" property to point to the instance of org.apache.cxf.jaxrs.ext.search.odata.ODataParser, as shown in this test, see the startServers function.

...

Code Block
xml
xml
 <cxf:bus>
  <cxf:properties>
    <entry key="search.query.parameter.name" value="$filter" />
    <entry key="search.parser">
      <bean class="org.apache.cxf.jaxrs.ext.search.odata.ODataParser">
         <constructor-arg value="#{ T(org.apache.cxf.jaxrs.ext.search.SearchBean) }" />
      </bean>
    </entry>
  </cxf:properties>
</cxf:bus>
 

 


Also note that Apache Olingo offers its own visitor model which can be used to work with JPA2, etc.

...

To demonstrate the full power of the CXF 3.0.2 content extraction and search capabiities, the demo project 'jax_rs_search' has been developed and is distributed in the samples bundle. The project could be found in the official Apache CXF Github repository. It integrates together Apache CXF, Apache Lucene and Apache Tika showing off some advanced features related to custom analyzers and different filter criteria (keyword and  phrase search).

...