2 Copyright (C) 2002-2004 SKYRIX Software AG
4 This file is part of OpenGroupware.org.
6 OGo 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 OGo 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 OGo; 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>
29 // TODO: better error output
31 @interface SoDAVSQLParser(Logging) /* this is available in NGExtensions */
32 - (void)logWithFormat:(NSString *)_fmt,...;
35 @interface EOSQLQualifierIdMapper : NSObject
38 @implementation SoDAVSQLParser
40 + (id)sharedSQLParser {
41 static SoDAVSQLParser *sharedParser = nil; // THREAD
42 if (sharedParser == nil)
43 sharedParser = [[SoDAVSQLParser alloc] init];
51 /* top level parsers */
53 - (EOFetchSpecification *)parseSQLSelectStatement:(NSString *)_sql {
54 EOFetchSpecification *fs;
56 unsigned len, remainingLen;
58 if ((len = [_sql length]) == 0) return nil;
60 us = calloc(len + 10, sizeof(unichar));
61 [_sql getCharacters:us];
66 if (![self parseSQL:&fs from:&pos length:&remainingLen strict:NO])
67 [self logWithFormat:@"parsing of SQL failed."];
71 return [fs autorelease];
74 - (EOQualifier *)_parseSQLWhereExpression:(NSString *)_sql {
75 // TODO: process %=>* and %%, and $
79 if ((len = [_sql length]) == 0) return nil;
81 // TODO: improve, real parsing in qualifier parser !
83 buf = calloc(len + 3, sizeof(unichar));
84 NSAssert(buf, @"could not allocate char buffer");
86 [_sql getCharacters:buf];
87 for (i = 0, didReplace = NO; i < len; i++) {
90 [self warnWithFormat:@"(%s): SQL string contains a '*': %@",
91 __PRETTY_FUNCTION__, _sql];
99 _sql = [NSString stringWithCharacters:buf length:len];
102 return [EOQualifier qualifierWithQualifierFormat:_sql];
105 /* parsing parts (exported for overloading in subclasses) */
108 uniIsCEq(unichar *haystack, const unsigned char *needle, unsigned len)
110 register unsigned idx;
111 for (idx = 0; idx < len; idx++) {
112 if (*needle == '\0') return YES;
113 if (toupper(haystack[idx]) != needle[idx]) return NO;
117 static inline void skipSpaces(unichar **pos, unsigned *len) {
119 if (!isspace(*pos[0])) return;
124 static void printUniStr(unichar *pos, unsigned len) __attribute__((unused));
125 static void printUniStr(unichar *pos, unsigned len) {
127 for (i = 0; i < len && i < 80; i++)
132 static inline BOOL isTokStopChar(unichar c) {
135 case ')': case '(': case '"': case '\'':
138 if (isspace(c)) return YES;
143 - (BOOL)parseToken:(const unsigned char *)tk
144 from:(unichar **)pos length:(unsigned *)len
145 consume:(BOOL)consume
147 /* ...[space] (strlen(tk)+1 chars) */
152 scur=*pos; slen=*len; // begin transaction
153 skipSpaces(&scur, &slen);
157 if (toupper(scur[0]) != tk[0])
159 if (tlen < slen) { /* if tok is not at the end */
160 if (!isTokStopChar(scur[tlen]))
161 return NO; /* not followed by a token stopper */
163 if (!uniIsCEq(scur, tk, tlen))
166 scur+=tlen; slen-=tlen;
168 if (consume) { *pos = scur; *len = slen; } // end tx
172 - (BOOL)parseIdentifier:(NSString **)result
173 from:(unichar **)pos length:(unsigned *)len
174 consume:(BOOL)consume
176 /* "attr" or attr (at least 1 char or 2 for ") */
180 if (result) *result = nil;
181 scur=*pos; slen=*len; // begin transaction
182 skipSpaces(&scur, &slen);
188 //printf("try quoted attr\n");
189 if (slen < 2) return NO;
190 scur++; slen--; /* skip quote */
194 if (consume) { *pos = scur; *len = slen; } // end transaction
196 //printf("is empty quoted\n");
199 if (slen < 2) return NO;
202 while ((slen > 0) && (*scur != '"')) {
203 if (*scur == '\\' && (slen > 1)) {
205 scur++; slen--; // skip one more (still needs to be filtered in result
209 if (slen > 0) { scur++; slen--; } /* skip quote */
211 // TODO: xhandle contained quoted chars ?
213 [[NSString alloc] initWithCharacters:start length:(scur-start-1)];
214 //NSLog(@"found qattr: %@", *result);
217 /* non-quoted attr */
220 if (slen < 1) return NO;
222 if ([self parseToken:"FROM" from:&scur length:&slen consume:NO]) {
223 /* not an attribute, the from starts ... */
224 // printf("rejected unquoted attr, is a FROM\n");
227 if ([self parseToken:"WHERE" from:&scur length:&slen consume:NO]) {
228 /* not an attribute, the where starts ... */
229 // printf("rejected unquoted attr, is a WHERE\n");
234 while ((slen > 0) && !isspace(*scur) && (*scur != ',')) {
238 *result = [[NSString alloc] initWithCharacters:start length:(scur-start)];
239 //NSLog(@"found attr: %@ (len=%i)", *result, (scur-start));
241 if (consume && result) { *pos = scur; *len = slen; } // end transaction
242 return *result ? YES : NO;
245 - (BOOL)parseTableName:(NSString **)result
246 from:(unichar **)pos length:(unsigned *)len
247 consume:(BOOL)consume
249 return [self parseIdentifier:result from:pos length:len consume:consume];
252 - (BOOL)parseIdentifierList:(NSArray **)result
253 from:(unichar **)pos length:(unsigned *)len
257 NSMutableArray *attrs = nil;
261 BOOL (*parser)(id, SEL, NSString **, unichar **, unsigned *, BOOL);
263 if (result) *result = nil;
264 scur=*pos; slen=*len; // begin transaction
265 skipSpaces(&scur, &slen);
266 parser = (void *)[self methodForSelector:_sel];
268 if (slen < 1) return NO; // not enough chars
271 /* a wildcard list, return 'nil' as result */
272 //printf("try wildcard\n");
273 scur++; slen--; // skip '*'
274 if (!(slen == 0 || isspace(*scur))) {
275 /* not followed by space or at end */
278 *pos = scur; *len = slen; // end transaction
283 if (!parser(self, _sel, &attr,&scur,&slen,YES))
284 /* well, we need at least one attribute to make it a list */
287 attrs = [[NSMutableArray alloc] initWithCapacity:32];
288 [attrs addObject:attr]; [attr release];
290 /* all the remaining attributes must be prefixed with a "," */
292 //printf("try next list attr comma\n");
293 skipSpaces(&scur, &slen);
295 if (*scur != ',') break;
296 scur++; slen--; // skip ','
298 //printf("try next list attr\n");
299 if (!parser(self, _sel, &attr,&scur,&slen,YES))
302 [attrs addObject:attr]; [attr release];
305 *pos = scur; *len = slen; // end transaction
310 - (BOOL)parseContainsQualifier:(EOQualifier **)q_
311 from:(unichar **)pos length:(unsigned *)len
313 /* contains('"hh@"') [12+ chars] */
318 skipSpaces(&scur, &slen);
320 if (slen < 12) return NO; // not enough chars
322 if (![self parseToken:"CONTAINS" from:pos length:len consume:YES])
324 skipSpaces(&scur, &slen);
325 [self parseToken:"('" from:&scur length:&slen consume:YES];
327 if (![self parseIdentifier:&s from:&scur length:&slen consume:YES])
330 skipSpaces(&scur, &slen);
331 [self parseToken:"')" from:&scur length:&slen consume:YES];
333 *q_ = [[EOQualifier qualifierWithQualifierFormat:
334 @"contentAsString doesContain: %@", s] retain];
336 *pos = scur; *len = slen; // end transaction
343 - (BOOL)parseQualifier:(EOQualifier **)result
344 from:(unichar **)pos length:(unsigned *)len
349 if (result) *result = nil;
350 scur=*pos; slen=*len; // begin transaction
351 skipSpaces(&scur, &slen);
353 if (slen < 3) return NO; // not enough chars
355 // for now should scan till we find either ORDER BY order GROUP BY
357 unichar *start = scur;
360 if (*scur == 'O' || *scur == 'o') {
361 if ([self parseToken:"ORDER" from:&scur length:&slen consume:NO]) {
362 //printf("FOUND ORDER TOKEN ...\n");
366 else if (*scur == 'G' || *scur == 'g') {
367 if ([self parseToken:"GROUP" from:&scur length:&slen consume:NO]) {
368 //printf("FOUND GROUP TOKEN ...\n");
380 s = [[NSString alloc] initWithCharacters:start length:(scur-start)];
381 if ([s length] == 0) {
385 if ((q = [self parseSQLWhereExpression:s]) == nil) {
389 *result = [q retain];
394 *pos = scur; *len = slen; // end transaction
398 - (BOOL)parseScope:(NSString **)_scope:(NSString **)_entity
399 from:(unichar **)pos length:(unsigned *)len
402 "('shallow traversal of "..."')"
403 "('hierarchical traversal of "..."')"
407 NSString *entityName;
411 if (_scope) *_scope = nil;
412 if (_entity) *_entity = nil;
413 scur=*pos; slen=*len; // begin transaction
414 skipSpaces(&scur, &slen);
415 if (slen < 14) return NO; // not enough chars
417 if (*scur != '(') return NO; // does not start with '('
418 scur++; slen--; // skip '('
419 skipSpaces(&scur, &slen);
421 if (*scur != '\'') return NO; // does not start with '(''
422 scur++; slen--; // skip single quote
426 if ([self parseToken:"SHALLOW" from:&scur length:&slen consume:YES])
428 else if ([self parseToken:"HIERARCHICAL" from:&scur length:&slen consume:YES])
430 else if ([self parseToken:"DEEP" from:&scur length:&slen consume:YES])
433 /* unknown traveral key */
436 /* some syntactic sugar (not strict about that ...) */
437 [self parseToken:"TRAVERSAL" from:&scur length:&slen consume:YES];
438 [self parseToken:"OF" from:&scur length:&slen consume:YES];
439 if (slen < 1) return NO; // not enough chars
442 skipSpaces(&scur, &slen);
443 if (![self parseTableName:&entityName from:&scur length:&slen consume:YES])
444 return NO; // failed to parse entity from scope
447 skipSpaces(&scur, &slen);
448 if (slen > 0 && *scur == '\'') {
449 scur++; slen--; // skip single quote
451 skipSpaces(&scur, &slen);
452 if (slen > 0 && *scur == ')') {
453 scur++; slen--; // skip ')'
456 if (_scope) *_scope = isShallow ? @"flat" : @"deep";
457 if (_entity) *_entity = entityName;
458 *pos = scur; *len = slen; // end transaction
462 - (BOOL)parseSELECT:(EOFetchSpecification **)result
463 from:(unichar **)pos length:(unsigned *)len
464 strict:(BOOL)beStrict
466 EOFetchSpecification *fs;
467 NSMutableDictionary *lHints;
468 NSString *scope = nil;
469 NSArray *attrs = nil;
470 NSArray *fromList = nil;
471 NSArray *orderList = nil;
472 NSArray *lSortOrderings = nil;
473 EOQualifier *q = nil;
476 BOOL missingByOfOrder = NO;
477 BOOL missingByOfGroup = NO;
481 if (![self parseToken:"SELECT" from:pos length:len consume:YES]) {
482 /* must begin with SELECT */
483 if (beStrict) return NO;
488 if (![self parseIdentifierList:&attrs from:pos length:len
489 selector:@selector(parseColumnName:from:length:consume:)]) {
490 [self logWithFormat:@"missing ID list .."];
493 //[self debugWithFormat:@"parsed attrs (%i): %@", [attrs count], attrs];
495 /* now a from is expected */
496 if ([self parseToken:"FROM" from:pos length:len consume:YES])
499 if (beStrict) return NO;
502 /* check whether it's followed by a scope */
503 if ([self parseToken:"SCOPE" from:pos length:len consume:YES]) {
504 NSString *scopeEntity = nil;
506 if (![self parseScope:&scope:&scopeEntity from:pos length:len]) {
507 if (beStrict) return NO;
511 [self logWithFormat:@"FOUND SCOPE: '%@'", scope];
515 fromList = [[NSArray alloc] initWithObjects:scopeEntity, nil];
516 [scopeEntity release];
519 if (![self parseIdentifierList:&fromList from:pos length:len
520 selector:@selector(parseTableName:from:length:consume:)]) {
521 [self logWithFormat:@"missing from list .."];
525 [self logWithFormat:@"parsed FROM list (%i): %@",
526 [fromList count], fromList];
531 if ([self parseToken:"WHERE" from:pos length:len consume:YES]) {
532 /* parse qualifier ... */
534 if ([self parseToken:"CONTAINS" from:pos length:len consume:NO]) {
535 if (![self parseContainsQualifier:&q from:pos length:len]) {
536 if (beStrict) return NO;
539 else if (![self parseQualifier:&q from:pos length:len]) {
540 if (beStrict) return NO;
543 [self logWithFormat:@"FOUND Qualifier: '%@'", q];
548 if ([self parseToken:"ORDER" from:pos length:len consume:YES]) {
549 if (![self parseToken:"BY" from:pos length:len consume:YES]) {
550 if (beStrict) return NO;
551 missingByOfOrder = YES;
554 if (![self parseIdentifierList:&orderList from:pos length:len
555 selector:@selector(parseColumnName:from:length:consume:)])
558 [self logWithFormat:@"parsed ORDER list (%i): %@",
559 [orderList count], orderList];
564 if ([self parseToken:"GROUP" from:pos length:len consume:YES]) {
565 if (![self parseToken:"BY" from:pos length:len consume:YES]) {
566 if (beStrict) return NO;
567 missingByOfGroup = YES;
571 //printUniStr(*pos, *len); // DEBUG
573 if (!hasSelect) [self logWithFormat:@"missing SELECT !"];
574 if (!hasFrom) [self logWithFormat:@"missing FROM !"];
575 if (missingByOfOrder) [self logWithFormat:@"missing BY in ORDER BY !"];
577 /* build fetchspec */
579 lHints = [[NSMutableDictionary alloc] initWithCapacity:16];
582 [lHints setObject:scope forKey:@"scope"];
583 [scope release]; scope = nil;
586 [lHints setObject:attrs forKey:@"attributes"];
587 [attrs release]; attrs = nil;
593 len = [orderList count];
594 ma = [[NSMutableArray alloc] initWithCapacity:len];
595 for (i = 0; i < len; i++) {
598 so = [EOSortOrdering sortOrderingWithKey:[orderList objectAtIndex:i]
599 selector:EOCompareAscending];
601 lSortOrderings = [ma shallowCopy];
603 [orderList release]; orderList = nil;
606 fs = [[EOFetchSpecification alloc]
607 initWithEntityName:[fromList componentsJoinedByString:@","]
609 sortOrderings:lSortOrderings
610 usesDistinct:NO isDeep:NO hints:lHints];
616 return fs ? YES : NO;
619 - (BOOL)parseSQL:(id *)result
620 from:(unichar **)pos length:(unsigned *)len
621 strict:(BOOL)beStrict
623 if (*len < 1) return NO;
625 if ([self parseToken:"SELECT" from:pos length:len consume:NO])
626 return [self parseSELECT:result from:pos length:len strict:beStrict];
628 //if ([self parseToken:"UPDATE" from:pos length:len consume:NO])
629 //if ([self parseToken:"INSERT" from:pos length:len consume:NO])
630 //if ([self parseToken:"DELETE" from:pos length:len consume:NO])
632 [self logWithFormat:@"tried to parse an unsupported SQL statement."];
636 /* WebDAV specific */
638 - (EOQualifier *)parseSQLWhereExpression:(NSString *)_sql {
639 // TODO: process %=>* and %%, and $
640 static EOSQLQualifierIdMapper *map = nil;
643 if ((q = [self _parseSQLWhereExpression:_sql]) == nil)
646 if (map == nil) map = [[EOSQLQualifierIdMapper alloc] init];
647 q = [q qualifierByApplyingKeyMap:(id)map];
651 - (BOOL)parseColumnName:(NSString **)result
652 from:(unichar **)pos length:(unsigned *)len
653 consume:(BOOL)consume
657 ok = [self parseIdentifier:result from:pos length:len consume:consume];
661 /* heuristics to morph fully qualified name to namespace/localname */
665 r = [*result rangeOfString:@"/"
666 options:NSLiteralSearch|NSBackwardsSearch];
668 r = [*result rangeOfString:@":"
669 options:NSLiteralSearch|NSBackwardsSearch];
674 l = [*result substringFromIndex:(r.location + r.length)];
675 ns = [*result substringToIndex:(r.location + 1)];
677 [*result autorelease];
678 *result = [[NSString alloc] initWithFormat:@"{%@}%@", ns, l];
683 @end /* SoDAVSQLParser */
685 @implementation SoDAVSQLParser(Tests)
687 + (void)testDAVQuery {
688 EOFetchSpecification *fs;
691 NSLog(@"testing: %@ --------------------", self);
695 @" \"http://schemas.microsoft.com/mapi/proptag/x0e230003\", \n"
696 @" \"urn:schemas:mailheader:subject\", \n"
697 @" \"urn:schemas:mailheader:from\",\n"
698 @" \"urn:schemas:mailheader:to\", \n"
699 @" \"urn:schemas:mailheader:cc\", \n"
700 @" \"urn:schemas:httpmail:read\", \n"
701 @" \"urn:schemas:httpmail:hasattachment\", \n"
702 @" \"DAV:getcontentlength\", \n"
703 @" \"urn:schemas:mailheader:date\", \n"
704 @" \"urn:schemas:httpmail:date\", \n"
705 @" \"urn:schemas:mailheader:received\", \n"
706 @" \"urn:schemas:mailheader:message-id\", \n"
707 @" \"urn:schemas:mailheader:in-reply-to\", \n"
708 @" \"urn:schemas:mailheader:references\" \n"
710 @" scope('shallow traversal of \"http://127.0.0.1:9000/o/ol/helge/INBOX\"')\n"
712 @" \"DAV:iscollection\" = False \n"
714 @" \"http://schemas.microsoft.com/mapi/proptag/x0c1e001f\" != 'SMTP'\n"
716 @" \"http://schemas.microsoft.com/mapi/proptag/x0e230003\" > 0 \n"
718 fs = [[self sharedSQLParser] parseSQLSelectStatement:sql];
720 NSLog(@" FS: %@", fs);
722 [self errorWithFormat:@" could not parse SQL: %@", sql];
729 if ((scope = [[fs hints] objectForKey:@"scope"]) == nil)
730 NSLog(@" INVALID: got no scope !");
731 if (![scope isEqualToString:@"flat"])
732 NSLog(@" INVALID: got scope %@, expected flat !", scope);
735 if ([fs queryWebDAVPropertyNamesOnly])
736 NSLog(@" INVALID: name query only, but queried several attrs !");
739 /* check qualifier */
740 if ((q = [fs qualifier]) == nil)
741 NSLog(@" INVALID: got not qualifier (expected one) !");
742 else if (![q isKindOfClass:[EOAndQualifier class]]) {
743 NSLog(@" INVALID: expected AND qualifier, got %@ !",
744 NSStringFromClass([q class]));
746 else if ([[(EOAndQualifier *)q qualifiers] count] != 3) {
747 NSLog(@" INVALID: expected 3 subqualifiers, got %i !",
748 [[(EOAndQualifier *)q qualifiers] count]);
751 /* check sortordering */
752 if ([fs sortOrderings] != nil) {
753 NSLog(@" INVALID: got sort orderings, specified none: %@ !",
758 if ((props = [[fs hints] objectForKey:@"attributes"]) == nil)
759 NSLog(@" INVALID: got not attributes (expected some) !");
760 else if (![props isKindOfClass:[NSArray class]]) {
761 NSLog(@" INVALID: attributes not delivered as array ?: %@",
762 NSStringFromClass([props class]));
764 else if ([props count] != 14) {
765 NSLog(@" INVALID: invalid attribute count, expected 14, got %i.",
770 NSLog(@"done test: %@ ------------------", self);
773 @end /* SoDAVSQLParser(Tests) */
775 @implementation EOSQLQualifierIdMapper
777 - (id)objectForKey:(NSString *)_key {
781 if (_key == nil) return nil;
783 r = [_key rangeOfString:@"/" options:NSLiteralSearch|NSBackwardsSearch];
785 r = [_key rangeOfString:@":" options:NSLiteralSearch|NSBackwardsSearch];
789 l = [_key substringFromIndex:(r.location + r.length)];
790 ns = [_key substringToIndex:(r.location + 1)];
792 return [NSString stringWithFormat:@"{%@}%@", ns, l];
795 @end /* EOSQLQualifierIdMapper */