/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 * 
 * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
 * 
 * The contents of this file are subject to the terms of either the GNU
 * General Public License Version 2 only ("GPL") or the Common Development
 * and Distribution License("CDDL") (collectively, the "License").  You
 * may not use this file except in compliance with the License. You can obtain
 * a copy of the License at https://glassfish.dev.java.net/public/CDDL+GPL.html
 * or glassfish/bootstrap/legal/LICENSE.txt.  See the License for the specific
 * language governing permissions and limitations under the License.
 * 
 * When distributing the software, include this License Header Notice in each
 * file and include the License file at glassfish/bootstrap/legal/LICENSE.txt.
 * Sun designates this particular file as subject to the "Classpath" exception
 * as provided by Sun in the GPL Version 2 section of the License file that
 * accompanied this code.  If applicable, add the following below the License
 * Header, with the fields enclosed by brackets [] replaced by your own
 * identifying information: "Portions Copyrighted [year]
 * [name of copyright owner]"
 * 
 * Contributor(s):
 * 
 * If you wish your version of this file to be governed by only the CDDL or
 * only the GPL Version 2, indicate your decision by adding "[Contributor]
 * elects to include this software in this distribution under the [CDDL or GPL
 * Version 2] license."  If you don't indicate a single choice of license, a
 * recipient has the option to distribute your version of this file under
 * either the CDDL, the GPL Version 2 or to extend the choice of license to
 * its licensees as provided above.  However, if you add GPL Version 2 code
 * and therefore, elected the GPL Version 2 license, then the option applies
 * only if the new code is made subject to such option by the copyright
 * holder.
 */

/*
 * ParameterTable.java
 *
 * Created on April 12, 2000
 */

package com.sun.jdo.spi.persistence.support.sqlstore.query.jqlc;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.ResourceBundle;

import com.sun.jdo.api.persistence.support.JDOQueryException;
import com.sun.jdo.api.persistence.support.JDOFatalInternalException;
import com.sun.jdo.spi.persistence.support.sqlstore.query.util.type.Type;
import com.sun.jdo.spi.persistence.support.sqlstore.query.util.type.StringType;
import com.sun.jdo.spi.persistence.support.sqlstore.query.util.type.PrimitiveType;
import com.sun.jdo.spi.persistence.support.sqlstore.query.util.type.WrapperClassType;
import com.sun.jdo.spi.persistence.support.sqlstore.query.util.type.MathType;
import com.sun.jdo.spi.persistence.support.sqlstore.query.util.type.DateType;

import com.sun.jdo.spi.persistence.support.sqlstore.ValueFetcher;
import com.sun.jdo.spi.persistence.utility.I18NHelper;
import com.sun.jdo.spi.persistence.utility.JavaTypeHelper;
import com.sun.jdo.spi.persistence.utility.ParameterInfo;

/**
 * The query parameter table
 *
 * @author  Michael Bouschen
 * @version 0.1
 */
public class ParameterTable
{
    /** Query parameter names */
    List names = null;

    /** Query parameter types */
    List types = null;

    /** Query Parameter values */
    transient List values = null;
    
    /** null key */
    private static final String NULL_ = "null"; //NOI18N

    /** true key */
    private static final String TRUE_ = "true"; //NOI18N

    /** false key */
    private static final String FALSE_ = "false"; //NOI18N

    /** other key */
    private static final String OTHER_ = "other"; //NOI18N

    /** noparams key */
    private static final String NOPARAMS_ = "noparams"; //NOI18N
    
    /** key parameter separator */
    private static final char PARAMKEY_SEPARATOR = '/';

    /**
     * Objects of this class represent the value for an unbound query parameter
     */
    static class Unbound { }

    /**
     * Singleton representing the value for an unbound query parameter
     */
    static final Unbound unbound = new Unbound();

    /** I18N support */
    protected final static ResourceBundle messages =
        I18NHelper.loadBundle(ParameterTable.class);

    /**
     *
     */
    public ParameterTable()
    {}

    /**
     * Copy constructor.
     * @param other the ParameterTable to be copied
     */
    public ParameterTable(ParameterTable other)
    {
        this.names = other.names;
        this.types = other.types;
        this.values = other.values;
    }

    /**
     * Adds a new query parameter with the specified type to the query 
     * parameter table. 
     */
    public void add(String name, Type type)
    {
        names.add(name);
        types.add(type);
    }

