/**
 * Copyright 2005-2013 The Kuali Foundation
 *
 * Licensed under the Educational Community 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.opensource.org/licenses/ecl2.php
 *
 * 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.kuali.rice.core.framework.persistence.jpa.criteria;

import org.kuali.rice.core.api.util.Truth;
import org.kuali.rice.core.api.util.type.TypeUtils;

import javax.persistence.Query;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;

/**
 * A criteria builder for JPQL Query objects.
 * 
 * <p>All entities in queries generated by this class will be given an alias, and an initial entity (with an initial alias)
 * is mandatory. If no alias is given or if a constructor without an alias parameter is used, a new alias name will be
 * auto-generated.
 * 
 * <p>The interpretation of the initial entity depends on what type of query is intended. If a SELECT query is desired,
 * the initial entity will be the first to appear in the FROM clause. If a DELETE query is desired, the initial
 * entity will be the one that appears in the DELETE FROM clause. If an UPDATE query is desired, the initial entity
 * will be the one that appears in the UPDATE clause. 
 * 
 * <p>Most of the methods of this class rely on String expressions representing entity properties, or functions
 * acting on constants or entity properties. If the expression contains functions or if
 * you wish to reference the property of an entity other than the first aliased entity of the query, then the
 * String expression must include the alias of the entity in the format __JPA_ALIAS[[index]]__ or the format
 * __JPA_ALIAS[['alias']]__ , where "index" is a numeric index pointing to the Nth alias of the current query in the
 * order that the aliases were added to the query (starting at zero), and where "'alias'" is the String name of the
 * entity alias enclosed in single quotes.
 * 
 * <p>If the String expression as defined above does not contain custom JPA aliases, then the initial alias of the current
 * Criteria (and a '.' to separate the alias from the expression) will be prepended to the expression automatically.
 * 
 * <p>If the current Criteria is intended for use as a sub-query, then aliases of the parent Criteria can be referred to
 * by name (so long as they do not conflict with the alias names in the sub-Criteria) or by specifying the bitwise NOT of
 * the alias index in the parent Criteria (-1 for 0, -2 for 1, etc.). When including a sub-query in a parent query, the
 * parent Criteria will handle the updating of the alias indexes and will auto-generate new aliases as needed if any of
 * the sub-query's defined aliases are duplicates of those in the parent query.
 * 
 * <p>For Criteria objects intended to be ANDed or ORed with an existing Criteria, it is assumed that any named or indexed
 * aliases refer to exactly the same aliases as in the existing Criteria. Any auto-prepended aliases as described above
 * will always be referred to by index, so it is safe for them to be included in the Criteria to be ANDed/ORed with the
 * existing one. Both of these details also apply when adding new Criteria to the HAVING clause of an existing Criteria, or
 * when inserting the negation of another Criteria's conditions into an existing Criteria as a "NOT (...)" expression.
 * 
 * <p>Also, during query construction, the Criteria API will automatically include substrings in the format
 * __JPA_PARAM[['parameter']]__ when adding input parameters, where 'parameter' is the auto-generated name of the
 * parameter. Care should be taken to ensure that __JPA_ALIAS[[...]]__ and __JPA_PARAM[[...]]__ expressions are not
 * being added to queries that wish to interpret such String patterns literally.
 * 
 * <p>Note that Criteria instances are not thread-safe, so external synchronization is necessary if operating on them
 * from multiple threads.
 * 
 * <p>TODO: Verify if any other features need to be added to this API.
 * 
 * @author Kuali Rice Team (rice.collab@kuali.org)
 */
@SuppressWarnings("unchecked")
public class Criteria {

	private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(Criteria.class);
	private static final Pattern APOS_PAT = Pattern.compile("'");
	private static final String[] LOOKUP_WILDCARDS = {"*", "?"};
	private static final String[] ESCAPED_LOOKUP_WILDCARDS = {"\\*", "\\?"};
	private static final char[] JPQL_WILDCARDS = {'%', '_'};
	
	/** The String representing the beginning of a by-name or by-index reference to an entity alias. */
	public static final String JPA_ALIAS_PREFIX = "__JPA_ALIAS[[";
	/** The String representing the beginning of a by-name reference to an input parameter. */
	public static final String JPA_PARAM_PREFIX = "__JPA_PARAM[[";
	/** The String representing the termination of by-name/index references to aliases or input parameters. */
	public static final String JPA_ALIAS_SUFFIX = "]]__";

	private static final String JPA_INITIAL_ALIAS = JPA_ALIAS_PREFIX + "0" + JPA_ALIAS_SUFFIX;
	private static final String JPA_PARENT_INITIAL_ALIAS = JPA_ALIAS_PREFIX + "-1" + JPA_ALIAS_SUFFIX;

	private static final String JPA_PARAM_PREFIX_WITH_COLON = ":" + JPA_PARAM_PREFIX;
	private static final int ALIAS_PREFIX_LEN = JPA_ALIAS_PREFIX.length();
	private static final int PARAM_PREFIX_LEN = JPA_PARAM_PREFIX.length();
	private static final int ALIAS_SUFFIX_LEN = JPA_ALIAS_SUFFIX.length();
	
	private Integer searchLimit;
	
	private int generatedAliasCount;
	
	private int generatedBindParamCount;
	
	private boolean distinct = false;
	
	private final List<String> entityAliases = new ArrayList<String>();
	
	private final List<String> indexedAliasPlaceholders = new ArrayList<String>();
	
	private final List<String> namedAliasPlaceholders = new ArrayList<String>();
	
	private final Map<String,Integer> aliasIndexes = new HashMap<String,Integer>();
	
	private final StringBuilder selectClause = new StringBuilder();
	
	private final StringBuilder fromClause = new StringBuilder();
	
	private final StringBuilder whereClause = new StringBuilder(50);
	
	private final StringBuilder groupByClause = new StringBuilder();
	
	private final StringBuilder havingClause = new StringBuilder();
	
	private final StringBuilder orderByClause = new StringBuilder();
	
	private final StringBuilder setClause = new StringBuilder();
	
	private final String initialEntityName;
	
	protected Map<String, Object> params = new LinkedHashMap<String, Object>();

	/**
	 * Constructs a new Criteria instance that includes the given initial entity. An alias will be
	 * auto-generated for this entity, and the entity's alias will be included in the SELECT clause if this
	 * Criteria is intended to be used as a SELECT query.
	 * 
	 * @param entityName The class name of the initial JPA entity.
	 */
	public Criteria(String entityName) {
		this(entityName, "a", true);
	}

	/**
	 * Constructs a new Criteria instance that includes the given initial entity. The given alias
	 * will be used for this entity if it is non-null, and the alias will be included in the SELECT clause
	 * if this Criteria is intended to be used as a SELECT query.
	 * 
	 * @param entityName The class name of the initial JPA entity.
	 * @param alias The alias to use for this entity; if it is null, a new alias will be auto-generated.
	 */
	public Criteria(String entityName, String alias) {
		this(entityName, alias, true);
	}
	
	/**
	 * Constructs a new Criteria instance that includes the given initial entity. An alias will be
	 * auto-generated for this entity, and the extra parameter indicates whether this alias should also be
	 * added to the query's SELECT clause (if this Criteria is intended to be used as a SELECT query).
	 * 
	 * @param entityName The class name of the initial JPA entity.
	 * @param includeEntityInSelect Indicates whether this entity's alias should be added to the SELECT clause.
	 */
	public Criteria(String entityName, boolean includeEntityInSelect) {
		this(entityName, "a", includeEntityInSelect);
	}
	
