ilscipio/scipio-erp

View on GitHub
framework/entity/src/org/ofbiz/entity/util/EntityQuery.java

Summary

Maintainability
C
1 day
Test Coverage
/*******************************************************************************
 * 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.ofbiz.entity.util;

import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.ofbiz.base.util.Debug;
import org.ofbiz.base.util.UtilGenerics;
import org.ofbiz.base.util.UtilMisc;
import org.ofbiz.base.util.UtilValidate;
import org.ofbiz.base.util.collections.PagedList;
import org.ofbiz.entity.Delegator;
import org.ofbiz.entity.EntityFieldNotFoundException;
import org.ofbiz.entity.GenericEntityException;
import org.ofbiz.entity.GenericPK;
import org.ofbiz.entity.GenericValue;
import org.ofbiz.entity.condition.EntityCondition;
import org.ofbiz.entity.condition.EntityJoinOperator;
import org.ofbiz.entity.model.DynamicViewEntity;
import org.ofbiz.entity.model.ModelEntity;

/**
 * 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 {

    private static final Debug.OfbizLogger module = Debug.getOfbizLogger(java.lang.invoke.MethodHandles.lookup().lookupClass());

    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;
    private boolean searchPkOnly = false;
    private Map<String, Object> fieldMap = 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.fieldMap = 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) {
        // SCIPIO: This needlessly complicates internals and is done later
        //this.whereEntityCondition = EntityCondition.makeCondition(UtilMisc.toMap(fields));
        this.fieldMap = 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 a list of EntityCondition objects to be combined together with given operator 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.
     * <p>
     * SCIPIO: New, added 2018-05-17.
     * @param conditions - A list of EntityCondition objects to be combined together as the WHERE clause for the query
     * @param operator - The join operator
     * @return this EntityQuery object, to enable chaining
     */
    public <T extends EntityCondition> EntityQuery where(List<T> conditions, EntityJoinOperator operator) {
        this.whereEntityCondition = EntityCondition.makeCondition(conditions, operator);
        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.
     * SCIPIO: now accepts null, which clears it (default).
     *
     * @param fetchSize - The fetch size for this query
     * @return this EntityQuery object, to enable chaining
     */
    public EntityQuery fetchSize(Integer fetchSize) {
        this.fetchSize = fetchSize;
        return this;
    }

    /** Specifies the max number of rows to return, 0 means all rows.
     * SCIPIO: now accepts null, which clears it (default).
     *
     * @param maxRows - the max number of rows to return
     * @return this EntityQuery object, to enable chaining
     */
    public EntityQuery maxRows(Integer 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.
     * SCIPIO: now accepts null, which clears it (default).
     *
     * @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)
     * SCIPIO: Now supports null which is same as false.
     *
     * @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 != null) ? useCache : false;
        return this;
    }

    /** Specifies whether the query should return only values that are currently active using from/thruDate fields.
     * <p>
     * SCIPIO: 2018-09-29: This method no longer throws exception if the date field names
     * are invalid for the entity; instead a detailed error is logged. This is an extremely
     * easy error to make, and otherwise can cause needless critical failures on small errors
     * during upgrades.
     *
     * @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 currently active using from/thruDate fields (SCIPIO).
     * <p>
     * SCIPIO: 2018-09-29: This method no longer throws exception if the date field names
     * are invalid for the entity; instead a detailed error is logged. This is an extremely
     * easy error to make, and otherwise can cause needless critical failures on small errors
     * during upgrades.
     *
     * @return this EntityQuery object, to enable chaining
     */
    public EntityQuery filterByDate(QueryDateFilter filter) {
        if (filter != null) {
            this.filterByDate = filter.isEnabled();
            this.filterByDateMoment = filter.getMoment();
            this.filterByFieldNames = filter.getFieldNames();
        } else {
            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.
     * <p>
     * SCIPIO: 2018-09-29: This method no longer throws exception if the date field names
     * are invalid for the entity; instead a detailed error is logged. This is an extremely
     * easy error to make, and otherwise can cause needless critical failures on small errors
     * during upgrades.
     *
     * @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.
     * <p>
     * SCIPIO: 2018-09-29: This method no longer throws exception if the date field names
     * are invalid for the entity; instead a detailed error is logged. This is an extremely
     * easy error to make, and otherwise can cause needless critical failures on small errors
     * during upgrades.
     *
     * @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.
     * <p>
     * SCIPIO: 2018-09-29: This method no longer throws exception if the date field names
     * are invalid for the entity; instead a detailed error is logged. This is an extremely
     * easy error to make, and otherwise can cause needless critical failures on small errors
     * during upgrades.
     *
     * @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.
     * <p>
     * SCIPIO: 2018-09-29: This method no longer throws exception if the date field names
     * are invalid for the entity; instead a detailed error is logged. This is an extremely
     * easy error to make, and otherwise can cause needless critical failures on small errors
     * during upgrades.
     *
     * @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;
    }

    /** SCIPIO: Specifies whether the query should return only values that are active during the specified moment using from/thruDate fields,
     * using the "now" timestamp (current time), with explicit boolean toggle.
     * Added 2018-10-19.
     * <p>
     * SCIPIO: 2018-09-29: This method no longer throws exception if the date field names
     * are invalid for the entity; instead a detailed error is logged. This is an extremely
     * easy error to make, and otherwise can cause needless critical failures on small errors
     * during upgrades.
     *
     * @param enable - Enable or disable the filter-by-date filter.
     * @return this EntityQuery object, to enable chaining
     */
    public EntityQuery filterByDate(boolean enable) {
        this.filterByDate = enable;
        this.filterByDateMoment = null;
        this.filterByFieldNames = null;
        return this;
    }

    /** SCIPIO: Specifies whether the query should return only values that are active during the specified moment using from/thruDate fields,
     * using the specified moment if non-null OR, if null, using the "now" timestamp (current time),
     * with explicit boolean toggle.
     * Added 2017-11-27.
     * <p>
     * SCIPIO: 2018-09-29: This method no longer throws exception if the date field names
     * are invalid for the entity; instead a detailed error is logged. This is an extremely
     * easy error to make, and otherwise can cause needless critical failures on small errors
     * during upgrades.
     *
     * @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(boolean enable, Timestamp moment) {
        this.filterByDate = enable;
        this.filterByDateMoment = (enable) ? moment : null;
        this.filterByFieldNames = null;
        return this;
    }

    /** SCIPIO: Specifies whether the query should return only values that are active during the specified moment using from/thruDate fields,
     * using the specified moment if non-null OR, if null, using the "now" timestamp (current time),
     * with explicit boolean toggle.
     * Added 2017-11-27.
     * <p>
     * SCIPIO: 2018-09-29: This method no longer throws exception if the date field names
     * are invalid for the entity; instead a detailed error is logged. This is an extremely
     * easy error to make, and otherwise can cause needless critical failures on small errors
     * during upgrades.
     *
     * @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(boolean enable, Date moment) {
        return this.filterByDate(enable, new java.sql.Timestamp(moment.getTime()));
    }

    /** SCIPIO: Specifies whether the query should return only values that are currently active using the specified from/thru field name pairs,
     * using the specified moment if non-null OR, if null, using the "now" timestamp (current time),
     * with explicit boolean toggle.
     * Added 2017-11-27.
     * <p>
     * SCIPIO: 2018-09-29: This method no longer throws exception if the date field names
     * are invalid for the entity; instead a detailed error is logged. This is an extremely
     * easy error to make, and otherwise can cause needless critical failures on small errors
     * during upgrades.
     *
     * @param enable Enable or disable the filter-by-date filter.
     * @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(boolean enable, String... filterByFieldName) {
        return this.filterByDate(enable, (Timestamp) null, filterByFieldName);
    }

    /** SCIPIO: Specifies whether the query should return only values that are active during the specified moment using the specified from/thru field name pairs,
     * using the specified moment if non-null OR, if null, using the "now" timestamp (current time),
     * with explicit boolean toggle.
     * Added 2017-11-27.
     * <p>
     * SCIPIO: 2018-09-29: This method no longer throws exception if the date field names
     * are invalid for the entity; instead a detailed error is logged. This is an extremely
     * easy error to make, and otherwise can cause needless critical failures on small errors
     * during upgrades.
     *
     * @param enable Enable or disable the filter-by-date filter.
     * @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(boolean enable, Timestamp moment, String... filterByFieldName) {
        if (enable) {
            // SCIPIO: NOTE: this stock method interprets null as "now".
            this.filterByDate(moment, filterByFieldName);
        } else {
            this.filterByDate = false;
            this.filterByDateMoment = null;
            this.filterByFieldNames = null;
        }
        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" + toLogAppend(), module); // SCIPIO: Improved logging
        }
        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
     * <p>SCIPIO: NOTE: This method has been modified to primarily use {@link Delegator#findOne(String, Map, boolean)}
     * for the majority of lookups which lookup using {@link #where(Object...)} and {@link #where(Map)}
     * (but currently not {@link #where(EntityCondition)}). For non-dynamic entities: orderBy, filterByDate and most of the
     * find options are almost meaningless and should usually not be set, but if set may trigger legacy use of
     * {@link #queryList()}. If you need the original behavior, {@link #queryOneFromList()} is now available.</p>
     * TODO: Apply findOne to more cases
     *
     * @return GenericValue representing the only result record from the query
     */
    public GenericValue queryOne() throws GenericEntityException {
        this.searchPkOnly = true;
        // SCIPIO: This behavior caused the PK entity cache to be unused, which is unfortunate because
        // it's simpler and more efficient for PK queries, so instead, use the Delegator PK lookup method wherever possible
        //GenericValue result =  EntityUtil.getOnly(queryList());
        GenericValue result;
        if (dynamicViewEntity == null && !filterByDate && orderBy == null && !hasEntityFindOptions() &&
                whereEntityCondition == null && havingEntityCondition == null && fieldMap != null) {
            //if (Debug.verboseOn()) {
            //    Debug.logVerbose("queryOne: using findOne() implementation" + toLogAppend(), module);
            //}
            // TODO: REMOVE: in the future this PK check can be removed for performance reasons,
            //  is here due to ofbiz flaw in makeWhereCondition we don't want
            GenericPK pk = GenericPK.create(delegator.getModelEntity(entityName));
            pk.setPKFields(fieldMap);
            if (pk.size() < fieldMap.size()) {
                String msg = "queryOne(): Passed primary key is not a valid primary key for entity [" + entityName +
                        "], truncating: " + fieldMap.keySet();
                Debug.logError(new IllegalArgumentException(msg), msg, module);
                fieldMap = pk;
            }
            result = delegator.findOne(entityName, fieldMap, useCache);
            if (result != null && fieldsToSelect != null) {
                result = result.select(fieldsToSelect);
            }
        } else {
            if (Debug.verboseOn()) {
                Debug.logVerbose("queryOne: using queryList() implementation" + toLogAppend(), module);
            }
            result = EntityUtil.getOnly(queryList());
        }
        return result;
    }

    /** Executes the EntityQuery and a single result record, using {@link #queryList()} (SCIPIO).
     * <p>SCIPIO: NOTE: This is simply the old implementation of {@link #queryOne()} in case needed in specific cases.</p>
     *
     * @return GenericValue representing the only result record from the query
     */
    public GenericValue queryOneFromList() throws GenericEntityException {
        this.searchPkOnly = true;
        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) {
            try (EntityListIterator iterator = queryIterator()) {
                return iterator.getResultsSizeAfterPartialList();
            }
        }
        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 {
            try (EntityListIterator it = queryIterator()) {
                result = it.getCompleteList();
            }
        }
        if (filterByDate && useCache) {
            try {
                return EntityUtil.filterByCondition(result, this.makeDateCondition());
            } catch(EntityFieldNotFoundException e) { // SCIPIO
                //Debug.logError(e, "Query error: " + e.getMessage() + toLogAppend(), module); // already logged
            }
        }
        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 boolean hasEntityFindOptions() { // SCIPIO
        return (resultSetType != EntityFindOptions.TYPE_FORWARD_ONLY) || (fetchSize != null) || (maxRows != null) || (distinct != null);
    }

    private EntityCondition makeWhereCondition(boolean usingCache) {
        if (whereEntityCondition == null && fieldMap != null) {
            if (this.searchPkOnly) {
                //Resolve if the map contains a sub map parameters, use a containsKeys to avoid error when a GenericValue is given as map
                @SuppressWarnings("unchecked")
                Map<String, Object> parameters = fieldMap.containsKey("parameters") ? (Map<String, Object>) fieldMap.get("parameters") : null;
                // SCIPIO: The parameters map behavior may need to be deprecated due to security concerns, so for now warn:
                if (parameters != null) {
                    Debug.logWarning("EntityQuery.makeWhereCondition for queryOne(): using implicit parameters map for search PK" +
                            " (did you pass context to where()?); deprecated for security" + toLogAppend(), module);
                }
                GenericPK pk = GenericPK.create(delegator.getModelEntity(entityName));
                pk.setPKFields(parameters);
                pk.setPKFields(fieldMap);
                this.whereEntityCondition = EntityCondition.makeCondition(pk.getPrimaryKey());
            } else {
                this.whereEntityCondition = EntityCondition.makeCondition(fieldMap);
            }
        }
        // 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) {
            try {
                if (whereEntityCondition != null) {
                    return EntityCondition.makeCondition(whereEntityCondition, this.makeDateCondition());
                } else {
                    return this.makeDateCondition();
                }
            } catch(EntityFieldNotFoundException e) { // SCIPIO
                //Debug.logError(e, "Query error: " + e.getMessage() + "; skipping date filter" + toLogAppend(), module); // already logged
            }
        }
        return whereEntityCondition;
    }

    private EntityCondition makeDateCondition() {
        List<EntityCondition> conditions = new ArrayList<>();
        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++);

            try { // SCIPIO
                checkEntityDateFields(delegator, entityName, fromDateFieldName, thruDateFieldName);
            } catch(EntityFieldNotFoundException e) {
                Debug.logError(e, "Query error: " + e.getMessage() + "; skipping date filter" + toLogAppend(), module); // SCIPIO: Improved logging
                continue;
            }

            if (filterByDateMoment == null) {
                conditions.add(EntityUtil.getFilterByDateExpr(fromDateFieldName, thruDateFieldName));
            } else {
                conditions.add(EntityUtil.getFilterByDateExpr(this.filterByDateMoment, fromDateFieldName, thruDateFieldName));
            }
        }

        if (conditions.isEmpty()) { // SCIPIO
            throw new EntityFieldNotFoundException("No date filters could be produced for entity '"
                    + entityName + " using field names: " + filterByFieldNames);
        }

        return EntityCondition.makeCondition(conditions);
    }

    /**
     * SCIPIO: 2018-09-29: When filterByDate is used on an entity without fromDate/thruDate, we will
     * log as an error instead of throwing exception and crashing the system.
     * This is because due to entitymodel changes it's extremely common to accidentally add
     * a .filterByDate() call, so at least this way this error will not cause significant damage.
     * Since in 90% of cases the bugfix is simply to remove the call, this is a fairly safe way to
     * address the issue.
     */
    private static void checkEntityDateFields(Delegator delegator, String entityName, String fromDateFieldName, String thruDateFieldName) throws EntityFieldNotFoundException {
        ModelEntity entityModel = delegator.getModelEntity(entityName);
        if (entityModel != null) { // If null, let regular call crash itself
            if (!entityModel.isField(fromDateFieldName)) {
                throw new EntityFieldNotFoundException("\"" + fromDateFieldName + "\" is not a field of "
                        + entityName);
            } else if (!entityModel.isField(thruDateFieldName)) {
                throw new EntityFieldNotFoundException("\"" + thruDateFieldName + "\" is not a field of "
                        + entityName);
            }
        }
    }

    public <T> List<T> getFieldList(final String fieldName) throws GenericEntityException {
        select(fieldName);
        try (EntityListIterator genericValueEli = queryIterator()) {
            if (Boolean.TRUE.equals(this.distinct)) {
                Set<T> distinctSet = new LinkedHashSet<T>(); // SCIPIO: fixed to LinkedHashSet from HashSet to preserve result order
                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 ArrayList<T>(); // SCIPIO: switched to ArrayList
                GenericValue value = null;
                while ((value = genericValueEli.next()) != null) {
                    T fieldValue = UtilGenerics.<T>cast(value.get(fieldName));
                    if (fieldValue != null) {
                        fieldList.add(fieldValue);
                    }
                }
                return fieldList;
            }
        }
    }

    public <T> Set<T> getFieldSet(final String fieldName) throws GenericEntityException { // SCIPIO
        select(fieldName);
        try (EntityListIterator genericValueEli = queryIterator()) {
            if (Boolean.TRUE.equals(this.distinct)) {
                Set<T> distinctSet = new LinkedHashSet<T>(); // SCIPIO: fixed to LinkedHashSet from HashSet to preserve result order
                GenericValue value = null;
                while ((value = genericValueEli.next()) != null) {
                    T fieldValue = UtilGenerics.<T>cast(value.get(fieldName));
                    if (fieldValue != null) {
                        distinctSet.add(fieldValue);
                    }
                }
                return distinctSet;
            } else {
                Set<T> fieldList = new LinkedHashSet<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;
            }
        }
    }

    public <T, C extends Collection<T>> C getFieldCollection(final String fieldName, final C collection) throws GenericEntityException { // SCIPIO
        select(fieldName);
        try (EntityListIterator genericValueEli = queryIterator()) {
            if (Boolean.TRUE.equals(this.distinct)) {
                Set<T> distinctSet = new LinkedHashSet<T>();
                GenericValue value = null;
                while ((value = genericValueEli.next()) != null) {
                    T fieldValue = UtilGenerics.<T>cast(value.get(fieldName));
                    if (fieldValue != null) {
                        distinctSet.add(fieldValue);
                    }
                }
                collection.addAll(distinctSet);
            } else {
                GenericValue value = null;
                while ((value = genericValueEli.next()) != null) {
                    T fieldValue = UtilGenerics.<T>cast(value.get(fieldName));
                    if (fieldValue != null) {
                        collection.add(fieldValue);
                    }
                }
            }
        }
        return collection;
    }

    /**
     * Query paged list.
     * @param viewIndex
     * @param viewSize
     * @return PagedList object with a subset of data items
     * @throws GenericEntityException
     * @see EntityUtil#getPagedList
     */
    public PagedList<GenericValue> queryPagedList(int viewIndex, int viewSize) throws GenericEntityException {
        try (EntityListIterator genericValueEli = queryIterator()) {
            return EntityUtil.getPagedList(genericValueEli, viewIndex, viewSize);
        }
    }

    /** SCIPIO: Executes the EntityQuery and returns a list of results; returns null if GenericEntityException.
     * NOTE: Unchecked exceptions representing programming errors may still be thrown.
     *
     * @return Returns a List of GenericValues representing the results of the query
     */
    public List<GenericValue> queryListSafe() {
        try {
            return queryList();
        } catch (GenericEntityException e) {
            Debug.logError(e, "Error in queryList: " + e.getMessage() + toLogAppend(), module);
            return null;
        }
    }

    /** SCIPIO: Executes the EntityQuery and returns an EntityListIterator representing the result of the query; returns null if GenericEntityException.
     * NOTE: Unchecked exceptions representing programming errors may still be thrown.
     *
     * 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 queryIteratorSafe() {
        try {
            return queryIterator();
        } catch (GenericEntityException e) {
            Debug.logError(e, "Error in queryIterator: " + e.getMessage() + toLogAppend(), module);
            return null;
        }
    }

    /** SCIPIO: Executes the EntityQuery and returns the first result; returns null if GenericEntityException.
     * NOTE: Unchecked exceptions representing programming errors may still be thrown.
     *
     * @return GenericValue representing the first result record from the query
     */
    public GenericValue queryFirstSafe() {
        try {
            return queryFirst();
        } catch (GenericEntityException e) {
            Debug.logError(e, "Error in queryFirst: " + e.getMessage() + toLogAppend(), module);
            return null;
        }
    }

    /** SCIPIO: Executes the EntityQuery and a single result record; returns null if GenericEntityException.
     * NOTE: Unchecked exceptions representing programming errors may still be thrown.
     *
     * @return GenericValue representing the only result record from the query
     */
    public GenericValue queryOneSafe() {
        try {
            return queryOne();
        } catch (GenericEntityException e) {
            Debug.logError(e, "Error in queryOne: " + e.getMessage() + toLogAppend(), module);
            return null;
        }
    }

    /** SCIPIO: Executes the EntityQuery and a single result record; returns null if GenericEntityException.
     * NOTE: Unchecked exceptions representing programming errors may still be thrown.
     *
     * @return GenericValue representing the only result record from the query
     */
    public GenericValue queryOneFromListSafe() {
        try {
            return queryOneFromList();
        } catch (GenericEntityException e) {
            Debug.logError(e, "Error in queryOneFromList: " + e.getMessage() + toLogAppend(), module);
            return null;
        }
    }

    /** SCIPIO: Executes the EntityQuery and returns the result count; returns null if GenericEntityException.
     * NOTE: Unchecked exceptions representing programming errors may still be thrown.
     *
     * If the query generates more than a single result then zero is returned.
     *
     * @return GenericValue representing the only result record from the query
     */
    public long queryCountSafe() {
        try {
            return queryCount();
        } catch (GenericEntityException e) {
            Debug.logError(e, "Error in queryCount: " + e.getMessage() + toLogAppend(), module);
            return 0;
        }
    }

    public <T> List<T> getFieldListSafe(String fieldName) { // SCIPIO
        try {
            return getFieldList(fieldName);
        } catch (GenericEntityException e) {
            Debug.logError(e, "Error in getFieldList(): " + e.getMessage() + toLogAppend(), module);
            return null;
        }
    }

    public <T> Set<T> getFieldSetSafe(String fieldName) { // SCIPIO
        try {
            return getFieldSet(fieldName);
        } catch (GenericEntityException e) {
            Debug.logError(e, "Error in getFieldList(): " + e.getMessage() + toLogAppend(), module);
            return null;
        }
    }

    public <T, C extends Collection<T>> C getFieldCollectionSafe(String fieldName, C collection) { // SCIPIO
        try {
            return getFieldCollection(fieldName, collection);
        } catch (GenericEntityException e) {
            Debug.logError(e, "Error in getFieldList(): " + e.getMessage() + toLogAppend(), module);
            return null;
        }
    }

    /**
     * SCIPIO: Query paged list; returns null if GenericEntityException.
     * NOTE: Unchecked exceptions representing programming errors may still be thrown.
     * @param viewIndex
     * @param viewSize
     * @return PagedList object with a subset of data items
     * @throws GenericEntityException
     * @see EntityUtil#getPagedList
     */
    public PagedList<GenericValue> queryPagedListSafe(int viewIndex, int viewSize) {
        try {
            return queryPagedList(viewIndex, viewSize);
        } catch (GenericEntityException e) {
            Debug.logError(e, "Error in queryPagedList(): " + e.getMessage() + toLogAppend(), module);
            return null;
        }
    }

    @Override
    public String toString() { // SCIPIO: Debugging help
        return "{entityName='" + entityName + '\'' +
                ", dynamicViewEntity=" + dynamicViewEntity +
                ", useCache=" + useCache +
                ", whereEntityCondition=" + whereEntityCondition +
                ", fieldsToSelect=" + fieldsToSelect +
                ", orderBy=" + orderBy +
                ", resultSetType=" + resultSetType +
                ", fetchSize=" + fetchSize +
                ", maxRows=" + maxRows +
                ", distinct=" + distinct +
                ", havingEntityCondition=" + havingEntityCondition +
                ", filterByDate=" + filterByDate +
                ", filterByDateMoment=" + filterByDateMoment +
                ", filterByFieldNames=" + filterByFieldNames +
                //", searchPkOnly=" + searchPkOnly +
                ", fieldMap=" + fieldMap +
                '}';
    }

    protected String toLogAppend() { // SCIPIO: Debugging help
        return " - " + this;
    }

}