/*
 * Copyright (c) 1998, 2018 Oracle and/or its affiliates. All rights reserved.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v. 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0,
 * or the Eclipse Distribution License v. 1.0 which is available at
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *
 * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
 */

// Contributors:
//     Oracle - initial API and implementation from Oracle TopLink
package org.eclipse.persistence.queries;

import java.security.AccessController;
import java.security.PrivilegedActionException;
import java.util.HashMap;
import java.util.Map;
import java.util.Vector;

import org.eclipse.persistence.exceptions.QueryException;
import org.eclipse.persistence.expressions.Expression;
import org.eclipse.persistence.internal.helper.ClassConstants;
import org.eclipse.persistence.internal.helper.Helper;
import org.eclipse.persistence.internal.security.PrivilegedAccessHelper;
import org.eclipse.persistence.internal.security.PrivilegedMethodInvoker;

/**
* <p><b>Purpose</b>:
* This policy defines the configuration options for a Query By Example query.
*
* <p><b>Description</b>:
* A Query By Example query is an <code>ObjectLevelReadQuery</code> where the
* selection criteria is built from an example domain object passed in via <code>setExampleObject</code>.
* <p>
* If no policy is specified the selection criteria is built from the example
* object in the following way:
* <ul>
* <li>Attributes of the example object set to <code>null</code> are ignored.
*
* <li>Attributes set to the default value for their primitive type (such as
* <code>0</code> for <code>int</code>) are ignored.
*
* <li>Unmapped attributes are ignored.
*
* <li>A domain object is returned by the query only if its values for all the
* included attributes equal those set in the example object.
* </ul><p>
* A policy can be set on the query to:
* <ul>
* <li>Always consider an attribute even if set to <code>null</code>
* or the default value for its type.  See {@link #alwaysIncludeAttribute alwaysIncludeAttribute}.
*
* <li>Ignore attributes set to some other special value.  See
* {@link #excludeValue(Object) excludeValue}.
*
* <li>Match a <code>null</code> attribute on the example object with domain objects that have
* either <code>null</code> for that attribute also, or have set a meaningful (<code>notNull</code>) value
* for that attribute.  See {@link #setShouldUseEqualityForNulls}.
*
* <li>Use specialized operations when comparing attributes set in the example object
* with those set in the domain objects.  Matching attributes can be those with
* values greater than, less than, like, or not equal to that set in the example
* object.  See {@link #addSpecialOperation}.
* </ul>
* <p>
* Note: When setting an attribute on the example object which is itself a java
* object with an ObjectReferenceMapping, the mapped components of that
* attribute will be considered, not the entire object.  There is no limit to
* how many mapped objects can be nested inside the example object.
* <p>
* Note: <code>setExampleObject</code> is different from <code>setSelectionObject</code> in
* <code>ReadObjectQuery</code> which reads a single object by first extracting
* the primary key from the selection object.
* <p>
* <b>Restrictions</b>:
* <ul>
* <li>Only attributes whose mappings are DirectToField, Aggregate (Embeddable), ObjectReference
* (OneToOne) or Collection type OneToMany/ManyToMany are considered in a Query By Example object.  The behaviour when an example object has attribute values for other mappings types is <b>undefined</b>.
* <ul><li>To ensure the example does not include any unsupported mappings the flag {@link #setValidateExample}
* should be set to true on the corresponding QueryByExamplePolicy to ensure no unsupported relationship types are used in the example.</li>
* <li> For OneToMany and ManyToMany mappings the elements within the collections and the references attribute values will be added to the expression as disjuncts (OR)</li>
* </ul>
* </li>
* </ul>
* <p>
* <b>Example</b>:
* <BLOCKQUOTE><PRE>
* // This example uses like for Strings and the salary must be greater
* // than zero.
* ReadAllQuery query = new ReadAllQuery();
* Employee employee = new Employee();
* employee.setFirstName("B%");
* employee.setLastName("S%");
* employee.setSalary(0);
* query.setExampleObject(employee);
* QueryByExamplePolicy policy = new QueryByExamplePolicy();
* policy.addSpecialOperation(String.class, "like");
* policy.addSpecialOperation(Integer.class, "greaterThan");
* policy.alwaysIncludeAttribute(Employee.class, "salary");
* query.setQueryByExamplePolicy(policy);
* Vector results = (Vector) session.executeQuery(query);
* </PRE></BLOCKQUOTE>
* @see ObjectLevelReadQuery#setExampleObject
* @see ObjectLevelReadQuery#setQueryByExamplePolicy
*
* @since TOPLink/Java 3.0
*/
public class QueryByExamplePolicy implements java.io.Serializable {
    //CR3400 Make Serializable
    public Map valuesToExclude = new HashMap();
    public Map attributesToAlwaysInclude = new HashMap();
    public Map specialOperations = new HashMap();
    public boolean shouldUseEqualityForNulls;
    protected boolean validateExample;

