| 1 |
#-*- coding=iso-8859-15 -*- |
|---|
| 2 |
# (C) Copyright 2004 Nuxeo SARL <http://nuxeo.com> |
|---|
| 3 |
# Author: Georges Racinet <gracinet@nuxeo.com> |
|---|
| 4 |
# |
|---|
| 5 |
# This program is free software; you can redistribute it and/or modify |
|---|
| 6 |
# it under the terms of the GNU General Public License version 2 as published |
|---|
| 7 |
# by the Free Software Foundation. |
|---|
| 8 |
# |
|---|
| 9 |
# This program is distributed in the hope that it will be useful, |
|---|
| 10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|---|
| 11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|---|
| 12 |
# GNU General Public License for more details. |
|---|
| 13 |
# |
|---|
| 14 |
# You should have received a copy of the GNU General Public License |
|---|
| 15 |
# along with this program; if not, write to the Free Software |
|---|
| 16 |
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA |
|---|
| 17 |
# 02111-1307, USA. |
|---|
| 18 |
# |
|---|
| 19 |
# $Id$ |
|---|
| 20 |
"""CPSDirectory.utils: a module that provides utilities to be |
|---|
| 21 |
used by CPSDirectory classes. """ |
|---|
| 22 |
|
|---|
| 23 |
from types import ListType, TupleType, StringType |
|---|
| 24 |
import re |
|---|
| 25 |
import operator |
|---|
| 26 |
from types import NoneType |
|---|
| 27 |
from DateTime import DateTime |
|---|
| 28 |
|
|---|
| 29 |
from Products.CPSUtil.text import toAscii |
|---|
| 30 |
|
|---|
| 31 |
def operator_in(a, b): |
|---|
| 32 |
# operator.contains with reversed operands |
|---|
| 33 |
return a in b |
|---|
| 34 |
|
|---|
| 35 |
def operator_notin(a, b): |
|---|
| 36 |
return a not in b |
|---|
| 37 |
|
|---|
| 38 |
|
|---|
| 39 |
class QueryMatcher(object): |
|---|
| 40 |
""" Hold/prepare a query and allow to match entries against it. |
|---|
| 41 |
|
|---|
| 42 |
>>> qm = QueryMatcher({'id' : 'foo', 'spam' : 'eggs'}, |
|---|
| 43 |
... accepted_keys=['id']) |
|---|
| 44 |
>>> qm.match({'id':'foo'}) |
|---|
| 45 |
True |
|---|
| 46 |
|
|---|
| 47 |
>>> qm = QueryMatcher({'id' : 'foo', 'spam' : 'eggs'}, |
|---|
| 48 |
... accepted_keys=['id']) |
|---|
| 49 |
>>> qm.match({'id':'the Foo'}) |
|---|
| 50 |
False |
|---|
| 51 |
|
|---|
| 52 |
>>> qm = QueryMatcher({'id' : 'foo'}, accepted_keys=['id'], |
|---|
| 53 |
... substring_keys=['id']) |
|---|
| 54 |
>>> qm.match({'id':'SpamFooEggs'}) |
|---|
| 55 |
True |
|---|
| 56 |
|
|---|
| 57 |
Substring behaviour and accents: |
|---|
| 58 |
>>> qm = QueryMatcher({'id' : 'fooe'}, accepted_keys=['id'], |
|---|
| 59 |
... substring_keys=['id']) |
|---|
| 60 |
>>> qm.match({'id':'SpamFooÉggs'}) |
|---|
| 61 |
True |
|---|
| 62 |
|
|---|
| 63 |
>>> qm = QueryMatcher({'id' : 'fooé'}, accepted_keys=['id'], |
|---|
| 64 |
... substring_keys=['id']) |
|---|
| 65 |
>>> qm.match({'id':'SpamFooEggs'}) |
|---|
| 66 |
True |
|---|
| 67 |
|
|---|
| 68 |
>>> qm = QueryMatcher({'enabled': False}, accepted_keys=['enabled']) |
|---|
| 69 |
>>> qm.match({'enabled': True}) |
|---|
| 70 |
False |
|---|
| 71 |
>>> qm.match({'enabled': False}) |
|---|
| 72 |
True |
|---|
| 73 |
|
|---|
| 74 |
>>> qm = QueryMatcher({'enabled': True}, accepted_keys=['enabled']) |
|---|
| 75 |
>>> qm.match({'enabled': True}) |
|---|
| 76 |
True |
|---|
| 77 |
>>> qm.match({'enabled': False}) |
|---|
| 78 |
False |
|---|
| 79 |
|
|---|
| 80 |
We don't fail if the entry lacks keys from the query |
|---|
| 81 |
>>> qm.match({}) |
|---|
| 82 |
False |
|---|
| 83 |
|
|---|
| 84 |
""" |
|---|
| 85 |
|
|---|
| 86 |
def __init__(self, query, accepted_keys=None, substring_keys=None): |
|---|
| 87 |
self._substring_keys = substring_keys |
|---|
| 88 |
_query = {} |
|---|
| 89 |
ops = {} |
|---|
| 90 |
for key, value in query.items(): |
|---|
| 91 |
if accepted_keys is not None and key not in accepted_keys: |
|---|
| 92 |
continue |
|---|
| 93 |
value, op = self._findType(key, value) |
|---|
| 94 |
if op is None: |
|---|
| 95 |
continue |
|---|
| 96 |
ops[key] = op |
|---|
| 97 |
_query[key] = value |
|---|
| 98 |
self.query = _query |
|---|
| 99 |
self.ops = ops |
|---|
| 100 |
|
|---|
| 101 |
def _findType(self, key, value, negate=False): |
|---|
| 102 |
"""Find op and value. |
|---|
| 103 |
""" |
|---|
| 104 |
if value in ('', None): # XXX |
|---|
| 105 |
if negate: |
|---|
| 106 |
raise NotImplementedError |
|---|
| 107 |
# Ignore empty searches |
|---|
| 108 |
return value, None |
|---|
| 109 |
elif isinstance(value, basestring): |
|---|
| 110 |
if (self._substring_keys is not None |
|---|
| 111 |
and key in self._substring_keys): |
|---|
| 112 |
if negate: |
|---|
| 113 |
raise NotImplementedError |
|---|
| 114 |
op = 'substring' |
|---|
| 115 |
value = value.lower() |
|---|
| 116 |
else: |
|---|
| 117 |
if negate: |
|---|
| 118 |
op = operator.ne |
|---|
| 119 |
else: |
|---|
| 120 |
op = operator.eq |
|---|
| 121 |
elif isinstance(value, (int, long, bool)): |
|---|
| 122 |
if negate: |
|---|
| 123 |
op = operator.ne |
|---|
| 124 |
else: |
|---|
| 125 |
op = operator.eq |
|---|
| 126 |
elif isinstance(value, (list, tuple)): |
|---|
| 127 |
if negate: |
|---|
| 128 |
op = operator_notin |
|---|
| 129 |
else: |
|---|
| 130 |
op = operator_in |
|---|
| 131 |
elif isinstance(value, dict) and 'query' in value: |
|---|
| 132 |
if negate and value.get('negate'): |
|---|
| 133 |
raise ValueError("Cannot double negate") |
|---|
| 134 |
query = value['query'] |
|---|
| 135 |
return self._findType(key, query, negate=True) |
|---|
| 136 |
elif isinstance(value, DateTime): |
|---|
| 137 |
if negate: |
|---|
| 138 |
op = operator.ne |
|---|
| 139 |
else: |
|---|
| 140 |
op = operator.eq |
|---|
| 141 |
else: |
|---|
| 142 |
raise ValueError("Bad value %s for '%s'" % (`value`, key)) |
|---|
| 143 |
return value, op |
|---|
| 144 |
|
|---|
| 145 |
def getKeysSet(self): |
|---|
| 146 |
return set(self.query) |
|---|
| 147 |
|
|---|
| 148 |
def match(self, entry): |
|---|
| 149 |
""" Does the entry match the query ? Boolean valued. |
|---|
| 150 |
""" |
|---|
| 151 |
ops = self.ops |
|---|
| 152 |
ok = True |
|---|
| 153 |
for key, value in self.query.items(): |
|---|
| 154 |
if key not in entry: |
|---|
| 155 |
ok = False |
|---|
| 156 |
break |
|---|
| 157 |
searched = entry[key] |
|---|
| 158 |
if isinstance(searched, (basestring, int, long, NoneType, DateTime)): |
|---|
| 159 |
# bool subclasses int |
|---|
| 160 |
searched = (searched,) |
|---|
| 161 |
matched = 0 |
|---|
| 162 |
value_re = None |
|---|
| 163 |
op = ops[key] |
|---|
| 164 |
if isinstance(value, basestring): |
|---|
| 165 |
if op == 'substring': |
|---|
| 166 |
value = toAscii(value).lower() |
|---|
| 167 |
if '*' in value or '?' in value: |
|---|
| 168 |
regexp = re.escape(value) |
|---|
| 169 |
regexp = regexp.replace('\\?', '.?') |
|---|
| 170 |
regexp = regexp.replace('\\*', '.*') |
|---|
| 171 |
value_re = re.compile(regexp) |
|---|
| 172 |
|
|---|
| 173 |
for item in searched: |
|---|
| 174 |
# Wild cards like * are currently accepted |
|---|
| 175 |
if op == 'substring': |
|---|
| 176 |
matched = value in toAscii(item).lower() or value == '*' |
|---|
| 177 |
else: # op is an operator |
|---|
| 178 |
matched = op(item, value) |
|---|
| 179 |
if matched: |
|---|
| 180 |
break |
|---|
| 181 |
if not matched: |
|---|
| 182 |
ok = False |
|---|
| 183 |
break |
|---|
| 184 |
return ok |
|---|