	/**
	 * Constructs a new Criteria instance that includes the given initial entity. The given alias
	 * will be used for this entity if it is non-null, and the extra parameter indicates whether the alias
	 * should be added to the SELECT clause (if this Criteria is intended to be used as a SELECT query).
	 * 
	 * @param entityName The class name of the initial JPA entity.
	 * @param alias The alias to use for this entity; if it is null, a new alias will be auto-generated.
	 * @param includeEntityAliasInSelect Indicates whether this entity's alias should be added to the SELECT clause.
	 */
	public Criteria(String entityName, String alias, boolean includeEntityAliasInSelect) {
		this.initialEntityName = entityName;
		from(entityName, alias, includeEntityAliasInSelect);
	}

	/*
	 * Adds the given alias to the Criteria's alias lists/maps. If the alias is null or it duplicates an
	 * existing one, a new alias will be auto-generated.
	 * 
	 * Returns the new name of the alias, or the passed-in name if non-null and no conflicts were found.
	 */
	private String addAlias(String alias) {
		if (alias == null) {
			alias = "a" + (generatedAliasCount++);
		}
		while (aliasIndexes.containsKey(alias)) {
			alias = "a" + (generatedAliasCount++);
		}
		entityAliases.add(alias);
		aliasIndexes.put(alias, entityAliases.size() - 1);
		indexedAliasPlaceholders.add(new StringBuilder(30).append(JPA_ALIAS_PREFIX).append(indexedAliasPlaceholders.size()).append(JPA_ALIAS_SUFFIX).toString());
		namedAliasPlaceholders.add(new StringBuilder(30).append(JPA_ALIAS_PREFIX).append('\'').append(alias).append('\'').append(JPA_ALIAS_SUFFIX).toString());
		return alias;
	}
	
	/**
	 * Adds the given expression to the SELECT clause. If no properly-referenced alias appears
	 * in the String expression, it will be assumed that the expression is accessing something
	 * on or through the initial entity. See the description of this class for more information
	 * on how String expressions are interpreted.
	 * 
	 * @param resultExpression The String expression to add to the SELECT clause.
	 */
	public void select(String resultExpression) {
		if (resultExpression.contains(JPA_ALIAS_PREFIX)) {
			selectClause.append((selectClause.length() > 0) ? ", " : "").append(resultExpression);
		} else {
			selectClause.append((selectClause.length() > 0) ? ", " : "").append(JPA_INITIAL_ALIAS).append('.').append(resultExpression);
		}
	}
	
	/**
	 * Adds a new JPA entity to the FROM clause. The entity will be given the provided alias if it
	 * is non-null and does not conflict with any existing alias names; otherwise, a new alias name
	 * will be auto-generated. An extra parameter is also included for indicating whether this
	 * Criteria should automatically include the entity's alias in the SELECT clause.
	 * 
	 * @param entityName The class name of the new entity.
	 * @param alias The alias to use for this entity.
	 * @param includeEntityAliasInSelect Indicates whether to include the entity's alias in the SELECT clause.
	 * @return The provided alias if it's non-null and does not conflict with any existing names, or an auto-generated alias name otherwise.
	 */
	public String from(String entityName, String alias, boolean includeEntityAliasInSelect) {
		alias = addAlias(alias);
		if (includeEntityAliasInSelect) {
			select(namedAliasPlaceholders.get(namedAliasPlaceholders.size() - 1));
		}
		fromClause.append((fromClause.length() > 0) ? ", " : " FROM ").append(entityName).append(" AS ").append(
				namedAliasPlaceholders.get(namedAliasPlaceholders.size() - 1));
		return alias;
	}
	
	/**
	 * Adds a new "IN (...)" expression containing the given collection expression into the FROM clause.
	 * The collection will be given the provided alias if it is non-null and does not conflict with
	 * any existing alias names; otherwise, a new alias name will be auto-generated. An extra parameter
	 * is also included for indicating whether this Criteria should automatically include the
	 * collection's alias in the SELECT clause.
	 * 
	 * <p>If no properly-referenced alias appears in the String expression, it will be assumed that the
	 * expression points to a collection that is accessible on or through the initial entity. See the
	 * description of this class for more information on how String expressions are interpreted.
	 * 
	 * @param collectionName A String expression that points to an entity collection.
	 * @param alias The alias to use for this collection expression.
	 * @param includeCollectionAliasInSelect Indicates whether to include the collection's alias in the SELECT clause.
	 * @return The provided alias if it's non-null and does not conflict with any existing names, or an auto-generated alias name otherwise.
	 */
	public String fromIn(String collectionName, String alias, boolean includeCollectionAliasInSelect) {
		alias = addAlias(alias);
		if (includeCollectionAliasInSelect) {
			select(namedAliasPlaceholders.get(namedAliasPlaceholders.size() - 1));
		}
		// The optional "AS" keyword is excluded when creating the "IN (...)" expression because Hibernate will throw an exception if it exists.
		if (collectionName.contains(JPA_ALIAS_PREFIX)) {
			fromClause.append(", IN (").append(collectionName).append(") ").append(namedAliasPlaceholders.get(namedAliasPlaceholders.size() - 1));
		} else {
			fromClause.append(", IN (").append(JPA_INITIAL_ALIAS).append('.').append(collectionName).append(") ").append(
					namedAliasPlaceholders.get(namedAliasPlaceholders.size() - 1));
		}
		return alias;
	}
	
	/**
	 * Adds a new JOIN to the most-recently-added entity in the FROM clause. The association
	 * will be given the provided alias if it is non-null and does not conflict with
	 * any existing alias names; otherwise, a new alias name will be auto-generated. An extra parameter
	 * is also included for indicating whether this Criteria should automatically include the
	 * association's alias in the SELECT clause.
	 * 
	 * <p>If no properly-referenced alias appears in the String expression, it will be assumed that the
	 * expression points to an association that is accessible on or through the initial entity. See the
	 * description of this class for more information on how String expressions are interpreted.
	 * 
	 * @param associationName A String expression that points to an entity association.
	 * @param alias The alias to use for this association expression.
	 * @param includeAssociationAliasInSelect Indicates whether to include the collection's alias in the SELECT clause.
	 * @param innerJoin If true, the join will be an inner join; otherwise, it will be a left (outer) join.
	 * @return The provided alias if it's non-null and does not conflict with any existing names, or an auto-generated alias name otherwise.
	 */
	public String join(String associationName, String alias, boolean includeAssociationAliasInSelect, boolean innerJoin) {
		alias = addAlias(alias);
		if (includeAssociationAliasInSelect) {
			select(namedAliasPlaceholders.get(namedAliasPlaceholders.size() - 1));
		}
		if (associationName.contains(JPA_ALIAS_PREFIX)) {
			fromClause.append(innerJoin ? " INNER JOIN " : " LEFT JOIN ").append(associationName).append(" AS ").append(
					namedAliasPlaceholders.get(namedAliasPlaceholders.size() - 1));
		} else {
			fromClause.append(innerJoin ? " INNER JOIN " : " LEFT JOIN ").append(JPA_INITIAL_ALIAS).append('.').append(
					associationName).append(" AS ").append(namedAliasPlaceholders.get(namedAliasPlaceholders.size() - 1));
		}
		return alias;
	}
	
	/**
	 * Adds a new JOIN FETCH to the most-recently-added entity in the FROM clause. If no
	 * properly-referenced alias appears in the String expression, it will be assumed that the
	 * expression points to an association that is accessible on or through the initial entity. See the
	 * description of this class for more information on how String expressions are interpreted.
	 * 
	 * @param associationName A String expression that points to an entity association.
	 * @param innerJoin If true, the join will be an inner join; otherwise, it will be a left (outer) join.
	 */
	public void joinFetch(String associationName, boolean innerJoin) {
		if (associationName.contains(JPA_ALIAS_PREFIX)) {
			fromClause.append(innerJoin ? " INNER JOIN FETCH " : " LEFT JOIN FETCH ").append(associationName);
		} else {
			fromClause.append(innerJoin ? " INNER JOIN FETCH " : " LEFT JOIN FETCH ").append(JPA_INITIAL_ALIAS).append('.').append(associationName);
		}
	}
	