    /**
     * Initializes the parameter declarations (names and types list). 
     * Needs to be called prior to any add call.
     */
    public void init()
    {
        this.names = new ArrayList();
        this.types = new ArrayList();
    }

    /**
     * Initializes the parameter values. This methods sets the values for all 
     * declared parameters to unbound.
     */
    public void initValueHandling()
    {
        values = new ArrayList(names.size());
        final int size = names.size();
        for (int i = 0; i < size; i++) {
            values.add(unbound);
        }
    }
    
    /**
     * Check actual query parameters specified as array and return the
     * ValueFetcher for the inputparameters.
     * @param actualParams
     * @return the parameters wrapped in the ValueFetcher
     */
    public void setValues(Object[] actualParams)
    {
        if (actualParams != null)
        {
            for (int i = 0; i < actualParams.length; i++)
            {
                Object value = actualParams[i];
                defineValueByIndex(i, value);
            }
        }
    }

    /**
     * Checks whether all parameters have an actual value.
     */
    public void checkUnboundParams()
    {
        final int size = values.size();
        for (int i = 0; i < size; i++)
        {
            if (values.get(i) == unbound)
            {
                throw new JDOQueryException(
                    I18NHelper.getMessage(messages, "jqlc.parametertable.checkunboundparams.unboundparam",  //NOI18N
                                          names.get(i)));
            }
        }
    }

    /**
     * Check actual query parameters specified as map and return the
     * ValueFetcher for the inputparameters.
     * @param actualParams
     * @return the parameters wrapped in the ValueFetcher
     */
    public void setValues(Map actualParams)
    {
        if (actualParams != null)
        {
            for (Iterator i = actualParams.entrySet().iterator(); i.hasNext();)
            {
                Map.Entry actualParam = (Map.Entry)i.next();
                String name = (String)actualParam.getKey();
                Object value = actualParam.getValue();
                defineValueByName(name, value);
            }
        }
    }

    /**
     * Returns the value of the parameter with the specified name.
     */
    public Object getValueByName(String name)
    {
        int index = names.indexOf(name);
        if (index == -1)
            throw new JDOFatalInternalException(I18NHelper.getMessage(
                messages,
                "jqlc.parametertable.getvaluebyname.undefined", //NOI18N
                name));

        return getValueByIndex(index);
    }

    /**
     * Returns the value of the parameter with the specified index.
     */
    public Object getValueByIndex(int index)
    {
        if ((index < 0) || (index >= values.size()))
            throw new JDOFatalInternalException(I18NHelper.getMessage(
                messages,
                "jqlc.parametertable.getvaluebyindex.wrongindex", //NOI18N
                String.valueOf(index)));

        return values.get(index);
    }

    /** Returns the list of parameter values. */
    public List getValues()
    {
        return values;
    }

    /**
     * Wraps the actual parameter array into a ValueFetcher instnace.
     * @return
     */
    public ValueFetcher getValueFetcher()
    {
        return new QueryValueFetcher(values.toArray(new Object[values.size()]));
    }

    /**
     * Calculates and returns the key for the RetrieveDesc cache based,
     * on the actual parameter values.
     * A <code>null</code> return means, the RetrieveDesc should not be
     * cached. 
     * Note, this method needs to be in sync with method inline.
     */
    public String getKeyForRetrieveDescCache()
    {
        StringBuffer key = new StringBuffer();
        final int size = values.size();
        for (int i = 0; i < size; i++) {
            // Do not cache RetrieveDesc if the parameter type is pc class
            // or java.lang.Object => return null
            if (isInlineType(types.get(i)))
                return null;

            Object item = values.get(i);
            if (item == null) {
                key.append(ParameterTable.NULL_);
            }
            else if (item instanceof Boolean) {
                if (((Boolean)item).booleanValue()) {
                    key.append(ParameterTable.TRUE_);
                } else {
                    key.append(ParameterTable.FALSE_);
                }
            } else {
                key.append(ParameterTable.OTHER_);
            }
            key.append(ParameterTable.PARAMKEY_SEPARATOR);
        }

        // If the key is 0 in length, the Query does not use any parameters.
        // But nevertheless we want cache the RD, thus we return a key for
        // no-parameter-queries
        if (key.length() == 0) {
            key.append(ParameterTable.NOPARAMS_);
        }

        return key.toString();
    }