    /**
     * PUBLIC:
     * Constructs a default policy equal to that used when no policy is specified.
     * <p>
     * Sets the default values to be excluded,
     * (that includes 0, false, empty String, etc).<p>
     * By default if an attribute is <code>null</code>, and yet has to be included at all times, equality (<code>isNull</code>)
     * is used for the comparison.  This is used for searching for an object with a <code>null</code> in a certain field.
     * @see #excludeDefaultPrimitiveValues
     * @see #setShouldUseEqualityForNulls setShouldUseEqualityForNulls(true)
     */
    public QueryByExamplePolicy() {
        this.valuesToExclude = new HashMap(10);
        this.attributesToAlwaysInclude = new HashMap(5);
        this.specialOperations = new HashMap(5);
        this.shouldUseEqualityForNulls = true;

        this.excludeDefaultPrimitiveValues();
    }

    /**
     * PUBLIC:
     * Allows operations other than <code>Expression.equal</code> to be used
     * for comparisons.
     * <p>
     * For example if an attribute of type <code>int</code> is
     * set to <code>x</code> in the example object, normally the query will be on all objects
     * whose attributes are also equal to <code>x</code>.  The query could however be all
     * objects whose attributes are not <code>x</code>, greater than <code>x</code>, or even less than or
     * equal to <code>x</code>.
     * <p>
     * Any comparison operation in {@link org.eclipse.persistence.expressions.Expression Expression} which takes the example attribute as a parameter
     * can be used.  A list of supported operations is provided below.
     * <p>
     * Note: A special operation can not be used for attributes set to <code>null</code>.  The only
     * options are {@link org.eclipse.persistence.expressions.Expression#isNull() isNull} (default) and
     * {@link org.eclipse.persistence.expressions.Expression#notNull() notNull}.  See
     * {@link #setShouldUseEqualityForNulls}.
     * @param attributeValueClass Attribute values of which type, for instance
     * <code>Integer</code>, to apply to.  Note for <code>int</code> attributes the
     * class is <code>Integer.class</code> not <code>int.class</code>.  This is not
     * the <code>Class</code> of the example object the attribute is an instance variable of.
     * @param operation Name of method in <code>Expression</code> used
     * @see org.eclipse.persistence.expressions.Expression#equal equal (default)
     * @see org.eclipse.persistence.expressions.Expression#notEqual notEqual
     * @see org.eclipse.persistence.expressions.Expression#equalsIgnoreCase equalsIgnoreCase
     * @see org.eclipse.persistence.expressions.Expression#lessThan lessThan
     * @see org.eclipse.persistence.expressions.Expression#lessThanEqual lessThanEqual
     * @see org.eclipse.persistence.expressions.Expression#greaterThan greaterThan
     * @see org.eclipse.persistence.expressions.Expression#greaterThanEqual greaterThanEqual
     * @see org.eclipse.persistence.expressions.Expression#like like
     * @see org.eclipse.persistence.expressions.Expression#likeIgnoreCase likeIgnoreCase
     * @see org.eclipse.persistence.expressions.Expression#containsAllKeyWords containsAllKeyWords
     * @see org.eclipse.persistence.expressions.Expression#containsAnyKeyWords containsAnyKeyWords
     * @see org.eclipse.persistence.expressions.Expression#containsSubstring(java.lang.String) containsSubstring
     * @see org.eclipse.persistence.expressions.Expression#containsSubstringIgnoringCase(java.lang.String) containsSubstringIgnoringCase
     */
    public void addSpecialOperation(Class attributeValueClass, String operation) {
        this.getSpecialOperations().put(attributeValueClass, operation);
    }

