#include "common.h"
#include <GDLAccess/EOAdaptorChannel.h>
+/*
+ Required database schema:
+
+ <arbitary table>
+ path
+ path1, path2, path3... [quickPathCount times]
+ folderName
+
+ TODO:
+ - add a local cache?
+*/
+
@implementation OCSFolderManager
static OCSFolderManager *fm = nil;
-static BOOL debugOn = YES;
+static BOOL debugOn = NO;
+static BOOL debugSQLGen = NO;
+static int quickPathCount = 4;
+static NSArray *emptyArray = nil;
+ (void)initialize {
NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
- debugOn = [ud boolForKey:@"OCSFolderManagerDebugEnabled"];
+ debugOn = [ud boolForKey:@"OCSFolderManagerDebugEnabled"];
+ debugSQLGen = [ud boolForKey:@"OCSFolderManagerSQLDebugEnabled"];
+ emptyArray = [[NSArray alloc] init];
}
+ (id)defaultFolderManager {
return self->folderInfoLocation;
}
+- (NSString *)folderInfoTableName {
+ return [[self folderInfoLocation] ocsTableName];
+}
+
/* checking connection */
- (EOAdaptorChannel *)acquireOpenChannel {
/* check whether table exists */
- sql = [@"SELECT COUNT(*) FROM " stringByAppendingString:
- [[self folderInfoLocation] ocsTableName]];
- sql = [sql stringByAppendingString:@" WHERE 1=2"];
+ sql = @"SELECT COUNT(*) FROM ";
+ sql = [sql stringByAppendingString:[self folderInfoTableName]];
+ sql = [sql stringByAppendingString:@" WHERE 1 = 2"];
ex = [[[channel evaluateExpressionX:sql] retain] autorelease];
[channel cancelFetch];
return ex != nil ? NO : YES;
}
+- (NSArray *)performSQL:(NSString *)_sql {
+ EOAdaptorChannel *channel;
+ NSException *ex;
+ NSMutableArray *rows;
+ NSDictionary *row;
+ NSArray *attrs;
+
+ /* acquire channel */
+
+ if ((channel = [self acquireOpenChannel]) == nil) {
+ if (debugOn) [self debugWithFormat:@"could not acquire channel!"];
+ return nil;
+ }
+ if (debugOn) [self debugWithFormat:@"acquired channel: %@", channel];
+
+ /* run SQL */
+
+ if ((ex = [channel evaluateExpressionX:_sql])) {
+ [self releaseChannel:channel];
+ return nil;
+ }
+
+ /* fetch results */
+
+ attrs = [channel describeResults];
+ rows = [NSMutableArray arrayWithCapacity:16];
+ while ((row = [channel fetchAttributes:attrs withZone:NULL]))
+ [rows addObject:row];
+
+ [self releaseChannel:channel];
+ return rows;
+}
+
+/* path SQL */
+
+- (NSString *)generateSQLWhereForInternalNames:(NSArray *)_names
+ exactMatch:(BOOL)_beExact orDirectSubfolderMatch:(BOOL)_directSubs
+{
+ /* generates a WHERE qualifier for matching the "quick" entries */
+ NSMutableString *sql;
+ unsigned i, count;
+
+ if ((count = [_names count]) == 0) {
+ [self debugWithFormat:@"WARNING(%s): passed in empty name array!",
+ __PRETTY_FUNCTION__];
+ return @"1 = 2";
+ }
+
+ sql = [NSMutableString stringWithCapacity:(count * 8)];
+ for (i = 0; i < quickPathCount; i++) {
+ NSString *pathColumn;
+ unsigned char buf[32];
+
+ sprintf(buf, "\"path%i\"", (i + 1));
+ pathColumn = [[NSString alloc] initWithCString:buf];
+
+ /* Note: the AND addition must be inside the if's for non-exact stuff */
+
+ if (i < count) {
+ /* exact match, regular column */
+ if ([sql length] > 0) [sql appendString:@" AND "];
+ [sql appendString:pathColumn];
+ [sql appendFormat:@" = '%@'", [_names objectAtIndex:i]];
+ }
+ else if (_beExact) {
+ /* exact match, ensure that all additional quick-cols are NULL */
+ if ([sql length] > 0) [sql appendString:@" AND "];
+ [sql appendString:pathColumn];
+ [sql appendString:@" IS NULL"];
+ //[self logWithFormat:@"BE EXACT, NULL columns"];
+ }
+ else if (_directSubs) {
+ /* fetch immediate subfolders */
+ if ([sql length] > 0) [sql appendString:@" AND "];
+ [sql appendString:pathColumn];
+ if (i == count) {
+ /* if it is a direct subfolder, the next path cannot be empty */
+ [sql appendString:@" IS NOT NULL"];
+ //[self logWithFormat:@"DIRECT SUBS, first level"];
+ }
+ else {
+ /* but for 'direct' subfolders, all following things must be empty */
+ [sql appendString:@" IS NULL"];
+ //[self logWithFormat:@"DIRECT SUBS, lower level"];
+ }
+ }
+
+ [pathColumn release];
+ }
+
+ if (_beExact && (count > quickPathCount)) {
+ [sql appendString:@" AND \"folderName\" = '"];
+ [sql appendString:[_names lastObject]];
+ [sql appendString:@"'"];
+ }
+
+ return sql;
+}
+
+- (NSString *)generateSQLPathFetchForInternalNames:(NSArray *)_names
+ exactMatch:(BOOL)_beExact orDirectSubfolderMatch:(BOOL)_directSubs
+{
+ /* fetches the 'path' subset for a given quick-names */
+ NSMutableString *sql;
+ NSString *ws;
+
+ ws = [self generateSQLWhereForInternalNames:_names
+ exactMatch:_beExact orDirectSubfolderMatch:_directSubs];
+ if ([ws length] == 0)
+ return nil;
+
+ sql = [NSMutableString stringWithCapacity:256];
+ [sql appendString:@"SELECT \"path\" FROM "];
+ [sql appendString:[self folderInfoTableName]];
+ [sql appendString:@" WHERE "];
+ [sql appendString:ws];
+ if (debugSQLGen) [self logWithFormat:@"PathFetch-SQL: %@", sql];
+ return sql;
+}
+
+/* handling folder names */
+
+- (BOOL)_isStandardizedPath:(NSString *)_path {
+ if (![_path isAbsolutePath]) return NO;
+ if ([_path rangeOfString:@".."].length > 0) return NO;
+ if ([_path rangeOfString:@"~"].length > 0) return NO;
+ if ([_path rangeOfString:@"//"].length > 0) return NO;
+ return YES;
+}
+
+- (NSString *)internalNameFromPath:(NSString *)_path {
+ // TODO: ensure proper path and SQL escaping!
+
+ if (![self _isStandardizedPath:_path]) {
+ [self debugWithFormat:@"%s: not a standardized path: '%@'",
+ __PRETTY_FUNCTION__, _path];
+ return nil;
+ }
+
+ if ([_path hasSuffix:@"/"] && [_path length] > 1)
+ _path = [_path substringToIndex:([_path length] - 1)];
+
+ return _path;
+}
+- (NSArray *)internalNamesFromPath:(NSString *)_path {
+ NSString *fname;
+ NSArray *fnames;
+
+ if ((fname = [self internalNameFromPath:_path]) == nil)
+ return nil;
+
+ if ([fname hasPrefix:@"/"])
+ fname = [fname substringFromIndex:1];
+
+ fnames = [fname componentsSeparatedByString:@"/"];
+ if ([fnames count] == 0)
+ return nil;
+
+ return fnames;
+}
+- (NSString *)pathFromInternalName:(NSString *)_name {
+ /* for incomplete pathes, like '/Users/helge/' */
+ return _name;
+}
+- (NSString *)pathPartFromInternalName:(NSString *)_name {
+ /* for incomplete pathes, like 'Users/' */
+ return _name;
+}
+
+- (BOOL)folderExistsAtPath:(NSString *)_path {
+ NSString *fname;
+ NSArray *fnames, *records;
+ NSString *sql;
+ unsigned count;
+
+ if ((fnames = [self internalNamesFromPath:_path]) == nil) {
+ [self debugWithFormat:@"got no internal names for path: '%@'", _path];
+ return NO;
+ }
+
+ sql = [self generateSQLPathFetchForInternalNames:fnames
+ exactMatch:YES orDirectSubfolderMatch:NO];
+ if ([sql length] == 0) {
+ [self debugWithFormat:@"got no SQL for names: %@", fnames];
+ return NO;
+ }
+
+ if ((records = [self performSQL:sql]) == nil) {
+ [self logWithFormat:@"ERROR(%s): executing SQL failed: '%@'",
+ __PRETTY_FUNCTION__, sql];
+ return NO;
+ }
+
+ if ((count = [records count]) == 0)
+ return NO;
+
+ fname = [self internalNameFromPath:_path];
+ if (count == 1) {
+ NSString *sname;
+
+ sname = [[records objectAtIndex:0] objectForKey:@"path"];
+ return [fname isEqualToString:sname];
+ }
+
+ [self logWithFormat:@"records: %@", records];
+
+ return NO;
+}
+
+- (NSArray *)listSubFoldersAtPath:(NSString *)_path recursive:(BOOL)_recursive{
+ NSMutableArray *result;
+ NSString *fname;
+ NSArray *fnames, *records;
+ NSString *sql;
+ unsigned i, count;
+
+ if ((fnames = [self internalNamesFromPath:_path]) == nil) {
+ [self debugWithFormat:@"got no internal names for path: '%@'", _path];
+ return nil;
+ }
+
+ sql = [self generateSQLPathFetchForInternalNames:fnames
+ exactMatch:NO orDirectSubfolderMatch:(_recursive ? NO : YES)];
+ if ([sql length] == 0) {
+ [self debugWithFormat:@"got no SQL for names: %@", fnames];
+ return nil;
+ }
+
+ if ((records = [self performSQL:sql]) == nil) {
+ [self logWithFormat:@"ERROR(%s): executing SQL failed: '%@'",
+ __PRETTY_FUNCTION__, sql];
+ return nil;
+ }
+
+ if ((count = [records count]) == 0)
+ return emptyArray;
+
+ result = [NSMutableArray arrayWithCapacity:(count > 128 ? 128 : count)];
+
+ fname = [self internalNameFromPath:_path];
+ fname = [fname stringByAppendingString:@"/"]; /* add slash */
+ for (i = 0; i < count; i++) {
+ NSString *sname, *spath;
+
+ sname = [[records objectAtIndex:0] objectForKey:@"path"];
+ if (![sname hasPrefix:fname]) /* does not match at all ... */
+ continue;
+
+ /* strip prefix and following slash */
+ sname = [sname substringFromIndex:[fname length]];
+ spath = [self pathPartFromInternalName:sname];
+
+ if (_recursive) {
+ if ([spath length] > 0) [result addObject:spath];
+ }
+ else {
+ /* direct children only, so exclude everything with a slash */
+ if ([sname rangeOfString:@"/"].length == 0 && [spath length] > 0)
+ [result addObject:spath];
+ }
+ }
+
+ return result;
+}
+
+/* cache management */
+
+- (void)reset {
+ /* does nothing in the moment, but we need a way to signal refreshes */
+}
+
/* debugging */
- (BOOL)isDebuggingEnabled {