# -*- coding: UTF-8 -*-
#    This file is part of arena.
#
#    arena is free software: you can redistribute it and/or modify it
#    under the terms of the GNU General Public License as published by the
#    Free Software Foundation; either version 3 of the License or, at your
#    option, any later version.
#
#    arena is distributed in the hope that it will be useful, but
#    WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABLITY
#    or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public
#    License for more details.
#
#    You should have received a copy of the GNU General Public License along
#    with arena.  If not, see <http://www.gnu.org/licenses/>.

## @file
#  This file defines a query class that can be used as the basis for SQL queries
#  executed against a database. 

from Library.DataSource.Queries import AQuery
from Library.String.Utilities import String

## Events generated by AnSQLQuery
sqEvents = {
    # Before the query string is prepared
    "prepareQueryString": None
}

## The delimiter used when creating a list for a query
sqListDelimiter = ","
## The delimiter used to separate field names and values
sqFieldValuesDelimiter = "="
## Special characters that must be escaped in SQL query strings
sqSpecialCharacters = {
    "'": "''"
}
## The character to use when quoting string literals in an SQL query string
sqQuoteCharacter = "'"

## A basic SQL query executed against a data source.
#
#  Note that this class does not implement the execute() method inherited from
#  ADataOperation and so it should not be instantiated directly.
class AnSQLQuery(AQuery):
    ## Construct a new query instance that will operate on the specified data
    #  source.
    def __init__(Self, **params):
        AQuery.__init__(Self, **params)

    ## Initializer
    def init(Self):
        result = AQuery.init(Self)

        if result:
            # Establish private attributes
            # The query string used by the query
            Self._queryString = ""
            
            # Update the events generated by the query
            Self._events.update(sqEvents)
            
        return result

    # Utility functions --------------------------------------------------------
    ## Escape special characters in the specified string so that it is
    #  safe for use with an SQL query.
    #
    #  This routine calls String.escape() using #sqSpecialCharacters.
    def escapeString(Self, thisString):
        if len(thisString) > 0:
            return String.escape(thisString, sqSpecialCharacters)

        return thisString

    ## Return the quoted form of the specified string, such that it is
    #  suitable for use with an SQL query.
    #
    #  This routine calls String.quote() using #sqQuoteCharacter.
    def quoteString(Self, thisString):
        return String.quote(thisString, sqQuoteCharacter)

    ## Process a value contained within a format string.
    #
    #  The method serves a callback for String.format(); it attempts to harden
    #  a given format string against SQL injection attacks by escaping string
    #  values passed to it.  All values passed to it are converted to strings,
    #  but only values that were originally strings are escaped. 
    def processFormatValue(Self, value, posValues, kwValues):
        fieldValue = ""
        # A string value in the sequence can be a sub-string or field name
        if isinstance(value, basestring):
            # If the value is a key in the keyed values list, then we have a
            # field name
            if value in kwValues:
                fieldValue = kwValues[value]
                if isinstance(fieldValue, basestring):
                    fieldValue = Self.escapeString(fieldValue)
                    fieldValue = Self.quoteString(fieldValue)

                else:
                    fieldValue = value

        # Otherwise, we have a positional index
        else:
            fieldValue = posValues[value]
            if isinstance(fieldValue, basestring):
                fieldValue = Self.escapeString(fieldValue)
                fieldValue = Self.quoteString(fieldValue)

        return str(fieldValue)

    ## Format a query string for use with the data source.
    #
    #  This routine attempts to harden the string against SQL injection attacks
    #  by passing all given string values to ADataSource.escapeString().  All
    #  values provided are converted to strings.
    #
    #  This routine is necessary because str.format() removes single-quote
    #  characters from string values as it processes them. 
    def formatString(Self, fmt, *posValues, **kwValues):
        formatLength = len(fmt)

        if formatLength == 0:
            return fmt

        return String.formatUsing(Self.processFormatValue, fmt, 
          *posValues, **kwValues)

    ## Return the specified identifier, delimited with the special quotes
    #  used by the data source for identifiers. 
    #
    #  If the data source for which the query is meant to be use does not use
    #  special quote characters, this routine should simply return the
    #  identifier string, unchanged.
    def quoteIdentifier(Self, thisIdentifier):
        return thisIdentifier

    ## Implode the identifiers contained in the specified list, placing
    #  database-specific quotation marks around each one.
    def implodeIdentifiersIn(Self, thisList, itemsDelimiter=sqListDelimiter):
        result = ""

        if len(thisList) > 0:
            index = 0

            for identifier in thisList:
                if index > 0:
                    result += itemsDelimiter

                result += Self.quoteIdentifier(identifier)
                index += 1

        return result

    ## Implode the values in the specified list, escaping any string
    #  values that are found.
    def implodeValuesIn(Self, thisList, itemsDelimiter=sqListDelimiter):
        result = ""

        index = 0
        for value in thisList:
            if index > 0:
                result += itemsDelimiter

            if isinstance(value, basestring):
                value = Self.escapeString(value)           
                value = Self.quoteString(value)               

            result += str(value)         
            index += 1

        return result

    ## Implode the fields and values in the specified dictionary object,
    #  quoting identifiers and escaping fields as appropriate.
    def implodeFieldsIn(Self, thisDict):
        result = ""

        if len(thisDict) > 0:
            index = 0
            for name, value in thisDict.iteritems():
                if index > 0:
                    result += sqListDelimiter

                name = Self.quoteIdentifier(name)

                if isinstance(value, basestring):
                    value = Self.escapeString(value)
                    value = Self.quoteString(value)

                result += name + sqFieldValuesDelimiter + str(value)
                index += 1

        return result
        
    ## Process a placeholder from a criterion format string.
    #
    #  Criterion format strings are simple expressions of the form:
    #  <tt>identifier OPERATOR identifier</tt>.  The \p identifier elements
    #  should be specified by placeholder values in the format string.
    #  This function will replace those placeholders with the values from the
    #  positional arguments supplied to it.
    def processCriterionPlaceholder(Self, value, posValues, keyValues):
        result = ""
        
        # Named placeholders are special within the criterion format string:
        # they don't refer to anything in the keyword parameters; rather, they
        # are used to specify where identifiers are placed (and quoted) in the
        # formatted string.
        if isinstance(value, basestring):
            # Split the value into its type and name
            identifier = value.split(":")
            
            if identifier[0].lower() == "field":
                result = Self.quoteIdentifier(identifier[1])
                
            else:
                result = str(identifier[0])
                
        else:
            result = posValues[value]          
            if isinstance(result, basestring):
                result = Self.escapeString(result)
                result = Self.quoteString(result)            
                
            elif isinstance(result, list):             
                result = Self.implodeValuesIn(result)             
                    
            elif isinstance(result, tuple):
                result = Self.implodeValuesIn(result)
        
        return str(result)
        

    ## Implode the fields and values in the specified criteria list,
    #  quoting the identifiers and escaping (and quoting) the string values.
    #
    #  Each criterion should be a simple expression of the form:
    #  <tt>identifier OPERATOR identifier</tt>.  The \p identifier elements
    #  of the criterion can be a field name or a list of values.
    #
    #  Each criterion should be a list (or tuple), where the first item is
    #  a format string and the remaining items are the values to be inserted
    #  into the format string.
    def implodeCriteriaIn(Self, thisList):
        result = ""
        index = 0
        # Parse the list
        for criterion in thisList:
            if index > 0:
                result += " "
                
            index += 1
            
            fmt = criterion[0]
            posValues = criterion[1:]
            
            result += String.formatUsing(Self.processCriterionPlaceholder,
                fmt, *posValues)
                
        return result

    ## Prepare the query string for submission to the data source.
    #
    #  This routine should be implemented by descendant classes.  It should
    #  construct the query string from the query attributes so that the query
    #  can be successfully submitted to the data source.
    def prepareQueryString(Self):
        Event = Self.generate("prepareQueryString")
        
        del Event

    # IsPrintable implementation -----------------------------------------------
    ## Construct a string representation of the data source.
    #
    #  This representation will be the query string that is used by the query,
    #  if that is available, or the display name of the query class otherwise.
    def toString(Self):
        if len(Self._queryString) > 0:
            return Self._queryString

        else:
            return AQuery.toString(Self)

    # Properties ---------------------------------------------------------------
    ## Return the string used by the query.
    def queryString(Self):
        return Self._queryString