    /**
     * PUBLIC:
     * Always considers the value for a particular attribute as meaningful in a
     * query by example.
     * <p>
     * Required to override the normal behavior which is to ignore an
     * attribute of the example object if set to <code>null</code>, or an excluded value
     * like <code>0</code>.
     * <p>
     * Example: To find all projects without a budget set <code>budget</code> to 0 in the
     * example object and call <code>alwaysIncludeAttribute(Project.class, "budget")</code>
     * on the policy.
     * <p>
     * @param exampleClass The class that the attribute belongs to, normally this is the example class unless using nested QBE.
     * @param attributeName The name of a mapped attribute.
     */
    public void alwaysIncludeAttribute(Class exampleClass, String attributeName) {
        Vector included = (Vector)getAttributesToAlwaysInclude().get(exampleClass);
        if (included == null) {
            included = new Vector(3);
        }
        included.addElement(attributeName);

        getAttributesToAlwaysInclude().put(exampleClass, included);
    }

    /**
     * INTERNAL:
     * This method is used to determine which operation to use for comparison (equal, or a special operation).
     */
    public Expression completeExpression(Expression expression, Object attributeValue, Class attributeValueClass) {
        String operation = this.getOperation(attributeValue.getClass());

        if (operation == null) {
            //it means no special operation used. Use equal.
            return expression.equal(attributeValue);
        }

        Class[] argTypes = { attributeValueClass };
        Object[] args = { attributeValue };
        try {
            java.lang.reflect.Method anOperator = Helper.getDeclaredMethod(ClassConstants.Expression_Class, operation, argTypes);
            if (anOperator == null) {
                throw QueryException.methodDoesNotExistOnExpression(operation, argTypes);
            }
            if (PrivilegedAccessHelper.shouldUsePrivilegedAccess()){
                try{
                    expression = (Expression)AccessController.doPrivileged(new PrivilegedMethodInvoker(anOperator, expression, args));
                }catch (PrivilegedActionException ex){
                    throw (RuntimeException) ex.getCause();
                }
            }else{
                expression = (Expression)PrivilegedAccessHelper.invokeMethod(anOperator, expression, args);
            }
        } catch (NoSuchMethodException nsme) {
            Class superClass = attributeValueClass.getSuperclass();
            if (superClass != null) {
                return completeExpression(expression, attributeValue, superClass);
            } else {
                throw QueryException.methodDoesNotExistOnExpression(operation, argTypes);
            }
        } catch (IllegalAccessException iae) {
            throw QueryException.methodDoesNotExistOnExpression(operation, argTypes);
        } catch (java.lang.reflect.InvocationTargetException ite) {
            throw QueryException.methodDoesNotExistOnExpression(operation, argTypes);
        }
        return expression;
    }

    /**
     * INTERNAL:
     * This method is used when the attribute value is null, but it has
     * to be included at all times. It determines whether to use isNull, or notNull.
     */
    public Expression completeExpressionForNull(Expression expression) {
        if (shouldUseEqualityForNulls()) {
            return expression.isNull();
        } else {
            return expression.notNull();
        }
    }

