/******************************************************************************* * 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 */ 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 */ 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 fields - 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 fields - 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 andConditions - 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 fields - 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 filterByFieldName - 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 filterByFieldName - 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) { 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 (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(); } } } }