Versions Compared

Key

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

...

JAX-RS Search

 : Search {span}

Table of Contents

FIQL search queries

...

For example, the following query

Code Block
java
java

?_s=name==CXF;version=ge=2.2

...

More complex composite expressions can also be expressed easily enough, examples:

Code Block
java
java

// Find all employees younger than 25 or older than 35 living in London
/employees?_s=(age=lt=25,age=gt=35);city==London

// Find all books on math or physics published in 1999 only.
/books?_s=date=lt=2000-01-01;date=gt=1999-01-01;(sub==math,sub==physics)

...

Note, when passing the FIQL queries via URI query parameters, either '_search' or '_s' query parameter has to be used to mark a FIQL expression for it not to 'interfere' with other optional query parameters. Starting from CXF 2.7.2 it is also possible to use the whole query component to convey a FIQL expression, example,

Code Block
java
java


// Find all books on math or physics published in 1999 only.
/books?date=lt=2000-01-01;date=gt=1999-01-01;(sub==math,sub==physics)

...

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>

...

So, suppose a list or map of Book instances is available. Here is one possible approach:

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;
 }
}

...

The preferred approach, when working with typed beans, is to register a bean properties map, using a "search.bean.property.map" contextual property or directly with SearchContext. For example, given

Code Block
java
java


public class Book {

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

@Embeddable
public class OwnerInfo {

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

@Embeddable
public class Name {

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

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>

will let users type and bookmark queries (and without seeing them producing unexpected results) like this one:

Code Block
java
java

//Find all the books owned by Fred with id greater than 100
/books?_s=id=gt=100;oname=Fred

Note, a property name such as "ownerinfo.name.name" uses '.' to let the parser navigate to the actual Name bean which has a 'name' property. This can be optimized in cases where the owner bean is known to have either a constructor or static valueOf() method accepting the 'name' property, for example, given

Code Block
java
java

public class Name {

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

...

You can also have many to one mappings, for example

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>

...

org.apache.cxf.jaxrs.ext.search.SearchBean is a utility bean class which can simplify analyzing the captured FIQL expressions and converting them to the other language expressions, in cases where having to update the bean class such as Book.class with all the properties that may need to be supported is not practical or the properties need to be managed manually. For example:

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

...

org.apache.cxf.jaxrs.ext.search.sql.SQLPrinterVisitor can be used for creating SQL expressions. For example:

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

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

...

For example, given:

Code Block
java
java


public class Book {

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

@Embeddable
public class OwnerInfo {

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

@Embeddable
public class Name {

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

@Embeddable
public class Address {

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


the following code can be used:

Code Block
java
java

import javax.persistence.EntityManager;
import javax.persistence.TypedQuery;

// init EntityManager as required
private EntityManager entityManager;

// Find the books owned by Barry who lives in London, published starting from the first month of 2000 
// ?_s="date=ge=2000-01-01;ownername=barry;address=london"

// this map will have to be set as a contextual property on the jaxrs endpoint
// it assumes that Book bean has nested OwnerInfo bean with nested Address and Name beans, 
// with the latter containing 'street' and 'name' property respectively

Map<String, String> beanPropertiesMap = new HashMap<String, String>();
beanPropertiesMap.put("address", "ownerInfo.address.street");
beanPropertiesMap.put("ownername", "ownerInfo.name.name");

// the actual application code
SearchCondition<Book> sc = searchContext.getCondition(Book.class);
SearchConditionVisitor<Book, TypedQuery<Book>> visitor = 
    new JPATypedQueryVisitor<Book>(entityManager, Book.class);
sc.accept(visitor);

TypedQuery<Book> typedQuery = visitor.getQuery();
List<Book> books = typedQuery.getResultList();

Using CriteriaQuery is preferred in cases when the actual result has to be shaped into a bean of different type, using one of JPA2 CriteriaBuilder's shape methods (array(), construct() or tuple()). For example:

Code Block
java
java


// Find the books owned by Barry who lives in London, published starting from the first month of 2000 
// ?_s="date=ge=2000-01-01;ownername=barry;address=london"

// this map will have to be set as a contextual property on the jaxrs endpoint
Map<String, String> beanPropertiesMap = new HashMap<String, String>();
beanPropertiesMap.put("address", "ownerInfo.address.street");
beanPropertiesMap.put("ownername", "ownerInfo.name.name");

// the actual application code
// Only Book 'id' and 'title' properties are extracted from the list of found books
 
SearchCondition<Book> sc = searchContext.getCondition(Book.class);
JPACriteriaQueryVisitor<Book, Tuple> visitor = 
    new JPACriteriaQueryVisitor<Book, Tuple>(entityManager, Book.class, Tuple.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.selectTuple(selections);

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

List<Tuple> tuples = typedQuery.getResultList();
for (Tuple tuple : tuples) {
  int bookId = tuple.get("id", String.class);
  String title = tuple.get("title", String.class);
  // add bookId & title to the response data
}

...

Or, instead of using Tuple, use a capturing bean like BeanInfo:

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 brewity
 }

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

...

First, one may want to get the count of records matching a given search expression, this actually can be done by checking the size of the result list:

Code Block
java
java

TypedQuery<Book> query = jpa.getQuery();
return query.getResultList().size();

However this can be very inefficient for large number of records, so using a CriteriaBuilder count operation is recommended, for example:

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

...

Example, "find the documents containing a 'text' term":

Code Block
java
java

import org.apache.lucene.search.Query;

SearchCondition<SearchBean> filter = new FiqlParser<SearchBean>(SearchBean.class).parse("ct==text");
LuceneQueryVisitor<SearchBean> lucene = new LuceneQueryVisitor<SearchBean>("ct", "contents");
lucene.visit(filter);
org.apache.lucene.search.Query termQuery = lucene.getQuery();
// use Query

...

Phrases are supported too. Suppose you have few documents with each of them containing name and value pairs like "name=Fred", "name=Barry" and you'd like to list only the documents containing "name=Fred":

Code Block
java
java


SearchCondition<SearchBean> filter = new FiqlParser<SearchBean>(SearchBean.class).parse("name==Fred");
LuceneQueryVisitor<SearchBean> lucene = new LuceneQueryVisitor<SearchBean>("contents");
lucene.visit(filter);
org.apache.lucene.search.Query phraseQuery = lucene.getQuery();
// use query

...

The converter is created like all other converters:

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

...

Untyped converters

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

...

import org.custom.search.Query;

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 getResult() {
        return queryStack.peek().get(0);
    }
}

...

If needed you can access a FIQL query directly and delegate it further to your own custom FIQL handler:

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 fiqlQuery = params.getFirst("_s");
        // delegate to your own custom handler 

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

...

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

and convert the queries:

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);
        // pass it to the SQL DB and return the list of Books
    }
}

where the client code may look like this:

Code Block
java
java

String address = "http://localhost:8080/bookstore/books/id=ge=123";
WebClient client = WebClient.create(address);
client.accept("application/xml");
List<Book> books = client.getCollection(Book.class);

...

If you'd like to generalize the processing of search queries and use FIQL visitors, you may want to consider setting up a contextual property "search.use.plain.queries" to "true" and get the plain query expressions converted to FIQL expressions internally.

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:

Code Block
java
java


// GET /search?ageFrom=10&ageTill=20
String exp = searchContext.getSearchExpression();
assertEquals("(age=ge=10,age=le=20)", exp);

...

By default, a FIQL expression is expected to be available in either '_s' or '_search' query.
For example, "find all the books with an 'id' property value less than 123":

Code Block
xml
xml

GET /books?_s=id=lt=123

Starting from CXF 2.6.2, it is possible to work with FIQL expressions included in URI path segments, for example, the same query can be expressed
in a number of ways:

Code Block
xml
xml


GET /books/id=lt=123
GET /books[id=lt=123]
GET /books(id=lt=123)
GET /books;id=lt=123

//etc, etc

Such expressions can be captured in the code using JAX-RS annotations:

Code Block
java
java

@Path("search")
public class BooksResource {
   @Context
   private SearchContext context;

