package cn.jimmyshi.beanquery;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.collections4.ComparatorUtils;
import org.apache.commons.collections4.Predicate;
import org.apache.commons.collections4.functors.TruePredicate;
import org.hamcrest.Matcher;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import cn.jimmyshi.beanquery.comparators.ComparableObjectComparator;
import cn.jimmyshi.beanquery.comparators.DelegatedSortOrderableComparator;
import cn.jimmyshi.beanquery.comparators.PropertyComparator;
import cn.jimmyshi.beanquery.comparators.SortOrderableComparator;
import cn.jimmyshi.beanquery.selectors.BeanSelector;
import cn.jimmyshi.beanquery.selectors.ClassSelector;
import cn.jimmyshi.beanquery.selectors.CompositeSelector;
import cn.jimmyshi.beanquery.selectors.KeyValueMapSelector;
import cn.jimmyshi.beanquery.selectors.NestedKeyValueMapSelector;
import cn.jimmyshi.beanquery.selectors.PropertySelector;
import cn.jimmyshi.beanquery.selectors.StringSelector;
/**
* Entry of the BeanQuery. Typical usage is as below.
* <pre>
* import static cn.jimmyshi.beanquery.BeanQuery.*;
* List<Map<String,Object>> result=select("name,price,mainAuthor.name as authorName")
* .from(collectionOfBooks)
* .where(
* anyOf(
* value("name", startsWith("Book1")),
* value("name", is("Book2"))
* ),
* allOf(
* value("price", greaterThan(53d)),
* value("price",lessThan(65d))
* )
* )
* .orderBy("name").desc()
* .execute();
* </pre>
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
public final class BeanQuery<T> extends BeanQueryCustomizedMatchers {
private transient Logger logger = LoggerFactory.getLogger(BeanQuery.class);
private final Selector<T> selector;
private Collection from;
private Predicate predicate = TruePredicate.truePredicate();
private SortOrderableComparator comparator;
private boolean descSorting = false;
private BeanQuery(Selector<T> selector) {
this.selector = selector;
}
/**
* Convert the select into a NestedKeyValueMapSelector.<br>
* Note<br>:
* This method is only available for BeanQueries with a KeyValuMapSelector. Calling this method on a BeanQeury with other types of selector will cause a IllegalStateExcewption.
*
*/
public BeanQuery<Map<String,Object>> nested(){
if(!(this.selector instanceof KeyValueMapSelector)){
throw new IllegalStateException("This is only for BeanQueries which has a KeyValueMapSelector selector. The current selector is a "+this.selector);
}
BeanQuery<Map<String,Object>> result= new BeanQuery<Map<String,Object>>(new NestedKeyValueMapSelector((KeyValueMapSelector)this.selector));
result.from=this.from;
result.predicate=this.predicate;
result.comparator=this.comparator;
result.descSorting=this.descSorting;
return result;
}
/**
* Specify where to query from.
*
* @param from
*/
public BeanQuery<T> from(Collection<?> from) {
this.from = from;
return this;
}
/**
* Same as <code>from(Collections.singleton(bean))</code>
*/
public BeanQuery<T> from(Object bean){
this.from=Collections.singleton(bean);
return this;
}
/**
* Support the Hamcrest Matcher as the query condition. Only items match this
* matcher will be chosen.
*
* @param matcher
*/
public BeanQuery<T> where(Matcher matcher) {
this.predicate = new MatcherPredicate(matcher);
return this;
}
/**
* Support multiple Hamcrest Matchers as the query condition. Only items match
* all the matchers will be chosen.
*/
public BeanQuery<T> where(Matcher... matchers) {
this.predicate = new MatcherPredicate(allOf(matchers));
return this;
}
/**
* Specify the property of the Beans to be compared when ordering the result.
* When comparing, the property value of the bean must be instance of
* {@link Comparable}, Otherwise it will be treated as a null value. The null
* value is sorted at the top in ASC sorting and at the bottom in DESC
* sorting. The default Order is ASC order.
*
* @param orderByProperty
*/
public BeanQuery<T> orderBy(String orderByProperty) {
this.comparator = new DelegatedSortOrderableComparator(new PropertyComparator(orderByProperty,
new ComparableObjectComparator()));
return this;
}
/**
* Specify the property of the beans to be compared by the
* propertyValueComparator when sorting the result. If there is not a
* accessible public read method of the property, the value of the property
* passed to the propertyValueComparator will be null.
*/
public BeanQuery<T> orderBy(String orderByProperty, Comparator propertyValueComparator) {
this.comparator = new DelegatedSortOrderableComparator(new PropertyComparator(orderByProperty,
propertyValueComparator));
return this;
}
/**
* Specify the comparator used to compare the bean when sorting the result.
*/
public BeanQuery<T> orderBy(Comparator beanComparator) {
this.comparator = new DelegatedSortOrderableComparator(beanComparator);
return this;
}
/**
* Using an array of Comparators, applied in sequence until one returns not equal or the array is exhausted.
*/
public BeanQuery<T> orderBy(Comparator... beanComparator) {
this.comparator = new DelegatedSortOrderableComparator(ComparatorUtils.chainedComparator(beanComparator));
return this;
}
/**
* Sort the result in DESC order. The default ordering is ASC order. If the
* {@link #orderBy(String)} is not specified, calling this method does not
* affect anything.
*/
public BeanQuery<T> desc() {
this.descSorting = true;
return this;
}
/**
* Sort the result in ASC order. The default ordering is ASC order. If the
* {@link #orderBy(String)} is not specified, calling this method does not
* affect anything.
*/
public BeanQuery<T> asc() {
this.descSorting = false;
return this;
}
/**
* Create a Comparator base on a property.
*/
public static SortOrderableComparator<?> orderByProperty(String propertyName){
return new DelegatedSortOrderableComparator(new PropertyComparator(propertyName,
new ComparableObjectComparator()));
}
/**
* A convenient method of from(from).execute();
*/
public List<T> executeFrom(Collection<?> from){
return from(from).execute();
}
/**
* Execute from a bean to check does it match the filtering condition and
* convert it.
*/
public T executeFrom(Object bean) {
List<T> executeFromCollectionResult = executeFrom(Collections.singleton(bean));
if (CollectionUtils.isEmpty(executeFromCollectionResult)) {
return null;
} else {
return executeFromCollectionResult.get(0);
}
}
/**
* Execute this Query. If query from a null or empty collection, an empty list
* will be returned.
*
* @return
*/
public List<T> execute() {
if (CollectionUtils.isEmpty(from)) {
logger.info("Querying from an empty collection, returning empty list.");
return Collections.emptyList();
}
List copied = new ArrayList(this.from);
logger.info("Start apply predicate [{}] to collection with [{}] items.", predicate, copied.size());
CollectionUtils.filter(copied, this.predicate);
logger.info("Done filtering collection, filtered result size is [{}]", copied.size());
if (null != this.comparator && copied.size()>1) {
Comparator actualComparator = this.descSorting ? comparator.desc() : comparator.asc();
logger.info("Start to sort the filtered collection with comparator [{}]", actualComparator);
Collections.sort(copied, actualComparator);
logger.info("Done sorting the filtered collection.");
}
logger.info("Start to slect from filtered collection with selector [{}].", selector);
List<T> select = this.selector.select(copied);
logger.info("Done select from filtered collection.");
return select;
}
/**
* Create a BeanQuery instance with multiple Selectors.
*
* @param selectors
*/
public static BeanQuery<Map<String, Object>> select(KeyValueMapSelector... selectors) {
return new BeanQuery<Map<String, Object>>(new CompositeSelector(selectors));
}
/**
* Create a BeanQuery instance with select String, the select String is in
* format "propertyName[ as alias][,propertyName[ as alias]]". For example:<br>
* <code>BeanQuery beanQuery=select("name, price as p, address.officeAddress as address");</code>
* When executing the BeanQuery instance created in above code will
* return a list of map with 3 keys:[name,p,address].
*/
public static BeanQuery<Map<String, Object>> select(String selectString) {
return new BeanQuery<Map<String, Object>>(new StringSelector(selectString));
}
/**
* Create a BeanQuery instance with some propertyString, a property string is
* in format "propertyName[ as alias]". For example:<br>
* <code>BeanQuery beanQuery=select("name","price as p", "address.officeAddress as address");</code>
* <br>
* When executing the BeanQuery instance created in above code will return a
* list of map with 3 keys:[name,p,address].
*/
public static BeanQuery<Map<String, Object>> select(String... propertyStrings) {
return new BeanQuery<Map<String, Object>>(new StringSelector(propertyStrings));
}
/**
* Create a BeanQuery instance without the function of convert result into Map
* function. If you just want to filter bean collection, sort bean collection
* and want to get the execute result as a list of beans, you should use this
* method to create a BeanQuery instance.
* @deprecated use {@link #select(Class)} method instead
*/
public static <T> BeanQuery<T> selectBean(Class<T> beanClass) {
return new BeanQuery<T>(new BeanSelector<T>(beanClass));
}
/**
* Create a BeanQuery instance without the function of convert result into Map
* function. If you just want to filter bean collection, sort bean collection
* and want to get the execute result as a list of beans, you should use this
* method to create a BeanQuery instance.
*/
public static <T> BeanQuery<T> select(Class<T> beanClass) {
return new BeanQuery<T>(new BeanSelector<T>(beanClass));
}
/**
* Allow client to create a BeanQuery instance with a customized selector.
*/
public static <T> BeanQuery<T> select(Selector<T> selector){
return new BeanQuery(selector);
}
/**
* Create a Selector that will use all the public readable
* propertyNames(except the class property) as the keys. When saying
* "public readable", means a property has a public read method, for example:
* <ul>
* <li><code>public String getName()</code> for property name.</li>
* <li><code>public boolean isActive()</code> for property active</li>
* </ul>
* Usage sample:<br>
* <code>
* BeanQuery beanQuery=select(allOf(Book.class).except("authorList","authorMap"));
* </code>
*/
public static ClassSelector allOf(Class clazz) {
return new ClassSelector(clazz);
}
/**
* Create a PropertySelector with the property name. Code sample below:<br>
* <code>
* BeanQuery beanQuery=select(property("name"), property("price"), property("price").as("p"));
* </code>
*/
public static PropertySelector property(String property) {
return new PropertySelector(property);
}
/**
* Create a matcher to apply on the property of the from items.Code sample
* below:<br>
* <code>
* BeanQuery beanQuery=select(allOf(Book.class)).from(bookList).where(value("name",startsWith("Book1")));
* </code>
*/
public static BeanPropertyMatcher value(String property, Matcher<?> matcher) {
return new BeanPropertyMatcher(property, matcher);
}
}