	/**
	 * Adds a new attribute assignment expression to the SET clause (for use with UPDATE queries). It is
	 * assumed that the given attribute belongs to the initial entity, so a custom alias should not be used.
	 * 
	 * @param attributeName An expression pointing to an attribute that is accessible on or through the initial entity.
	 * @param value The value to assign to the attribute.
	 */
	public void set(String attributeName, Object value) {
		setClause.append((setClause.length() > 0) ? ", " : " SET ").append(JPA_INITIAL_ALIAS).append('.').append(attributeName).append(
				" = ").append(addAttr(fixPeriods(attributeName), value));
	}
	
	/**
	 * Adds one new attribute assignment expression to the SET clause for each entry in the given Map (for use with
	 * UPDATE queries). It is assumed that the attributes belong to the initial entity, so custom aliases should not be used.
	 *
	 * @param attributes A Map containing expressions pointing to initial-entity-accessible attributes and their associated values.
	 */
	public void set(Map<String,Object> attributes) {
		for (Map.Entry<String,Object> attribute : attributes.entrySet()) {
			setClause.append((setClause.length() > 0) ? ", " : " SET ").append(JPA_INITIAL_ALIAS).append('.').append(attribute.getKey()).append(
					" = ").append(addAttr(fixPeriods(attribute.getKey()), attribute.getValue()));
		}
	}
	
	/*
	 * Converts an Object into a JPQL-valid constant, or creates a new input parameter for it, depending on the Object type.
	 * Booleans are converted to "true" or "false", numbers are converted to their String representations, Strings are
	 * surrounded in single quotes and their existing single quotes are doubled, Class objects are converted to their
	 * getName() values and are left unquoted, null values are ignored, and all other Objects are assigned to an input
	 * parameter. The fixed constant or parameter name will be appended to the end of the given StringBuilder.
	 */
	private void fixValue(StringBuilder queryClause, Object value) {
		if (value == null) { return; }
		Class<?> propertyType = value.getClass();
		if(TypeUtils.isIntegralClass(propertyType) || TypeUtils.isDecimalClass(propertyType)) {
			queryClause.append(value.toString());
		} else if (TypeUtils.isStringClass(propertyType)) {
			queryClause.append('\'').append(fixSingleQuotes(value.toString())).append('\'');
		} else if (TypeUtils.isBooleanClass(propertyType)) {
			queryClause.append(value.toString());
		} else if (value instanceof Class) {
			queryClause.append(((Class)value).getName());
		} else {
			queryClause.append(addAttr("bind_param" + (++generatedBindParamCount), value));
		}
	}
	
	/*
	 * Fixes the single quotes of the given String by doubling them. Note that this method
	 * will *not* wrap the final value in single quotes.
	 */
	private String fixSingleQuotes(String value) {
		return APOS_PAT.matcher(value).replaceAll("''");
	}
	
	/*
	 * Fixes the periods of the given String by converting them to underscores.
	 */
	private String fixPeriods(String value) {
		return value.replace('.', '_');
	}
	
	/*
	 * Fixes the search pattern by converting all non-escaped lookup wildcards ("*" and "?") into their
	 * respective JPQL wildcards ("%" and "_"). Any lookup wildcards escaped by a backslash are converted
	 * into their non-backslashed equivalents.
	 */
	private String fixSearchPattern(String value) {
		StringBuilder fixedPattern = new StringBuilder(value);
		int valueLen = value.length();
		String lookupWildcard;
		String escapedLookupWildcard;
		char jpqlWildcard;
		// Convert all non-escaped  wildcards.
		for (int i = 0; i < LOOKUP_WILDCARDS.length; i++) {
			lookupWildcard = LOOKUP_WILDCARDS[i];
			escapedLookupWildcard = ESCAPED_LOOKUP_WILDCARDS[i];
			jpqlWildcard = JPQL_WILDCARDS[i];
			int wildcardIndex = fixedPattern.indexOf(lookupWildcard);
			int escapedWildcardIndex = fixedPattern.indexOf(escapedLookupWildcard);
			while (wildcardIndex != -1) {
				if (wildcardIndex == 0 || escapedWildcardIndex != wildcardIndex - 1) {
					fixedPattern.setCharAt(wildcardIndex, jpqlWildcard);
					wildcardIndex = fixedPattern.indexOf(lookupWildcard, wildcardIndex);
				} else {
					fixedPattern.replace(escapedWildcardIndex, wildcardIndex + 1, lookupWildcard);
					wildcardIndex = fixedPattern.indexOf(lookupWildcard, wildcardIndex);
					escapedWildcardIndex = fixedPattern.indexOf(escapedLookupWildcard, wildcardIndex);
				}
			}
		}
		return fixedPattern.toString();
	}
	
	/*
	 * Automatically appends a new " AND " to the WHERE clause if adding more than one condition to it,
	 * and also appends the initial entity's alias and a '.' if the expression does not contain any
	 * properly-referenced aliases (as defined in the description of this class).
	 */
	private void preparePrefixIfNecessary(String attribute) {
		whereClause.append((whereClause.length() > 0) ? " AND " : "");
		if (!attribute.contains(JPA_ALIAS_PREFIX)) {
			whereClause.append(JPA_INITIAL_ALIAS).append('.');
		}
	}
	
	/*
	 * Generates a new input parameter name and associates it with the given value, or (if the value
	 * is a Class object) just returns the value's getName() String w/out adding it to a parameter. The
	 * parameter's name will resemble the "attribute" String with the periods converted to
	 * underscores, unless such a parameter already exists or "attribute" contains a properly-referenced
	 * alias (in which case a new unique name will be auto-generated).
	 * Returns the finalized input parameter name as a "__JPA_PARAM[['...']]__" String (with a ":" prefix)
	 * if the value is not a Class; otherwise, returns the getName() String of the Class object.
	 */
	private String prepareAttribute(String attribute, Object value) {
		return ( value instanceof Class ? ((Class)value).getName() :
			( attribute.contains(JPA_ALIAS_PREFIX) ? addAttr("bind_param" + (++generatedBindParamCount), value) : addAttr(fixPeriods(attribute), value) ) );
	}
	
	/*
	 * Generates new input parameter names using "attribute" as a base, and associates each new
	 * parameter with the corresponding provided value (unless the given value is a Class object,
	 * in which case the Class's getName() String will be used w/out adding it to a parameter).
	 * The parameters' names will resemble the "attribute" String (with the periods converted to
	 * underscores) plus the suffix "_bN" (where N is the index of the value associated with the
	 * parameter), unless such parameters already exist or "attribute" contains a
	 * properly-referenced alias (in which case new unique names will be auto-generated).
	 * Returns an array of parameter names as "__JPA_PARAM[['...']]__" Strings (with ":" prefixes),
	 * where each name corresponds to the associated value at the same index, and where the returned
	 * array has a length equal to the number of provided values (but note that if a given value is a
	 * Class object, its getName() String will be returned at that index instead and no input
	 * parameter will be added for it).
	 */
	private String[] prepareAttributes(String attribute, Object... values) {
		int count = values.length;
		String[] fixedAttrs = new String[count];
		if (attribute.contains(JPA_ALIAS_PREFIX)) {
			int tempParamCount = ++generatedBindParamCount;
			for (int i = 0; i < count; i++) {
				fixedAttrs[i] = (values[i] instanceof Class) ? ((Class)values[i]).getName() :
					addAttr(new StringBuilder(20).append("bind_param").append(tempParamCount).append("_b").append(i + 1).toString(), values[i]);
			}
		} else {
			attribute = fixPeriods(attribute);
			for (int i = 0; i < count; i++) {
				fixedAttrs[i] = (values[i] instanceof Class) ? ((Class)values[i]).getName() :
					addAttr(new StringBuilder(attribute.length() + 5).append(attribute).append("_b").append(i + 1).toString(), values[i]);
			}
		}
		return fixedAttrs;
	}
	
