...
: Search {span}
...
For example, the following query
Code Block |
---|
|
?_s=name==CXF;version=ge=2.2
|
...
More complex composite expressions can also be expressed easily enough, examples:
Code Block |
---|
|
// 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 |
---|
|
// 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 |
---|
|
<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 |
---|
|
@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 |
---|
|
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 |
---|
|
<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 |
---|
|
//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 |
---|
|
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 |
---|
|
<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 |
---|
|
// ?_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 |
---|
|
// ?_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 |
---|
|
// ?_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 |
---|
|
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 |
---|
|
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 |
---|
|
// 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 |
---|
|
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 |
---|
|
@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 |
---|
|
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 |
---|
|
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 |
---|
|
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 |
---|
|
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 |
---|
|
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 |
---|
|
// 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();
|
...
Code Block |
---|
|
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 |
---|
|
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 |
---|
|
@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 |
---|
|
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 |
---|
|
@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 |
---|
|
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 |
---|
|
// 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 |
---|
|
// 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 |
---|
|
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 |
---|
|
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 |
---|
|
@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 |
---|
|
@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 |
---|
|
@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 |
---|
|
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 |
---|
|
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 |
---|
|
// 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 |
---|
|
// 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 |
---|
|
// 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 |
---|
|
// 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 |
---|
|
?_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 |
---|
|
?_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 |
---|
|
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);
|
...