2 Copyright (C) 2002-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 "SoDAVSQLParser.h"
23 #include <EOControl/EOQualifier.h>
24 #include <EOControl/EOFetchSpecification.h>
25 #include <EOControl/EOSortOrdering.h>
26 #include <EOControl/EOClassDescription.h>
30 // TODO: better error output
32 @interface SoDAVSQLParser(Logging) /* this is available in NGExtensions */
33 - (void)logWithFormat:(NSString *)_fmt,...;
36 @interface EOSQLQualifierIdMapper : NSObject
39 @implementation SoDAVSQLParser
41 + (id)sharedSQLParser {
42 static SoDAVSQLParser *sharedParser = nil; // THREAD
43 if (sharedParser == nil)
44 sharedParser = [[SoDAVSQLParser alloc] init];
52 /* top level parsers */
54 - (EOFetchSpecification *)parseSQLSelectStatement:(NSString *)_sql {
55 EOFetchSpecification *fs;
57 unsigned len, remainingLen;
59 if ((len = [_sql length]) == 0) return nil;
61 us = calloc(len + 10, sizeof(unichar));
62 [_sql getCharacters:us];
67 if (![self parseSQL:&fs from:&pos length:&remainingLen strict:NO])
68 [self logWithFormat:@"parsing of SQL failed."];
72 return [fs autorelease];
75 - (EOQualifier *)_parseSQLWhereExpression:(NSString *)_sql {
76 // TODO: process %=>* and %%, and $
80 if ((len = [_sql length]) == 0) return nil;
82 // TODO: improve, real parsing in qualifier parser !
84 buf = calloc(len + 3, sizeof(unichar));
85 NSAssert(buf, @"could not allocate char buffer");
87 [_sql getCharacters:buf];
88 for (i = 0, didReplace = NO; i < len; i++) {
91 [self warnWithFormat:@"(%s): SQL string contains a '*': %@",
92 __PRETTY_FUNCTION__, _sql];
100 _sql = [NSString stringWithCharacters:buf length:len];
103 return [EOQualifier qualifierWithQualifierFormat:_sql];
106 /* parsing parts (exported for overloading in subclasses) */
109 uniIsCEq(unichar *haystack, const char *needle, unsigned len)
111 register unsigned idx;
112 for (idx = 0; idx < len; idx++) {
113 if (*needle == '\0') return YES;
114 if (toupper(haystack[idx]) != (unsigned char)needle[idx]) return NO;
118 static inline void skipSpaces(unichar **pos, unsigned *len) {
120 if (!isspace(*pos[0])) return;
125 static void printUniStr(unichar *pos, unsigned len) __attribute__((unused));
126 static void printUniStr(unichar *pos, unsigned len) {
128 for (i = 0; i < len && i < 80; i++)
133 static inline BOOL isTokStopChar(unichar c) {
136 case ')': case '(': case '"': case '\'':
139 if (isspace(c)) return YES;
144 - (BOOL)parseToken:(const char *)tk
145 from:(unichar **)pos length:(unsigned *)len
146 consume:(BOOL)consume
148 /* ...[space] (strlen(tk)+1 chars) */
153 scur=*pos; slen=*len; // begin transaction
154 skipSpaces(&scur, &slen);
158 if (toupper(scur[0]) != tk[0])
160 if (tlen < slen) { /* if tok is not at the end */
161 if (!isTokStopChar(scur[tlen]))
162 return NO; /* not followed by a token stopper */
164 if (!uniIsCEq(scur, tk, tlen))
167 scur+=tlen; slen-=tlen;
169 if (consume) { *pos = scur; *len = slen; } // end tx
173 - (BOOL)parseIdentifier:(NSString **)result
174 from:(unichar **)pos length:(unsigned *)len
175 consume:(BOOL)consume
177 /* "attr" or attr (at least 1 char or 2 for ") */
181 if (result) *result = nil;
182 scur=*pos; slen=*len; // begin transaction
183 skipSpaces(&scur, &slen);
189 //printf("try quoted attr\n");
190 if (slen < 2) return NO;
191 scur++; slen--; /* skip quote */
195 if (consume) { *pos = scur; *len = slen; } // end transaction
197 //printf("is empty quoted\n");
200 if (slen < 2) return NO;
203 while ((slen > 0) && (*scur != '"')) {
204 if (*scur == '\\' && (slen > 1)) {
206 scur++; slen--; // skip one more (still needs to be filtered in result
210 if (slen > 0) { scur++; slen--; } /* skip quote */
212 // TODO: xhandle contained quoted chars ?
214 [[NSString alloc] initWithCharacters:start length:(scur-start-1)];
215 //NSLog(@"found qattr: %@", *result);
218 /* non-quoted attr */
221 if (slen < 1) return NO;
223 if ([self parseToken:"FROM" from:&scur length:&slen consume:NO]) {
224 /* not an attribute, the from starts ... */
225 // printf("rejected unquoted attr, is a FROM\n");
228 if ([self parseToken:"WHERE" from:&scur length:&slen consume:NO]) {
229 /* not an attribute, the where starts ... */
230 // printf("rejected unquoted attr, is a WHERE\n");
235 while ((slen > 0) && !isspace(*scur) && (*scur != ',')) {
239 *result = [[NSString alloc] initWithCharacters:start length:(scur-start)];
240 //NSLog(@"found attr: %@ (len=%i)", *result, (scur-start));
242 if (consume && result) { *pos = scur; *len = slen; } // end transaction
243 return *result ? YES : NO;
246 - (BOOL)parseTableName:(NSString **)result
247 from:(unichar **)pos length:(unsigned *)len
248 consume:(BOOL)consume
250 return [self parseIdentifier:result from:pos length:len consume:consume];
253 - (BOOL)parseIdentifierList:(NSArray **)result
254 from:(unichar **)pos length:(unsigned *)len
258 NSMutableArray *attrs = nil;
262 BOOL (*parser)(id, SEL, NSString **, unichar **, unsigned *, BOOL);
264 if (result) *result = nil;
265 scur=*pos; slen=*len; // begin transaction
266 skipSpaces(&scur, &slen);
267 parser = (void *)[self methodForSelector:_sel];
269 if (slen < 1) return NO; // not enough chars
272 /* a wildcard list, return 'nil' as result */
273 //printf("try wildcard\n");
274 scur++; slen--; // skip '*'
275 if (!(slen == 0 || isspace(*scur))) {
276 /* not followed by space or at end */
279 *pos = scur; *len = slen; // end transaction
284 if (!parser(self, _sel, &attr,&scur,&slen,YES))
285 /* well, we need at least one attribute to make it a list */
288 attrs = [[NSMutableArray alloc] initWithCapacity:32];
289 [attrs addObject:attr]; [attr release];
291 /* all the remaining attributes must be prefixed with a "," */
293 //printf("try next list attr comma\n");
294 skipSpaces(&scur, &slen);
296 if (*scur != ',') break;
297 scur++; slen--; // skip ','
299 //printf("try next list attr\n");
300 if (!parser(self, _sel, &attr,&scur,&slen,YES))
303 [attrs addObject:attr]; [attr release];
306 *pos = scur; *len = slen; // end transaction
311 - (BOOL)parseContainsQualifier:(EOQualifier **)q_
312 from:(unichar **)pos length:(unsigned *)len
314 /* contains('"hh@"') [12+ chars] */
319 skipSpaces(&scur, &slen);
321 if (slen < 12) return NO; // not enough chars
323 if (![self parseToken:"CONTAINS" from:pos length:len consume:YES])
325 skipSpaces(&scur, &slen);
326 [self parseToken:"('" from:&scur length:&slen consume:YES];
328 if (![self parseIdentifier:&s from:&scur length:&slen consume:YES])
331 skipSpaces(&scur, &slen);
332 [self parseToken:"')" from:&scur length:&slen consume:YES];
334 *q_ = [[EOQualifier qualifierWithQualifierFormat:
335 @"contentAsString doesContain: %@", s] retain];
337 *pos = scur; *len = slen; // end transaction
344 - (BOOL)parseQualifier:(EOQualifier **)result
345 from:(unichar **)pos length:(unsigned *)len
350 if (result) *result = nil;
351 scur=*pos; slen=*len; // begin transaction
352 skipSpaces(&scur, &slen);
354 if (slen < 3) return NO; // not enough chars
356 // for now should scan till we find either ORDER BY order GROUP BY
358 unichar *start = scur;
361 if (*scur == 'O' || *scur == 'o') {
362 if ([self parseToken:"ORDER" from:&scur length:&slen consume:NO]) {
363 //printf("FOUND ORDER TOKEN ...\n");
367 else if (*scur == 'G' || *scur == 'g') {
368 if ([self parseToken:"GROUP" from:&scur length:&slen consume:NO]) {
369 //printf("FOUND GROUP TOKEN ...\n");
381 s = [[NSString alloc] initWithCharacters:start length:(scur-start)];
382 if ([s length] == 0) {
386 if ((q = [self parseSQLWhereExpression:s]) == nil) {
390 *result = [q retain];
395 *pos = scur; *len = slen; // end transaction
399 - (BOOL)parseScope:(NSString **)_scope:(NSString **)_entity
400 from:(unichar **)pos length:(unsigned *)len
403 "('shallow traversal of "..."')"
404 "('hierarchical traversal of "..."')"
408 NSString *entityName;
412 if (_scope) *_scope = nil;
413 if (_entity) *_entity = nil;
414 scur=*pos; slen=*len; // begin transaction
415 skipSpaces(&scur, &slen);
416 if (slen < 14) return NO; // not enough chars
418 if (*scur != '(') return NO; // does not start with '('
419 scur++; slen--; // skip '('
420 skipSpaces(&scur, &slen);
422 if (*scur != '\'') return NO; // does not start with '(''
423 scur++; slen--; // skip single quote
427 if ([self parseToken:"SHALLOW" from:&scur length:&slen consume:YES])
429 else if ([self parseToken:"HIERARCHICAL" from:&scur length:&slen consume:YES])
431 else if ([self parseToken:"DEEP" from:&scur length:&slen consume:YES])
434 /* unknown traveral key */
437 /* some syntactic sugar (not strict about that ...) */
438 [self parseToken:"TRAVERSAL" from:&scur length:&slen consume:YES];
439 [self parseToken:"OF" from:&scur length:&slen consume:YES];
440 if (slen < 1) return NO; // not enough chars
443 skipSpaces(&scur, &slen);
444 if (![self parseTableName:&entityName from:&scur length:&slen consume:YES])
445 return NO; // failed to parse entity from scope
448 skipSpaces(&scur, &slen);
449 if (slen > 0 && *scur == '\'') {
450 scur++; slen--; // skip single quote
452 skipSpaces(&scur, &slen);
453 if (slen > 0 && *scur == ')') {
454 scur++; slen--; // skip ')'
457 if (_scope) *_scope = isShallow ? @"flat" : @"deep";
458 if (_entity) *_entity = entityName;
459 *pos = scur; *len = slen; // end transaction
463 - (BOOL)parseSELECT:(EOFetchSpecification **)result
464 from:(unichar **)pos length:(unsigned *)len
465 strict:(BOOL)beStrict
467 EOFetchSpecification *fs;
468 NSMutableDictionary *lHints;
469 NSString *scope = nil;
470 NSArray *attrs = nil;
471 NSArray *fromList = nil;
472 NSArray *orderList = nil;
473 NSArray *lSortOrderings = nil;
474 EOQualifier *q = nil;
477 BOOL missingByOfOrder = NO;
478 BOOL missingByOfGroup = NO;
482 if (![self parseToken:"SELECT" from:pos length:len consume:YES]) {
483 /* must begin with SELECT */
484 if (beStrict) return NO;
489 if (![self parseIdentifierList:&attrs from:pos length:len
490 selector:@selector(parseColumnName:from:length:consume:)]) {
491 [self logWithFormat:@"missing ID list .."];
494 //[self debugWithFormat:@"parsed attrs (%i): %@", [attrs count], attrs];
496 /* now a from is expected */
497 if ([self parseToken:"FROM" from:pos length:len consume:YES])
500 if (beStrict) return NO;
503 /* check whether it's followed by a scope */
504 if ([self parseToken:"SCOPE" from:pos length:len consume:YES]) {
505 NSString *scopeEntity = nil;
507 if (![self parseScope:&scope:&scopeEntity from:pos length:len]) {
508 if (beStrict) return NO;
512 [self logWithFormat:@"FOUND SCOPE: '%@'", scope];
516 fromList = [[NSArray alloc] initWithObjects:scopeEntity, nil];
517 [scopeEntity release];
520 if (![self parseIdentifierList:&fromList from:pos length:len
521 selector:@selector(parseTableName:from:length:consume:)]) {
522 [self logWithFormat:@"missing from list .."];
526 [self logWithFormat:@"parsed FROM list (%i): %@",
527 [fromList count], fromList];
532 if ([self parseToken:"WHERE" from:pos length:len consume:YES]) {
533 /* parse qualifier ... */
535 if ([self parseToken:"CONTAINS" from:pos length:len consume:NO]) {
536 if (![self parseContainsQualifier:&q from:pos length:len]) {
537 if (beStrict) return NO;
540 else if (![self parseQualifier:&q from:pos length:len]) {
541 if (beStrict) return NO;
544 [self logWithFormat:@"FOUND Qualifier: '%@'", q];
549 if ([self parseToken:"ORDER" from:pos length:len consume:YES]) {
550 if (![self parseToken:"BY" from:pos length:len consume:YES]) {
551 if (beStrict) return NO;
552 missingByOfOrder = YES;
555 if (![self parseIdentifierList:&orderList from:pos length:len
556 selector:@selector(parseColumnName:from:length:consume:)])
559 [self logWithFormat:@"parsed ORDER list (%i): %@",
560 [orderList count], orderList];
565 if ([self parseToken:"GROUP" from:pos length:len consume:YES]) {
566 if (![self parseToken:"BY" from:pos length:len consume:YES]) {
567 if (beStrict) return NO;
568 missingByOfGroup = YES;
572 //printUniStr(*pos, *len); // DEBUG
574 if (!hasSelect) [self logWithFormat:@"missing SELECT !"];
575 if (!hasFrom) [self logWithFormat:@"missing FROM !"];
576 if (missingByOfOrder) [self logWithFormat:@"missing BY in ORDER BY !"];
578 /* build fetchspec */
580 lHints = [[NSMutableDictionary alloc] initWithCapacity:16];
583 [lHints setObject:scope forKey:@"scope"];
584 [scope release]; scope = nil;
587 [lHints setObject:attrs forKey:@"attributes"];
588 [attrs release]; attrs = nil;
594 len = [orderList count];
595 ma = [[NSMutableArray alloc] initWithCapacity:len];
596 for (i = 0; i < len; i++) {
599 so = [EOSortOrdering sortOrderingWithKey:[orderList objectAtIndex:i]
600 selector:EOCompareAscending];
602 lSortOrderings = [ma shallowCopy];
604 [orderList release]; orderList = nil;
607 fs = [[EOFetchSpecification alloc]
608 initWithEntityName:[fromList componentsJoinedByString:@","]
610 sortOrderings:lSortOrderings
611 usesDistinct:NO isDeep:NO hints:lHints];
617 return fs ? YES : NO;
620 - (BOOL)parseSQL:(id *)result
621 from:(unichar **)pos length:(unsigned *)len
622 strict:(BOOL)beStrict
624 if (*len < 1) return NO;
626 if ([self parseToken:"SELECT" from:pos length:len consume:NO])
627 return [self parseSELECT:result from:pos length:len strict:beStrict];
629 //if ([self parseToken:"UPDATE" from:pos length:len consume:NO])
630 //if ([self parseToken:"INSERT" from:pos length:len consume:NO])
631 //if ([self parseToken:"DELETE" from:pos length:len consume:NO])
633 [self logWithFormat:@"tried to parse an unsupported SQL statement."];
637 /* WebDAV specific */
639 - (EOQualifier *)parseSQLWhereExpression:(NSString *)_sql {
640 // TODO: process %=>* and %%, and $
641 static EOSQLQualifierIdMapper *map = nil;
644 if ((q = [self _parseSQLWhereExpression:_sql]) == nil)
647 if (map == nil) map = [[EOSQLQualifierIdMapper alloc] init];
648 q = [q qualifierByApplyingKeyMap:(id)map];
652 - (BOOL)parseColumnName:(NSString **)result
653 from:(unichar **)pos length:(unsigned *)len
654 consume:(BOOL)consume
658 ok = [self parseIdentifier:result from:pos length:len consume:consume];
662 /* heuristics to morph fully qualified name to namespace/localname */
666 r = [*result rangeOfString:@"/"
667 options:NSLiteralSearch|NSBackwardsSearch];
669 r = [*result rangeOfString:@":"
670 options:NSLiteralSearch|NSBackwardsSearch];
675 l = [*result substringFromIndex:(r.location + r.length)];
676 ns = [*result substringToIndex:(r.location + 1)];
678 [*result autorelease];
679 *result = [[NSString alloc] initWithFormat:@"{%@}%@", ns, l];
684 @end /* SoDAVSQLParser */
686 @implementation SoDAVSQLParser(Tests)
688 + (void)testDAVQuery {
689 EOFetchSpecification *fs;
692 NSLog(@"testing: %@ --------------------", self);
696 @" \"http://schemas.microsoft.com/mapi/proptag/x0e230003\", \n"
697 @" \"urn:schemas:mailheader:subject\", \n"
698 @" \"urn:schemas:mailheader:from\",\n"
699 @" \"urn:schemas:mailheader:to\", \n"
700 @" \"urn:schemas:mailheader:cc\", \n"
701 @" \"urn:schemas:httpmail:read\", \n"
702 @" \"urn:schemas:httpmail:hasattachment\", \n"
703 @" \"DAV:getcontentlength\", \n"
704 @" \"urn:schemas:mailheader:date\", \n"
705 @" \"urn:schemas:httpmail:date\", \n"
706 @" \"urn:schemas:mailheader:received\", \n"
707 @" \"urn:schemas:mailheader:message-id\", \n"
708 @" \"urn:schemas:mailheader:in-reply-to\", \n"
709 @" \"urn:schemas:mailheader:references\" \n"
711 @" scope('shallow traversal of \"http://127.0.0.1:9000/o/ol/helge/INBOX\"')\n"
713 @" \"DAV:iscollection\" = False \n"
715 @" \"http://schemas.microsoft.com/mapi/proptag/x0c1e001f\" != 'SMTP'\n"
717 @" \"http://schemas.microsoft.com/mapi/proptag/x0e230003\" > 0 \n"
719 fs = [[self sharedSQLParser] parseSQLSelectStatement:sql];
721 NSLog(@" FS: %@", fs);
723 [self errorWithFormat:@" could not parse SQL: %@", sql];
730 if ((scope = [[fs hints] objectForKey:@"scope"]) == nil)
731 NSLog(@" INVALID: got no scope !");
732 if (![scope isEqualToString:@"flat"])
733 NSLog(@" INVALID: got scope %@, expected flat !", scope);
736 if ([fs queryWebDAVPropertyNamesOnly])
737 NSLog(@" INVALID: name query only, but queried several attrs !");
740 /* check qualifier */
741 if ((q = [fs qualifier]) == nil)
742 NSLog(@" INVALID: got not qualifier (expected one) !");
743 else if (![q isKindOfClass:[EOAndQualifier class]]) {
744 NSLog(@" INVALID: expected AND qualifier, got %@ !",
745 NSStringFromClass([q class]));
747 else if ([[(EOAndQualifier *)q qualifiers] count] != 3) {
748 NSLog(@" INVALID: expected 3 subqualifiers, got %i !",
749 [[(EOAndQualifier *)q qualifiers] count]);
752 /* check sortordering */
753 if ([fs sortOrderings] != nil) {
754 NSLog(@" INVALID: got sort orderings, specified none: %@ !",
759 if ((props = [[fs hints] objectForKey:@"attributes"]) == nil)
760 NSLog(@" INVALID: got not attributes (expected some) !");
761 else if (![props isKindOfClass:[NSArray class]]) {
762 NSLog(@" INVALID: attributes not delivered as array ?: %@",
763 NSStringFromClass([props class]));
765 else if ([props count] != 14) {
766 NSLog(@" INVALID: invalid attribute count, expected 14, got %i.",
771 NSLog(@"done test: %@ ------------------", self);
774 @end /* SoDAVSQLParser(Tests) */
776 @implementation EOSQLQualifierIdMapper
778 - (id)objectForKey:(NSString *)_key {
782 if (_key == nil) return nil;
784 r = [_key rangeOfString:@"/" options:NSLiteralSearch|NSBackwardsSearch];
786 r = [_key rangeOfString:@":" options:NSLiteralSearch|NSBackwardsSearch];
790 l = [_key substringFromIndex:(r.location + r.length)];
791 ns = [_key substringToIndex:(r.location + 1)];
793 return [NSString stringWithFormat:@"{%@}%@", ns, l];
796 @end /* EOSQLQualifierIdMapper */