	/*
	 * Automatically appends a new " AND " to the WHERE clause if adding more than one condition to it,
	 * and also appends the initial entity's alias and a '.' if the expression does not contain any
	 * properly-referenced aliases (as defined in the description of this class). In addition, a new
	 * input parameter name is generated, and the parameter will be associated with the given value (unless
	 * the object is a Class object, in which case its getName() value will be used instead and no input
	 * parameter will be created for it). The parameter's name will resemble the "attribute" String with
	 * the periods converted to underscores, unless such a parameter already exists or "attribute" contains
	 * a properly-referenced alias (in which case a new unique name will be auto-generated).
	 * Returns the finalized input parameter name as a "__JPA_PARAM[['...']]__" String (with a ":" prefix)
	 * if it is not a Class object; otherwise, its getName() String will be returned instead.
	 */
	private String preparePrefixAndAttributeIfNecessary(String attribute, Object value) {
		whereClause.append((whereClause.length() > 0) ? " AND " : "");
		if (value instanceof Class) {
			return ((Class)value).getName();
		} else if (attribute.contains(JPA_ALIAS_PREFIX)) {
			return addAttr("bind_param" + (++generatedBindParamCount), value);
		} else {
			whereClause.append(JPA_INITIAL_ALIAS).append('.');
			return addAttr(fixPeriods(attribute), value);
		}
	}
	
	/*
	 * Adds a new input parameter and its value to the parameter Map, auto-generating a new key if the provided one already exists.
	 * Returns the finalized key as a "__JPA_PARAM[['...']]__" String with a prepended ":".
	 */
	private String addAttr(String string, Object value) {
		while (params.containsKey(string)) {
			string = "bind_param" + (++generatedBindParamCount);
		}
		params.put(string, value);
		return new StringBuilder(45).append(JPA_PARAM_PREFIX_WITH_COLON).append('\'').append(string).append('\'').append(JPA_ALIAS_SUFFIX).toString();
	}
	
	/**
	 * Adds a new BETWEEN condition to the WHERE clause, ANDing it with any existing conditions if necessary.
	 * 
	 * @param attribute An expression representing the subject of the BETWEEN condition.
	 * @param value1 The lower value of the BETWEEN condition.
	 * @param value2 The upper value of the BETWEEN condition.
	 */
	public void between(String attribute, Object value1, Object value2) {
		preparePrefixIfNecessary(attribute);
		String[] fixedAttrs = prepareAttributes(attribute, value1, value2);
		whereClause.append(attribute).append(" BETWEEN ").append(fixedAttrs[0]).append(" AND ").append(fixedAttrs[1]);
	}
	
	/**
	 * Adds a new NOT BETWEEN condition to the WHERE clause, ANDing it with any existing conditions if necessary.
	 * 
	 * @param attribute An expression representing the subject of the NOT BETWEEN condition.
	 * @param value1 The lower value of the NOT BETWEEN condition.
	 * @param value2 The upper value of the NOT BETWEEN condition.
	 */
	public void notBetween(String attribute, Object value1, Object value2) {
		preparePrefixIfNecessary(attribute);
		String[] fixedAttrs = prepareAttributes(attribute, value1, value2);
		whereClause.append(attribute).append(" NOT BETWEEN ").append(fixedAttrs[0]).append(" AND ").append(fixedAttrs[1]);
	}
	
	/**
	 * Adds an equality condition to the WHERE clause, ANDing it with any existing conditions if necessary.
	 * 
	 * @param attribute An expression representing the left-hand side of this condition.
	 * @param value The value to check for equality with.
	 */
	public void eq(String attribute, Object value) {
		String fixedAttr = preparePrefixAndAttributeIfNecessary(attribute, value);
		whereClause.append(attribute).append(" = ").append(fixedAttr);
	}

	/**
	 * Adds a greater-than condition to the WHERE clause, ANDing it with any existing conditions if necessary.
	 * 
	 * @param attribute An expression representing the left-hand side of this condition.
	 * @param value The value on the right-hand side of the greater-than condition.
	 */
	public void gt(String attribute, Object value) {
		String fixedAttr = preparePrefixAndAttributeIfNecessary(attribute, value);
		whereClause.append(attribute).append(" > ").append(fixedAttr);
	}

	/**
	 * Adds a greater-than-or-equal condition to the WHERE clause, ANDing it with any existing conditions if necessary.
	 * 
	 * @param attribute An expression representing the left-hand side of this condition.
	 * @param value The value on the right-hand side of the greater-than-or-equal condition.
	 */
	public void gte(String attribute, Object value) {
		String fixedAttr = preparePrefixAndAttributeIfNecessary(attribute, value);
		whereClause.append(attribute).append(" >= ").append(fixedAttr);
	}

	/**
	 * Adds a new LIKE condition to the WHERE clause, ANDing it with any existing conditions if necessary.
	 * If the provided value is not a String, its toString() representation will be used as the pattern.
	 * 
	 * @param attribute An expression representing the left-hand side of this condition.
	 * @param value The pattern to compare with for "likeness".
	 */
	public void like(String attribute, Object value) {
		String fixedAttr = preparePrefixAndAttributeIfNecessary(attribute, fixSearchPattern(value.toString()));
		whereClause.append(attribute).append(" LIKE ").append(fixedAttr);
	}

	/**
	 * Adds a new NOT LIKE condition to the WHERE clause, ANDing it with any existing conditions if necessary.
	 * If the provided value is not a String, its toString() representation will be used as the pattern.
	 * 
	 * @param attribute An expression representing the left-hand side of this condition.
	 * @param value The pattern to compare with for "non-likeness".
	 */
	public void notLike(String attribute, Object value) {
		String fixedAttr = preparePrefixAndAttributeIfNecessary(attribute, fixSearchPattern(value.toString()));
		whereClause.append(attribute).append(" NOT LIKE ").append(fixedAttr);
	}

	/**
	 * Adds a new LIKE condition (containing an ESCAPE clause) to the WHERE clause, ANDing it with any
	 * existing conditions if necessary.
	 * If the provided value is not a String, its toString() representation will be used as the pattern.
	 * 
	 * @param attribute An expression representing the left-hand side of this condition.
	 * @param value The pattern to compare with for "likeness".
	 * @param escapeChar The designated wildcard escape character for the given value.
	 */
	public void likeEscape(String attribute, Object value, char escapeChar) {
		String fixedAttr = preparePrefixAndAttributeIfNecessary(attribute, fixSearchPattern(value.toString()));
		whereClause.append(attribute).append(" LIKE ").append(fixedAttr).append(" ESCAPE ").append(
				prepareAttribute(JPA_ALIAS_PREFIX, Character.valueOf(escapeChar)));
	}
	
	/**
	 * Adds a new NOT LIKE condition (containing an ESCAPE clause) to the WHERE clause, ANDing it with any
	 * existing conditions if necessary.
	 * If the provided value is not a String, its toString() representation will be used as the pattern.
	 * 
	 * @param attribute An expression representing the left-hand side of this condition.
	 * @param value The pattern to compare with for "non-likeness".
	 * @param escapeChar The designated wildcard escape character for the given value.
	 */
	public void notLikeEscape(String attribute, Object value, char escapeChar) {
		String fixedAttr = preparePrefixAndAttributeIfNecessary(attribute, fixSearchPattern(value.toString()));
		whereClause.append(attribute).append(" NOT LIKE ").append(fixedAttr).append(" ESCAPE ").append(
				prepareAttribute(JPA_ALIAS_PREFIX, Character.valueOf(escapeChar)));
	}
	
