/* MDKQuery.h * * Copyright (C) 2006 Free Software Foundation, Inc. * * Author: Enrico Sersale * Date: August 2006 * * This file is part of the GNUstep GWorkspace application * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111 USA. */ #include "MDKQuery.h" #include "MDKQueryManager.h" #include "SQLite.h" #include "FSNode.h" static NSArray *attrNames = nil; static NSDictionary *attrInfo = nil; static NSString *path_sep(void); BOOL subPathOfPath(NSString *p1, NSString *p2); static NSArray *basesetAttributes(void) { static NSArray *attributes = nil; if (attributes == nil) { attributes = [[NSArray alloc] initWithObjects: @"GSMDItemFSName", @"GSMDItemFSExtension", @"GSMDItemFSType", @"GSMDItemFSSize", // FSAttribute @"GSMDItemFSModificationDate", // FSAttribute @"GSMDItemFSOwnerUser", // FSAttribute @"GSMDItemFSOwnerGroup", // FSAttribute @"GSMDItemFinderComment", @"GSMDItemApplicationName", @"GSMDItemRole", @"GSMDItemUnixExtensions", @"GSMDItemTitle", @"GSMDItemAuthors", @"GSMDItemCopyrightDescription", nil]; } return attributes; } enum { SUBCLOSED = 1, BUILT = 2, STOPPED = 4, GATHERING = 8, WAITSTART = 16, UPDATE_ENABLE = 32, UPDATING = 64 }; #define CHECKDELEGATE(s) \ ((delegate != nil) \ && [delegate respondsToSelector: @selector(s)]) @interface MDKAttributeQuery : MDKQuery { } - (BOOL)validateOperatorTypeForAttribute:(NSDictionary *)attrinfo; - (void)setOperatorFromType; @end @interface MDKTextContentQuery : MDKQuery { } @end @interface MDKQueryScanner : NSScanner { MDKQuery *rootQuery; MDKQuery *currentQuery; } + (MDKQueryScanner *)scannerWithString:(NSString *)string forRootQuery:(MDKQuery *)query; - (void)parseQuery; - (void)parse; - (MDKQuery *)parseComparison; - (NSString *)scanAttributeName; - (NSDictionary *)scanSearchValueForAttributeType:(int)type; - (BOOL)scanQueryKeyword:(NSString *)key; @end @implementation MDKQuery - (void)dealloc { RELEASE (subqueries); TEST_RELEASE (attribute); TEST_RELEASE (searchValue); TEST_RELEASE (operator); TEST_RELEASE (searchPaths); RELEASE (srcTable); RELEASE (destTable); TEST_RELEASE (joinTable); RELEASE (queryNumber); RELEASE (sqlDescription); RELEASE (sqlUpdatesDescription); TEST_RELEASE (categoryNames); TEST_RELEASE (groupedResults); TEST_RELEASE (fsfilters); [super dealloc]; } + (void)initialize { static BOOL initialized = NO; if (initialized == NO) { NSBundle *bundle = [NSBundle bundleForClass: [self class]]; NSString *dictpath = [bundle pathForResource: @"attributes" ofType: @"plist"]; NSDictionary *dict = [NSDictionary dictionaryWithContentsOfFile: dictpath]; NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; NSDictionary *domain = [defaults persistentDomainForName: @"MDKQuery"]; if (dict == nil) { [NSException raise: NSInternalInconsistencyException format: @"\"%@\" doesn't contain a dictionary!", dictpath]; } ASSIGN (attrInfo, [dict objectForKey: @"attributes"]); ASSIGN (attrNames, [attrInfo allKeys]); if (domain == nil) { domain = [NSDictionary dictionaryWithObjectsAndKeys: basesetAttributes(), @"user-attributes", [dict objectForKey: @"categories"], @"categories", nil]; [defaults setPersistentDomain: domain forName: @"MDKQuery"]; [defaults synchronize]; } else { NSArray *entry = nil; BOOL modified = NO; NSMutableDictionary *mdom = nil; entry = [domain objectForKey: @"user-attributes"]; if ((entry == nil) || ([entry count] == 0)) { mdom = [domain mutableCopy]; [mdom setObject: basesetAttributes() forKey: @"user-attributes"]; modified = YES; } entry = [domain objectForKey: @"categories"]; if ((entry == nil) || ([entry count] == 0)) { if (mdom == nil) { mdom = [domain mutableCopy]; } [mdom setObject: [dict objectForKey: @"categories"] forKey: @"categories"]; modified = YES; } if (modified) { [defaults setPersistentDomain: mdom forName: @"MDKQuery"]; [defaults synchronize]; RELEASE (mdom); } } initialized = YES; } } + (NSArray *)attributesNames { return attrNames; } + (NSDictionary *)attributesInfo { return attrInfo; } + (void)updateUserAttributes:(NSArray *)userattrs { NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; NSMutableDictionary *domain; [defaults synchronize]; domain = [[defaults persistentDomainForName: @"MDKQuery"] mutableCopy]; [domain setObject: userattrs forKey: @"user-attributes"]; [defaults setPersistentDomain: domain forName: @"MDKQuery"]; [defaults synchronize]; RELEASE (domain); } + (NSString *)attributeDescription:(NSString *)attrname { NSDictionary *dict = [attrInfo objectForKey: attrname]; if (dict) { return [dict objectForKey: @"description"]; } return nil; } + (NSDictionary *)attributeWithName:(NSString *)attrname { return [attrInfo objectForKey: attrname]; } + (NSDictionary *)attributesWithMask:(MDKAttributeMask)mask { NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; NSDictionary *domain = [defaults persistentDomainForName: @"MDKQuery"]; NSArray *userSet = [domain objectForKey: @"user-attributes"]; NSMutableDictionary *attributes = [NSMutableDictionary dictionary]; unsigned i; for (i = 0; i < [attrNames count]; i++) { NSString *attrname = [attrNames objectAtIndex: i]; NSDictionary *attrdict = [attrInfo objectForKey: attrname]; BOOL insert = YES; #define CHECK_MASK(m, condition) \ if (insert && (mask & m)) insert = condition CHECK_MASK(MDKAttributeSearchable, [[attrdict objectForKey: @"searchable"] boolValue]); CHECK_MASK(MDKAttributeFSType, [[attrdict objectForKey: @"fsattribute"] boolValue]); CHECK_MASK(MDKAttributeUserSet, [userSet containsObject: attrname]); CHECK_MASK(MDKAttributeBaseSet, [basesetAttributes() containsObject: attrname]); if (insert && ([attributes objectForKey: attrname] == nil)) { [attributes setObject: attrdict forKey: attrname]; } } return attributes; } + (NSArray *)categoryNames { NSDictionary *dict = [self categoryInfo]; if (dict) { return [dict keysSortedByValueUsingSelector: @selector(compareAccordingToIndex:)]; } return nil; } + (NSDictionary *)categoryInfo { NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; NSDictionary *domain; [defaults synchronize]; domain = [defaults persistentDomainForName: @"MDKQuery"]; return [domain objectForKey: @"categories"]; } + (void)updateCategoryInfo:(NSDictionary *)info { NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; NSMutableDictionary *domain; [defaults synchronize]; domain = [[defaults persistentDomainForName: @"MDKQuery"] mutableCopy]; [domain setObject: info forKey: @"categories"]; [defaults setPersistentDomain: domain forName: @"MDKQuery"]; [defaults synchronize]; RELEASE (domain); } + (id)query { return AUTORELEASE ([MDKQuery new]); } + (MDKQuery *)queryFromString:(NSString *)qstr inDirectories:(NSArray *)searchdirs { MDKQuery *query = [self query]; NSMutableString *mqstr = [[qstr mutableCopy] autorelease]; MDKQueryScanner *scanner; [query setSearchPaths: searchdirs]; [mqstr replaceOccurrencesOfString: @"(" withString: @" ( " options: NSLiteralSearch range: NSMakeRange(0, [mqstr length])]; [mqstr replaceOccurrencesOfString: @")" withString: @" ) " options: NSLiteralSearch range: NSMakeRange(0, [mqstr length])]; scanner = [MDKQueryScanner scannerWithString: mqstr forRootQuery: query]; [scanner parseQuery]; return query; } + (MDKQuery *)queryWithContentsOfFile:(NSString *)path { NSDictionary *dict = [NSDictionary dictionaryWithContentsOfFile: path]; if (dict) { id descr = [dict objectForKey: @"description"]; id paths = [dict objectForKey: @"searchpaths"]; if (descr && [descr isKindOfClass: [NSString class]]) { return [self queryFromString: descr inDirectories: paths]; } } return nil; } - (id)init { self = [super init]; if (self) { unsigned long memaddr = (unsigned long)self; unsigned long num; attribute = nil; searchValue = nil; caseSensitive = NO; operatorType = MDKEqualToOperatorType; operator = nil; searchPaths = nil; ASSIGN (srcTable, @"paths"); qmanager = [MDKQueryManager queryManager]; num = [qmanager tableNumber] + memaddr; ASSIGN (destTable, ([NSString stringWithFormat: @"tab_%i", num])); num = [qmanager queryNumber] + memaddr; ASSIGN (queryNumber, [NSNumber numberWithUnsignedLong: num]); joinTable = nil; subqueries = [NSMutableArray new]; parentQuery = nil; compoundOperator = MDKCompoundOperatorNone; sqlDescription = [NSMutableDictionary new]; [sqlDescription setObject: [NSMutableArray array] forKey: @"pre"]; [sqlDescription setObject: [NSString string] forKey: @"join"]; [sqlDescription setObject: [NSMutableArray array] forKey: @"post"]; [sqlDescription setObject: queryNumber forKey: @"qnumber"]; sqlUpdatesDescription = [NSMutableDictionary new]; [sqlUpdatesDescription setObject: [NSMutableArray array] forKey: @"pre"]; [sqlUpdatesDescription setObject: [NSString string] forKey: @"join"]; [sqlUpdatesDescription setObject: [NSMutableArray array] forKey: @"post"]; [sqlUpdatesDescription setObject: queryNumber forKey: @"qnumber"]; categoryNames = nil; fsfilters = nil; reportRawResults = NO; status = 0; delegate = nil; } return self; } - (id)initForAttribute:(NSString *)attr searchValue:(NSString *)value operatorType:(MDKOperatorType)optype { [self subclassResponsibility: _cmd]; return nil; } - (BOOL)writeToFile:(NSString *)path atomically:(BOOL)flag { if ([self isRoot] == NO) { [NSException raise: NSInternalInconsistencyException format: @"%@ is not the root query.", [self description]]; } else if ([self isBuilt] == NO) { [NSException raise: NSInternalInconsistencyException format: @"%@ is not built.", [self description]]; } else { CREATE_AUTORELEASE_POOL(arp); NSMutableDictionary *dict = [NSMutableDictionary dictionary]; BOOL written; [dict setObject: [self description] forKey: @"description"]; if (searchPaths && [searchPaths count]) { [dict setObject: searchPaths forKey: @"searchpaths"]; } written = [dict writeToFile: path atomically: flag]; RELEASE (arp); return written; } return NO; } - (void)setCaseSensitive:(BOOL)csens { caseSensitive = csens; } - (void)setTextOperatorForCaseSensitive:(BOOL)csens { [self subclassResponsibility: _cmd]; } - (void)setSearchPaths:(NSArray *)srcpaths { if (srcpaths) { unsigned i; for (i = 0; i < [subqueries count]; i++) { [[subqueries objectAtIndex: i] setSearchPaths: srcpaths]; } ASSIGN (searchPaths, srcpaths); } else { DESTROY (searchPaths); } } - (NSArray *)searchPaths { return searchPaths; } - (void)setSrcTable:(NSString *)srctab { if (srctab) { ASSIGN (srcTable, srctab); } } - (NSString *)srcTable { return srcTable; } - (void)setDestTable:(NSString *)dsttab { if (dsttab) { ASSIGN (destTable, dsttab); } } - (NSString *)destTable { return destTable; } - (MDKQuery *)queryWithDestTable:(NSString *)tab { unsigned i; if ([destTable isEqual: tab]) { return self; } for (i = 0; i < [subqueries count]; i++) { MDKQuery *query = [subqueries objectAtIndex: i]; if ([query queryWithDestTable: tab] != nil) { return query; } } return nil; } - (void)setJoinTable:(NSString *)jtab { if (jtab) { ASSIGN (joinTable, jtab); if (parentQuery != nil) { [parentQuery setJoinTable: joinTable]; } } } - (NSString *)joinTable { return joinTable; } - (void)setCompoundOperator:(MDKCompoundOperator)op { compoundOperator = op; } - (MDKCompoundOperator)compoundOperator { return compoundOperator; } - (void)setParentQuery:(MDKQuery *)query { MDKQuery *leftSibling; parentQuery = query; leftSibling = [self leftSibling]; if (compoundOperator == GMDAndCompoundOperator) { if (leftSibling) { [self setSrcTable: [leftSibling destTable]]; /* destTable is set in -init */ [parentQuery setDestTable: [self destTable]]; } else { [self setSrcTable: [parentQuery srcTable]]; [self setDestTable: [parentQuery destTable]]; } } else if (compoundOperator == GMDOrCompoundOperator) { if (leftSibling) { [self setSrcTable: [leftSibling srcTable]]; [self setDestTable: [leftSibling destTable]]; } else { [self setSrcTable: [parentQuery srcTable]]; [self setDestTable: [parentQuery destTable]]; } } else { /* first subquery */ if (leftSibling == nil) { [self setSrcTable: [parentQuery srcTable]]; [self setDestTable: [parentQuery destTable]]; } else { [NSException raise: NSInternalInconsistencyException format: @"invalid compound operator"]; } } } - (MDKQuery *)parentQuery { return parentQuery; } - (MDKQuery *)leftSibling { MDKQuery *sibling = nil; if (parentQuery) { NSArray *subs = [parentQuery subqueries]; unsigned index = [subs indexOfObject: self]; if (index > 0) { sibling = [subs objectAtIndex: index - 1]; } } else { [NSException raise: NSInternalInconsistencyException format: @"query not in tree"]; } return sibling; } - (BOOL)hasParentWithCompound:(MDKCompoundOperator)op { Class c = [MDKQuery class]; MDKQuery *query = self; while (query != nil) { query = [query parentQuery]; if (query && [query isMemberOfClass: c]) { MDKCompoundOperator qop = [query compoundOperator]; if (qop == op) { break; } else if (qop != MDKCompoundOperatorNone) { query = nil; } } else { query = nil; } } return (query && (query != self)); } - (MDKQuery *)rootQuery { MDKQuery *query = self; while (1) { MDKQuery *pre = [query parentQuery]; if (pre != nil) { query = pre; } else { break; } } return query; } - (BOOL)isRoot { return (parentQuery == nil); } - (MDKQuery *)appendSubqueryWithCompoundOperator:(MDKCompoundOperator)op { if ([self isClosed] == NO) { MDKQuery *query = [MDKQuery query]; [subqueries addObject: query]; [query setCompoundOperator: op]; [query setParentQuery: self]; [query setSearchPaths: searchPaths]; return query; } [NSException raise: NSInternalInconsistencyException format: @"trying to append to a closed query."]; return nil; } - (void)appendSubquery:(id)query compoundOperator:(MDKCompoundOperator)op { if ([self isClosed] == NO) { if ([subqueries containsObject: query] == NO) { [subqueries addObject: query]; [query setCompoundOperator: op]; [query setParentQuery: self]; [query setSearchPaths: searchPaths]; } } else { [NSException raise: NSInternalInconsistencyException format: @"trying to append to a closed query."]; } } - (void)appendSubqueryWithCompoundOperator:(MDKCompoundOperator)op attribute:(NSString *)attr searchValue:(NSString *)value operatorType:(MDKOperatorType)optype caseSensitive:(BOOL)csens { if ([self isClosed] == NO) { Class queryClass; id query = nil; if ([attr isEqual: @"GSMDItemTextContent"]) { queryClass = [MDKTextContentQuery class]; } else { queryClass = [MDKAttributeQuery class]; } query = [[queryClass alloc] initForAttribute: attr searchValue: value operatorType: optype]; if (query) { [query setCaseSensitive: csens]; [query setSearchPaths: searchPaths]; [subqueries addObject: query]; [query setCompoundOperator: op]; [query setParentQuery: self]; RELEASE (query); } else { [NSException raise: NSInvalidArgumentException format: @"invalid arguments for query %@ %@", attr, value]; } } else { [NSException raise: NSInternalInconsistencyException format: @"trying to append to a closed query."]; } } - (void)appendSubqueriesFromString:(NSString *)qstr { if ([self isRoot]) { NSMutableString *mqstr = [[qstr mutableCopy] autorelease]; MDKQueryScanner *scanner; [mqstr replaceOccurrencesOfString: @"(" withString: @" ( " options: NSLiteralSearch range: NSMakeRange(0, [mqstr length])]; [mqstr replaceOccurrencesOfString: @")" withString: @" ) " options: NSLiteralSearch range: NSMakeRange(0, [mqstr length])]; scanner = [MDKQueryScanner scannerWithString: mqstr forRootQuery: self]; [scanner parseQuery]; } else { [NSException raise: NSInternalInconsistencyException format: @"%@ is not the root query.", [self description]]; } } - (void)closeSubqueries { if ([self isClosed] == NO) { if (parentQuery) { [parentQuery setDestTable: destTable]; } status |= SUBCLOSED; } else { [NSException raise: NSInternalInconsistencyException format: @"trying to close a closed query."]; } } - (BOOL)isClosed { return ((status & SUBCLOSED) == SUBCLOSED); } - (NSArray *)subqueries { return subqueries; } - (BOOL)buildQuery { if ([self isClosed]) { unsigned i; status |= BUILT; for (i = 0; i < [subqueries count]; i++) { if ([[subqueries objectAtIndex: i] buildQuery] == NO) { status &= ~BUILT; break; } } if ([self isBuilt] && [self isRoot]) { ASSIGN (groupedResults, [NSMutableDictionary dictionary]); ASSIGN (categoryNames, [MDKQuery categoryNames]); for (i = 0; i < [categoryNames count]; i++) { NSDictionary *dict = [NSDictionary dictionaryWithObjectsAndKeys: [NSMutableArray array], @"nodes", [NSMutableArray array], @"scores", nil]; [groupedResults setObject: dict forKey: [categoryNames objectAtIndex: i]]; } } return [self isBuilt]; } else { [NSException raise: NSInternalInconsistencyException format: @"trying to build an unclosed query."]; } return NO; } - (BOOL)isBuilt { return ((status & BUILT) == BUILT); } - (void)setFSFilters:(NSArray *)filters { ASSIGN (fsfilters, filters); } - (NSArray *)fsfilters { return fsfilters; } - (void)appendSQLToPreStatements:(NSString *)sqlstr checkExisting:(BOOL)check { if ([self isRoot]) { CREATE_AUTORELEASE_POOL(arp); NSMutableString *sqlUpdatesStr = [sqlstr mutableCopy]; NSMutableArray *sqlpre = [sqlDescription objectForKey: @"pre"]; if ((check == NO) || ([sqlpre containsObject: sqlstr] == NO)) { [sqlpre addObject: sqlstr]; } [sqlUpdatesStr replaceOccurrencesOfString: @"paths" withString: @"updated_paths" options: NSLiteralSearch range: NSMakeRange(0, [sqlUpdatesStr length])]; sqlpre = [sqlUpdatesDescription objectForKey: @"pre"]; if ((check == NO) || ([sqlpre containsObject: sqlUpdatesStr] == NO)) { [sqlpre addObject: sqlUpdatesStr]; } RELEASE (sqlUpdatesStr); RELEASE (arp); } else { [NSException raise: NSInternalInconsistencyException format: @"%@ is not the root query.", [self description]]; } } - (void)appendSQLToPostStatements:(NSString *)sqlstr checkExisting:(BOOL)check { if ([self isRoot]) { CREATE_AUTORELEASE_POOL(arp); NSMutableString *sqlUpdatesStr = [sqlstr mutableCopy]; NSMutableArray *sqlpost = [sqlDescription objectForKey: @"post"]; if ((check == NO) || ([sqlpost containsObject: sqlstr] == NO)) { [sqlpost addObject: sqlstr]; } [sqlUpdatesStr replaceOccurrencesOfString: @"paths" withString: @"updated_paths" options: NSLiteralSearch range: NSMakeRange(0, [sqlUpdatesStr length])]; sqlpost = [sqlUpdatesDescription objectForKey: @"post"]; if ((check == NO) || ([sqlpost containsObject: sqlUpdatesStr] == NO)) { [sqlpost addObject: sqlUpdatesStr]; } RELEASE (sqlUpdatesStr); RELEASE (arp); } else { [NSException raise: NSInternalInconsistencyException format: @"%@ is not the root query.", [self description]]; } } - (NSString *)description { NSMutableString *descr = [NSMutableString string]; unsigned i; if ([self isRoot] == NO) { [descr appendString: @"("]; } for (i = 0; i < [subqueries count]; i++) { MDKQuery *query = [subqueries objectAtIndex: i]; MDKCompoundOperator op = [query compoundOperator]; switch (op) { case GMDAndCompoundOperator: [descr appendString: @" && "]; break; case GMDOrCompoundOperator: [descr appendString: @" || "]; break; case MDKCompoundOperatorNone: default: [descr appendString: @" "]; break; } [descr appendString: [[subqueries objectAtIndex: i] description]]; } if ([self isRoot] == NO) { [descr appendString: @" )"]; } return descr; } @end @implementation MDKAttributeQuery - (void)dealloc { [super dealloc]; } - (id)initForAttribute:(NSString *)attr searchValue:(NSString *)value operatorType:(MDKOperatorType)optype { self = [super init]; if (self) { ASSIGN (attribute, attr); ASSIGN (searchValue, stringForQuery(value)); operatorType = optype; status |= SUBCLOSED; if ([attrNames containsObject: attribute]) { NSDictionary *info = [attrInfo objectForKey: attribute]; if ([self validateOperatorTypeForAttribute: info] == NO) { DESTROY (self); return self; } attributeType = [[info objectForKey: @"type"] intValue]; switch (attributeType) { case STRING: case ARRAY: case DATA: [self setTextOperatorForCaseSensitive: NO]; break; case NUMBER: case DATE: [self setOperatorFromType]; break; default: DESTROY (self); break; } } else { DESTROY (self); } } return self; } - (BOOL)validateOperatorTypeForAttribute:(NSDictionary *)attrinfo { int attrtype = [[attrinfo objectForKey: @"type"] intValue]; if ((attrtype == STRING) || (attrtype == DATA)) { if ((operatorType != MDKEqualToOperatorType) && (operatorType != MDKNotEqualToOperatorType)) { return NO; } } else if (attrtype == ARRAY) { int elemtype = [[attrinfo objectForKey: @"elements_type"] intValue]; if ((elemtype == STRING) || (elemtype == DATA)) { if ((operatorType != MDKEqualToOperatorType) && (operatorType != MDKNotEqualToOperatorType)) { return NO; } } else { return NO; } } else if (attrtype == NUMBER) { int numtype = [[attrinfo objectForKey: @"number_type"] intValue]; if (numtype == NUM_BOOL) { if ((operatorType != MDKEqualToOperatorType) && (operatorType != MDKNotEqualToOperatorType)) { return NO; } } } else if (attrtype == DATE) { if ([searchValue floatValue] == 0.0) { return NO; } } else { return NO; } return YES; } - (void)setOperatorFromType { switch (operatorType) { case MDKLessThanOperatorType: ASSIGN (operator, @"<"); break; case MDKLessThanOrEqualToOperatorType: ASSIGN (operator, @"<="); break; case MDKGreaterThanOperatorType: ASSIGN (operator, @">"); break; case MDKGreaterThanOrEqualToOperatorType: ASSIGN (operator, @">="); break; case MDKNotEqualToOperatorType: ASSIGN (operator, @"!="); break; case MDKInRangeOperatorType: /* FIXME */ break; case MDKEqualToOperatorType: default: ASSIGN (operator, @"=="); break; } } - (void)setCaseSensitive:(BOOL)csens { if ((attributeType == STRING) || (attributeType == ARRAY) || (attributeType == DATA)) { [self setTextOperatorForCaseSensitive: csens]; } caseSensitive = csens; } - (void)setTextOperatorForCaseSensitive:(BOOL)csens { NSString *wc = (csens ? @"%" : @"*"); NSString *wildcard = (csens ? @"*" : @"%"); if (operatorType == MDKEqualToOperatorType) { ASSIGN (operator, (csens ? @"GLOB" : @"LIKE")); } else { ASSIGN (operator, (csens ? @"NOT GLOB" : @"NOT LIKE")); } if ([searchValue rangeOfString: wc].location != NSNotFound) { NSMutableString *mvalue = [searchValue mutableCopy]; [mvalue replaceOccurrencesOfString: wc withString: wildcard options: NSLiteralSearch range: NSMakeRange(0, [mvalue length])]; ASSIGN (searchValue, [mvalue makeImmutableCopyOnFail: NO]); RELEASE (mvalue); } caseSensitive = csens; } - (MDKQuery *)appendSubqueryWithCompoundOperator:(MDKCompoundOperator)op { [NSException raise: NSInternalInconsistencyException format: @"Cannot append to a MDKAttributeQuery instance."]; return nil; } - (void)appendSubquery:(id)query compoundOperator:(MDKCompoundOperator)op { [NSException raise: NSInternalInconsistencyException format: @"Cannot append to a MDKAttributeQuery instance."]; } - (void)appendSubqueryWithCompoundOperator:(MDKCompoundOperator)op attribute:(NSString *)attr searchValue:(NSString *)value operatorType:(MDKOperatorType)optype caseSensitive:(BOOL)csens { [NSException raise: NSInternalInconsistencyException format: @"Cannot append to a MDKAttributeQuery instance."]; } - (BOOL)buildQuery { MDKQuery *root = [self rootQuery]; MDKQuery *leftSibling = [self leftSibling]; NSMutableString *sqlstr; sqlstr = [NSString stringWithFormat: @"CREATE TEMP TABLE %@ " @"(id INTEGER UNIQUE ON CONFLICT IGNORE, " @"path TEXT UNIQUE ON CONFLICT IGNORE, " @"words_count INTEGER, " @"score REAL); ", destTable]; [root appendSQLToPreStatements: sqlstr checkExisting: YES]; sqlstr = [NSString stringWithFormat: @"CREATE TEMP TRIGGER %@_trigger " @"BEFORE INSERT ON %@ " @"BEGIN " @"UPDATE %@ " @"SET score = (score + new.score) " @"WHERE id = new.id; " @"END;", destTable, destTable, destTable]; [root appendSQLToPreStatements: sqlstr checkExisting: YES]; sqlstr = [NSMutableString string]; [sqlstr appendFormat: @"INSERT INTO %@ (id, path, words_count, score) " @"SELECT " @"%@.id, " @"%@.path, " @"%@.words_count, " @"attributeScore('%@', '%@', attributes.attribute, %i, %i) " @"FROM %@, attributes " @"WHERE attributes.key = '%@' ", destTable, srcTable, srcTable, srcTable, attribute, searchValue, attributeType, operatorType, srcTable, attribute]; [sqlstr appendFormat: @"AND attributes.attribute %@ ", operator]; if ((attributeType == STRING) || (attributeType == DATA)) { [sqlstr appendString: @"'"]; [sqlstr appendString: searchValue]; [sqlstr appendString: @"' "]; } else if (attributeType == ARRAY) { [sqlstr appendString: @"'"]; [sqlstr appendString: (caseSensitive ? @"*" : @"%")]; [sqlstr appendString: searchValue]; [sqlstr appendString: (caseSensitive ? @"*" : @"%")]; [sqlstr appendString: @"' "]; } else if (attributeType == NUMBER) { NSDictionary *info = [attrInfo objectForKey: attribute]; int numtype = [[info objectForKey: @"number_type"] intValue]; [sqlstr appendFormat: @"(cast (%@ as ", searchValue]; if (numtype == NUM_FLOAT) { [sqlstr appendString: @"REAL)) "]; } else { [sqlstr appendString: @"INTEGER)) "]; } } else if (attributeType == DATE) { [sqlstr appendFormat: @"(cast (%@ as REAL)) ", searchValue]; } else { return NO; } [sqlstr appendFormat: @"AND attributes.path_id = %@.id ", srcTable]; if (searchPaths) { unsigned count = [searchPaths count]; unsigned i; [sqlstr appendString: @"AND ("]; for (i = 0; i < count; i++) { NSString *path = [searchPaths objectAtIndex: i]; NSString *minpath = [NSString stringWithFormat: @"%@%@*", path, path_sep()]; [sqlstr appendFormat: @"(%@.path = '%@' OR %@.path GLOB '%@') ", srcTable, path, srcTable, minpath]; if (i != (count - 1)) { [sqlstr appendString: @"OR "]; } } [sqlstr appendString: @")"]; } [sqlstr appendString: @";"]; [root appendSQLToPreStatements: sqlstr checkExisting: NO]; if (((leftSibling != nil) && (compoundOperator == GMDAndCompoundOperator)) || ((leftSibling == nil) && [self hasParentWithCompound: GMDAndCompoundOperator])) { NSMutableString *joinquery = [NSMutableString string]; [joinquery appendFormat: @"INSERT INTO %@ (id, path, words_count, score) " @"SELECT " @"%@.id, " @"%@.path, " @"%@.words_count, " @"%@.score " @"FROM " @"%@, %@ " @"WHERE " @"%@.id = %@.id; ", destTable, srcTable, srcTable, srcTable, srcTable, srcTable, destTable, srcTable, destTable]; [root appendSQLToPreStatements: joinquery checkExisting: NO]; } [root appendSQLToPostStatements: [NSString stringWithFormat: @"DROP TABLE %@", destTable] checkExisting: YES]; [parentQuery setJoinTable: destTable]; status |= BUILT; return [self isBuilt]; } - (NSString *)description { NSMutableString *descr = [NSMutableString string]; NSString *svalue = searchValue; BOOL txtype = ((attributeType == STRING) || (attributeType == ARRAY) || (attributeType == DATA)); [descr appendString: attribute]; switch (operatorType) { case MDKLessThanOperatorType: [descr appendString: @" < "]; break; case MDKLessThanOrEqualToOperatorType: [descr appendString: @" <= "]; break; case MDKGreaterThanOperatorType: [descr appendString: @" > "]; break; case MDKGreaterThanOrEqualToOperatorType: [descr appendString: @" >= "]; break; case MDKEqualToOperatorType: [descr appendString: @" == "]; break; case MDKNotEqualToOperatorType: [descr appendString: @" != "]; break; case MDKInRangeOperatorType: /* TODO */ break; default: break; } if (txtype) { NSMutableString *mvalue = [[searchValue mutableCopy] autorelease]; [mvalue replaceOccurrencesOfString: @"%" withString: @"*" options: NSLiteralSearch range: NSMakeRange(0, [mvalue length])]; svalue = mvalue; [descr appendString: @"\""]; } [descr appendString: svalue]; if (txtype) { [descr appendString: @"\""]; if (caseSensitive == NO) { [descr appendString: @"c"]; } } return descr; } @end @implementation MDKTextContentQuery - (void)dealloc { [super dealloc]; } - (id)initForAttribute:(NSString *)attr searchValue:(NSString *)value operatorType:(MDKOperatorType)optype { self = [super init]; if (self) { if ((optype != MDKEqualToOperatorType) && (optype != MDKNotEqualToOperatorType)) { DESTROY (self); return self; } ASSIGN (attribute, attr); attributeType = STRING; ASSIGN (searchValue, stringForQuery(value)); operatorType = optype; [self setTextOperatorForCaseSensitive: YES]; status |= SUBCLOSED; } return self; } - (void)setCaseSensitive:(BOOL)csens { [self setTextOperatorForCaseSensitive: csens]; } - (void)setTextOperatorForCaseSensitive:(BOOL)csens { NSString *wc = (csens ? @"%" : @"*"); NSString *wildcard = (csens ? @"*" : @"%"); ASSIGN (operator, (csens ? @"GLOB" : @"LIKE")); if ([searchValue rangeOfString: wc].location != NSNotFound) { NSMutableString *mvalue = [searchValue mutableCopy]; [mvalue replaceOccurrencesOfString: wc withString: wildcard options: NSLiteralSearch range: NSMakeRange(0, [mvalue length])]; ASSIGN (searchValue, [mvalue makeImmutableCopyOnFail: NO]); RELEASE (mvalue); } caseSensitive = csens; } - (MDKQuery *)appendSubqueryWithCompoundOperator:(MDKCompoundOperator)op { [NSException raise: NSInternalInconsistencyException format: @"Cannot append to a MDKTextContentQuery instance."]; return nil; } - (void)appendSubquery:(id)query compoundOperator:(MDKCompoundOperator)op { [NSException raise: NSInternalInconsistencyException format: @"Cannot append to a MDKTextContentQuery instance."]; } - (void)appendSubqueryWithCompoundOperator:(MDKCompoundOperator)op attribute:(NSString *)attr searchValue:(NSString *)value operatorType:(MDKOperatorType)optype caseSensitive:(BOOL)csens { [NSException raise: NSInternalInconsistencyException format: @"Cannot append to a MDKTextContentQuery instance."]; } - (BOOL)buildQuery { MDKQuery *root = [self rootQuery]; MDKQuery *leftSibling = [self leftSibling]; NSMutableString *sqlstr; sqlstr = [NSString stringWithFormat: @"CREATE TEMP TABLE %@ " @"(id INTEGER UNIQUE ON CONFLICT IGNORE, " @"path TEXT UNIQUE ON CONFLICT IGNORE, " @"words_count INTEGER, " @"score REAL); ", destTable]; [root appendSQLToPreStatements: sqlstr checkExisting: YES]; sqlstr = [NSString stringWithFormat: @"CREATE TEMP TRIGGER %@_trigger " @"BEFORE INSERT ON %@ " @"BEGIN " @"UPDATE %@ " @"SET score = (score + new.score) " @"WHERE id = new.id; " @"END;", destTable, destTable, destTable]; [root appendSQLToPreStatements: sqlstr checkExisting: YES]; sqlstr = [NSMutableString string]; if (operatorType == MDKEqualToOperatorType) { [sqlstr appendFormat: @"INSERT INTO %@ (id, path, words_count, score) " @"SELECT " @"%@.id, " @"%@.path, " @"%@.words_count, " @"wordScore('%@', words.word, postings.word_count, %@.words_count) " @"FROM words, %@, postings ", destTable, srcTable, srcTable, srcTable, searchValue, srcTable, srcTable]; [sqlstr appendFormat: @"WHERE words.word %@ '", operator]; [sqlstr appendString: searchValue]; [sqlstr appendString: @"' "]; [sqlstr appendFormat: @"AND postings.word_id = words.id " @"AND %@.id = postings.path_id ", srcTable]; } else { /* MDKNotEqualToOperatorType */ [sqlstr appendFormat: @"INSERT INTO %@ (id, path, words_count, score) " @"SELECT " @"%@.id AS tid, " @"%@.path, " @"%@.words_count, " @"(1.0 / %@.words_count) " @"FROM %@ ", destTable, srcTable, srcTable, srcTable, srcTable, srcTable]; [sqlstr appendString: @"WHERE " @"(SELECT words.word " @"FROM words, postings " @"WHERE postings.path_id = tid " @"AND words.id = postings.word_id "]; [sqlstr appendFormat: @"AND words.word %@ '", operator]; [sqlstr appendString: searchValue]; [sqlstr appendString: @"') ISNULL "]; } if (searchPaths) { unsigned count = [searchPaths count]; unsigned i; [sqlstr appendString: @"AND ("]; for (i = 0; i < count; i++) { NSString *path = [searchPaths objectAtIndex: i]; NSString *minpath = [NSString stringWithFormat: @"%@%@*", path, path_sep()]; [sqlstr appendFormat: @"(%@.path = '%@' OR %@.path GLOB '%@') ", srcTable, path, srcTable, minpath]; if (i != (count - 1)) { [sqlstr appendString: @"OR "]; } } [sqlstr appendString: @") "]; } [sqlstr appendString: @";"]; [root appendSQLToPreStatements: sqlstr checkExisting: NO]; if (((leftSibling != nil) && (compoundOperator == GMDAndCompoundOperator)) || ((leftSibling == nil) && [self hasParentWithCompound: GMDAndCompoundOperator])) { NSMutableString *joinquery = [NSMutableString string]; [joinquery appendFormat: @"INSERT INTO %@ (id, path, words_count, score) " @"SELECT " @"%@.id, " @"%@.path, " @"%@.words_count, " @"%@.score " @"FROM " @"%@, %@ " @"WHERE " @"%@.id = %@.id; ", destTable, srcTable, srcTable, srcTable, srcTable, srcTable, destTable, srcTable, destTable]; [root appendSQLToPreStatements: joinquery checkExisting: NO]; } [root appendSQLToPostStatements: [NSString stringWithFormat: @"DROP TABLE %@", destTable] checkExisting: YES]; [parentQuery setJoinTable: destTable]; status |= BUILT; return [self isBuilt]; } - (NSString *)description { NSMutableString *descr = [NSMutableString string]; NSMutableString *mvalue = [[searchValue mutableCopy] autorelease]; [descr appendString: attribute]; if (operatorType == MDKEqualToOperatorType) { [descr appendString: @" == "]; } else { [descr appendString: @" != "]; } [descr appendString: @"\""]; [mvalue replaceOccurrencesOfString: @"%" withString: @"*" options: NSLiteralSearch range: NSMakeRange(0, [mvalue length])]; [descr appendString: mvalue]; [descr appendString: @"\""]; if (caseSensitive == NO) { [descr appendString: @"c"]; } return descr; } @end @implementation MDKQuery (gathering) - (void)setDelegate:(id)adelegate { if ([self isRoot]) { delegate = adelegate; } else { [NSException raise: NSInternalInconsistencyException format: @"only the root query can have a delegate."]; } } - (NSDictionary *)sqlDescription { if ([self isRoot]) { NSString *jtable = [self joinTable]; NSString *joinquery = [NSString stringWithFormat: @"SELECT %@.path, " @"%@.score " @"FROM %@ " @"ORDER BY " @"%@.score DESC, " @"%@.path ASC;", jtable, jtable, jtable, jtable, jtable]; [sqlDescription setObject: joinquery forKey: @"join"]; return sqlDescription; } else { [NSException raise: NSInternalInconsistencyException format: @"%@ is not the root query.", [self description]]; } return nil; } - (NSDictionary *)sqlUpdatesDescription { if ([self isRoot]) { [sqlUpdatesDescription setObject: [[self sqlDescription] objectForKey: @"join"] forKey: @"join"]; return sqlUpdatesDescription; } else { [NSException raise: NSInternalInconsistencyException format: @"%@ is not the root query.", [self description]]; } return nil; } - (NSNumber *)queryNumber { return queryNumber; } - (void)startGathering { if (([self isGathering] == NO) && ([self waitingStart] == NO)) { status &= ~STOPPED; status |= WAITSTART; [qmanager startQuery: self]; } } - (void)setStarted { status &= ~WAITSTART; status |= GATHERING; if (CHECKDELEGATE (queryDidStartGathering:)) { [delegate queryDidStartGathering: self]; } } - (BOOL)waitingStart { return ((status & WAITSTART) == WAITSTART); } - (BOOL)isGathering { return ((status & GATHERING) == GATHERING); } - (void)gatheringDone { if ([self isStopped]) { status &= ~(GATHERING | UPDATING); } else { status &= ~GATHERING; } if (CHECKDELEGATE (queryDidEndGathering:)) { [delegate queryDidEndGathering: self]; } if ([self updatesEnabled] && ([self isUpdating] == NO) && ([self isStopped] == NO)) { status |= UPDATING; [qmanager startUpdateForQuery: self]; } } - (void)stopQuery { status |= STOPPED; status &= ~WAITSTART; } - (BOOL)isStopped { return ((status & STOPPED) == STOPPED); } - (void)setUpdatesEnabled:(BOOL)enabled { if (enabled) { status |= UPDATE_ENABLE; } else { status &= ~(UPDATE_ENABLE | UPDATING); } } - (BOOL)updatesEnabled { return ((status & UPDATE_ENABLE) == UPDATE_ENABLE); } - (BOOL)isUpdating { return ((status & UPDATING) == UPDATING); } - (void)updatingStarted { if (CHECKDELEGATE (queryDidStartUpdating:)) { [delegate queryDidStartUpdating: self]; } } - (void)updatingDone { if (CHECKDELEGATE (queryDidEndUpdating:)) { [delegate queryDidEndUpdating: self]; } } - (void)appendResults:(NSArray *)lines { if (reportRawResults) { if (CHECKDELEGATE (appendRawResults:)) { [delegate appendRawResults: lines]; } } else { CREATE_AUTORELEASE_POOL(arp); NSMutableArray *catnames = [NSMutableArray array]; BOOL sort = [self isUpdating]; unsigned i; for (i = 0; i < [lines count]; i++) { NSArray *line = [lines objectAtIndex: i]; FSNode *node = [FSNode nodeWithPath: [line objectAtIndex: 0]]; NSNumber *score = [line objectAtIndex: 1]; if (node && [node isValid]) { BOOL caninsert = YES; if (fsfilters && [fsfilters count]) { caninsert = [qmanager filterNode: node withFSFilters: fsfilters]; } if (caninsert) { NSString *category = [qmanager categoryNameForNode: node]; [self insertNode: node andScore: score inDictionary: [groupedResults objectForKey: category] needSorting: sort]; if ([catnames containsObject: category] == NO) { [catnames addObject: category]; } } } } if (CHECKDELEGATE (queryDidUpdateResults:forCategories:)) { [delegate queryDidUpdateResults: self forCategories: catnames]; } RELEASE (arp); } } - (void)insertNode:(FSNode *)node andScore:(NSNumber *)score inDictionary:(NSDictionary *)dict needSorting:(BOOL)sort { NSMutableArray *nodes = [dict objectForKey: @"nodes"]; NSMutableArray *scores = [dict objectForKey: @"scores"]; if ([self isUpdating]) { unsigned index = [nodes indexOfObject: node]; if (index != NSNotFound) { [nodes removeObjectAtIndex: index]; [scores removeObjectAtIndex: index]; } } if (sort) { unsigned count = [nodes count]; int ins = 0; if (count) { int first = 0; int last = count; int pos = 0; NSComparisonResult result; while (1) { if (first == last) { ins = first; break; } pos = (int)((first + last) / 2); result = [[scores objectAtIndex: pos] compare: score]; if (result == NSOrderedSame) { result = [[nodes objectAtIndex: pos] compareAccordingToPath: node]; } if ((result == NSOrderedDescending) || (result == NSOrderedSame)) { first = pos + 1; } else { last = pos; } } } [nodes insertObject: node atIndex: ins]; [scores insertObject: score atIndex: ins]; } else { [nodes addObject: node]; [scores addObject: score]; } } - (void)removePaths:(NSArray *)paths { CREATE_AUTORELEASE_POOL(arp); NSMutableArray *catnames = [NSMutableArray array]; unsigned index; unsigned i; for (i = 0; i < [paths count]; i++) { FSNode *node = [FSNode nodeWithPath: [paths objectAtIndex: i]]; NSString *catname; NSDictionary *catdict; NSMutableArray *catnodes; NSMutableArray *catscores; if ([node isValid]) { catname = [qmanager categoryNameForNode: node]; catdict = [groupedResults objectForKey: catname]; catnodes = [catdict objectForKey: @"nodes"]; catscores = [catdict objectForKey: @"scores"]; index = [catnodes indexOfObject: node]; } else { unsigned j; for (j = 0; j < [categoryNames count]; j++) { catname = [categoryNames objectAtIndex: j]; catdict = [groupedResults objectForKey: catname]; catnodes = [catdict objectForKey: @"nodes"]; catscores = [catdict objectForKey: @"scores"]; index = [catnodes indexOfObject: node]; if (index != NSNotFound) { break; } } } if (index != NSNotFound) { [catnodes removeObjectAtIndex: index]; [catscores removeObjectAtIndex: index]; if ([catnames containsObject: catname] == NO) { [catnames addObject: catname]; } } } if ((index != NSNotFound) && CHECKDELEGATE (queryDidUpdateResults:forCategories:)) { [delegate queryDidUpdateResults: self forCategories: catnames]; } RELEASE (arp); } - (void)removeNode:(FSNode *)node { NSString *catname; NSDictionary *catdict; NSMutableArray *catnodes; NSMutableArray *catscores; unsigned index; if ([node isValid]) { catname = [qmanager categoryNameForNode: node]; catdict = [groupedResults objectForKey: catname]; catnodes = [catdict objectForKey: @"nodes"]; catscores = [catdict objectForKey: @"scores"]; index = [catnodes indexOfObject: node]; } else { unsigned i; for (i = 0; i < [categoryNames count]; i++) { catname = [categoryNames objectAtIndex: i]; catdict = [groupedResults objectForKey: catname]; catnodes = [catdict objectForKey: @"nodes"]; catscores = [catdict objectForKey: @"scores"]; index = [catnodes indexOfObject: node]; if (index != NSNotFound) { break; } } } if (index != NSNotFound) { [catnodes removeObjectAtIndex: index]; [catscores removeObjectAtIndex: index]; if (CHECKDELEGATE (queryDidUpdateResults:forCategories:)) { [delegate queryDidUpdateResults: self forCategories: [NSArray arrayWithObject: catname]]; } } } - (NSDictionary *)groupedResults { return groupedResults; } - (NSArray *)resultNodesForCategory:(NSString *)catname { NSDictionary *catdict = [groupedResults objectForKey: catname]; if (catdict) { return [catdict objectForKey: @"nodes"]; } return nil; } - (int)resultsCountForCategory:(NSString *)catname { NSArray *catdnodes = [self resultNodesForCategory: catname]; return (catdnodes ? [catdnodes count] : 0); } - (void)setReportRawResults:(BOOL)value { reportRawResults = value; } @end @implementation MDKQueryScanner + (MDKQueryScanner *)scannerWithString:(NSString *)string forRootQuery:(MDKQuery *)query { MDKQueryScanner *scanner = [[MDKQueryScanner alloc] initWithString: string]; scanner->rootQuery = query; scanner->currentQuery = query; return AUTORELEASE (scanner); } - (void)parseQuery { while ([self isAtEnd] == NO) { [self parse]; } [rootQuery closeSubqueries]; [rootQuery buildQuery]; } - (void)parse { MDKCompoundOperator op = MDKCompoundOperatorNone; static unsigned int parsed = 0; #define PARSEXCEPT(x, e) \ if (x > 0) [NSException raise: NSInvalidArgumentException format: e] #define COMPOUND 1 #define SUBOPEN 2 #define SUBCLOSE 4 #define COMPARISION 8 if ([self scanQueryKeyword: @"&&"]) { op = GMDAndCompoundOperator; } else if ([self scanQueryKeyword: @"||"]) { op = GMDOrCompoundOperator; } if (op != MDKCompoundOperatorNone) { PARSEXCEPT ((parsed & COMPOUND), @"double compound operator"); PARSEXCEPT ((parsed & SUBOPEN), @"compound operator without arguments"); parsed &= ~(SUBOPEN | SUBCLOSE | COMPARISION); parsed |= COMPOUND; } if ([self scanString: @"(" intoString: NULL]) { PARSEXCEPT (!(((parsed & SUBOPEN) == SUBOPEN) || ((parsed & COMPOUND) == COMPOUND) || ((parsed == 0) && (currentQuery == rootQuery))), @"subquery without compound operator"); parsed &= ~(COMPOUND | SUBCLOSE | COMPARISION); parsed |= SUBOPEN; currentQuery = [currentQuery appendSubqueryWithCompoundOperator: op]; } else if ([self scanString: @")" intoString: NULL]) { PARSEXCEPT ((parsed & COMPOUND), @"compound operator without arguments"); parsed &= ~(COMPOUND | SUBOPEN | COMPARISION); parsed |= SUBCLOSE; [currentQuery closeSubqueries]; if ((currentQuery == rootQuery) == NO) { currentQuery = [currentQuery parentQuery]; } } else { MDKQuery *query = [self parseComparison]; PARSEXCEPT ((parsed & COMPARISION), @"subquery without compound operator"); parsed &= ~(COMPOUND | SUBOPEN | SUBCLOSE); parsed |= COMPARISION; [currentQuery appendSubquery: query compoundOperator: op]; } } - (MDKQuery *)parseComparison { NSString *attribute; NSDictionary *attrinfo; int attrtype; NSDictionary *valueInfo; NSString *searchValue; MDKOperatorType optype; BOOL caseSens; Class queryClass; id query = nil; #define CHK_ATTR_TYPE(x) \ do { if ((attrtype == STRING) || (attrtype == ARRAY) || (attrtype == DATA)) \ [NSException raise: NSInvalidArgumentException \ format: @"Invalid attribute type for operator: %@", x]; \ } while (0) attribute = [self scanAttributeName]; attrinfo = [[MDKQuery attributesInfo] objectForKey: attribute]; attrtype = [[attrinfo objectForKey: @"type"] intValue]; if ([self scanString: @"<" intoString: NULL]) { optype = MDKLessThanOperatorType; CHK_ATTR_TYPE (@"<"); } else if ([self scanString: @"<=" intoString: NULL]) { optype = MDKLessThanOrEqualToOperatorType; CHK_ATTR_TYPE (@"<="); } else if ([self scanString: @">" intoString: NULL]) { optype = MDKGreaterThanOperatorType; CHK_ATTR_TYPE (@">"); } else if ([self scanString: @">=" intoString: NULL]) { optype = MDKGreaterThanOrEqualToOperatorType; CHK_ATTR_TYPE (@">="); } else if ([self scanString: @"==" intoString: NULL]) { optype = MDKEqualToOperatorType; } else if ([self scanString: @"!=" intoString: NULL]) { optype = MDKNotEqualToOperatorType; } else if ([self scanString: @"---------------------" intoString: NULL]) { /* TODO TODO TODO TODO TODO TODO TODO */ optype = MDKInRangeOperatorType; CHK_ATTR_TYPE (@"---------------------"); } else { NSString *str = [[self string] substringFromIndex: [self scanLocation]]; [NSException raise: NSInvalidArgumentException format: @"Invalid query operator: %@", str]; } valueInfo = [self scanSearchValueForAttributeType: attrtype]; searchValue = [valueInfo objectForKey: @"value"]; caseSens = [[valueInfo objectForKey: @"case_sens"] boolValue]; if ([attribute isEqual: @"GSMDItemTextContent"]) { queryClass = [MDKTextContentQuery class]; } else { queryClass = [MDKAttributeQuery class]; } query = [[queryClass alloc] initForAttribute: attribute searchValue: searchValue operatorType: optype]; if (query) { [query setCaseSensitive: caseSens]; } return TEST_AUTORELEASE (query); } - (NSString *)scanAttributeName { NSCharacterSet *set = [NSCharacterSet whitespaceAndNewlineCharacterSet]; NSString *attrname; if ([self scanUpToCharactersFromSet: set intoString: &attrname] && attrname) { if ([[MDKQuery attributesNames] containsObject: attrname]) { return attrname; } } [NSException raise: NSInvalidArgumentException format: @"unable to parse the attribute name"]; return nil; } - (NSDictionary *)scanSearchValueForAttributeType:(int)type { NSCharacterSet *set = [NSCharacterSet whitespaceAndNewlineCharacterSet]; BOOL scanQuote = ((type == STRING) || (type == ARRAY) || (type == DATA)); BOOL caseSens = YES; NSString *value; NSMutableDictionary *dict = [NSMutableDictionary dictionary]; if (scanQuote && ([self scanString: @"\"" intoString: NULL] == NO)) { scanQuote = NO; } if (scanQuote) { NSString *modifiers; if (([self scanUpToString: @"\"" intoString: &value] && value) == NO) { [NSException raise: NSInvalidArgumentException format: @"Missing \" in query"]; } if ([self scanUpToCharactersFromSet: set intoString: &modifiers] && modifiers) { if ([modifiers rangeOfString: @"c"].location != NSNotFound) { caseSens = NO; } } } else { if (([self scanUpToCharactersFromSet: set intoString: &value] && value) == NO) { [NSException raise: NSInvalidArgumentException format: @"unable to parse value"]; } } [dict setObject: value forKey: @"value"]; [dict setObject: [NSNumber numberWithBool: caseSens] forKey: @"case_sens"]; return dict; } - (BOOL)scanQueryKeyword:(NSString *)key { unsigned loc = [self scanLocation]; [self setCaseSensitive: NO]; if ([self scanString: key intoString: NULL] == NO) { return NO; } else { NSCharacterSet *set = [NSCharacterSet alphanumericCharacterSet]; unichar c = [[self string] characterAtIndex: [self scanLocation]]; if ([set characterIsMember: c] == NO) { return YES; } } [self setScanLocation: loc]; return NO; } @end @implementation NSDictionary (CategorySort) - (NSComparisonResult)compareAccordingToIndex:(NSDictionary *)dict { NSNumber *p1 = [self objectForKey: @"index"]; NSNumber *p2 = [dict objectForKey: @"index"]; return [p1 compare: p2]; } @end static NSString *path_sep(void) { static NSString *separator = nil; if (separator == nil) { #if defined(__MINGW32__) separator = @"\\"; #else separator = @"/"; #endif RETAIN (separator); } return separator; } BOOL subPathOfPath(NSString *p1, NSString *p2) { int l1 = [p1 length]; int l2 = [p2 length]; if ((l1 > l2) || ([p1 isEqual: p2])) { return NO; } else if ([[p2 substringToIndex: l1] isEqual: p1]) { if ([[p2 pathComponents] containsObject: [p1 lastPathComponent]]) { return YES; } } return NO; }