   //GET /books[id=lt=123]
   @GET
   @Path("books[{search}]") 
   public List<Book> findSelectedBooks(@PathParam("search") String searchExpression) {
       return doFindSelectedBooks(searchExpression);
   }

   //GET /books(id=lt=123)
   @GET
   @Path("books({search})") 
   public List<Book> findSelectedBooks(@PathParam("search") String searchExpression) {
       return doFindSelectedBooks(searchExpression);
   }

   //GET /books/id=lt=123
   @GET
   @Path("books/{search}") 
   public List<Book> findSelectedBooks(@PathParam("search") String searchExpression) {
       return doFindSelectedBooks(searchExpression);
   }

   //GET /books;id=lt=123
   @GET
   @Path("books;{search}") 
   public List<Book> findSelectedBooks(@PathParam("search") String searchExpression) {
       return doFindSelectedBooks(searchExpression);
   }

   public List<Book> doFindSelectedBooks(String searchExpression) {
       SearchCondition<Book> sc = context.getCondition(searchExpression, Book.class);
   
       // JPA2 enity manager is initialized earlier
       JPATypedQuery<Book> visitor = new JPATypedQueryVisitor<Book>(entityManager, Book.class);
       sc.accept(visitor);
   
       TypedQuery<Book> typedQuery = visitor.getQuery();
       return typedQuery.getResultList();
   }

}

...

Consider the query like "find chapters with a given chapter id from all the books with 'id' less than 123".
One easy way to manage such queries is to make FIQL and JAX-RS work together. For example:

Code Block
java
java

@Path("search")
public class BooksResource {
   @Context
   private SearchContext context;

   //GET /books[id=lt=123]/chapter/1
   @GET
   @Path("books[{search}]/chapter/{id}") 
   public List<Chapter> findSelectedChapters(@PathParam("search") String searchExpression,
                                       @PathParam("id") int chapterIndex) {
       return doFindSelectedChapters(searchExpression, chapterIndex);
   }