	/**
	 * Adds a less-than condition to the WHERE clause, ANDing it with any existing conditions if necessary.
	 * 
	 * @param attribute An expression representing the left-hand side of this condition.
	 * @param value The value on the right-hand side of the less-than condition.
	 */
	public void lt(String attribute, Object value) {
		String fixedAttr = preparePrefixAndAttributeIfNecessary(attribute, value);
		whereClause.append(attribute).append(" < ").append(fixedAttr);
	}

	/**
	 * Adds a less-than-or-equal condition to the WHERE clause, ANDing it with any existing conditions if necessary.
	 * 
	 * @param attribute An expression representing the left-hand side of this condition.
	 * @param value The value on the right-hand side of the less-than-or-equal condition.
	 */
	public void lte(String attribute, Object value) {
		String fixedAttr = preparePrefixAndAttributeIfNecessary(attribute, value);
		whereClause.append(attribute).append(" <= ").append(fixedAttr);
	}

	/**
	 * Adds a non-equality condition to the WHERE clause, ANDing it with any existing conditions if necessary.
	 * 
	 * @param attribute An expression representing the left-hand side of this condition.
	 * @param value The value on the right-hand side of the non-equality condition.
	 */
	public void ne(String attribute, Object value) {
		String fixedAttr = preparePrefixAndAttributeIfNecessary(attribute, value);
		whereClause.append(attribute).append(" <> ").append(fixedAttr);
	}

	/**
	 * Adds an IS NULL condition to the WHERE clause, ANDing it with any existing conditions if necessary.
	 * 
	 * @param attribute An expression representing what needs the null check.
	 */
	public void isNull(String attribute) {
		preparePrefixIfNecessary(attribute);
		whereClause.append(attribute).append(" IS NULL");
	}

	/**
	 * Adds an IS NOT NULL condition to the WHERE clause, ANDing it with any existing conditions if necessary.
	 * 
	 * @param attribute An expression representing what needs the not-null check.
	 */
	public void notNull(String attribute) {
		preparePrefixIfNecessary(attribute);
		whereClause.append(attribute).append(" IS NOT NULL");
	}
	
	/**
	 * Adds a MEMBER OF condition to the WHERE clause, ANDing it with any existing conditions if necessary.
	 * Uses an input parameter for the single-value part of the condition.
	 * 
	 * @param value An Object that may or may not be in the specified collection.
	 * @param collection An expression representing a collection.
	 */
	public void memberOf(Object value, String collection) {
		String fixedAttr = prepareAttribute(collection, value);
		preparePrefixIfNecessary(JPA_ALIAS_PREFIX);
		whereClause.append(fixedAttr).append(" MEMBER OF ").append(
				collection.contains(JPA_ALIAS_PREFIX) ? "" : JPA_INITIAL_ALIAS + ".").append(collection);
	}
	
	/**
	 * Adds a MEMBER OF condition to the WHERE clause, ANDing it with any existing conditions if necessary.
	 * Uses an expression for the single-value part of the condition.
	 * 
	 * @param attribute An expression pointing to an attribute that may or may not be in the given collection.
	 * @param collection An expression representing a collection.
	 */
	public void memberOf(String attribute, String collection) {
		preparePrefixIfNecessary(attribute);
		whereClause.append(attribute).append(" MEMBER OF ").append(
				collection.contains(JPA_ALIAS_PREFIX) ? "" : JPA_INITIAL_ALIAS + ".").append(collection);
	}
	
	/**
	 * Adds a NOT MEMBER OF condition to the WHERE clause, ANDing it with any existing conditions if necessary.
	 * Uses an input parameter for the single-value part of the condition.
	 * 
	 * @param value An Object that may or may not be in the specified collection.
	 * @param collection An expression representing a collection.
	 */
	public void notMemberOf(Object value, String collection) {
		String fixedAttr = prepareAttribute(collection, value);
		preparePrefixIfNecessary(JPA_ALIAS_PREFIX);
		whereClause.append(fixedAttr).append(" NOT MEMBER OF ").append(
				collection.contains(JPA_ALIAS_PREFIX) ? "" : JPA_INITIAL_ALIAS + ".").append(collection);
	}
	
	/**
	 * Adds a NOT MEMBER OF condition to the WHERE clause, ANDing it with any existing conditions if necessary.
	 * Uses an expression for the single-value part of the condition.
	 * 
	 * @param attribute An expression pointing to an attribute that may or may not be in the given collection.
	 * @param collection An expression representing a collection.
	 */
	public void notMemberOf(String attribute, String collection) {
		preparePrefixIfNecessary(attribute);
		whereClause.append(attribute).append(" NOT MEMBER OF ").append(
				collection.contains(JPA_ALIAS_PREFIX) ? "" : JPA_INITIAL_ALIAS + ".").append(collection);
	}
	
	/**
	 * Adds an IS EMPTY condition to the WHERE clause, ANDing it with any existing conditions if necessary.
	 * 
	 * @param collection An expression representing a collection.
	 */
	public void isEmpty(String collection) {
		preparePrefixIfNecessary(collection);
		whereClause.append(collection).append(" IS EMPTY");
	}
	
	/**
	 * Adds an IS NOT EMPTY condition to the WHERE clause, ANDing it with any existing conditions if necessary.
	 * 
	 * @param collection An expression representing a collection.
	 */
	public void notEmpty(String collection) {
		preparePrefixIfNecessary(collection);
		whereClause.append(collection).append(" IS NOT EMPTY");
	}
	
	/**
	 * Converts the given Criteria into a sub-SELECT query, surrounds it in parentheses, and adds a new
	 * "IN (...)" condition containing the resulting expression into the WHERE clause, while ANDing
	 * the new condition with any existing conditions if necessary.
	 * 
	 * @param attribute An expression representing the left-hand side of the "IN (...)" condition.
	 * @param in The Criteria representing the sub-query of the "IN (...)" condition.
	 */
	public void in(String attribute, Criteria in) {
		if (in == null) { throw new IllegalArgumentException("'IN' Criteria cannot be null"); }
		preparePrefixIfNecessary(attribute);
		whereClause.append(attribute).append(" IN ");
		subQuery(in);
	}
	
	/**
	 * Converts the given Criteria into a sub-SELECT query, surrounds it in parentheses, and adds a new
	 * "NOT IN (...)" condition containing the resulting expression into the WHERE clause, while ANDing
	 * the new condition with any existing conditions if necessary.
	 * 
	 * @param attribute An expression representing the left-hand side of the "NOT IN (...)" condition.
	 * @param in The Criteria representing the sub-query of the "NOT IN (...)" condition.
	 */
	public void notIn(String attribute, Criteria notIn) {
		if (notIn == null) { throw new IllegalArgumentException("'NOT IN' Criteria cannot be null"); }
		preparePrefixIfNecessary(attribute);
		whereClause.append(attribute).append(" NOT IN ");
		subQuery(notIn);
	}
	
	/**
	 * Takes the individual elements of the provided collection, converts them to literals or assigns them to input
	 * parameters as needed, and adds a new "IN (...)" condition to the WHERE clause containing those elements,
	 * ANDing it with any existing conditions if necessary. String, number, boolean, and Class collection elements
	 * will be converted into literals, and all other non-null collection elements will be assigned to input parameters.
	 * 
	 * @param attribute An expression representing the left-hand side of the "IN (...)" condition.
	 * @param values The collection of values to check against.
	 */
	public void in(String attribute, Collection<?> values) {
		preparePrefixIfNecessary(attribute);
		whereClause.append(attribute).append(" IN (");
		appendBodyOfIn(values);
		whereClause.append(')');
	}
	