    /**
     * PUBLIC:
     * Ignores attributes set to the default value for their primitive type.
     * <p>
     * For instance <code>0</code> is used as <code>null</code> for deciding
     * which <code>int</code> attributes of the example object can be ignored in a
     * query by example.
     * <p>
     * Called by the constructor.
     */
    public void excludeDefaultPrimitiveValues() {
        excludeValue(0);
        excludeValue(0.0);
        excludeValue(false);
        excludeValue((short)0);
        excludeValue('\u0000');
        excludeValue((long)0);
        excludeValue((byte)0);
        excludeValue(0.0f);
        excludeValue("");
    }

    /**
     * PUBLIC:
     * An attribute in the example object set to an excluded value will be
     * ignored in a Query By Example.<p>
     * The default excluded value for <code>byte</code> is <code>0</code>.
     */
    public void excludeValue(byte value) {
        excludeValue(Byte.valueOf(value));
    }

    /**
     * PUBLIC:
     * An attribute in the example object set to an excluded value will be
     * ignored in a Query By Example.<p>
     * The default excluded value for <code>char</code> is <code>'\u0000'</code>.
     */
    public void excludeValue(char value) {
        excludeValue(Character.valueOf(value));
    }

    /**
     * PUBLIC:
     * An attribute in the example object set to an excluded value will be
     * ignored in a Query By Example.<p>
     * The default excluded value for <code>double</code> is <code>0.0</code>.
     */
    public void excludeValue(double value) {
        excludeValue(Double.valueOf(value));
    }

    /**
     * PUBLIC:
     * An attribute in the example object set to an excluded value will be
     * ignored in a Query By Example.<p>
     * The default excluded value for <code>float</code> is <code>0.0f</code>.
     */
    public void excludeValue(float value) {
        excludeValue(Float.valueOf(value));
    }

    /**
     * PUBLIC:
     * An attribute in the example object set to be an excluded value will be
     * ignored in a Query By Example.<p>
     * The default excluded value for <code>int</code> is <code>0</code>.
     */
    public void excludeValue(int value) {
        excludeValue(Integer.valueOf(value));
    }

    /**
     * PUBLIC:
     * An attribute in the example object set to an excluded value will be
     * ignored in a Query By Example.<p>
     * The default excluded value for <code>long</code> is <code>0</code>.
     */
    public void excludeValue(long value) {
        excludeValue(Long.valueOf(value));
    }

    /**
     * PUBLIC:
     * An attribute in the example object set to an excluded value will be
     * ignored in a Query By Example.<p>
     * The default excluded value for <code>String</code> is <code>""</code>.<p>
     * Note: <code>null</code> is special and always considered an excluded value.
     */
    public void excludeValue(Object value) {
        this.valuesToExclude.put(value, value);
    }

    /**
     * PUBLIC:
     * An attribute in the example object set to an excluded value will be
     * ignored in a Query By Example.<p>
     * The default excluded value for <code>short</code> is <code>0</code>.
     */
    public void excludeValue(short value) {
        excludeValue(Short.valueOf(value));
    }

    /**
     * PUBLIC:
     * An attribute in the example object set to an excluded value will be
     * ignored in a Query By Example.<p>
     * The default excluded value for <code>boolean</code> is <code>false</code>.
     */
    public void excludeValue(boolean value) {
        excludeValue(Boolean.valueOf(value));
    }

    /**
     * PUBLIC:
     * Attributes to always consider even if set to <code>null</code> or an excluded
     * value like <code>0</code> or <code>false</code>.
     * @see #alwaysIncludeAttribute
     */
    public Map getAttributesToAlwaysInclude() {
        return attributesToAlwaysInclude;
    }

    /**
     * INTERNAL:
     * determines which operation to use for comparison.
     */
    public String getOperation(Class aClass) {
        String operation = (String)this.getSpecialOperations().get(aClass);
        if (operation != null) {
            if (!operation.equals("equal")) {
                return operation;
            }
        }
        return null;

    }

