/*
* This software is distributed under the terms of the FSF
* Gnu Lesser General Public License (see lgpl.txt).
*
* This program is distributed WITHOUT ANY WARRANTY. See the
* GNU General Public License for more details.
*/
package com.scooterframework.orm.activerecord;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.scooterframework.orm.misc.JdbcPageListSource;
import com.scooterframework.orm.misc.Paginator;
import com.scooterframework.orm.sqldataexpress.util.SqlConstants;
/**
* <p>QueryBuilder class provides flexible methods for building a query.</p>
*
* @author (Fei) John Chen
*/
public class QueryBuilder {
private TableGateway tg;
private String conditionsSQL;
private Map<String, Object> conditionsSQLDataMap;
private Map<String, String> options = new HashMap<String, String>();
private boolean usedWhere;
private boolean usedIncludes;
private boolean usedGroupBy;
private boolean usedHaving;
private boolean usedOrderBy;
private boolean usedLimit;
private boolean usedOffset;
private boolean usedPage;
QueryBuilder(TableGateway tg) {
this.tg = tg;
}
/**
* <p>Returns all the records that satisfy the query built by
* the <tt>QueryBuilder</tt>.</p>
*
* @return a list of ActiveRecord objects
*/
public List<ActiveRecord> getRecords() {
validateBuild();
return tg.findAll(conditionsSQL, conditionsSQLDataMap, options);
}
/**
* <p>Returns the record that satisfies the query built by
* the <tt>QueryBuilder</tt>.</p>
*
* @return the ActiveRecord object
*/
public ActiveRecord getRecord() {
validateBuild();
return tg.findFirst(conditionsSQL, conditionsSQLDataMap, options);
}
/**
* <p>Returns a Paginator that satisfy the query built by
* the <tt>QueryBuilder</tt>. Total number of records and paged list of
* records can be obtained from the returned Paginator instance.</p>
*
* @return a Paginator instance
*/
public Paginator getPaginator() {
validateBuild();
Class<? extends ActiveRecord> modelClass = tg.getModelClass();
Map<String, String> inputOptions = new HashMap<String, String>();
for (Map.Entry<String, String> entry : options.entrySet()) {
String key = entry.getKey();
if (Paginator.key_limit.equals(key) ||
Paginator.key_offset.equals(key) ||
Paginator.key_npage.equals(key)) continue;
inputOptions.put(key, entry.getValue());
}
Map<String, String> pagingControl = new HashMap<String, String>();
if (options.containsKey(ActiveRecordConstants.key_limit))
pagingControl.put(Paginator.key_limit, options.get(ActiveRecordConstants.key_limit));
if (options.containsKey(ActiveRecordConstants.key_offset))
pagingControl.put(Paginator.key_offset, options.get(ActiveRecordConstants.key_offset));
if (options.containsKey(ActiveRecordConstants.key_page))
pagingControl.put(Paginator.key_npage, options.get(ActiveRecordConstants.key_page));
return new Paginator(new JdbcPageListSource(modelClass, inputOptions), pagingControl);
}
private void validateBuild() {
if (usedHaving && !usedGroupBy) {
throw new RuntimeException("Group-by clause must be used with having clause.");
}
if (usedOffset && usedPage) {
throw new RuntimeException("Page and offset cannot be set both at the same time.");
}
}
/**
* <p>Setup where clause.</p>
*
* @param conditionsSQL a valid SQL query where clause string
* @return current <tt>QueryBuilder</tt> instance
*/
public QueryBuilder where(String conditionsSQL) {
return where(conditionsSQL, (Map<String, Object>)null);
}
/**
* <p>Setup where clause.</p>
*
* @param conditionsSQL a valid SQL query where clause string
* @param conditionsSQLData an array of data for the <tt>conditionsSQL</tt> string
* @return current <tt>QueryBuilder</tt> instance
*/
public QueryBuilder where(String conditionsSQL, Object[] conditionsSQLData) {
if (conditionsSQLData == null || conditionsSQLData.length == 0) {
return where(conditionsSQL);
}
Map<String, Object> map = new HashMap<String, Object>(conditionsSQLData.length);
int index = 1;
for(Object o : conditionsSQLData) {
map.put("" + index, o);
index++;
}
return where(conditionsSQL, map);
}
/**
* <p>Setup where clause.</p>
*
* @param conditionsSQL a valid SQL query where clause string
* @param conditionsSQLDataMap a map of data for the keys in the <tt>conditionsSQL</tt> string
* @return current <tt>QueryBuilder</tt> instance
*/
public QueryBuilder where(String conditionsSQL, Map<String, Object> conditionsSQLDataMap) {
if (usedWhere)
throw new RuntimeException("where() can only be called once.");
usedWhere = true;
this.conditionsSQL = conditionsSQL;
this.conditionsSQLDataMap = conditionsSQLDataMap;
return this;
}
/**
* <p>Setup associated models for eager loading.</p>
*
* @param includes a string of associated models
* @return current <tt>QueryBuilder</tt> instance
*/
public QueryBuilder includes(String includes) {
if (usedIncludes)
throw new RuntimeException("includes() can only be called once.");
usedIncludes = true;
options.put(ActiveRecordConstants.key_include, includes);
return this;
}
/**
* <p>Setup associated models for eager loading.</p>
*
* @param includes a string of associated models
* @param joinType type of join
* @return current <tt>QueryBuilder</tt> instance
*/
public QueryBuilder includes(String includes, String joinType) {
if (usedIncludes)
throw new RuntimeException("includes() can only be called once.");
usedIncludes = true;
options.put(ActiveRecordConstants.key_include, includes);
options.put(ActiveRecordConstants.key_join_type, joinType);
return this;
}
/**
* <p>Setup associated models for eager loading.</p>
*
* <p>If <tt>strict</tt> is true, then child records can only be accessed
* through their parent.</p>
*
* @param includes a string of associated models
* @param strict true if strict
* @return current <tt>QueryBuilder</tt> instance
*/
public QueryBuilder includes(String includes, boolean strict) {
if (usedIncludes)
throw new RuntimeException("includes() can only be called once.");
usedIncludes = true;
if (strict) {
options.put(ActiveRecordConstants.key_strict_include, includes);
}
else {
options.put(ActiveRecordConstants.key_include, includes);
}
return this;
}
/**
* <p>Setup group-by clause.</p>
*
* @param groupBy a valid SQL query group-by clause string
* @return current <tt>QueryBuilder</tt> instance
*/
public QueryBuilder groupBy(String groupBy) {
if (usedGroupBy)
throw new RuntimeException("groupBy() can only be called once.");
usedGroupBy = true;
options.put(SqlConstants.key_group_by, groupBy);
return this;
}
/**
* <p>Setup having clause.</p>
*
* @param having a valid SQL query having clause string
* @return current <tt>QueryBuilder</tt> instance
*/
public QueryBuilder having(String having) {
if (usedHaving)
throw new RuntimeException("having() can only be called once.");
usedHaving = true;
options.put(SqlConstants.key_having, having);
return this;
}
/**
* <p>Setup group-by clause.</p>
*
* @param orderBy a valid SQL query order-by clause string
* @return current <tt>QueryBuilder</tt> instance
*/
public QueryBuilder orderBy(String orderBy) {
if (usedOrderBy)
throw new RuntimeException("orderBy() can only be called once.");
usedOrderBy = true;
options.put(SqlConstants.key_order_by, orderBy);
return this;
}
/**
* <p>Setup limit for number of records per retrieval.</p>
*
* @param limit number of records for each retrieval
* @return current <tt>QueryBuilder</tt> instance
*/
public QueryBuilder limit(int limit) {
if (usedLimit)
throw new RuntimeException("limit() can only be called once.");
usedLimit = true;
options.put(ActiveRecordConstants.key_limit, limit + "");
return this;
}
/**
* <p>Setup number of records to skip.</p>
*
* @param offset number of records to skip
* @return current <tt>QueryBuilder</tt> instance
*/
public QueryBuilder offset(int offset) {
if (usedOffset)
throw new RuntimeException("offset() can only be called once.");
usedOffset = true;
options.put(ActiveRecordConstants.key_offset, offset + "");
return this;
}
/**
* <p>Setup current page number.
* All records in previous pages are skipped.</p>
*
* <p>The first page is 1, not 0.</p>
*
* @param page current page number
* @return current <tt>QueryBuilder</tt> instance
*/
public QueryBuilder page(int page) {
if (usedPage)
throw new RuntimeException("page() can only be called once.");
usedPage = true;
options.put(ActiveRecordConstants.key_page, page + "");
return this;
}
}