	/**
	 * Takes the individual elements of the provided collection, converts them to literals or assigns them to input
	 * parameters as needed, and adds a new "IN (...)" condition to the WHERE clause containing those elements,
	 * ANDing it with any existing conditions if necessary. String, number, boolean, and Class collection elements
	 * will be converted into literals, and all other non-null collection elements will be assigned to input parameters.
	 *  
	 * @param attribute An expression representing the left-hand side of the "NOT IN (...)" condition.
	 * @param values The collection of values to check against.
	 */
	public void notIn(String attribute, Collection<?> values) {
		preparePrefixIfNecessary(attribute);
		whereClause.append(attribute).append(" NOT IN (");
		appendBodyOfIn(values);
		whereClause.append(')');
	}

	/**
	 * Takes the provided values, converts them to literals or assigns them to input parameters as needed, and adds
	 * a new "IN (...)" condition to the WHERE clause containing those elements, ANDing it with any existing
	 * conditions if necessary. String, number, boolean, and Class values will be converted into literals, and all
	 * other non-null values will be assigned to input parameters.
	 * 
	 * @param attribute An expression representing the left-hand side of the "IN (...)" condition.
	 * @param values The values to check against.
	 */
	public void in(String attribute, Object... values) {
		preparePrefixIfNecessary(attribute);
		whereClause.append(attribute).append(" IN (");
		appendBodyOfIn(Arrays.asList(values));
		whereClause.append(')');
	}
	
	/**
	 * Takes the provided values, converts them to literals or assigns them to input parameters as needed, and adds
	 * a new "NOT IN (...)" condition to the WHERE clause containing those elements, ANDing it with any existing
	 * conditions if necessary. String, number, boolean, and Class values will be converted into literals, and all
	 * other non-null values will be assigned to input parameters.
	 * 
	 * @param attribute An expression representing the left-hand side of the "NOT IN (...)" condition.
	 * @param values The values to check against.
	 */
	public void notIn(String attribute, Object... values) {
		preparePrefixIfNecessary(attribute);
		whereClause.append(attribute).append(" NOT IN (");
		appendBodyOfIn(Arrays.asList(values));
		whereClause.append(')');
	}
	
	/*
	 * Creates the body of an "IN (...)" condition using the provided collection of values.
	 * String, number, and boolean values are converted into literals, and all other
	 * non-null values are assigned to input parameters.
	 */
	private void appendBodyOfIn(Collection<?> values) {
		Iterator<?> valuesIter = values.iterator();
		if (valuesIter.hasNext()) {
			fixValue(whereClause, valuesIter.next());
			while (valuesIter.hasNext()) {
				whereClause.append(", ");
				fixValue(whereClause, valuesIter.next());
			}
		}
	}
	
	/**
	 * Takes the conditions from the given Criteria's WHERE clause, surrounds them in parentheses, and ANDs
	 * the resulting expression with this instance's WHERE clause (or makes the expression become the new
	 * WHERE clause of this instance if its current WHERE clause is empty). Any input parameters from the
	 * given Criteria will be copied over to this Criteria, and will be renamed as needed if conflicts arise.
	 * 
	 * @param and The Criteria instance whose conditions should be ANDed with those in this Criteria instance.
	 */
	public void and(Criteria and) {
		if (and == null) { throw new IllegalArgumentException("'AND' Criteria cannot be null"); }
		if (and.whereClause.length() > 0) {
			int oldLen = whereClause.length();
			preparePrefixIfNecessary(JPA_ALIAS_PREFIX);
			whereClause.append('(').append(and.whereClause).append(')');
			copyParams(and, oldLen, true);
		}
	}

	/**
	 * Takes the conditions from the given Criteria's WHERE clause, surrounds them in parentheses, and adds
	 * a new "NOT (...)" condition containing the resulting expression to this instance's WHERE clause (or
	 * adds a new "AND NOT(...)" condition if the WHERE clause on the existing Criteria is not empty). Any
	 * input parameters from the given Criteria will be copied over to this Criteria, and will be renamed
	 * as needed if conflicts arise.
	 * 
	 * @param not The Criteria instance whose group of conditions should be negated.
	 */
	public void not(Criteria not) {
		if (not == null) { throw new IllegalArgumentException("'NOT' Criteria cannot be null"); }
		if (not.whereClause.length() > 0) {
			int oldLen = whereClause.length();
			preparePrefixIfNecessary(JPA_ALIAS_PREFIX);
			whereClause.append("NOT (").append(not.whereClause).append(')');
			copyParams(not, oldLen, true);
		}
	}
	
	/**
	 * Takes the conditions from the given Criteria's WHERE clause, surrounds them in parentheses, and ORs
	 * the resulting expression with this instance's WHERE clause (or makes the expression become the new
	 * WHERE clause of this instance if its current WHERE clause is empty). Any input parameters from the
	 * given Criteria will be copied over to this Criteria, and will be renamed as needed if conflicts arise.
	 * 
	 * @param or The Criteria instance whose conditions should be ORed with those in this Criteria instance.
	 */
	public void or(Criteria or) {
		if (or == null) { throw new IllegalArgumentException("'OR' Criteria cannot be null"); }
		if (or.whereClause.length() > 0) {
			int oldLen = whereClause.length();
			whereClause.append((oldLen > 0) ? " OR (" : "(").append(or.whereClause).append(')');
			copyParams(or, oldLen, true);
		}
	}
	
	/**
	 * Takes the conditions from the given Criteria's WHERE clause, surrounds them in parentheses, and adds
	 * a new "OR NOT (...)" condition containing the resulting expression to this instance's WHERE clause (or
	 * just adds a new "NOT (...)" condition if its current WHERE clause is empty). Any input parameters from the
	 * given Criteria will be copied over to this Criteria, and will be renamed as needed if conflicts arise.
	 * 
	 * @param orNot The Criteria instance whose group of conditions should be negated and then ORed with those in this Criteria instance.
	 */
	public void orNot(Criteria orNot) {
		if (orNot == null) { throw new IllegalArgumentException("'OR NOT' Criteria cannot be null"); }
		if (orNot.whereClause.length() > 0) {
			int oldLen = whereClause.length();
			whereClause.append((oldLen > 0) ? " OR NOT (" : "NOT (").append(orNot.whereClause).append(')');
			copyParams(orNot, oldLen, true);
		}
	}
	
	/**
	 * Converts the given Criteria into a sub-SELECT query, surrounds it in parentheses, and adds a new
	 * EXISTS condition containing the resulting expression into the WHERE clause, while ANDing the new
	 * condition with any existing conditions if necessary.
	 * 
	 * @param exists The Criteria instance representing the sub-query of the EXISTS condition.
	 */
	public void exists(Criteria exists) {
		if (exists == null) { throw new IllegalArgumentException("'EXISTS' Criteria cannot be null"); }
		preparePrefixIfNecessary(JPA_ALIAS_PREFIX);
		whereClause.append("EXISTS ");
		subQuery(exists);
    }

	/**
	 * Converts the given Criteria into a sub-SELECT query, surrounds it in parentheses, and adds a new
	 * NOT EXISTS condition containing the resulting expression into the WHERE clause, while ANDing the
	 * new condition with any existing conditions if necessary.
	 * 
	 * @param exists The Criteria instance representing the sub-query of the NOT EXISTS condition.
	 */
	public void notExists(Criteria notExists) {
		if (notExists == null) { throw new IllegalArgumentException("'NOT EXISTS' Criteria cannot be null"); }
		preparePrefixIfNecessary(JPA_ALIAS_PREFIX);
		whereClause.append("NOT EXISTS ");
		subQuery(notExists);
    }
	
	/**
	 * Inserts raw JPQL into the WHERE clause. An extra whitespace character will automatically be prepended
	 * (if the WHERE clause is non-empty) and appended to the JPQL string.
	 * 
	 * @param jpql The raw JPQL to insert.
	 */
	public void rawJpql(String jpql) {
		whereClause.append((whereClause.length() > 0) ? " " : "").append(jpql).append(' ');
	}
	
