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) */
148 tlen = strlen((const char *)tk);
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:(const unsigned char *)"FROM"
220 from:&scur length:&slen consume:NO]) {
221 /* not an attribute, the from starts ... */
222 // printf("rejected unquoted attr, is a FROM\n");
225 if ([self parseToken:(const unsigned char *)"WHERE"
226 from:&scur length:&slen consume:NO]) {
227 /* not an attribute, the where starts ... */
228 // printf("rejected unquoted attr, is a WHERE\n");
233 while ((slen > 0) && !isspace(*scur) && (*scur != ',')) {
237 *result = [[NSString alloc] initWithCharacters:start length:(scur-start)];
238 //NSLog(@"found attr: %@ (len=%i)", *result, (scur-start));
240 if (consume && result) { *pos = scur; *len = slen; } // end transaction
241 return *result ? YES : NO;
243 - (BOOL)parseColumnName:(NSString **)result
244 from:(unichar **)pos length:(unsigned *)len
245 consume:(BOOL)consume
247 return [self parseIdentifier:result from:pos length:len consume:consume];
249 - (BOOL)parseTableName:(NSString **)result
250 from:(unichar **)pos length:(unsigned *)len
251 consume:(BOOL)consume
253 return [self parseIdentifier:result from:pos length:len consume:consume];
256 - (BOOL)parseIdentifierList:(NSArray **)result
257 from:(unichar **)pos length:(unsigned *)len
261 NSMutableArray *attrs = nil;
265 BOOL (*parser)(id, SEL, NSString **, unichar **, unsigned *, BOOL);
267 if (result) *result = nil;
268 scur=*pos; slen=*len; // begin transaction
269 skipSpaces(&scur, &slen);
270 parser = (void *)[self methodForSelector:_sel];
272 if (slen < 1) return NO; // not enough chars
275 /* a wildcard list, return 'nil' as result */
276 //printf("try wildcard\n");
277 scur++; slen--; // skip '*'
278 if (!(slen == 0 || isspace(*scur))) {
279 /* not followed by space or at end */
282 *pos = scur; *len = slen; // end transaction
287 if (!parser(self, _sel, &attr,&scur,&slen,YES))
288 /* well, we need at least one attribute to make it a list */
291 attrs = [[NSMutableArray alloc] initWithCapacity:32];
292 [attrs addObject:attr]; [attr release];
294 /* all the remaining attributes must be prefixed with a "," */
296 //printf("try next list attr comma\n");
297 skipSpaces(&scur, &slen);
299 if (*scur != ',') break;
300 scur++; slen--; // skip ','
302 //printf("try next list attr\n");
303 if (!parser(self, _sel, &attr,&scur,&slen,YES))
306 [attrs addObject:attr]; [attr release];
309 *pos = scur; *len = slen; // end transaction
314 - (BOOL)parseContainsQualifier:(EOQualifier **)q_
315 from:(unichar **)pos length:(unsigned *)len
317 /* contains('"hh@"') [12+ chars] */
322 skipSpaces(&scur, &slen);
324 if (slen < 12) return NO; // not enough chars
326 if (![self parseToken:(const unsigned char *)"CONTAINS"
327 from:pos length:len consume:YES])
329 skipSpaces(&scur, &slen);
330 [self parseToken:(const unsigned char *)"('"
331 from:&scur length:&slen consume:YES];
333 if (![self parseIdentifier:&s from:&scur length:&slen consume:YES])
336 skipSpaces(&scur, &slen);
337 [self parseToken:(const unsigned char *)"')"
338 from:&scur length:&slen consume:YES];
340 *q_ = [[EOQualifier qualifierWithQualifierFormat:
341 @"contentAsString doesContain: %@", s] retain];
343 *pos = scur; *len = slen; // end transaction
350 - (BOOL)parseQualifier:(EOQualifier **)result
351 from:(unichar **)pos length:(unsigned *)len
356 if (result) *result = nil;
357 scur=*pos; slen=*len; // begin transaction
358 skipSpaces(&scur, &slen);
360 if (slen < 3) return NO; // not enough chars
362 // for now should scan till we find either ORDER BY order GROUP BY
364 unichar *start = scur;
367 if (*scur == 'O' || *scur == 'o') {
368 if ([self parseToken:(const unsigned char *)"ORDER"
369 from:&scur length:&slen consume:NO]) {
370 //printf("FOUND ORDER TOKEN ...\n");
374 else if (*scur == 'G' || *scur == 'g') {
375 if ([self parseToken:(const unsigned char *)"GROUP"
376 from:&scur length:&slen consume:NO]) {
377 //printf("FOUND GROUP TOKEN ...\n");
389 s = [[NSString alloc] initWithCharacters:start length:(scur-start)];
390 if ([s length] == 0) {
394 if ((q = [self parseSQLWhereExpression:s]) == nil) {
398 *result = [q retain];
403 *pos = scur; *len = slen; // end transaction
407 - (BOOL)parseScope:(NSString **)_scope:(NSString **)_entity
408 from:(unichar **)pos length:(unsigned *)len
411 "('shallow traversal of "..."')"
412 "('hierarchical traversal of "..."')"
416 NSString *entityName;
420 if (_scope) *_scope = nil;
421 if (_entity) *_entity = nil;
422 scur=*pos; slen=*len; // begin transaction
423 skipSpaces(&scur, &slen);
424 if (slen < 14) return NO; // not enough chars
426 if (*scur != '(') return NO; // does not start with '('
427 scur++; slen--; // skip '('
428 skipSpaces(&scur, &slen);
430 if (*scur != '\'') return NO; // does not start with '(''
431 scur++; slen--; // skip single quote
435 if ([self parseToken:(const unsigned char *)"SHALLOW"
436 from:&scur length:&slen consume:YES])
438 else if ([self parseToken:(const unsigned char *)"HIERARCHICAL"
439 from:&scur length:&slen consume:YES])
441 else if ([self parseToken:(const unsigned char *)"DEEP"
442 from:&scur length:&slen consume:YES])
445 /* unknown traveral key */
448 /* some syntactic sugar (not strict about that ...) */
449 [self parseToken:(const unsigned char *)"TRAVERSAL"
450 from:&scur length:&slen consume:YES];
451 [self parseToken:(const unsigned char *)"OF"
452 from:&scur length:&slen consume:YES];
453 if (slen < 1) return NO; // not enough chars
456 skipSpaces(&scur, &slen);
457 if (![self parseTableName:&entityName from:&scur length:&slen consume:YES])
458 return NO; // failed to parse entity from scope
461 skipSpaces(&scur, &slen);
462 if (slen > 0 && *scur == '\'') {
463 scur++; slen--; // skip single quote
465 skipSpaces(&scur, &slen);
466 if (slen > 0 && *scur == ')') {
467 scur++; slen--; // skip ')'
470 if (_scope) *_scope = isShallow ? @"flat" : @"deep";
471 if (_entity) *_entity = entityName;
472 *pos = scur; *len = slen; // end transaction
476 - (BOOL)parseSELECT:(EOFetchSpecification **)result
477 from:(unichar **)pos length:(unsigned *)len
478 strict:(BOOL)beStrict
480 EOFetchSpecification *fs;
481 NSMutableDictionary *lHints;
482 NSString *scope = nil;
483 NSArray *attrs = nil;
484 NSArray *fromList = nil;
485 NSArray *orderList = nil;
486 NSArray *lSortOrderings = nil;
487 EOQualifier *q = nil;
490 BOOL missingByOfOrder = NO;
491 BOOL missingByOfGroup = NO;
495 if (![self parseToken:(const unsigned char *)"SELECT"
496 from:pos length:len consume:YES]) {
497 /* must begin with SELECT */
498 if (beStrict) return NO;
503 if (![self parseIdentifierList:&attrs from:pos length:len
504 selector:@selector(parseColumnName:from:length:consume:)]) {
505 [self logWithFormat:@"missing ID list .."];
508 //[self debugWithFormat:@"parsed attrs (%i): %@", [attrs count], attrs];
510 /* now a from is expected */
511 if ([self parseToken:(const unsigned char *)"FROM"
512 from:pos length:len consume:YES])
515 if (beStrict) return NO;
518 /* check whether it's followed by a scope */
519 if ([self parseToken:(const unsigned char *)"SCOPE"
520 from:pos length:len consume:YES]) {
521 NSString *scopeEntity = nil;
523 if (![self parseScope:&scope:&scopeEntity from:pos length:len]) {
524 if (beStrict) return NO;
528 [self logWithFormat:@"FOUND SCOPE: '%@'", scope];
532 fromList = [[NSArray alloc] initWithObjects:scopeEntity, nil];
533 [scopeEntity release];
536 if (![self parseIdentifierList:&fromList from:pos length:len
537 selector:@selector(parseTableName:from:length:consume:)]) {
538 [self logWithFormat:@"missing from list .."];
542 [self logWithFormat:@"parsed FROM list (%i): %@",
543 [fromList count], fromList];
548 if ([self parseToken:(const unsigned char *)"WHERE"
549 from:pos length:len consume:YES]) {
550 /* parse qualifier ... */
552 if ([self parseToken:(const unsigned char *)"CONTAINS"
553 from:pos length:len consume:NO]) {
554 if (![self parseContainsQualifier:&q from:pos length:len]) {
555 if (beStrict) return NO;
558 else if (![self parseQualifier:&q from:pos length:len]) {
559 if (beStrict) return NO;
562 [self logWithFormat:@"FOUND Qualifier: '%@'", q];
567 if ([self parseToken:(const unsigned char *)"ORDER"
568 from:pos length:len consume:YES]) {
569 if (![self parseToken:(const unsigned char *)"BY"
570 from:pos length:len consume:YES]) {
571 if (beStrict) return NO;
572 missingByOfOrder = YES;
575 if (![self parseIdentifierList:&orderList from:pos length:len
576 selector:@selector(parseColumnName:from:length:consume:)])
579 [self logWithFormat:@"parsed ORDER list (%i): %@",
580 [orderList count], orderList];
585 if ([self parseToken:(const unsigned char *)"GROUP"
586 from:pos length:len consume:YES]) {
587 if (![self parseToken:(const unsigned char *)"BY"
588 from:pos length:len consume:YES]) {
589 if (beStrict) return NO;
590 missingByOfGroup = YES;
594 //printUniStr(*pos, *len); // DEBUG
596 if (!hasSelect) [self logWithFormat:@"missing SELECT !"];
597 if (!hasFrom) [self logWithFormat:@"missing FROM !"];
598 if (missingByOfOrder) [self logWithFormat:@"missing BY in ORDER BY !"];
600 /* build fetchspec */
602 lHints = [[NSMutableDictionary alloc] initWithCapacity:16];
605 [lHints setObject:scope forKey:@"scope"];
606 [scope release]; scope = nil;
609 [lHints setObject:attrs forKey:@"attributes"];
610 [attrs release]; attrs = nil;
616 len = [orderList count];
617 ma = [[NSMutableArray alloc] initWithCapacity:len];
618 for (i = 0; i < len; i++) {
621 so = [EOSortOrdering sortOrderingWithKey:[orderList objectAtIndex:i]
622 selector:EOCompareAscending];
624 lSortOrderings = [ma shallowCopy];
626 [orderList release]; orderList = nil;
629 fs = [[EOFetchSpecification alloc]
630 initWithEntityName:[fromList componentsJoinedByString:@","]
632 sortOrderings:lSortOrderings
633 usesDistinct:NO isDeep:NO hints:lHints];
639 return fs ? YES : NO;
642 - (BOOL)parseSQL:(id *)result
643 from:(unichar **)pos length:(unsigned *)len
644 strict:(BOOL)beStrict
646 if (*len < 1) return NO;
648 if ([self parseToken:(const unsigned char *)"SELECT"
649 from:pos length:len consume:NO])
650 return [self parseSELECT:result from:pos length:len strict:beStrict];
652 //if ([self parseToken:"UPDATE" from:pos length:len consume:NO])
653 //if ([self parseToken:"INSERT" from:pos length:len consume:NO])
654 //if ([self parseToken:"DELETE" from:pos length:len consume:NO])
656 [self logWithFormat:@"tried to parse an unsupported SQL statement."];
660 @end /* EOSQLParser */
662 @implementation EOSQLParser(Tests)
664 + (void)testDAVQuery {
665 EOFetchSpecification *fs;
668 NSLog(@"testing: %@ --------------------", self);
672 @" \"http://schemas.microsoft.com/mapi/proptag/x0e230003\", \n"
673 @" \"urn:schemas:mailheader:subject\", \n"
674 @" \"urn:schemas:mailheader:from\",\n"
675 @" \"urn:schemas:mailheader:to\", \n"
676 @" \"urn:schemas:mailheader:cc\", \n"
677 @" \"urn:schemas:httpmail:read\", \n"
678 @" \"urn:schemas:httpmail:hasattachment\", \n"
679 @" \"DAV:getcontentlength\", \n"
680 @" \"urn:schemas:mailheader:date\", \n"
681 @" \"urn:schemas:httpmail:date\", \n"
682 @" \"urn:schemas:mailheader:received\", \n"
683 @" \"urn:schemas:mailheader:message-id\", \n"
684 @" \"urn:schemas:mailheader:in-reply-to\", \n"
685 @" \"urn:schemas:mailheader:references\" \n"
687 @" scope('shallow traversal of \"http://127.0.0.1:9000/o/ol/helge/INBOX\"')\n"
689 @" \"DAV:iscollection\" = False \n"
691 @" \"http://schemas.microsoft.com/mapi/proptag/x0c1e001f\" != 'SMTP'\n"
693 @" \"http://schemas.microsoft.com/mapi/proptag/x0e230003\" > 0 \n"
695 fs = [[self sharedSQLParser] parseSQLSelectStatement:sql];
697 NSLog(@" FS: %@", fs);
699 NSLog(@" ERROR: could not parse SQL: %@", sql);
706 if ((scope = [[fs hints] objectForKey:@"scope"]) == nil)
707 NSLog(@" INVALID: got no scope !");
708 if (![scope isEqualToString:@"flat"])
709 NSLog(@" INVALID: got scope %@, expected flat !", scope);
712 if ([fs queryWebDAVPropertyNamesOnly])
713 NSLog(@" INVALID: name query only, but queried several attrs !");
716 /* check qualifier */
717 if ((q = [fs qualifier]) == nil)
718 NSLog(@" INVALID: got not qualifier (expected one) !");
719 else if (![q isKindOfClass:[EOAndQualifier class]]) {
720 NSLog(@" INVALID: expected AND qualifier, got %@ !",
721 NSStringFromClass([q class]));
723 else if ([[(EOAndQualifier *)q qualifiers] count] != 3) {
724 NSLog(@" INVALID: expected 3 subqualifiers, got %i !",
725 [[(EOAndQualifier *)q qualifiers] count]);
728 /* check sortordering */
729 if ([fs sortOrderings] != nil) {
730 NSLog(@" INVALID: got sort orderings, specified none: %@ !",
735 if ((props = [[fs hints] objectForKey:@"attributes"]) == nil)
736 NSLog(@" INVALID: got not attributes (expected some) !");
737 else if (![props isKindOfClass:[NSArray class]]) {
738 NSLog(@" INVALID: attributes not delivered as array ?: %@",
739 NSStringFromClass([props class]));
741 else if ([props count] != 14) {
742 NSLog(@" INVALID: invalid attribute count, expected 14, got %i.",
747 NSLog(@"done test: %@ ------------------", self);
750 @end /* EOSQLParser(Tests) */