    /**
     * Returns true if the parameter with the specified index should be inlined
     * by the optimizer.
     * Note, this method needs to be in sync with method
     * getKeyForRetrieveDescCache. 
     * @param paramName the parameter
     * @return true if the specified parameter should be inlined.
     */
    public boolean inline(String paramName)
    {
        int index = names.indexOf(paramName);
        Object value = values.get(index);

        if (isInlineType(types.get(index))) return true;

        if (value == null) return true;

        if (value instanceof Boolean) return true;

        return false;
    }

    /**
     * Returns <code>true</code> if the specified parameter denotes a type
     * whose values should be inlined by the query optimizer if a query 
     * parameter s is declared with such a type.
     */
    private boolean isInlineType(Object type)
    { 
        // Check for types that are supported by JDBC, such that the
        // parameter can be mapped to a JDBC parameter, these are:
        // - String
        // - primitive types (int, float, etc.)
        // - wrapper class types (Integer, Float, etc.)
        // - BigDecimal, BigInteger
        // - Date class types
        // All other types including pc classes, java.lang.Object, etc.
        // should be inlined.
        if ((type instanceof StringType) ||
            (type instanceof PrimitiveType) ||
            (type instanceof WrapperClassType) ||
            (type instanceof MathType) ||
            (type instanceof DateType))
            return false;
        return true;
    }

    /**
     * Returns the parameter index for the specified parameter name.
     * @deprecated
     */
    public Integer getIndexForParamName(String paramName)
    {
        return new Integer(names.indexOf(paramName));
    }

    /**
     * Returns the parameter info for the specified parameter name.
     * @param paramName
     * @return corresponding parameterInfo
     */
    public ParameterInfo getParameterInfoForParamName(String paramName)
    {
        return getParameterInfoForParamName(paramName, null);
    }

    /**
     * Returns the parameter info for the specified parameter name
     * and associated field.
     * If the associated field is not known, then null is used as
     * input parameter.
     * @param paramName
     * @param associatedField
     * @return corresponding parameterInfo
     */
    public ParameterInfo getParameterInfoForParamName(String paramName,
            String associatedField)
    {
        int index = names.indexOf(paramName);
        Type type = (Type)types.get(index);
        return new ParameterInfo(index, type.getEnumType(), associatedField);
    }

    /**
     *
     */
    private void defineValueByName(String name, Object value)
    {
        int index = names.indexOf(name);
        if (index == -1)
            throw new JDOQueryException(
                I18NHelper.getMessage(messages, "jqlc.parametertable.definevaluebyname.undefinedparam", name)); //NOI18N
        defineValueByIndex(index, value);
    }

    /**
     *
     */
    private void defineValueByIndex(int index, Object value)
    {
        // index < 0 => implementation error
        if (index < 0)
            throw new JDOFatalInternalException(I18NHelper.getMessage(
                messages,
                "jqlc.parametertable.definevaluebyindex.wrongindex", //NOI18N
                String.valueOf(index)));          

        // index > type.size => too many actual parameters
        if (index >= types.size())
            throw new JDOQueryException(
                I18NHelper.getMessage(messages, "jqlc.parametertable.definevaluebyindex.wrongnumberofargs")); //NOI18N

        // check type compatibility of actual and formal parameter
        Class formalType = ((Type)types.get(index)).getJavaClass();
        if (!isCompatibleValue(formalType, value))
        {
            String actualTypeName = ((value==null) ? "<type of null>" : value.getClass().getName());
            throw new JDOQueryException(
                I18NHelper.getMessage(messages, "jqlc.parametertable.definevaluebyindex.typemismatch",  //NOI18N
                                      actualTypeName, formalType.getName()));
        }

        // everything is ok => set the actual parameters's value
        values.set(index, value);
    }

    /**
     * Checks whether the type of the specified value is compatible with the
     * specified formal type.
     * @param name the formal type.
     * @param the value to be checked
     * @return <code>true</code> if the type of the value is compatible with the
     * formal type; <code>false</code> otherwise.
     */
    private boolean isCompatibleValue(Class formalType, Object value)
    {
        boolean isCompatible = true;

        // handle value == null
        if (value == null) {
            isCompatible = !formalType.isPrimitive();
        }
        else {
            Class actualType = value.getClass();
            if (formalType.isPrimitive())
                formalType = JavaTypeHelper.getWrapperClass(formalType);

            isCompatible = formalType.isAssignableFrom(actualType);
        }
        return isCompatible;
    }

}