	/**
	 * Adds a new expression to the GROUP BY clause. If no properly-referenced alias appears in
	 * the String expression, it will be assumed that the expression points to something
	 * that is accessible on or through the initial entity. See the description of this class
	 * for more information on how String expressions are interpreted.
	 * 
	 * @param attribute The expression representing what the query results should be grouped by.
	 */
	public void groupBy(String attribute) {
		groupByClause.append((groupByClause.length() > 0) ? ", " : " GROUP BY ");
		if (!attribute.contains(JPA_ALIAS_PREFIX)) {
			groupByClause.append(JPA_INITIAL_ALIAS).append('.');
		}
		groupByClause.append(attribute);
	}
	
	/**
	 * Takes the conditions from the given Criteria's WHERE clause and then ANDs them with this
	 * instance's HAVING clause (or makes them become the new HAVING clause of this instance if
	 * its current HAVING clause is empty). Any input parameters from the given Criteria will be
	 * copied over to this Criteria, and will be renamed as needed if conflicts arise.
	 * 
	 * @param having The Criteria instance whose WHERE clause should represent this instance's HAVING clause (or a piece of it).
	 */
	public void having(Criteria having) {
		if (having == null) { throw new IllegalArgumentException("'HAVING' Criteria cannot be null"); }
		if (having.whereClause.length() > 0) {
			int oldLen = havingClause.length();
			havingClause.append((oldLen > 0) ? " AND " : " HAVING ").append(having.whereClause);
			copyParams(having, oldLen, false);
		}
	}
	
	/**
	 * Adds a new expression to the ORDER BY clause. If no properly-referenced alias appears in
	 * the String expression, it will be assumed that the expression points to something
	 * that is accessible on or through the initial entity. See the description of this class
	 * for more information on how String expressions are interpreted.
	 * 
	 * @param attribute The expression representing what the query results should be ordered by.
	 * @param sortAscending If true, the order will be ascending; otherwise, it will be descending.
	 */
	public void orderBy(String attribute, boolean sortAscending) {
		orderByClause.append((orderByClause.length() > 0) ? ", " : " ORDER BY ");
		if (!attribute.contains(JPA_ALIAS_PREFIX)) {
			orderByClause.append(JPA_INITIAL_ALIAS).append('.');
		}
		orderByClause.append(attribute).append(sortAscending ? " ASC" : " DESC");
	}
	
	/*
	 * Copies the input parameters from the given child Criteria to the current Criteria. Any parameter placeholders from the sub-query will
	 * be renamed accordingly if they conflict with those defined by the parent Criteria.
	 * 
	 * It is assumed that the sub-query has already been appended to the clause given by modifyWhereClause
	 * (WHERE if true, HAVING if false), starting at the index given by startIndex.
	 */
	private void copyParams(Criteria subCriteria, int startIndex, boolean modifyWhereClause) {
		final StringBuilder tempClause = modifyWhereClause ? whereClause : havingClause;
		Map<String,String> modifiedNames = new HashMap<String,String>();
		int paramClose = 0;
		int paramIndex = tempClause.indexOf(JPA_PARAM_PREFIX_WITH_COLON, startIndex);
		String newPlaceholder = null;
		
		for (Map.Entry<String,Object> param : subCriteria.params.entrySet()) {
			if (params.containsKey(param.getKey())) {
				modifiedNames.put(param.getKey(), addAttr(param.getKey(), param.getValue()));
			} else {
				params.put(param.getKey(), param.getValue());
			}
		}
		
		while (paramIndex >= 0) {
			paramClose = tempClause.indexOf(JPA_ALIAS_SUFFIX, paramIndex);
			newPlaceholder = modifiedNames.get(tempClause.substring(paramIndex + PARAM_PREFIX_LEN + 2, paramClose - 1));
			if (newPlaceholder != null) {
				tempClause.replace(paramIndex, paramClose + ALIAS_SUFFIX_LEN, newPlaceholder);
			}
			paramIndex = tempClause.indexOf(JPA_PARAM_PREFIX_WITH_COLON, paramIndex + 1);
		}
	}
	
	/*
	 * Converts the given Criteria into a sub-query and appends it to the WHERE clause. Any named aliases of the sub-query will be renamed
	 * accordingly if they conflict with those defined by the parent Criteria, and all indexed aliases will be updated accordingly. In
	 * addition, the input parameters will be copied over and renamed as needed.
	 */
	private void subQuery(Criteria subCriteria) {
		Map<String,String> modifiedAliases = new HashMap<String,String>();
		int count = subCriteria.entityAliases.size();
		int paramIndex = 0;
		int paramClose = 0;
		String newPlaceholder = null;

		for (int i = entityAliases.size() - 1; i >= 0; i--) {
			modifiedAliases.put(JPA_ALIAS_PREFIX + (~i) + JPA_ALIAS_SUFFIX, indexedAliasPlaceholders.get(i));
		}
		for (int i = 0; i < count; i++) {
			String oldAlias = subCriteria.entityAliases.get(i);
			if (!addAlias(oldAlias).equals(oldAlias)) {
				modifiedAliases.put(subCriteria.namedAliasPlaceholders.get(i), namedAliasPlaceholders.get(namedAliasPlaceholders.size() - 1));
			}
			modifiedAliases.put(subCriteria.indexedAliasPlaceholders.get(i), indexedAliasPlaceholders.get(indexedAliasPlaceholders.size() - 1));
		}
		
		count = whereClause.length();
		whereClause.append(subCriteria.distinct ? "(SELECT DISTINCT " : "(SELECT ").append(subCriteria.selectClause).append(subCriteria.fromClause).append(
				(subCriteria.whereClause.length() > 0) ? " WHERE " : "").append(subCriteria.whereClause).append(
						subCriteria.groupByClause).append(subCriteria.havingClause).append(')');
		
		paramIndex = whereClause.indexOf(JPA_ALIAS_PREFIX, count);
		while (paramIndex >= 0) {
			paramClose = whereClause.indexOf(JPA_ALIAS_SUFFIX, paramIndex) + ALIAS_SUFFIX_LEN;
			newPlaceholder = modifiedAliases.get(whereClause.substring(paramIndex, paramClose));
			if (newPlaceholder != null) {
				whereClause.replace(paramIndex, paramClose, newPlaceholder);
			}
			paramIndex = whereClause.indexOf(JPA_ALIAS_PREFIX, paramIndex + 1);
		}
		copyParams(subCriteria, count, true);
	}
	
	/**
	 * Converts the current Criteria instance into a JPQL query.
	 * 
	 * @param type An indicator of what type of query should be generated (SELECT, UPDATE, or DELETE).
	 * @return A String representing the JPQL query generated by this Criteria instance.
	 */
	public String toQuery(QueryByCriteria.QueryByCriteriaType type) {
		return toQuery(type, new String[0]);
	}
	
	/**
	 * Converts the current Criteria instance into a JPQL query. If a SELECT query is desired, then
	 * alternative String expressions to insert into the SELECT clause can be specified, though doing
	 * so is not required and must be done with care (see below).
	 * 
	 * <p>WARNING! If queryAttr is non-null and has a length greater than zero, and if a SELECT query is
	 * desired, then this method will completely overwrite the SELECT clause so that it only contains
	 * the expressions given by the queryAttr array.
	 * 
	 * @param type An indicator of what type of query should be generated (SELECT, UPDATE, or DELETE).
	 * @param queryAttr (Optional) An array of String expressions to use as the values to be returned by the SELECT clause (if creating a SELECT query).
	 * @return A String representing the JPQL query generated by this Criteria instance.
	 */
	public String toQuery(QueryByCriteria.QueryByCriteriaType type, String[] queryAttr) {
		StringBuilder newQuery;
		int querySize = selectClause.length() + fromClause.length() + whereClause.length() + groupByClause.length() + havingClause.length() + orderByClause.length() + 7;
		// Construct the Criteria appropriately.
		switch (type) {
			case SELECT :
				if (queryAttr != null && queryAttr.length > 0) {
					if (selectClause.length() > 0) { selectClause.delete(0, selectClause.length()); }
					for (int i = 0; i < queryAttr.length; i++) {
						select(queryAttr[i]);
					}
				}
				newQuery = new StringBuilder(querySize + (distinct ? 16 : 7)).append(distinct ? "SELECT DISTINCT " : "SELECT ").append(
						selectClause).append(fromClause).append(whereClause.length() > 0 ? " WHERE " : "").append(whereClause).append(
								groupByClause).append(havingClause).append(orderByClause);
				break;
			case UPDATE :
				newQuery = new StringBuilder(querySize + setClause.length() + 7).append("UPDATE ").append(initialEntityName).append(" AS ").append(
						getAlias()).append(setClause).append(whereClause.length() > 0 ? " WHERE " : "").append(whereClause);
				break;
			case DELETE :
				newQuery = new StringBuilder(querySize + 6).append("DELETE").append(fromClause).append(
						whereClause.length() > 0 ? " WHERE " : "").append(whereClause);
				break;
			default :
				return null;
		}
		return fix(newQuery);
	}
	
