2 // QueryStringParser.cs
4 // Copyright (C) 2004-2005 Novell, Inc.
8 // Permission is hereby granted, free of charge, to any person obtaining a
9 // copy of this software and associated documentation files (the "Software"),
10 // to deal in the Software without restriction, including without limitation
11 // the rights to use, copy, modify, merge, publish, distribute, sublicense,
12 // and/or sell copies of the Software, and to permit persons to whom the
13 // Software is furnished to do so, subject to the following conditions:
15 // The above copyright notice and this permission notice shall be included in
16 // all copies or substantial portions of the Software.
18 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
23 // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
24 // DEALINGS IN THE SOFTWARE.
28 using System
.Collections
;
31 using FSQ
=Beagle
.Daemon
.FileSystemQueryable
;
33 namespace Beagle
.Daemon
{
35 public class QueryStringParser
{
37 private QueryStringParser () { }
// a static class
39 // Returns an ICollection of QueryPart objects.
40 static public ICollection
Parse (string query_string
)
43 token_list
= new ArrayList ();
46 Token token
= ExtractToken (ref query_string
);
49 token_list
.Add (token
);
52 return TokensToQueryParts (token_list
);
55 /////////////////////////////////////////////////////////
57 private enum TokenType
{
67 public TokenType Type
= TokenType
.Unknown
;
72 // Our tiny query language:
73 // prefix terms by + or - to require or prohibit them
76 static private Token
ExtractToken (ref string in_string
)
78 if (in_string
== null || in_string
.Length
== 0)
81 // Find the first non-whitespace character.
84 bool first_is_singleton
= false;
85 for (int i
= 0; i
< in_string
.Length
; ++i
) {
86 first
= in_string
[i
];
87 if (! Char
.IsWhiteSpace (first
)) {
89 if (i
== in_string
.Length
- 1)
90 first_is_singleton
= true;
95 // This is only whitespace, and thus doesn't
96 // contain any tokens.
101 token
= new Token ();
103 // Based on the first character, decide what kind of a
104 // token this is. Advance first_pos as necessary to
105 // skip special characters (like + and -)
109 if (first_is_singleton
) {
110 token
.Type
= TokenType
.Meta
;
111 token
.Text
= "DanglingPlus";
116 token
.Type
= TokenType
.Plus
;
121 if (first_is_singleton
) {
122 token
.Type
= TokenType
.Meta
;
123 token
.Text
= "DanglingMinus";
128 token
.Type
= TokenType
.Minus
;
133 if (first_is_singleton
) {
134 token
.Type
= TokenType
.Meta
;
135 token
.Text
= "DanglingQuote";
140 token
.Type
= TokenType
.StandAlone
;
141 token
.IsQuoted
= true;
146 token
.Type
= TokenType
.StandAlone
;
151 last
= token
.IsQuoted
? '"' : ' ';
154 last_pos
= in_string
.IndexOf (last
, first_pos
);
156 if (last_pos
== -1) {
157 // We don't worry about missing close-quotes.
158 // FIXME: Maybe we should, or at least return a meta-token
159 token
.Text
= in_string
.Substring (first_pos
);
162 token
.Text
= in_string
.Substring (first_pos
, last_pos
- first_pos
);
163 if (last_pos
== in_string
.Length
-1)
166 in_string
= in_string
.Substring (last_pos
+1);
169 // Trap the OR operator
170 if (token
.Type
== TokenType
.StandAlone
&& token
.Text
== "OR") {
171 token
.Type
= TokenType
.Operator
;
175 // Ah, the dreaded "internal error".
176 if (token
.Type
== TokenType
.Unknown
)
177 throw new Exception ("Internal QueryStringParser.ExtractToken Error");
182 // FIXME support Date queries
183 static private QueryPart
TokenToQueryPart (Token token
) {
184 string query_text
= token
.Text
;
186 // also support extension query of form
188 // i.e. the extension is just given as a query word
189 bool is_extension_query
= ((query_text
[0] == '.') && (query_text
.Length
!= 1));
190 if (is_extension_query
) {
191 QueryPart_Property query_part
= new QueryPart_Property ();
192 query_part
.Key
= FSQ
.FileSystemQueryable
.FilenameExtensionPropKey
;
193 query_part
.Value
= query_text
.ToLower (); // the whole .abc part
194 query_part
.Type
= PropertyType
.Keyword
;
195 Logger
.Log
.Debug ("Extension query:" + query_text
.ToLower ());
199 int pos
= query_text
.IndexOf (":");
201 QueryPart_Text query_part
= new QueryPart_Text ();
202 query_part
.Text
= query_text
;
203 Logger
.Log
.Debug ("Parsed query '" + query_text
+ "' as text_query");
207 string prop_name
= query_text
.Substring (0, pos
);
208 string prop_string
= null;
210 PropertyType prop_type
;
211 is_present
= PropertyKeywordFu
.GetPropertyDetails (prop_name
, out prop_string
, out prop_type
);
212 // if prop_name is not present in the mapping, assume the query is a text query
213 // i.e. if token is foo:bar and there is no mappable property named foo,
214 // assume "foo:bar" as text query
215 // FIXME the analyzer changes the text query "foo:bar" to "foo bar"
216 // which might not be the right thing to do
218 QueryPart_Text query_part
= new QueryPart_Text ();
219 query_part
.Text
= query_text
;
220 Logger
.Log
.Debug ("Could not find property, parsed query '" + query_text
+ "' as text_query");
224 QueryPart_Property query_part_prop
= new QueryPart_Property ();
225 query_part_prop
.Key
= prop_string
;
226 query_part_prop
.Value
= query_text
.Substring (pos
+ 1);
227 // if query was of type ext:mp3 or extension:mp3
228 // change value to .mp3
229 // if query was of type ext:
230 // change value to ""
231 // Change extension query value to lowercase - thats how they are stored on disk
232 if (query_part_prop
.Key
== FSQ
.FileSystemQueryable
.FilenameExtensionPropKey
&&
233 query_part_prop
.Value
!= String
.Empty
)
234 query_part_prop
.Value
= "." + query_part_prop
.Value
.ToLower ();
235 query_part_prop
.Type
= prop_type
;
236 Logger
.Log
.Debug ("Parsed query '" + query_text
+
237 "' as prop query:key=" + query_part_prop
.Key
+
238 ", value=" + query_part_prop
.Value
+
239 " and property type=" + query_part_prop
.Type
);
240 return query_part_prop
;
243 static private ICollection
TokensToQueryParts (ArrayList token_list
)
246 parts
= new ArrayList ();
249 ArrayList or_list
= null;
251 while (i
< token_list
.Count
) {
253 token
= token_list
[i
] as Token
;
255 if (token
.Type
== TokenType
.Meta
) {
260 // Skip any extra operators
261 if (token
.Type
== TokenType
.Operator
) {
266 // Assemble a part for this token.
268 QueryPart query_part
= TokenToQueryPart (token
);
269 if (token
.Type
== TokenType
.Minus
)
270 query_part
.Logic
= QueryPartLogic
.Prohibited
;
272 query_part
.Logic
= QueryPartLogic
.Required
;
275 if (or_list
!= null) {
276 or_list
.Add (query_part
);
280 Token next_token
= null;
281 if (i
< token_list
.Count
- 1)
282 next_token
= token_list
[i
+1] as Token
;
285 // If the next token is an or, start an or_list
286 // (if we don't have one already) and skip
287 // ahead to the next part.
288 if (next_token
!= null
289 && next_token
.Type
== TokenType
.Operator
290 && next_token
.Text
== "Or") {
291 if (or_list
== null) {
292 or_list
= new ArrayList ();
293 or_list
.Add (query_part
);
299 // If we have a non-empty or-list going,
300 // Create the appropriate QueryPart and add it
302 if (or_list
!= null) {
303 QueryPart_Or or_part
;
304 or_part
= new QueryPart_Or ();
305 or_part
.Logic
= QueryPartLogic
.Required
;
306 foreach (QueryPart sub_part
in or_list
)
307 or_part
.Add (sub_part
);
312 // Add the next text part
313 if (query_part
!= null)
314 parts
.Add (query_part
);
319 // If we ended with an or_parts list, do the right thing.
320 if (or_list
!= null) {
321 QueryPart_Or or_part
;
322 or_part
= new QueryPart_Or ();
323 or_part
.Logic
= QueryPartLogic
.Required
;
324 foreach (QueryPart sub_part
in or_list
)
325 or_part
.Add (sub_part
);