Add unit test for the query parser, fix some bugs in said query parser, and upgrade...
[python-rwhoisd.git] / rwhoisd / QueryParser.py
1 # This file is part of python-rwhoisd
2 #
3 # Copyright (C) 2003, David E. Blacka
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 as published by
7 # the Free Software Foundation; either version 2 of the License, or
8 # (at your option) any later version.
9 #
10 # This program is distributed in the hope that it will be useful, but
11 # WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13 # General Public License for more details.
14 #
15 # You should have received a copy of the GNU General Public License
16 # along with this program; if not, write to the Free Software
17 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
18 # USA
19
20 # This module uses PLY (Python Lex-Yacc).  See
21 # http://www.dabeaz.com/ply/ for more info.
22
23 # This module is actually the grammar definition.  The lexer finds
24 # variables and functions starting with 't_', as well as a list called
25 # 'tokens'.  The parser (i.e., yacc) finds things starting with 'p_'.
26
27 # queryparser.db must be set to a DB class instance.
28 db = None
29
30 # Define the Lexer for the RWhois query language
31
32 tokens = (
33     'VALUE',
34     'QUOTEDVALUE',
35     'CLASS',
36     'ATTR',
37     'AND',
38     'OR',
39     'EQ',
40     'NEQ'
41     )
42
43 # whitespace
44 t_ignore = ' \t'
45 # equality
46 t_EQ = r'='
47 # inequality
48 t_NEQ = r'!='
49
50 # for now, quoted values must have the wildcards inside the quotes.
51 # I kind of wonder if anyone would ever notice this.
52 t_QUOTEDVALUE = r'["\']\*?[^"*\n]+\*{0,2}["\']'
53
54 def t_firstvalue(t):
55     r'^\*?[^\s"\'=*]+\*{0,2}'
56
57     if db.is_objectclass(t.value):
58         t.type = 'CLASS'
59     elif db.is_attribute(t.value):
60         t.type = 'ATTR'
61     else:
62         t.type = 'VALUE'
63     return t
64
65 def t_VALUE(t):
66     r'\*?[^\s"\'=!*]+\*{0,2}'
67
68     v = t.value.upper()
69     if v == 'AND':
70         t.type = 'AND'
71         t.value = v
72         return t
73     if v == 'OR':
74         t.type = 'OR'
75         t.value = v
76         return t
77     if db.is_attribute(t.value):
78         t.type = 'ATTR'
79     else:
80         t.type = 'VALUE'
81     return t
82
83
84 def t_error(t):
85     pass
86     # print "Illegal character '%r'" % t.value[0]
87     # t.type = 'ERR'
88     # t.skip(1)
89
90 # initalize the lexer
91 import lex
92 lex.lex()
93
94 # Define the parser for the query language
95
96 # 'value' productions are simple strings
97 # 'querystr' productions are tuples (either 1 or 3 values)
98 # 'query' productions are Query objects
99 # 'total' productions are Query objects
100
101 def p_total_class_query(t):
102     'total : CLASS query'
103
104     t[0] = t[2]
105     t[0].set_class(t[1])
106
107 def p_total_query(t):
108     'total : query'
109
110     t[0] = t[1]
111
112
113 def p_query_oper_querystr(t):
114     '''query : query AND querystr
115              | query OR  querystr'''
116
117     t[0] = t[1]
118     if t[2] == 'OR':
119         t[0].cur_clause  = [ t[3] ]
120         t[0].clauses.append(t[0].cur_clause)
121     else:
122         t[0].cur_clause.append(t[3])
123
124 def p_query_querystr(t):
125     'query : querystr'
126
127     t[0] = Query()
128     t[0].cur_clause = [ t[1] ]
129     t[0].clauses.append(t[0].cur_clause)
130
131 def p_querystr_attr_value(t):
132     '''querystr : ATTR EQ value
133                 | ATTR NEQ value'''
134
135     t[0] = (t[1], t[2], t[3])
136
137 def p_querystr_attr_attr(t):
138     '''querystr : ATTR EQ ATTR
139                 | ATTR NEQ ATTR'''
140
141     t[0] = (t[1], t[2], t[3])
142
143 def p_querystr_value(t):
144     '''querystr : ATTR
145                 | value'''
146
147     t[0] = (None, '=', t[1])
148
149 def p_value(t):
150     'value : VALUE'
151
152     t[1] = t[1].strip()
153     if t[1]:
154         t[0] = t[1]
155
156 def p_quotedvalue(t):
157     'value : QUOTEDVALUE'
158
159     t[0] = t[1].strip('"')
160
161
162 def p_error(t):
163      raise yacc.YaccError, "Syntax error at %r" % t.value
164
165     
166 import types
167 class Query:
168     """A representation of a parsed RWhois query."""
169     
170     def __init__(self):
171         self.clauses     = []
172         self.cur_clause  = None
173         self.objectclass = None
174         self.prepared    = False
175         
176     def __str__(self):
177         self._prepare()
178         res = ''
179         for i in range(len(self.clauses)):
180             cl = self.clauses[i]
181             res += "clause %d:\n" % i
182             for item in cl:
183                 res += "  " + repr(item) + "\n"
184         return res
185
186     def __repr__(self):
187         return "<Query:\n" + str(self) + ">"
188
189     def _prepare(self):
190         """Prepare the query for use.  For now, this means propagating
191         an objectclass restriction to all query clauses."""
192         
193         if self.prepared: return
194         if self.objectclass:
195             for c in self.clauses:
196                 c.append(("class-name", "=", self.objectclass))
197
198     def clauses(self):
199         """Return the query clauses.  This is a list of AND clauses,
200         which are, in turn, lists of query terms.  Query terms are 3
201         element tuples: (attr, op, value)."""
202         
203         return self.clauses
204     
205     def set_class(self, objectclass):
206         """Set the query-wide objectclass restriction."""
207
208         # note: we don't allow the code to set this more than once,
209         # because we would have to code the removal of the previous
210         # class restriction from the query clauses, and it just isn't
211         # worth it.  Queries are built, used and thrown away.
212         assert not self.prepared
213         self.objectclass = objectclass
214         return
215
216
217 import yacc
218 import Rwhois
219
220 def get_parser():
221     """Return a parser instances.  Parser objects should not be shared
222     amongst threads."""
223
224     return yacc.yacc()
225
226 def parse(p, query):
227     """Parse a query, raising a RwhoisError in case of parse failure.
228     Returns a Query object."""
229
230     # before using any parser objects, the database backend must be
231     # set (and it shared by all parsers).
232     assert db
233     try:
234         return p.parse(query)
235     except (lex.LexError, yacc.YaccError), e:
236         raise Rwhois.RwhoisError, (350, "Invalid Query Syntax: " + e.message)