	/**
	 * Converts the current Criteria instance into a JPQL SELECT query, but replaces the existing SELECT clause with "SELECT COUNT(*)" instead.
	 * 
	 * @return A JPQL query String given by this Criteria, but with "SELECT COUNT(*)" as the SELECT clause instead.
	 */
	public String toCountQuery() {
		StringBuilder newQuery = new StringBuilder(
				fromClause.length() + whereClause.length() + groupByClause.length() + havingClause.length() + orderByClause.length() + 15 + 7).append(
						"SELECT COUNT(*)").append(fromClause).append(whereClause.length() > 0 ? " WHERE " : "").append(whereClause).append(
								groupByClause).append(havingClause).append(orderByClause);
		return fix(newQuery);
	}

	/*
	 * Converts all the named and indexed alias/parameter placeholders into the actual aliases/parameters.
	 * You can view the resulting JPQL String in the logs by configuring a log4j DEBUG output level for
	 * this class.
	 */
	private String fix(StringBuilder newQuery) {
		Map<String,String> modifiedAliases = new HashMap<String,String>();
		for (int i = entityAliases.size() - 1; i >= 0; i--) {
			modifiedAliases.put(indexedAliasPlaceholders.get(i), entityAliases.get(i));
			modifiedAliases.put(namedAliasPlaceholders.get(i), entityAliases.get(i));
		}
		
		// resolve the aliases.
		String newParam = null;
		int paramClose = 0;
		int paramIndex = newQuery.indexOf(JPA_ALIAS_PREFIX);
		while (paramIndex >= 0) {
			paramClose = newQuery.indexOf(JPA_ALIAS_SUFFIX, paramIndex) + ALIAS_SUFFIX_LEN;
			newParam = modifiedAliases.get(newQuery.substring(paramIndex, paramClose));
			if (newParam != null) {
				newQuery.replace(paramIndex, paramClose, newParam);
			} else {
				LOG.error("Detected an unresolvable JPA alias when constructing query: " + newQuery.substring(paramIndex, paramClose));
				throw new IllegalStateException("Detected an unresolvable alias: " + newQuery.substring(paramIndex, paramClose));
			}
			paramIndex = newQuery.indexOf(JPA_ALIAS_PREFIX, paramIndex + 1);
		}
		
		// Resolve the parameters.
		paramIndex = newQuery.indexOf(JPA_PARAM_PREFIX);
		while (paramIndex >= 0) {
			paramClose = newQuery.indexOf(JPA_ALIAS_SUFFIX, paramIndex);
			newParam = newQuery.substring(paramIndex + PARAM_PREFIX_LEN + 1, paramClose - 1);
			if (params.containsKey(newParam)) {
				newQuery.replace(paramIndex, paramClose + ALIAS_SUFFIX_LEN, newParam);
			} else {
				LOG.error("Detected an unresolvable input parameter when constructing query: " + newQuery.substring(paramIndex, paramClose + ALIAS_SUFFIX_LEN));
				throw new IllegalStateException("Detected an unresolvable input parameter: " + newQuery.substring(paramIndex, paramClose + ALIAS_SUFFIX_LEN));
			}
			paramIndex = newQuery.indexOf(JPA_PARAM_PREFIX, paramIndex + 1);
		}
		
		String finishedQuery = newQuery.toString();
		if (LOG.isDebugEnabled()) {
			LOG.debug("********** SEARCH JPQL QUERY **********");
			LOG.debug(finishedQuery);
			LOG.debug("***************************************");
		}
		return finishedQuery;
	}
	
	// Keep this package access so the QueryByCriteria can call it from this package.
	/**
	 * Populates the given Query instance with the parameters stored in this Criteria. It is assumed that the given query
	 * is using this Criteria's toQuery() or toCountQuery() value as its JPQL String.
	 * 
	 * @param query A JPA Query instance containing this Criteria's toQuery() or toCountQuery() value for its JPQL String.
	 */
	public void prepareParameters(Query query) {
		for (Map.Entry<String, Object> param : params.entrySet()) {
			Object value = param.getValue();
			if (value != null) {
				if (value instanceof String) {
					if (query.getParameter(param.getKey()).getParameterType().equals(java.lang.Boolean.class)) {
						value = Truth.strToBooleanIgnoreCase((String) value, Boolean.FALSE);
					} else {
						//value = ((String)value).replaceAll("\\*", "%");
					}
				}
				query.setParameter(param.getKey(), value);
			}
		}
	}
	
	/**
	 * Retrieves the current limit on the result set size for this Criteria, if any (only relevant if creating a SELECT query).
	 * 
	 * @return The current limit on the number of results to be returned by this Criteria's query, or null (default) if no such limit has been set.
	 */
	public Integer getSearchLimit() {
		return this.searchLimit;
	}

	/**
	 * Sets the current limit on the result set size for this Criteria (only relevant if creating a SELECT query).
	 * 
	 * @param searchLimit The new limit on the number of results to be returned by this Criteria's query.
	 */
	public void setSearchLimit(Integer searchLimit) {
		this.searchLimit = searchLimit;
	}

	/**
	 * Sets whether or not the query should include the DISTINCT keyword in the SELECT clause, if creating a SELECT query.
	 * If this property is not set explicitly, it is assumed that DISTINCT should *not* be included in the SELECT clause.
	 * 
	 * @param distinct An indicator for whether to do a SELECT DISTINCT or just a SELECT.
	 */
	public void distinct(boolean distinct){
		this.distinct = distinct;
	}

	/**
	 * Retrieves the initial alias of this Criteria instance.
	 * 
	 * @return The initial alias.
	 */
    public String getAlias() {
        return this.entityAliases.get(0);
    }
    
    /**
     * Retrieves the alias associated with the given index.
     * 
     * @param index The index pointing to a given alias.
     * @return The alias at the given index.
     */
    public String getAlias(int index) {
    	return this.entityAliases.get(index);
    }
    
    /**
     * Retrieves the index that points to the given alias.
     * 
     * @param alias The indexed alias.
     * @return The index of the alias, or -1 if no such index was found.
     */
    public int getAliasIndex(String alias) {
    	Integer tempIndex = aliasIndexes.get(alias);
    	return (tempIndex != null) ? tempIndex.intValue() : -1;
    }
    
    /**
     * Retrieves a copy of all the aliases defined for this Criteria instance.
     * The index of a given alias corresponds to the index at which the alias
     * is located at in the returned List.
     * 
     * @return A List containing all the defined aliases of this Criteria instance.
     */
    public List<String> getAliases() {
    	return new ArrayList(this.entityAliases);
    }
    
    /**
     * @return the name of the class of the initial Entity this Criteria will search for
     */
    public String getEntityName() {
    	return this.initialEntityName;
    }	
}
