2 Copyright (C) 2000-2005 SKYRIX Software AG
4 This file is part of SOPE.
6 SOPE is free software; you can redistribute it and/or modify it under
7 the terms of the GNU Lesser General Public License as published by the
8 Free Software Foundation; either version 2, or (at your option) any
11 SOPE is distributed in the hope that it will be useful, but WITHOUT ANY
12 WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
14 License for more details.
16 You should have received a copy of the GNU Lesser General Public
17 License along with SOPE; see the file COPYING. If not, write to the
18 Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA
22 #include "EOSQLParser.h"
23 #include "EOQualifier.h"
24 #include "EOFetchSpecification.h"
25 #include "EOSortOrdering.h"
26 #include "EOClassDescription.h"
29 // TODO: better error output
31 @interface EOSQLParser(Logging) /* this is available in NGExtensions */
32 - (void)logWithFormat:(NSString *)_fmt,...;
35 @implementation EOSQLParser
37 + (id)sharedSQLParser {
38 static EOSQLParser *sharedParser = nil; // THREAD
39 if (sharedParser == nil)
40 sharedParser = [[EOSQLParser alloc] init];
48 /* top level parsers */
50 - (EOFetchSpecification *)parseSQLSelectStatement:(NSString *)_sql {
51 EOFetchSpecification *fs;
53 unsigned len, remainingLen;
55 if ((len = [_sql length]) == 0) return nil;
57 us = calloc(len + 10, sizeof(unichar));
58 [_sql getCharacters:us];
63 if (![self parseSQL:&fs from:&pos length:&remainingLen strict:NO])
64 [self logWithFormat:@"parsing of SQL failed."];
68 return [fs autorelease];
71 - (EOQualifier *)parseSQLWhereExpression:(NSString *)_sql {
72 // TODO: process %=>* and %%, and $
76 if ((len = [_sql length]) == 0) return nil;
78 // TODO: improve, real parsing in qualifier parser !
80 buf = calloc(len + 3, sizeof(unichar));
81 NSAssert(buf, @"could not allocate char buffer");
83 [_sql getCharacters:buf];
84 for (i = 0, didReplace = NO; i < len; i++) {
87 NSLog(@"WARNING(%s): SQL string contains a '*': %@",
88 __PRETTY_FUNCTION__, _sql);
96 _sql = [NSString stringWithCharacters:buf length:len];
99 return [EOQualifier qualifierWithQualifierFormat:_sql];
102 /* parsing parts (exported for overloading in subclasses) */
105 uniIsCEq(unichar *haystack, const unsigned char *needle, unsigned len)
107 register unsigned idx;
108 for (idx = 0; idx < len; idx++) {
109 if (*needle == '\0') return YES;
110 if (toupper(haystack[idx]) != needle[idx]) return NO;
114 static inline void skipSpaces(unichar **pos, unsigned *len) {
116 if (!isspace(*pos[0])) return;
121 static void printUniStr(unichar *pos, unsigned len) __attribute__((unused));
122 static void printUniStr(unichar *pos, unsigned len) {
124 for (i = 0; i < len && i < 80; i++)
129 static inline BOOL isTokStopChar(unichar c) {
132 case ')': case '(': case '"': case '\'':
135 if (isspace(c)) return YES;
140 - (BOOL)parseToken:(const unsigned char *)tk
141 from:(unichar **)pos length:(unsigned *)len
142 consume:(BOOL)consume
144 /* ...[space] (strlen(tk)+1 chars) */
149 scur=*pos; slen=*len; // begin transaction
150 skipSpaces(&scur, &slen);
154 if (toupper(scur[0]) != tk[0])
156 if (tlen < slen) { /* if tok is not at the end */
157 if (!isTokStopChar(scur[tlen]))
158 return NO; /* not followed by a token stopper */
160 if (!uniIsCEq(scur, tk, tlen))
163 scur+=tlen; slen-=tlen;
165 if (consume) { *pos = scur; *len = slen; } // end tx
169 - (BOOL)parseIdentifier:(NSString **)result
170 from:(unichar **)pos length:(unsigned *)len
171 consume:(BOOL)consume
173 /* "attr" or attr (at least 1 char or 2 for ") */
177 if (result) *result = nil;
178 scur=*pos; slen=*len; // begin transaction
179 skipSpaces(&scur, &slen);
185 //printf("try quoted attr\n");
186 if (slen < 2) return NO;
187 scur++; slen--; /* skip quote */
191 if (consume) { *pos = scur; *len = slen; } // end transaction
193 //printf("is empty quoted\n");
196 if (slen < 2) return NO;
199 while ((slen > 0) && (*scur != '"')) {
200 if (*scur == '\\' && (slen > 1)) {
202 scur++; slen--; // skip one more (still needs to be filtered in result
206 if (slen > 0) { scur++; slen--; } /* skip quote */
208 // TODO: xhandle contained quoted chars ?
210 [[NSString alloc] initWithCharacters:start length:(scur-start-1)];
211 //NSLog(@"found qattr: %@", *result);
214 /* non-quoted attr */
217 if (slen < 1) return NO;
219 if ([self parseToken:"FROM" from:&scur length:&slen consume:NO]) {
220 /* not an attribute, the from starts ... */
221 // printf("rejected unquoted attr, is a FROM\n");
224 if ([self parseToken:"WHERE" from:&scur length:&slen consume:NO]) {
225 /* not an attribute, the where starts ... */
226 // printf("rejected unquoted attr, is a WHERE\n");
231 while ((slen > 0) && !isspace(*scur) && (*scur != ',')) {
235 *result = [[NSString alloc] initWithCharacters:start length:(scur-start)];
236 //NSLog(@"found attr: %@ (len=%i)", *result, (scur-start));
238 if (consume && result) { *pos = scur; *len = slen; } // end transaction
239 return *result ? YES : NO;
241 - (BOOL)parseColumnName:(NSString **)result
242 from:(unichar **)pos length:(unsigned *)len
243 consume:(BOOL)consume
245 return [self parseIdentifier:result from:pos length:len consume:consume];
247 - (BOOL)parseTableName:(NSString **)result
248 from:(unichar **)pos length:(unsigned *)len
249 consume:(BOOL)consume
251 return [self parseIdentifier:result from:pos length:len consume:consume];
254 - (BOOL)parseIdentifierList:(NSArray **)result
255 from:(unichar **)pos length:(unsigned *)len
259 NSMutableArray *attrs = nil;
263 BOOL (*parser)(id, SEL, NSString **, unichar **, unsigned *, BOOL);
265 if (result) *result = nil;
266 scur=*pos; slen=*len; // begin transaction
267 skipSpaces(&scur, &slen);
268 parser = (void *)[self methodForSelector:_sel];
270 if (slen < 1) return NO; // not enough chars
273 /* a wildcard list, return 'nil' as result */
274 //printf("try wildcard\n");
275 scur++; slen--; // skip '*'
276 if (!(slen == 0 || isspace(*scur))) {
277 /* not followed by space or at end */
280 *pos = scur; *len = slen; // end transaction
285 if (!parser(self, _sel, &attr,&scur,&slen,YES))
286 /* well, we need at least one attribute to make it a list */
289 attrs = [[NSMutableArray alloc] initWithCapacity:32];
290 [attrs addObject:attr]; [attr release];
292 /* all the remaining attributes must be prefixed with a "," */
294 //printf("try next list attr comma\n");
295 skipSpaces(&scur, &slen);
297 if (*scur != ',') break;
298 scur++; slen--; // skip ','
300 //printf("try next list attr\n");
301 if (!parser(self, _sel, &attr,&scur,&slen,YES))
304 [attrs addObject:attr]; [attr release];
307 *pos = scur; *len = slen; // end transaction
312 - (BOOL)parseContainsQualifier:(EOQualifier **)q_
313 from:(unichar **)pos length:(unsigned *)len
315 /* contains('"hh@"') [12+ chars] */
320 skipSpaces(&scur, &slen);
322 if (slen < 12) return NO; // not enough chars
324 if (![self parseToken:"CONTAINS" from:pos length:len consume:YES])
326 skipSpaces(&scur, &slen);
327 [self parseToken:"('" from:&scur length:&slen consume:YES];
329 if (![self parseIdentifier:&s from:&scur length:&slen consume:YES])
332 skipSpaces(&scur, &slen);
333 [self parseToken:"')" from:&scur length:&slen consume:YES];
335 *q_ = [[EOQualifier qualifierWithQualifierFormat:
336 @"contentAsString doesContain: %@", s] retain];
338 *pos = scur; *len = slen; // end transaction
345 - (BOOL)parseQualifier:(EOQualifier **)result
346 from:(unichar **)pos length:(unsigned *)len
351 if (result) *result = nil;
352 scur=*pos; slen=*len; // begin transaction
353 skipSpaces(&scur, &slen);
355 if (slen < 3) return NO; // not enough chars
357 // for now should scan till we find either ORDER BY order GROUP BY
359 unichar *start = scur;
362 if (*scur == 'O' || *scur == 'o') {
363 if ([self parseToken:"ORDER" from:&scur length:&slen consume:NO]) {
364 //printf("FOUND ORDER TOKEN ...\n");
368 else if (*scur == 'G' || *scur == 'g') {
369 if ([self parseToken:"GROUP" from:&scur length:&slen consume:NO]) {
370 //printf("FOUND GROUP TOKEN ...\n");
382 s = [[NSString alloc] initWithCharacters:start length:(scur-start)];
383 if ([s length] == 0) {
387 if ((q = [self parseSQLWhereExpression:s]) == nil) {
391 *result = [q retain];
396 *pos = scur; *len = slen; // end transaction
400 - (BOOL)parseScope:(NSString **)_scope:(NSString **)_entity
401 from:(unichar **)pos length:(unsigned *)len
404 "('shallow traversal of "..."')"
405 "('hierarchical traversal of "..."')"
409 NSString *entityName;
413 if (_scope) *_scope = nil;
414 if (_entity) *_entity = nil;
415 scur=*pos; slen=*len; // begin transaction
416 skipSpaces(&scur, &slen);
417 if (slen < 14) return NO; // not enough chars
419 if (*scur != '(') return NO; // does not start with '('
420 scur++; slen--; // skip '('
421 skipSpaces(&scur, &slen);
423 if (*scur != '\'') return NO; // does not start with '(''
424 scur++; slen--; // skip single quote
428 if ([self parseToken:"SHALLOW" from:&scur length:&slen consume:YES])
430 else if ([self parseToken:"HIERARCHICAL" from:&scur length:&slen consume:YES])
432 else if ([self parseToken:"DEEP" from:&scur length:&slen consume:YES])
435 /* unknown traveral key */
438 /* some syntactic sugar (not strict about that ...) */
439 [self parseToken:"TRAVERSAL" from:&scur length:&slen consume:YES];
440 [self parseToken:"OF" from:&scur length:&slen consume:YES];
441 if (slen < 1) return NO; // not enough chars
444 skipSpaces(&scur, &slen);
445 if (![self parseTableName:&entityName from:&scur length:&slen consume:YES])
446 return NO; // failed to parse entity from scope
449 skipSpaces(&scur, &slen);
450 if (slen > 0 && *scur == '\'') {
451 scur++; slen--; // skip single quote
453 skipSpaces(&scur, &slen);
454 if (slen > 0 && *scur == ')') {
455 scur++; slen--; // skip ')'
458 if (_scope) *_scope = isShallow ? @"flat" : @"deep";
459 if (_entity) *_entity = entityName;
460 *pos = scur; *len = slen; // end transaction
464 - (BOOL)parseSELECT:(EOFetchSpecification **)result
465 from:(unichar **)pos length:(unsigned *)len
466 strict:(BOOL)beStrict
468 EOFetchSpecification *fs;
469 NSMutableDictionary *lHints;
470 NSString *scope = nil;
471 NSArray *attrs = nil;
472 NSArray *fromList = nil;
473 NSArray *orderList = nil;
474 NSArray *lSortOrderings = nil;
475 EOQualifier *q = nil;
478 BOOL missingByOfOrder = NO;
479 BOOL missingByOfGroup = NO;
483 if (![self parseToken:"SELECT" from:pos length:len consume:YES]) {
484 /* must begin with SELECT */
485 if (beStrict) return NO;
490 if (![self parseIdentifierList:&attrs from:pos length:len
491 selector:@selector(parseColumnName:from:length:consume:)]) {
492 [self logWithFormat:@"missing ID list .."];
495 //[self debugWithFormat:@"parsed attrs (%i): %@", [attrs count], attrs];
497 /* now a from is expected */
498 if ([self parseToken:"FROM" from:pos length:len consume:YES])
501 if (beStrict) return NO;
504 /* check whether it's followed by a scope */
505 if ([self parseToken:"SCOPE" from:pos length:len consume:YES]) {
506 NSString *scopeEntity = nil;
508 if (![self parseScope:&scope:&scopeEntity from:pos length:len]) {
509 if (beStrict) return NO;
513 [self logWithFormat:@"FOUND SCOPE: '%@'", scope];
517 fromList = [[NSArray alloc] initWithObjects:scopeEntity, nil];
518 [scopeEntity release];
521 if (![self parseIdentifierList:&fromList from:pos length:len
522 selector:@selector(parseTableName:from:length:consume:)]) {
523 [self logWithFormat:@"missing from list .."];
527 [self logWithFormat:@"parsed FROM list (%i): %@",
528 [fromList count], fromList];
533 if ([self parseToken:"WHERE" from:pos length:len consume:YES]) {
534 /* parse qualifier ... */
536 if ([self parseToken:"CONTAINS" from:pos length:len consume:NO]) {
537 if (![self parseContainsQualifier:&q from:pos length:len]) {
538 if (beStrict) return NO;
541 else if (![self parseQualifier:&q from:pos length:len]) {
542 if (beStrict) return NO;
545 [self logWithFormat:@"FOUND Qualifier: '%@'", q];
550 if ([self parseToken:"ORDER" from:pos length:len consume:YES]) {
551 if (![self parseToken:"BY" from:pos length:len consume:YES]) {
552 if (beStrict) return NO;
553 missingByOfOrder = YES;
556 if (![self parseIdentifierList:&orderList from:pos length:len
557 selector:@selector(parseColumnName:from:length:consume:)])
560 [self logWithFormat:@"parsed ORDER list (%i): %@",
561 [orderList count], orderList];
566 if ([self parseToken:"GROUP" from:pos length:len consume:YES]) {
567 if (![self parseToken:"BY" from:pos length:len consume:YES]) {
568 if (beStrict) return NO;
569 missingByOfGroup = YES;
573 //printUniStr(*pos, *len); // DEBUG
575 if (!hasSelect) [self logWithFormat:@"missing SELECT !"];
576 if (!hasFrom) [self logWithFormat:@"missing FROM !"];
577 if (missingByOfOrder) [self logWithFormat:@"missing BY in ORDER BY !"];
579 /* build fetchspec */
581 lHints = [[NSMutableDictionary alloc] initWithCapacity:16];
584 [lHints setObject:scope forKey:@"scope"];
585 [scope release]; scope = nil;
588 [lHints setObject:attrs forKey:@"attributes"];
589 [attrs release]; attrs = nil;
595 len = [orderList count];
596 ma = [[NSMutableArray alloc] initWithCapacity:len];
597 for (i = 0; i < len; i++) {
600 so = [EOSortOrdering sortOrderingWithKey:[orderList objectAtIndex:i]
601 selector:EOCompareAscending];
603 lSortOrderings = [ma shallowCopy];
605 [orderList release]; orderList = nil;
608 fs = [[EOFetchSpecification alloc]
609 initWithEntityName:[fromList componentsJoinedByString:@","]
611 sortOrderings:lSortOrderings
612 usesDistinct:NO isDeep:NO hints:lHints];
618 return fs ? YES : NO;
621 - (BOOL)parseSQL:(id *)result
622 from:(unichar **)pos length:(unsigned *)len
623 strict:(BOOL)beStrict
625 if (*len < 1) return NO;
627 if ([self parseToken:"SELECT" from:pos length:len consume:NO])
628 return [self parseSELECT:result from:pos length:len strict:beStrict];
630 //if ([self parseToken:"UPDATE" from:pos length:len consume:NO])
631 //if ([self parseToken:"INSERT" from:pos length:len consume:NO])
632 //if ([self parseToken:"DELETE" from:pos length:len consume:NO])
634 [self logWithFormat:@"tried to parse an unsupported SQL statement."];
638 @end /* EOSQLParser */
640 @implementation EOSQLParser(Tests)
642 + (void)testDAVQuery {
643 EOFetchSpecification *fs;
646 NSLog(@"testing: %@ --------------------", self);
650 @" \"http://schemas.microsoft.com/mapi/proptag/x0e230003\", \n"
651 @" \"urn:schemas:mailheader:subject\", \n"
652 @" \"urn:schemas:mailheader:from\",\n"
653 @" \"urn:schemas:mailheader:to\", \n"
654 @" \"urn:schemas:mailheader:cc\", \n"
655 @" \"urn:schemas:httpmail:read\", \n"
656 @" \"urn:schemas:httpmail:hasattachment\", \n"
657 @" \"DAV:getcontentlength\", \n"
658 @" \"urn:schemas:mailheader:date\", \n"
659 @" \"urn:schemas:httpmail:date\", \n"
660 @" \"urn:schemas:mailheader:received\", \n"
661 @" \"urn:schemas:mailheader:message-id\", \n"
662 @" \"urn:schemas:mailheader:in-reply-to\", \n"
663 @" \"urn:schemas:mailheader:references\" \n"
665 @" scope('shallow traversal of \"http://127.0.0.1:9000/o/ol/helge/INBOX\"')\n"
667 @" \"DAV:iscollection\" = False \n"
669 @" \"http://schemas.microsoft.com/mapi/proptag/x0c1e001f\" != 'SMTP'\n"
671 @" \"http://schemas.microsoft.com/mapi/proptag/x0e230003\" > 0 \n"
673 fs = [[self sharedSQLParser] parseSQLSelectStatement:sql];
675 NSLog(@" FS: %@", fs);
677 NSLog(@" ERROR: could not parse SQL: %@", sql);
684 if ((scope = [[fs hints] objectForKey:@"scope"]) == nil)
685 NSLog(@" INVALID: got no scope !");
686 if (![scope isEqualToString:@"flat"])
687 NSLog(@" INVALID: got scope %@, expected flat !", scope);
690 if ([fs queryWebDAVPropertyNamesOnly])
691 NSLog(@" INVALID: name query only, but queried several attrs !");
694 /* check qualifier */
695 if ((q = [fs qualifier]) == nil)
696 NSLog(@" INVALID: got not qualifier (expected one) !");
697 else if (![q isKindOfClass:[EOAndQualifier class]]) {
698 NSLog(@" INVALID: expected AND qualifier, got %@ !",
699 NSStringFromClass([q class]));
701 else if ([[(EOAndQualifier *)q qualifiers] count] != 3) {
702 NSLog(@" INVALID: expected 3 subqualifiers, got %i !",
703 [[(EOAndQualifier *)q qualifiers] count]);
706 /* check sortordering */
707 if ([fs sortOrderings] != nil) {
708 NSLog(@" INVALID: got sort orderings, specified none: %@ !",
713 if ((props = [[fs hints] objectForKey:@"attributes"]) == nil)
714 NSLog(@" INVALID: got not attributes (expected some) !");
715 else if (![props isKindOfClass:[NSArray class]]) {
716 NSLog(@" INVALID: attributes not delivered as array ?: %@",
717 NSStringFromClass([props class]));
719 else if ([props count] != 14) {
720 NSLog(@" INVALID: invalid attribute count, expected 14, got %i.",
725 NSLog(@"done test: %@ ------------------", self);
728 @end /* EOSQLParser(Tests) */