SearchKey.py 9.54 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244
##############################################################################
#
# Copyright (c) 2005 Nexedi SARL and Contributors. All Rights Reserved.
#                     Ivan Tyagov <ivan@nexedi.com>
#
# WARNING: This program as such is intended to be used by professional
# programmers who take the whole responsability of assessing all potential
# consequences resulting from its eventual inadequacies and bugs
# End users who are looking for a ready-to-use solution with commercial
# garantees and support are strongly adviced to contract a Free Software
# Service Company
#
# This program 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 2
# of the License,] or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY 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 this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
#
##############################################################################

from Products.ZSQLCatalog.Query.SimpleQuery import SimpleQuery as Query
from Products.ZSQLCatalog.Query.ComplexQuery import ComplexQuery
from Products.ZSQLCatalog.SQLCatalog import getSearchKeyInstance

import ply.yacc as yacc
import ply.lex as lex

class SearchKey:
  """ BaseKey is a base class that implements a parser of 
      search grammar used in ERP5. It also implements all generic 
      search key class methods."""

  # main logical operators
  operators = ('OR', 'AND',)
  default_operator = '='

  # in ERP5 search grammar white space is extremely important
  # so we can not ignore it.
  #t_ignore  = ' \t' 

  # no need to rack down line numbers
  #def t_newline(self, t):
  #  r'\n+'
  #  #t.lexer.lineno += len(t.value)

  def t_error(self, t):
    #print "Illegal character '%s'" % t.value[0]
    t.lexer.skip(1)

  def p_error(self, p):
    pass

  def build(self, **kwargs):
    """ This method will initialize respective search key class with 
        tokens' definitions. """
    self.lexer = lex.lex(object = self, **kwargs)

  def tokenize(self, data):
    """ Return list of tokens according to respective 
        search key tokens' definitions. """
    result = []
    self.lexer.input(data)
    while 1:
      tok = self.lexer.token()
      if not tok: 
        break
      result.append(tok) 
    return result

  # Grouping of tokens
  def getOperatorForTokenList(self, tokens):
    """ Generic implementation that will return respective 
        operator for a token list. The first found occurence wins."""
    token = tokens[0]
    token_type = token.type
    if token_type in self.sub_operators:
      return token.value, tokens[1:]
    else:
      return self.default_operator, tokens    

  def groupByLogicalOperator(self, tokens, logical_operator ='OR'):
    """ Split tokens list into one or many OR concatanated tokens list 
    """
    sub_tokens_or_groups = []
    tmp_token_list = []
    for token in tokens:
      if token.type != logical_operator:
        tmp_token_list.append(token)
      else:
        sub_tokens_or_groups.append(tmp_token_list)
        tmp_token_list = []
    # append remainig last tokens
    sub_tokens_or_groups.append(tmp_token_list)
    return sub_tokens_or_groups    

  # SQL quoting (each search key should override them it if needed)
  def quoteSQLKey(self, key, format):
    """ Return a quoted string of the value. """
    return key

  def quoteSQLString(self, value, format):
    """ Return a quoted string of the value. """
    return "'%s'" %value

  # SQL generation
  def buildSQLExpression(self, key, value, 
                               format=None, mode=None, range_value=None, stat__=0):
    """ Generic implementation. Leave details to respective key. """
    if range_value is not None:
      # if range_value we handle directly (i.e no parsing of search string)
      where_expressions, select_expressions = \
         self.buildSQLExpressionFromRange(key, value, 
                                          format, mode, range_value, stat__) 
    else:
      # search string parsing is needed
      where_expressions, select_expressions = \
        self.buildSQLExpressionFromSearchString(key, str(value), 
                                                format, mode, range_value, stat__) 
    return where_expressions, select_expressions

  def buildSQLExpressionFromSearchString(self, key, value, format, mode, range_value, stat__):
    complex_query = self.buildQuery(key, value, format, mode, range_value, stat__)
    if complex_query is None:
      # Query could not be generated from search string
      sql_expression = {'where_expression': '1', 
                        'select_expression_list': []}
    else:
      sql_expression = complex_query(keyword_search_keys = [],
                                     datetime_search_keys = [],
                                     full_text_search_keys = [])
    return sql_expression['where_expression'], sql_expression['select_expression_list']

  def buildQuery(self, key, value, format, mode, range_value, stat__):
    """ Build Query """
    query_list = []
    # tokenize searchs string into tokens for Search Key
    tokens = self.tokenize(value)

    # split tokens list into one or more 'OR' tokens lists
    tokens_or_groups = self.groupByLogicalOperator(tokens, 'OR')

    # remove empty tokens lists
    tokens_or_groups = filter(lambda x: len(x), tokens_or_groups)

    # get a ComplexQuery for a sub token list
    for tokens_or_group in tokens_or_groups:
      query = self.buildQueryForTokenList(tokens_or_group, key, value, format)
      if query is not None:
        # query could be generated for token list
        query_list.append(query)

    if len(query_list):
      # join query list in one really big ComplexQuery
      return ComplexQuery(*query_list,
                          **{'operator':'OR'})

  def buildQueryForTokenList(self, tokens, key, value, format):
    """ Build a ComplexQuery for a token list """
    query_list = []
    logical_groups = self.groupByLogicalOperator(tokens, 'AND')
    for group_tokens in logical_groups:
      token_values = [x.value for x in group_tokens]
      sub_operator, sub_tokens = self.getOperatorForTokenList(group_tokens)
      sub_tokens_values = [x.value for x in sub_tokens]
      query_kw = {key: ' '.join(sub_tokens_values),
                  'type': self.default_key_type,
                  'format': format,
                  'range': sub_operator}
      query_list.append(Query(**query_kw))

    # join query list in one really big ComplexQuery
    complex_query = ComplexQuery(*query_list, 
                                 **{'operator': 'AND'})
    return complex_query

  def buildSQLExpressionFromRange(self, key, value, format, mode, range_value, stat__):
    """ This method will generate SQL expressions
       from explicitly passed list of values and 
       range_value in ('min', 'max', ..)"""
    key = self.quoteSQLKey(key, format)  
    where_expression = ''
    select_expressions = []
    if isinstance(value, (list, tuple)):
      if len(value) > 1:
        # value should contain at least two items
        query_min = self.quoteSQLString(value[0], format)
        query_max = self.quoteSQLString(value[1], format)
      else:
        # value contains only one item
        query_min = query_max = self.quoteSQLString(value[0], format)          
    else:
      query_min = query_max = self.quoteSQLString(value, format)
    if range_value == 'min':
      where_expression = "%s >= %s" % (key, query_min)
    elif range_value == 'max':
      where_expression = "%s < %s" % (key, query_max)
    elif range_value == 'minmax' :
      where_expression = "%s >= %s AND %s < %s" % (key, query_min, key, query_max)
    elif range_value == 'minngt' :
      where_expression = "%s >= %s AND %s <= %s" % (key, query_min, key, query_max)
    elif range_value == 'ngt':
      where_expression =  "%s <= %s" % (key, query_max)
    elif range_value == 'nlt':
      where_expression = "%s > %s" % (key, query_max)
    elif range_value == 'like':
      where_expression = "%s LIKE %s" % (key, query_max)
    elif range_value == 'not_like':
      where_expression = "%s NOT LIKE %s" % (key, query_max)
    elif range_value in ('=', '>', '<', '>=', '<=','!=',):
      where_expression = "%s %s %s" % (key, range_value, query_max)    
    return where_expression, select_expressions


##  def groupByOperator(self, tokens, group_by_operators_list = operators):
##    """ Generic implementation of splitting tokens into logical
##        groups defided by respective list of logical operator
##        defined for respective search key.  """
##    items = []
##    last_operator = None
##    operators_mapping_list = []
##    last_operator = {'operator': None,
##                     'tokens': []}
##    for token in tokens:
##      token_type = token.type
##      token_value = token.value
##      if token_type in group_by_operators_list:
##        # (re) init it
##        last_operator = {'operator': token,
##                         'tokens': []}
##        operators_mapping_list.append(last_operator)
##      else:
##        # not an operator just a value token
##        last_operator['tokens'].append(token)
##        if last_operator not in operators_mapping_list:
##          operators_mapping_list.append(last_operator)
##    return operators_mapping_list