/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the  "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
/*
 * $Id$
 */

/**
  * This is used by the SQLDocumentHandler for processing JDBC queries.
  * This prepares JDBC PreparedStatement or CallableStatements and the
  * input/output of parameters from/to variables.
  *
 */

package org.apache.xalan.lib.sql;

import java.util.*;
import java.sql.*;
import org.apache.xpath.objects.*;
import org.apache.xalan.extensions.ExpressionContext;
import org.apache.xml.utils.QName;
import javax.xml.transform.TransformerException;



public class SQLQueryParser
{
  /**
   * If the parser used inline parser to pull out variables then
   * this will be true. The default is not to use the Inline Parser.
   */
  private boolean           m_InlineVariables  = false;

  /**
   *
   */
  private boolean           m_IsCallable = false;

  /**
   *
   */
  private String            m_OrigQuery = null;

  /**
   *
   */
  private StringBuffer      m_ParsedQuery = null;

  /**
   *
   */
  private Vector            m_Parameters = null;

  /**
   *
   */
  private boolean           m_hasOutput = false;

  /**
   *
   */
  private boolean           m_HasParameters;

  public static final int         NO_OVERRIDE = 0;
  public static final int         NO_INLINE_PARSER = 1;
  public static final int         INLINE_PARSER  = 2;

  /**
   * The SQLStatement Parser will be created as a psuedo SINGLETON per
   * XConnection. Since we are only caching the Query and its parsed results
   * we may be able to use this as a real SINGLETON. It all depends on how
   * Statement Caching will play out.
   */
  public SQLQueryParser()
  {
    init();
  }

  /**
   * Constructor, used to create a new parser entry
   */
  private SQLQueryParser(String query)
  {
    m_OrigQuery = query;
  }

  /**
   * On a per Xconnection basis, we will create a SQLStatemenetParser, from
   * this parser, individual parsers will be created. The Init method is defined
   * to initialize all the internal structures that maintains the pool of parsers.
   */
  private void init()
  {
    // Do nothing for now.
  }

  /**
   * Produce an SQL Statement Parser based on the incomming query.
   *
   * For now we will just create a new object, in the future we may have this
   * interface cache the queries so that we can take advantage of a preparsed
   * String.
   *
   * If the Inline Parser is not enabled in the Options, no action will be
   * taken on the parser. This option can be set by the Stylesheet. If the
   * option is not set or cleared, a default value will be set determined
   * by the way variables were passed into the system.
   */
  public SQLQueryParser parse(XConnection xconn, String query, int override)
  {
    SQLQueryParser parser = new SQLQueryParser(query);

    // Try to implement caching here, if we found a parser in the cache
    // then just return the instance otherwise
    parser.parse(xconn, override);

    return parser;
  }



  /**
   * Produce an SQL Statement Parser based on the incomming query.
   *
   * For now we will just create a new object, in the future we may have this
   * interface cache the queries so that we can take advantage of a preparsed
   * String.
   *
   * If the Inline Parser is not enabled in the Options, no action will be
   * taken on the parser. This option can be set by the Stylesheet. If the
   * option is not set or cleared, a default value will be set determined
   * by the way variables were passed into the system.
   */
  private void parse(XConnection xconn, int override)
  {
    // Grab the Feature here. We could maintain it from the Parent Parser
    // but that may cause problems if a single XConnection wants to maintain
    // both Inline Variable Statemens along with NON inline variable statements.

    m_InlineVariables = "true".equals(xconn.getFeature("inline-variables"));
    if (override == NO_INLINE_PARSER) m_InlineVariables = false;
    else if (override == INLINE_PARSER) m_InlineVariables = true;

    if (m_InlineVariables) inlineParser();

  }

  /**
   * If a SQL Statement does not have any parameters, then it can be executed
   * directly. Most SQL Servers use this as a performance advantage since no
   * parameters need to be parsed then bound.
   */
  public boolean hasParameters()
  {
    return m_HasParameters;
  }

  /**
   * If the Inline Parser is used, the parser will note if this stastement is
   * a plain SQL Statement or a Called Procedure. Called Procudures generally
   * have output parameters and require special handling.
   *
   * Called Procudures that are not processed with the Inline Parser will
   * still be executed but under the context of a PreparedStatement and
   * not a CallableStatement. Called Procudures that have output parameters
   * MUST be handled with the Inline Parser.
   */
  public boolean isCallable()
  {
    return m_IsCallable;
  }


  /**
   *
   */
  public Vector getParameters()
  {
    return m_Parameters;
  }

  /**
   * The XConnection will use this method to store the Parameters
   * that were supplied by the style sheet in the case where the
   * inline parser was not used
   */
  public void setParameters(Vector p)
  {
    m_HasParameters = true;
    m_Parameters = p;
  }

  /**
   * Return a copy of the parsed SQL query that will be set to the
   * Database system to execute. If the inline parser was not used,
   * then the original query will be returned.
   */
  public String getSQLQuery()
  {
    if (m_InlineVariables) return m_ParsedQuery.toString();
    else return m_OrigQuery;
  }