    /**
     * PUBLIC:
     * The special operations to use in place of <code>equal</code>.
     * @return A hashtable where the keys are <code>Class</code> objects and the values
     * are the names of operations to use for attributes of that <code>Class</code>.
     * @see #addSpecialOperation
     */
    public Map getSpecialOperations() {
        return specialOperations;
    }

    /**
     * PUBLIC:
     * Decides which attributes to ignore based on the values they are set to.
     * <p>
     * If an attribute of the example domain object is set to one of these values it will
     * be ignored, and not considered in the query.
     * <p>
     * Attributes set to excluded values are not always ignored.
     * See {@link #alwaysIncludeAttribute alwaysIncludeAttribute}.
     * @return valuesToExclude The keys and values are values to exclude (key == value).  Primitives are
     * wrapped, so <code>int 0</code> will be stored as <code>Integer(0)</code>.
     * @see #excludeValue
     * @see #excludeDefaultPrimitiveValues
     * @see #includeAllValues
     */
    public Map getValuesToExclude() {
        return valuesToExclude;
    }

    /**
     * PUBLIC:
     * Considers all mapped attributes in the example object as meaningful in a
     * Query By Example.<p>
     * Note: Even attributes of the example object that are
     * not set, and therefore zero or empty by default, will be included.<p>
     * Reverses a previous call to {@link #excludeDefaultPrimitiveValues}.
     */
    public void includeAllValues() {
        setValuesToExclude(new HashMap(5));
    }

    /**
     * INTERNAL:
     * returns whether the attributeName is to be always included.
     */
    public boolean isAlwaysIncluded(Class theClass, String attributeName) {
        Vector values = (Vector)this.getAttributesToAlwaysInclude().get(theClass);
        if (values != null) {
            return (values.contains(attributeName));
        }
        return false;

    }

    /**
     * INTERNAL:
     * returns if the value is in the values to be excluded automatically.
     */
    public boolean isExcludedValue(Object value) {
        return this.getValuesToExclude().containsKey(value);

    }

    /**
     * PUBLIC:
     * Considers all attributes set to a previously excluded value on the example object.
     * <p>
     * Primitive values to be removed must first be wrapped inside an Object.
     * <p>
     * @param value No attributes set to <code>value</code> will be excluded from a Query By Example.
     * <code>value.getClass()</code> is a key of the Hashtable returned by {@link #getValuesToExclude}.
     * <p>Note: There is a distinction between an attribute and the value
     * it is set to.  An attribute can be included independently of its value with
     * {@link #alwaysIncludeAttribute alwaysIncludeAttribute} (recommended).  It can also be included
     * by making the value it is set to no longer excluded.
     * <p>Note: <code>null</code> values are special and will always be excluded.
     * @see #excludeDefaultPrimitiveValues
     * @see #includeAllValues
     * @see #excludeValue(Object)
     */
    public void removeFromValuesToExclude(Object value) {
        getValuesToExclude().remove(value);
    }

    /**
     * INTERNAL:
     * It is possible to generate a Hashtable (keys are the Class, and values the attribute names)
     * of the attributes to be included at all times (even if the value is null, or the value
     * belongs to the values to be excluced automatically).
     */
    public void setAttributesToAlwaysInclude(Map newAttributesToAlwaysInclude) {
        attributesToAlwaysInclude = newAttributesToAlwaysInclude;
    }