   public List<Chapter> doFindSelectedChapters(String searchExpression, int chapterIndex) {
       SearchCondition<Book> sc = context.getCondition(searchExpression, Book.class);
   
       // JPA2 enity manager is initialized earlier
       JPATypedQuery<Book> visitor = new JPATypedQueryVisitor<Book>(entityManager, Book.class);
       sc.accept(visitor);
   
       TypedQuery<Book> typedQuery = visitor.getQuery();
       List<Book> books = typedQuery.getResultList();

       List<Chapter> chapters = new ArrayList<Chapter>(books.size);
       for (Book book : books) {
           chapters.add(book.getChapter(chapterIndex)); 
       }   
       return chapters;
   }

}

...

One way to handle is to follow the example from the previous section with few modifications:

Code Block
java
java

@Path("search")
public class BooksResource {
   @Context
   private SearchContext context;

   //GET /books(id=gt=300)/chapters(id=lt=5)
   @GET
   @Path("books({search1})/chapter/{search2}") 
   public List<Chapter> findSelectedChapters(@PathParam("search1") String bookExpression,
                                       @PathParam("search2") String chapterExpression) {
       return doFindSelectedBooks(bookExpression, chapterExpression);
   }

   public List<Chapter> doFindSelectedChapters(String bookExpression, String chapterExpression) {
       // find the books first
       
       SearchCondition<Book> bookCondition = context.getCondition(searchExpression, Book.class);
   
       JPATypedQuery<Book> visitor = new JPATypedQueryVisitor<Book>(entityManager, Book.class);
       bookCondition.visit(visitor);
       TypedQuery<Book> typedQuery = visitor.getQuery();
       List<Book> books = typedQuery.getResultList();

       // now get the chapters
       SearchCondition<Chapter> chapterCondition = context.getCondition(chapterExpression, Chapter.class);
       List<Chapter> chapters = new ArrayList<Chapter>();
       for (Book book : books) {
           chapters.addAll(chapterCondition.findAll(book.getChapters()); 
       }   
       return chapters;
   }

}

...

Perhaps a simpler approach, especially in case of JPA2, is to start looking for Chapters immediately, assuming Chapter classes have a one to one bidirectional relationship with Book:

Code Block
java
java


public class Chapter {
   private int id;
   private Book book;

   @OneToOne(mappedBy="book")
   public Book getBook() {}
}

@Path("search")
public class BooksResource {
   @Context
   private SearchContext context;

   //GET /chapters(bookId=gt=300,id=lt=5)
   @GET
   @Path("chapters({search})") 
   public List<Chapter> findSelectedChapters(@PathParam("search") String chapterExpression) {
       
       SearchCondition<Chapter> chapterCondition = context.getCondition(chapterExpression, Chapter.class);
   
       JPATypedQuery<Chapter> visitor = new JPATypedQueryVisitor<Chapter>(entityManager, Chapter.class);
       chapterCondition.visit(visitor);
       TypedQuery<Chapter> typedQuery = visitor.getQuery();
       return typedQuery.getResultList();
   }

}

...

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

SearchConditionBuilder b = SearchConditionBuilder.instance();
String fiqlQuery = b.is("id").greaterThan(123).query();

WebClient wc = WebClient.create("http://books.com/search");
wc.query("_s", fiqlQuery);
// find all the books with id greater than 123 
Collection books = wc.getCollection(Book.class);

Here is an example of building more complex queries:

Code Block
java
java

// OR condition
String ret = b.is("foo").greaterThan(20).or().is("foo").lessThan(10).query();
assertEquals("foo=gt=20,foo=lt=10", ret);

// AND condition
String ret = b.is("foo").greaterThan(20).and().is("bar").equalTo("plonk").query();
assertEquals("foo=gt=20;bar==plonk", ret);

// Complex condition
String ret = b.is("foo").equalTo(123.4).or().and(
            b.is("bar").equalTo("asadf*"), 
            b.is("baz").lessThan(20)).query();
assertEquals("foo==123.4,(bar==asadf*;baz=lt=20.0)", ret);

Note, starting from CXF 2.7.1 the following can be used to make connecting multiple primitive expressions simpler:

Code Block
java
java

// AND condition, '.and("bar")' is a shortcut for "and().is("bar")", similar shortcut is supported for 'or'
String ret = b.is("foo").greaterThan(20).and("bar").equalTo("plonk").query();
assertEquals("foo=gt=20;bar==plonk", ret);

More updates to the builder API are available on the trunk:

Code Block
java
java

// OR condition
String ret = b.is("foo").equalTo(20).or().is("foo").equalTo(10).query();
assertEquals("foo==20,foo==10", ret);

// Same query, shorter expression
String ret = b.is("foo").equalTo(20, 10).query();
assertEquals("foo==20,foo==10", ret);

and

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


...

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

Code Block
java
java

?_search=date=le=2010-03-11

A custom date format can be supported. Use "search.date-format" contextual property, example, "search.date-format"="yyyy-MM-dd'T'HH:mm:ss" will let users type:

Code Block
java
java

?_search=time=le=2010-03-11T18:00:00

...

At the moment, for custom date formats be recognized by SearchConditionBuilder, FIQLSearchConditionBuilder has to be created explicitly:

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

...