  /**
   * The SQL Statement Parser, when an Inline Parser is used, tracks the XSL
   * variables used to populate a statement. The data use to popoulate a
   * can also be provided. If the data is provided, it will overide the
   * populastion using XSL variables. When the Inline PArser is not used, then
   * the Data will always be provided.
   *
   */
  public void populateStatement(PreparedStatement stmt, ExpressionContext ctx)
  {
    // Set input parameters from variables.
//    for ( int indx = returnParm ? 1 : 0 ; indx < m_Parameters.size() ; indx++ )

    for ( int indx = 0 ; indx < m_Parameters.size() ; indx++ )
    {
      QueryParameter parm = (QueryParameter) m_Parameters.elementAt(indx);

      try
      {

        if (m_InlineVariables)
        {
          XObject value = (XObject)ctx.getVariableOrParam(new QName(parm.getName()));

          if (value != null)
          {
            stmt.setObject(
              indx + 1,
              value.object(),
              parm.getType(), 4);	// Currently defaulting scale to 4 - should read this!
          }
          else
          {
            stmt.setNull(indx + 1, parm.getType());
          }
        }
        else
        {
          String value = parm.getValue();

          if (value != null)
          {
            stmt.setObject(
              indx + 1,
              value,
              parm.getType(), 4);	// Currently defaulting scale to 4 - should read this!
          }
          else
          {
            stmt.setNull(indx + 1, parm.getType());
          }
        }
      }
      catch (Exception tx)
      {
//        if ( ! parm.isOutput() ) throw new SQLException(tx.toString());
      }
    }

  }

  public void registerOutputParameters(CallableStatement cstmt) throws SQLException
  {
    // Register output parameters if call.
    if ( m_IsCallable && m_hasOutput )
    {
      for ( int indx = 0 ; indx < m_Parameters.size() ; indx++ )
      {
        QueryParameter parm = (QueryParameter) m_Parameters.elementAt(indx);
        if ( parm.isOutput() )
        {
          //System.out.println("chrysalisSQLStatement() Registering output parameter for parm " + indx);
          cstmt.registerOutParameter(indx + 1, parm.getType());
        }
      }
    }
 }

  /**
   *
   */
  protected void inlineParser()
  {
    QueryParameter  curParm = null;
    int	            state = 0;
    StringBuffer    tok = new StringBuffer();
    boolean         firstword = true;

    if (m_Parameters == null) m_Parameters = new Vector();

    if (m_ParsedQuery == null) m_ParsedQuery = new StringBuffer();

    for ( int idx = 0 ; idx < m_OrigQuery.length() ; idx++ )
    {
      char ch = m_OrigQuery.charAt(idx);
      switch ( state )
      {

        case	0:	// Normal
          if ( ch == '\'' ) state = 1;
          else if ( ch == '?' ) state = 4;
          else if ( firstword && (Character.isLetterOrDigit(ch) || ch == '#') )
          {
            tok.append(ch);
            state = 3;
          }
          m_ParsedQuery.append(ch);
          break;

        case	1:	// In String
          if ( ch == '\'' ) state = 0;
          else if ( ch == '\\' ) state = 2;
          m_ParsedQuery.append(ch);
          break;

        case	2:	// In escape
          state = 1;
          m_ParsedQuery.append(ch);
          break;

        case	3:	// First word
          if ( Character.isLetterOrDigit(ch) || ch == '#' || ch == '_' ) tok.append(ch);
          else
          {
            if ( tok.toString().equalsIgnoreCase("call") )
            {
              m_IsCallable = true;
              if ( curParm != null )
              {
                // returnParm = true;
                curParm.setIsOutput(true);
                // hasOutput = true;
              }
            }
            firstword = false;
            tok = new StringBuffer();
            if ( ch == '\'' ) state = 1;
            else if ( ch == '?' ) state = 4;
            else state = 0;
          }

          m_ParsedQuery.append(ch);
          break;

        case	4:	// Get variable definition
          if ( ch == '[' ) state = 5;
          break;

        case	5:	// Read variable type.
          if ( !Character.isWhitespace(ch) && ch != '=' )
          {
            tok.append(Character.toUpperCase(ch));
          }
          else if ( tok.length() > 0 )
          {
            // OK we have at least one parameter.
            m_HasParameters = true;

            curParm = new QueryParameter();

            curParm.setTypeName(tok.toString());
//            curParm.type = map_type(curParm.typeName);
            m_Parameters.addElement(curParm);
            tok = new StringBuffer();
            if ( ch == '=' ) state = 7;
            else state = 6;
          }
          break;

        case	6:	// Look for '='
          if ( ch == '=' ) state = 7;
          break;

        case	7:	// Read variable name.
          if ( !Character.isWhitespace(ch) && ch != ']' ) tok.append(ch);
          else if ( tok.length() > 0 )
          {
            curParm.setName(tok.toString());
            tok = new StringBuffer();
            if ( ch == ']' )
            {
              //param_output.addElement(new Boolean(false));
              state = 0;
            }
            else state = 8;
          }
          break;

        case	8:	// Look for "OUTput.
          if ( !Character.isWhitespace(ch) && ch != ']' )
          {
            tok.append(ch);
          }
          else if ( tok.length() > 0 )
          {
            tok.setLength(3);
            if ( tok.toString().equalsIgnoreCase("OUT") )
            {
              curParm.setIsOutput(true);
              m_hasOutput = true;
            }

            tok = new StringBuffer();
            if ( ch == ']' )
            {
              state = 0;
            }
          }
          break;
      }
    }


    // Prepare statement or call.
    if ( m_IsCallable )
    {
      m_ParsedQuery.insert(0, '{');
      m_ParsedQuery.append('}');
    }

  }

}