    /**
     * PUBLIC:
     * Matches an included <code>null</code> attribute in the example object
     * either to objects with that attribute also set to <code>null</code> or to any
     * value other than <code>null</code>.
     * <p>
     * Set to <code>false</code> to only select objects where certain attributes have been set.
     * <p>
     * Example: to find all Employees with an assigned <code>address</code>, set
     * attribute <code>address</code> to <code>null</code> in the example object,
     * call <code>alwaysIncludeAttribute(Employee.class, "address")</code> and then
     * call <code>setShouldUseEqualityForNulls(false)</code>.
     * <p>
     * Note: Unless an attribute set to <code>null</code> is specifically included, it
     * will not be considered at all in the Query By Example.
     * @param shouldUseEqualityForNulls If true (by default) uses <code>isNull</code> else <code>notNull</code>.
     * @see #addSpecialOperation addSpecialOperation
     * @see #alwaysIncludeAttribute alwaysIncludeAttribute
     */
    public void setShouldUseEqualityForNulls(boolean shouldUseEqualityForNulls) {
        this.shouldUseEqualityForNulls = shouldUseEqualityForNulls;
    }

    /**
     * PUBLIC:
     * The special operations to use in place of <code>equal</code>.<p>
     * @param newOperations A hashtable where the keys are <code>Class</code> objects and the values
     * are the names of operations to use for attributes of that <code>Class</code>.
     * @see #addSpecialOperation
     */
    public void setSpecialOperations(Map newOperations) {
        specialOperations = newOperations;
    }

    /**
     * PUBLIC:
     * When set to <code>true</code> the example object will be validated for unsupported mapping types.
     * If you wish these mapping types to be ignored either set this flag to <code>false</code> or add the attribute
     * to the list of ignored attributes in this policy
     */
    public void setValidateExample(boolean validate){
        this.validateExample = validate;
    }

    /**
     * PUBLIC:
     * Decides which attributes to ignore based on the values they are set to.
     * <p>
     * An attribute of the example domain object set to one of these values will
     * be ignored, and not considered in the query.
     * <p>
     * Attributes set to excluded values are not always ignored.
     * See {@link #alwaysIncludeAttribute alwaysIncludeAttribute}.
     * @param newValuesToExclude The keys and values are values to exclude (key == value).  Primitives are
     * wrapped, so <code>int 0</code> will be stored as <code>Integer(0)</code>.
     * @see #excludeValue
     * @see #excludeDefaultPrimitiveValues
     * @see #includeAllValues
     */
    public void setValuesToExclude(Map newValuesToExclude) {
        valuesToExclude = newValuesToExclude;
    }

    /**
     * INTERNAL:
     * This method determines whether an attribute pair is be included in the query.
     */
    public boolean shouldIncludeInQuery(Class aClass, String attributeName, Object attributeValue) {
        if (attributeValue == null) {
            if (this.isAlwaysIncluded(aClass, attributeName)) {
                //this attribute is to be included always, even if its value is null.
                return true;
            } else {
                return false;
            }
        }

        if (this.isExcludedValue(attributeValue)) {
            if (this.isAlwaysIncluded(aClass, attributeName)) {
                //this attribute is to be included always, even if its value belongs to the list of values to be excluded.
                return true;
            } else {
                return false;
            }
        }

        return true;
    }

    /**
     * PUBLIC:
     * Matches an included <code>null</code> attribute in the example object
     * either to objects with that attribute also set to <code>null</code> or to any
     * value other than <code>null</code>.
     * <p>
     * Set to <code>false</code> to only select objects where certain attributes have been set.
     * <p>
     * Example: to find all Employees with an assigned <code>address</code>, set
     * attribute <code>address</code> to <code>null</code> in the example object,
     * call <code>alwaysIncludeAttribute(Employee.class, "address")</code> and then
     * call <code>setShouldUseEqualityForNulls(false)</code>.
     * <p>
     * @return If true (by default) uses <code>isNull</code> else <code>notNull</code>.
     * @see #addSpecialOperation addSpecialOperation
     * @see #alwaysIncludeAttribute alwaysIncludeAttribute
     */
    public boolean shouldUseEqualityForNulls() {
        return shouldUseEqualityForNulls;
    }

    /**
     * PUBLIC:
     * Returns true if the example object used with this policy should be validated for attributes
     * with unsupported mappings.
     */
    public boolean shouldValidateExample(){
        return this.validateExample;
    }
}
