blob: 45a9e52b7758fca442d40d71a8d0d6b7fb0ff15b [file] [log] [blame]
/*******************************************************************************
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*******************************************************************************/
package org.apache.ofbiz.entity.util;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.ofbiz.base.util.Debug;
import org.apache.ofbiz.base.util.UtilGenerics;
import org.apache.ofbiz.base.util.UtilMisc;
import org.apache.ofbiz.base.util.UtilValidate;
import org.apache.ofbiz.base.util.collections.PagedList;
import org.apache.ofbiz.entity.Delegator;
import org.apache.ofbiz.entity.GenericEntityException;
import org.apache.ofbiz.entity.GenericValue;
import org.apache.ofbiz.entity.condition.EntityCondition;
import org.apache.ofbiz.entity.model.DynamicViewEntity;
/**
* Used to setup various options for and subsequently execute entity queries.
*
* All methods to set options modify the EntityQuery instance then return this modified object to allow method call chaining. It is
* important to note that this object is not immutable and is modified internally, and returning EntityQuery is just a
* self reference for convenience.
*
* After a query the object can be further modified and then used to perform another query if desired.
*/
public class EntityQuery {
public static final String module = EntityQuery.class.getName();
private Delegator delegator;
private String entityName = null;
private DynamicViewEntity dynamicViewEntity = null;
private boolean useCache = false;
private EntityCondition whereEntityCondition = null;
private Set<String> fieldsToSelect = null;
private List<String> orderBy = null;
private Integer resultSetType = EntityFindOptions.TYPE_FORWARD_ONLY;
private Integer fetchSize = null;
private Integer maxRows = null;
private Boolean distinct = null;
private EntityCondition havingEntityCondition = null;
private boolean filterByDate = false;
private Timestamp filterByDateMoment;
private List<String> filterByFieldNames = null;
/** Construct an EntityQuery object for use against the specified Delegator
* @param delegator - The delegator instance to use for the query
* @return Returns a new EntityQuery object
*/
public static EntityQuery use(Delegator delegator) {
return new EntityQuery(delegator);
}
/** Construct an EntityQuery object for use against the specified Delegator
* @param delegator - The delegator instance to use for the query
* @return Returns a new EntityQuery object
*/
public EntityQuery(Delegator delegator) {
this.delegator = delegator;
}
/** Set the fields to be returned when the query is executed.
*
* Note that the select methods are not additive, if a subsequent
* call is made to select then the existing fields for selection
* will be replaced.
* @param fieldsToSelect - A Set of Strings containing the field names to be selected
* @return this EntityQuery object, to enable chaining
*/
public EntityQuery select(Set<String> fieldsToSelect) {
this.fieldsToSelect = fieldsToSelect;
return this;
}
/** Set the fields to be returned when the query is executed.
*
* Note that the select methods are not additive, if a subsequent
* call is made to select then the existing fields for selection
* will be replaced.
* @param fieldsToSelect - Strings containing the field names to be selected
* @return this EntityQuery object, to enable chaining
*/
public EntityQuery select(String...fields) {
this.fieldsToSelect = UtilMisc.toSetArray(fields);
return this;
}
/** Set the entity to query against
* @param entityName - The name of the entity to query against
* @return this EntityQuery object, to enable chaining
*/
public EntityQuery from(String entityName) {
this.entityName = entityName;
this.dynamicViewEntity = null;
return this;
}
/** Set the entity to query against
* @param dynamicViewEntity - The DynamicViewEntity object to query against
* @return this EntityQuery object, to enable chaining
*/
public EntityQuery from(DynamicViewEntity dynamicViewEntity) {
this.dynamicViewEntity = dynamicViewEntity;
this.entityName = null;
return this;
}
/** Set the EntityCondition to be used as the WHERE clause for the query
*
* NOTE: Each successive call to any of the where(...) methods will replace the currently set condition for the query.
* @param entityCondition - An EntityCondition object to be used as the where clause for this query
* @return this EntityQuery object, to enable chaining
*/
public EntityQuery where(EntityCondition entityCondition) {
this.whereEntityCondition = entityCondition;
return this;
}
/** Set a Map of field name/values to be ANDed together as the WHERE clause for the query
*
* NOTE: Each successive call to any of the where(...) methods will replace the currently set condition for the query.
* @param fieldMap - A Map of field names/values to be ANDed together as the where clause for the query
* @return this EntityQuery object, to enable chaining
*/
public EntityQuery where(Map<String, Object> fieldMap) {
this.whereEntityCondition = EntityCondition.makeCondition(fieldMap);
return this;
}
/** Set a series of field name/values to be ANDed together as the WHERE clause for the query
*
* NOTE: Each successive call to any of the where(...) methods will replace the currently set condition for the query.
* @param fieldMap - A series of field names/values to be ANDed together as the where clause for the query
* @return this EntityQuery object, to enable chaining
*/
public EntityQuery where(Object...fields) {
this.whereEntityCondition = EntityCondition.makeCondition(UtilMisc.toMap(fields));
return this;
}
/** Set a series of EntityConditions to be ANDed together as the WHERE clause for the query
*
* NOTE: Each successive call to any of the where(...) methods will replace the currently set condition for the query.
* @param entityCondition - A series of EntityConditions to be ANDed together as the where clause for the query
* @return this EntityQuery object, to enable chaining
*/
public EntityQuery where(EntityCondition...entityCondition) {
this.whereEntityCondition = EntityCondition.makeCondition(Arrays.asList(entityCondition));
return this;
}
/** Set a list of EntityCondition objects to be ANDed together as the WHERE clause for the query
*
* NOTE: Each successive call to any of the where(...) methods will replace the currently set condition for the query.
* @param fieldMap - A list of EntityCondition objects to be ANDed together as the WHERE clause for the query
* @return this EntityQuery object, to enable chaining
*/
public <T extends EntityCondition> EntityQuery where(List<T> andConditions) {
this.whereEntityCondition = EntityCondition.makeCondition(andConditions);
return this;
}
/** Set the EntityCondition to be used as the HAVING clause for the query.
*
* NOTE: Each successive call to any of the having(...) methods will replace the currently set condition for the query.
* @param entityCondition - The EntityCondition object that specifies how to constrain
* this query after any groupings are done (if this is a view
* entity with group-by aliases)
* @return this EntityQuery object, to enable chaining
*/
public EntityQuery having(EntityCondition entityCondition) {
this.havingEntityCondition = entityCondition;
return this;
}
/** The fields of the named entity to order the resultset by; optionally add a " ASC" for ascending or " DESC" for descending
*
* NOTE: Each successive call to any of the orderBy(...) methods will replace the currently set orderBy fields for the query.
* @param orderBy - The fields of the named entity to order the resultset by
* @return this EntityQuery object, to enable chaining
*/
public EntityQuery orderBy(List<String> orderBy) {
this.orderBy = orderBy;
return this;
}
/** The fields of the named entity to order the resultset by; optionally add a " ASC" for ascending or " DESC" for descending
*
* NOTE: Each successive call to any of the orderBy(...) methods will replace the currently set orderBy fields for the query.
* @param orderBy - The fields of the named entity to order the resultset by
* @return this EntityQuery object, to enable chaining
*/
public EntityQuery orderBy(String...fields) {
this.orderBy = Arrays.asList(fields);
return this;
}
/** Indicate that the ResultSet object's cursor may move only forward (this is the default behavior)
*
* @return this EntityQuery object, to enable chaining
*/
public EntityQuery cursorForwardOnly() {
this.resultSetType = EntityFindOptions.TYPE_FORWARD_ONLY;
return this;
}
/** Indicate that the ResultSet object's cursor is scrollable but generally sensitive to changes to the data that underlies the ResultSet.
*
* @return this EntityQuery object, to enable chaining
*/
public EntityQuery cursorScrollSensitive() {
this.resultSetType = EntityFindOptions.TYPE_SCROLL_SENSITIVE;
return this;
}
/** Indicate that the ResultSet object's cursor is scrollable but generally not sensitive to changes to the data that underlies the ResultSet.
*
* @return this EntityQuery object, to enable chaining
*/
public EntityQuery cursorScrollInsensitive() {
this.resultSetType = EntityFindOptions.TYPE_SCROLL_INSENSITIVE;
return this;
}
/** Specifies the fetch size for this query. -1 will fall back to datasource settings.
*
* @param fetchSize - The fetch size for this query
* @return this EntityQuery object, to enable chaining
*/
public EntityQuery fetchSize(int fetchSize) {
this.fetchSize = fetchSize;
return this;
}
/** Specifies the max number of rows to return, 0 means all rows.
*
* @param maxRows - the max number of rows to return
* @return this EntityQuery object, to enable chaining
*/
public EntityQuery maxRows(int maxRows) {
this.maxRows = maxRows;
return this;
}
/** Specifies that the values returned should be filtered to remove duplicate values.
*
* @return this EntityQuery object, to enable chaining
*/
public EntityQuery distinct() {
this.distinct = true;
return this;
}
/** Specifies whether the values returned should be filtered to remove duplicate values.
*
* @param distinct - boolean indicating whether the values returned should be filtered to remove duplicate values
* @return this EntityQuery object, to enable chaining
*/
public EntityQuery distinct(boolean distinct) {
this.distinct = distinct;
return this;
}
/** Specifies whether results should be read from the cache (or written to the cache if the results have not yet been cached)
*
* @return this EntityQuery object, to enable chaining
*/
public EntityQuery cache() {
this.useCache = true;
return this;
}
/** Specifies whether results should be read from the cache (or written to the cache if the results have not yet been cached)
*
* @param useCache - boolean to indicate if the cache should be used or not
* @return this EntityQuery object, to enable chaining
*/
public EntityQuery cache(boolean useCache) {
this.useCache = useCache;
return this;
}
/** Specifies whether the query should return only values that are currently active using from/thruDate fields.
*
* @return this EntityQuery object, to enable chaining
*/
public EntityQuery filterByDate() {
this.filterByDate = true;
this.filterByDateMoment = null;
this.filterByFieldNames = null;
return this;
}
/** Specifies whether the query should return only values that are active during the specified moment using from/thruDate fields.
*
* @param moment - Timestamp representing the moment in time that the values should be active during
* @return this EntityQuery object, to enable chaining
*/
public EntityQuery filterByDate(Timestamp moment) {
if (moment != null) {
this.filterByDate = true;
this.filterByDateMoment = moment;
this.filterByFieldNames = null;
} else {
// Maintain existing behavior exhibited by EntityUtil.filterByDate(moment) when moment is null and perform no date filtering
this.filterByDate = false;
this.filterByDateMoment = null;
this.filterByFieldNames = null;
}
return this;
}
/** Specifies whether the query should return only values that are active during the specified moment using from/thruDate fields.
*
* @param moment - Date representing the moment in time that the values should be active during
* @return this EntityQuery object, to enable chaining
*/
public EntityQuery filterByDate(Date moment) {
this.filterByDate(new java.sql.Timestamp(moment.getTime()));
return this;
}
/** Specifies whether the query should return only values that are currently active using the specified from/thru field name pairs.
*
* @param fromThruFieldName - String pairs representing the from/thru date field names e.g. "fromDate", "thruDate", "contactFromDate", "contactThruDate"
* @return this EntityQuery object, to enable chaining
*/
public EntityQuery filterByDate(String... filterByFieldName) {
return this.filterByDate(null, filterByFieldName);
}
/** Specifies whether the query should return only values that are active during the specified moment using the specified from/thru field name pairs.
*
* @param moment - Timestamp representing the moment in time that the values should be active during
* @param fromThruFieldName - String pairs representing the from/thru date field names e.g. "fromDate", "thruDate", "contactFromDate", "contactThruDate"
* @return this EntityQuery object, to enable chaining
*/
public EntityQuery filterByDate(Timestamp moment, String... filterByFieldName) {
this.filterByDate = true;
this.filterByDateMoment = moment;
if (filterByFieldName.length % 2 != 0) {
throw new IllegalArgumentException("You must pass an even sized array to this method, each pair should represent a from date field name and a thru date field name");
}
this.filterByFieldNames = Arrays.asList(filterByFieldName);
return this;
}
/** Executes the EntityQuery and returns a list of results
*
* @return Returns a List of GenericValues representing the results of the query
*/
public List<GenericValue> queryList() throws GenericEntityException {
return query(null);
}
/** Executes the EntityQuery and returns an EntityListIterator representing the result of the query.
*
* NOTE: THAT THIS MUST BE CLOSED (preferably in a finally block) WHEN YOU
* ARE DONE WITH IT, AND DON'T LEAVE IT OPEN TOO LONG BEACUSE IT
* WILL MAINTAIN A DATABASE CONNECTION.
*
* @return Returns an EntityListIterator representing the result of the query
*/
public EntityListIterator queryIterator() throws GenericEntityException {
if (useCache) {
Debug.logWarning("Call to iterator() with cache, ignoring cache", module);
}
if (dynamicViewEntity == null) {
return delegator.find(entityName, makeWhereCondition(false), havingEntityCondition, fieldsToSelect, orderBy, makeEntityFindOptions());
} else {
return delegator.findListIteratorByCondition(dynamicViewEntity, makeWhereCondition(false), havingEntityCondition, fieldsToSelect, orderBy, makeEntityFindOptions());
}
}
/** Executes the EntityQuery and returns the first result
*
* @return GenericValue representing the first result record from the query
*/
public GenericValue queryFirst() throws GenericEntityException {
EntityFindOptions efo = makeEntityFindOptions();
// Only limit results when the query isn't filtering by date in memory against a cached result
if (!this.useCache && !this.filterByDate) {
efo.setMaxRows(1);
}
GenericValue result = EntityUtil.getFirst(query(efo));
return result;
}
/** Executes the EntityQuery and a single result record
*
* @return GenericValue representing the only result record from the query
*/
public GenericValue queryOne() throws GenericEntityException {
GenericValue result = EntityUtil.getOnly(queryList());
return result;
}
/** Executes the EntityQuery and returns the result count
*
* If the query generates more than a single result then an exception is thrown
*
* @return GenericValue representing the only result record from the query
*/
public long queryCount() throws GenericEntityException {
if (dynamicViewEntity != null) {
EntityListIterator iterator = null;
try {
iterator = queryIterator();
return iterator.getResultsSizeAfterPartialList();
} finally {
if (iterator != null) {
iterator.close();
}
}
}
return delegator.findCountByCondition(entityName, makeWhereCondition(false), havingEntityCondition, makeEntityFindOptions());
}
private List<GenericValue> query(EntityFindOptions efo) throws GenericEntityException {
EntityFindOptions findOptions = null;
if (efo == null) {
findOptions = makeEntityFindOptions();
} else {
findOptions = efo;
}
List<GenericValue> result = null;
if (dynamicViewEntity == null && this.havingEntityCondition == null) {
result = delegator.findList(entityName, makeWhereCondition(useCache), fieldsToSelect, orderBy, findOptions, useCache);
} else {
EntityListIterator it = queryIterator();
result = it.getCompleteList();
it.close();
}
if (filterByDate && useCache) {
return EntityUtil.filterByCondition(result, this.makeDateCondition());
}
return result;
}
private EntityFindOptions makeEntityFindOptions() {
EntityFindOptions findOptions = new EntityFindOptions();
if (resultSetType != null) {
findOptions.setResultSetType(resultSetType);
}
if (fetchSize != null) {
findOptions.setFetchSize(fetchSize);
}
if (maxRows != null) {
findOptions.setMaxRows(maxRows);
}
if (distinct != null) {
findOptions.setDistinct(distinct);
}
return findOptions;
}
private EntityCondition makeWhereCondition(boolean usingCache) {
// we don't use the useCache field here because not all queries will actually use the cache, e.g. findCountByCondition never uses the cache
if (filterByDate && !usingCache) {
if (whereEntityCondition != null) {
return EntityCondition.makeCondition(whereEntityCondition, this.makeDateCondition());
} else {
return this.makeDateCondition();
}
}
return whereEntityCondition;
}
private EntityCondition makeDateCondition() {
List<EntityCondition> conditions = new ArrayList<EntityCondition>();
if (UtilValidate.isEmpty(this.filterByFieldNames)) {
this.filterByDate(filterByDateMoment, "fromDate", "thruDate");
}
for (int i = 0; i < this.filterByFieldNames.size();) {
String fromDateFieldName = this.filterByFieldNames.get(i++);
String thruDateFieldName = this.filterByFieldNames.get(i++);
if (filterByDateMoment == null) {
conditions.add(EntityUtil.getFilterByDateExpr(fromDateFieldName, thruDateFieldName));
} else {
conditions.add(EntityUtil.getFilterByDateExpr(this.filterByDateMoment, fromDateFieldName, thruDateFieldName));
}
}
return EntityCondition.makeCondition(conditions);
}
public <T> List<T> getFieldList(final String fieldName) throws GenericEntityException {select(fieldName);
EntityListIterator genericValueEli = null;
try {
genericValueEli = queryIterator();
if (Boolean.TRUE.equals(this.distinct)) {
Set<T> distinctSet = new HashSet<T>();
GenericValue value = null;
while ((value = genericValueEli.next()) != null) {
T fieldValue = UtilGenerics.<T>cast(value.get(fieldName));
if (fieldValue != null) {
distinctSet.add(fieldValue);
}
}
return new ArrayList<T>(distinctSet);
}
else {
List<T> fieldList = new LinkedList<T>();
GenericValue value = null;
while ((value = genericValueEli.next()) != null) {
T fieldValue = UtilGenerics.<T>cast(value.get(fieldName));
if (fieldValue != null) {
fieldList.add(fieldValue);
}
}
return fieldList;
}
}
finally {
if (genericValueEli != null) {
genericValueEli.close();
}
}
}
/**
* @param viewIndex
* @param viewSize
* @return PagedList object with a subset of data items
* @throws GenericEntityException
* @see EntityUtil#getPagedList
*/
public PagedList<GenericValue> queryPagedList(final int viewIndex, final int viewSize) throws GenericEntityException {
EntityListIterator genericValueEli = null;
try {
genericValueEli = queryIterator();
return EntityUtil.getPagedList(genericValueEli, viewIndex, viewSize);
}
finally {
if (genericValueEli != null) {
genericValueEli.close();
}
}
}
}