/******************************************************************************
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/.
*
* Software distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for
* the specific language governing rights and limitations under the License.
*
* The Original Code is: Jsoda
* The Initial Developer of the Original Code is: William Wong (williamw520@gmail.com)
* Portions created by William Wong are Copyright (C) 2012 William Wong, All Rights Reserved.
*
******************************************************************************/
package wwutil.jsoda;
import java.io.*;
import java.util.*;
import java.lang.reflect.*;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
* Query object to capture common query properties for both SimpleDB and DynamoDB
*/
public class Query<T>
{
private static Log log = LogFactory.getLog(Query.class);
/** Select types
* 1. select all => select * from table
* 2. select id => select itemName() from table
* 3. select id and rangeKey => select itemName() from table
* 4. select id and others => select others from table. PK decoded from itemName in results.
* 5. select id and rangeKey and others => select others from table. PK decoded from itemName in results.
* 6. select rangeKey => select id and rangeKey from table. (always need id)
* 7. select rangeKey and others => select id and rangeKey and others from table. (always need id)
* 8. select others => select others from table
*/
static final int SELECT_ALL = 1;
static final int SELECT_ID = 2;
static final int SELECT_ID_RANGE = 3;
static final int SELECT_ID_OTHERS = 4;
static final int SELECT_ID_RANGE_OTHERS = 5;
static final int SELECT_RANGE = 6;
static final int SELECT_RANGE_OTHERS = 7;
static final int SELECT_OTHERS = 8;
Class<T> modelClass;
String modelName;
Jsoda jsoda;
List<String> selectTerms = new ArrayList<String>();
List<Filter> filters = new ArrayList<Filter>();
List<String> orderbyFields = new ArrayList<String>();
int limit = 0;
boolean consistentRead = false;
int selectType = SELECT_ALL;
boolean beforeRun = true;
Object nextKey = null;
private boolean queryParsed = false;
/** Create a Query object to build query, to run on the Jsoda object. */
public Query(Class<T> modelClass, Jsoda jsoda) {
this.modelClass = modelClass;
this.modelName = jsoda.getModelName(modelClass);
this.jsoda = jsoda;
}
// Select
/** Add a field to select from the query.
* <pre>
* query.select("field1")
* </pre>
*/
public Query<T> select(String field) {
jsoda.validateField(modelName, field);
selectTerms.add(field);
return this;
}
/** Add the list of fields to select from the query.
* <pre>
* query.select("field1", "field2", "field3")
* query.select(new String[] {"field1", "field2", "field3"})
* </pre>
*/
public Query<T> select(String... fields) {
for (String field : fields)
select(field);
return this;
}
// Filter conditions
public Query<T> is_null(String field) {
filters.add(new Filter(jsoda, modelName, field, Filter.NULL));
return this;
}
public Query<T> is_not_null(String field) {
filters.add(new Filter(jsoda, modelName, field, Filter.NOT_NULL));
return this;
}
public Query<T> eq(String field, Object operand) {
filters.add(new Filter(jsoda, modelName, field, Filter.EQ, operand));
return this;
}
public Query<T> ne(String field, Object operand) {
filters.add(new Filter(jsoda, modelName, field, Filter.NE, operand));
return this;
}
public Query<T> le(String field, Object operand) {
filters.add(new Filter(jsoda, modelName, field, Filter.LE, operand));
return this;
}
public Query<T> lt(String field, Object operand) {
filters.add(new Filter(jsoda, modelName, field, Filter.LT, operand));
return this;
}
public Query<T> ge(String field, Object operand) {
filters.add(new Filter(jsoda, modelName, field, Filter.GE, operand));
return this;
}
public Query<T> gt(String field, Object operand) {
filters.add(new Filter(jsoda, modelName, field, Filter.GT, operand));
return this;
}
public Query<T> like(String field, Object operand) {
filters.add(new Filter(jsoda, modelName, field, Filter.LIKE, operand));
return this;
}
public Query<T> not_like(String field, Object operand) {
filters.add(new Filter(jsoda, modelName, field, Filter.NOT_LIKE, operand));
return this;
}
public Query<T> contains(String field, Object operand) {
filters.add(new Filter(jsoda, modelName, field, Filter.CONTAINS, operand));
return this;
}
public Query<T> not_contains(String field, Object operand) {
filters.add(new Filter(jsoda, modelName, field, Filter.NOT_CONTAINS, operand));
return this;
}
public Query<T> begins_with(String field, Object operand) {
filters.add(new Filter(jsoda, modelName, field, Filter.BEGINS_WITH, operand));
return this;
}
public Query<T> between(String field, Object startValue, Object endValue) {
filters.add(new Filter(jsoda, modelName, field, Filter.BETWEEN, startValue, endValue));
return this;
}
public Query<T> in(String field, Object... operands) {
filters.add(new Filter(jsoda, modelName, field, Filter.IN, operands));
return this;
}
// Order by
public Query<T> order_by(String field) {
if (jsoda.getField(modelName, field) == null)
throw new IllegalArgumentException("Field " + field + " does not exist in " + modelName);
orderbyFields.add("+" + field);
return this;
}
public Query<T> order_by_desc(String field) {
if (jsoda.getField(modelName, field) == null)
throw new IllegalArgumentException("Field " + field + " does not exist in " + modelName);
orderbyFields.add("-" + field);
return this;
}
// Query configuration
public Query<T> limit(int limit) {
this.limit = limit;
return this;
}
public Query<T> consistentRead(boolean consistentRead) {
this.consistentRead = consistentRead;
return this;
}
private void parseQuery() {
if (queryParsed)
return;
queryParsed = true;
selectType = determineSelectType();
}
private int determineSelectType() {
if (selectTerms.size() == 0) {
return SELECT_ALL;
}
// Determine select type of the query.
boolean selectId = false;
boolean selectRange = false;
for (String term : selectTerms) {
if (jsoda.isIdField(modelName, term))
selectId = true;
if (jsoda.isRangeField(modelName, term))
selectRange = true;
}
if (!selectRange) {
if (selectId) {
if (selectTerms.size() == 1) {
// Only id field
return SELECT_ID;
} else {
// Has id and other fields
return SELECT_ID_OTHERS;
}
} else {
// No id nor range field, but selectTerms.size() > 0, just the other fields.
return SELECT_OTHERS;
}
} else {
if (selectId) {
// Has id and range fields in select
if (selectTerms.size() == 2) {
return SELECT_ID_RANGE;
} else {
return SELECT_ID_RANGE_OTHERS;
}
} else {
// Has range field but no id field in select
if (selectTerms.size() == 1) {
return SELECT_RANGE;
} else {
return SELECT_RANGE_OTHERS;
}
}
}
}
/** Execute the query to return the count, not the items. */
public long count()
throws JsodaException
{
parseQuery();
return jsoda.getDb(modelName).queryCount(modelClass, this);
}
/** Execute the query and start returning result items. It might or might not return the entire result set.
* Call run() again to get the next batch of items. It will return an empty list when there's no more result.
*
* A typical loop to handle successive result batch. See another style of simpler iteration in hasNext()'s doc.
* <pre>
* List<T> items;
* while ((items = query.run()).size() != 0) {
* for (T item : items)
* dump(item);
* }
* </pre>
*/
public List<T> run()
throws JsodaException
{
try {
parseQuery();
List<T> resultObjs = jsoda.getDb(modelName).queryRun(modelClass, this, !beforeRun);
for (T obj : resultObjs) {
jsoda.postLoadSteps(obj, toCache()); // do callPostLoad and caching.
}
beforeRun = false;
return resultObjs;
} catch(JsodaException je) {
throw je;
} catch(Exception e) {
throw new JsodaException("Failed to run query", e);
}
}
/** Quick check to see if there are more result to return. Before run() is called, hasNext() always returns true.
* This simplies the iteration loop. The typical loop is:
* <pre>
* while (query.hasNext()) {
* for (Model1 item : query.run()) {
* }
* }
* </pre>
*/
public boolean hasNext() {
return beforeRun || jsoda.getDb(modelName).queryHasNext(this);
}
/** Reset the result set if there's any. Restart the query if run() is called again. */
public Query<T> reset() {
beforeRun = true;
nextKey = null;
return this;
}
private boolean toCache() {
// Besides select *, all other select types have partial fields.
// Don't cache partial field object.
return selectTerms.size() == 0; // no term => select *
}
}