/* Copyright (C) 2004 SKYRIX Software AG This file is part of OpenGroupware.org. OGo is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2, or (at your option) any later version. OGo 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with OGo; see the file COPYING. If not, write to the Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ // $Id$ #include "OCSFolderManager.h" #include "OCSChannelManager.h" #include "OCSFolderType.h" #include "OCSFolder.h" #include "NSURL+OCS.h" #include "EOAdaptorChannel+OCS.h" #include "common.h" #include /* Required database schema: c_path c_path1, path2, path3... [quickPathCount times] c_foldername TODO: - add a local cache? */ @implementation OCSFolderManager static OCSFolderManager *fm = nil; static BOOL debugOn = NO; static BOOL debugSQLGen = NO; static BOOL debugPathTraversal = NO; static int quickPathCount = 4; static NSArray *emptyArray = nil; static NSString *OCSPathColumnName = @"c_path"; static NSString *OCSPathRecordName = @"cPath"; static NSString *OCSTypeColumnName = @"c_folder_type"; static NSString *OCSTypeRecordName = @"cFolderType"; static NSString *OCSGenericFolderTypeName = @"Container"; static const char *OCSPathColumnPattern = "c_path%i"; + (void)initialize { NSUserDefaults *ud = [NSUserDefaults standardUserDefaults]; debugOn = [ud boolForKey:@"OCSFolderManagerDebugEnabled"]; debugSQLGen = [ud boolForKey:@"OCSFolderManagerSQLDebugEnabled"]; emptyArray = [[NSArray alloc] init]; } + (id)defaultFolderManager { NSString *s; NSURL *url; if (fm) return fm; s = [[NSUserDefaults standardUserDefaults] stringForKey:@"OCSFolderInfoURL"]; if ([s length] == 0) { NSLog(@"ERROR(%s): default 'OCSFolderInfoURL' is not configured.", __PRETTY_FUNCTION__); return nil; } if ((url = [NSURL URLWithString:s]) == nil) { NSLog(@"ERROR(%s): default 'OCSFolderInfoURL' is not a valid URL: '%@'", __PRETTY_FUNCTION__, s); return nil; } if ((fm = [[self alloc] initWithFolderInfoLocation:url]) == nil) { NSLog(@"ERROR(%s): could not create folder manager with URL: '%@'", __PRETTY_FUNCTION__, [url absoluteString]); return nil; } NSLog(@"Note: setup default manager at: %@", url); return fm; } - (id)initWithFolderInfoLocation:(NSURL *)_url { if (_url == nil) { [self logWithFormat:@"ERROR(%s): missing folder info url!", __PRETTY_FUNCTION__]; [self release]; return nil; } if ((self = [super init])) { OCSFolderType *cal, *contact; self->channelManager = [[OCSChannelManager defaultChannelManager] retain]; self->folderInfoLocation = [_url retain]; if ([[self folderInfoTableName] length] == 0) { [self logWithFormat:@"ERROR(%s): missing tablename in URL: %@", __PRETTY_FUNCTION__, [_url absoluteString]]; [self release]; return nil; } /* register default folder types */ cal = [[OCSFolderType alloc] initWithFolderTypeName:@"appointment"]; contact = [[OCSFolderType alloc] initWithFolderTypeName:@"contact"]; self->nameToType = [[NSDictionary alloc] initWithObjectsAndKeys: cal, @"appointment", contact, @"contact", nil]; [cal release]; cal = nil; [contact release]; contact = nil; } return self; } - (void)dealloc { [self->nameToType release]; [self->folderInfoLocation release]; [self->channelManager release]; [super dealloc]; } /* accessors */ - (NSURL *)folderInfoLocation { return self->folderInfoLocation; } - (NSString *)folderInfoTableName { return [[self folderInfoLocation] ocsTableName]; } /* connection */ - (OCSChannelManager *)channelManager { return self->channelManager; } - (EOAdaptorChannel *)acquireOpenChannel { EOAdaptorChannel *ch; ch = [[self channelManager] acquireOpenChannelForURL: [self folderInfoLocation]]; return ch; } - (void)releaseChannel:(EOAdaptorChannel *)_channel { [[self channelManager] releaseChannel:_channel]; if (debugOn) [self debugWithFormat:@"released channel: %@", _channel]; } - (BOOL)canConnect { return [[self channelManager] canConnect:[self folderInfoLocation]]; } - (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]) != nil) { [self logWithFormat:@"ERROR(%s): cannot execute SQL '%@': %@", __PRETTY_FUNCTION__, _sql, ex]; [self releaseChannel:channel]; return nil; } /* fetch results */ attrs = [channel describeResults]; rows = [NSMutableArray arrayWithCapacity:16]; while ((row = [channel fetchAttributes:attrs withZone:NULL]) != nil) [rows addObject:row]; [self releaseChannel:channel]; return rows; } /* row factory */ - (OCSFolder *)folderForRecord:(NSDictionary *)_record { OCSFolder *folder; OCSFolderType *folderType; NSString *folderTypeName, *locationString, *folderName, *path; NSNumber *folderId; NSURL *location, *quickLocation; if (_record == nil) return nil; folderTypeName = [_record objectForKey:@"cFolderType"]; if ((folderType = [self folderTypeWithName:folderTypeName]) == nil) { [self logWithFormat:@"ERROR(%s): got no type from folder record: %@", __PRETTY_FUNCTION__, _record]; return nil; } folderId = [_record objectForKey:@"cFolderId"]; folderName = [_record objectForKey:@"cPath"]; path = [self pathFromInternalName:folderName]; locationString = [_record objectForKey:@"cLocation"]; location = [locationString isNotNull] ? [NSURL URLWithString:locationString] : nil; if (location == nil) { [self logWithFormat:@"ERROR(%s): missing folder location in record: %@", __PRETTY_FUNCTION__, _record]; return nil; } locationString = [_record objectForKey:@"cQuickLocation"]; quickLocation = [locationString isNotNull] ? [NSURL URLWithString:locationString] : nil; if (quickLocation == nil) { [self logWithFormat:@"WARNING(%s): missing quick location in record: %@", __PRETTY_FUNCTION__, _record]; } folder = [[OCSFolder alloc] initWithPath:path primaryKey:folderId folderTypeName:folderTypeName folderType:folderType location:location quickLocation:quickLocation folderManager:self]; return [folder autorelease]; } /* 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, OCSPathColumnPattern, (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"]; if (debugPathTraversal) [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"]; if (debugPathTraversal) [self logWithFormat:@"DIRECT SUBS, first level"]; } else { /* but for 'direct' subfolders, all following things must be empty */ [sql appendString:@" IS NULL"]; if (debugPathTraversal) [self logWithFormat:@"DIRECT SUBS, lower level"]; } } [pathColumn release]; } if (_beExact && (count > quickPathCount)) { [sql appendString:@" AND \"c_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 \"c_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; } - (NSDictionary *)filterRecords:(NSArray *)_records forPath:(NSString *)_path { unsigned i, count; NSString *name; if (_records == nil) return nil; if ((name = [self internalNameFromPath:_path]) == nil) return nil; for (i = 0, count = [_records count]; i < count; i++) { NSDictionary *record; NSString *recName; record = [_records objectAtIndex:i]; recName = [record objectForKey:OCSPathRecordName]; #if 0 [self logWithFormat:@"check '%@' vs '%@' (%@)...", name, recName,[_records objectAtIndex:i]]; #endif if ([name isEqualToString:recName]) return [_records objectAtIndex:i]; } return nil; } - (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) { NSDictionary *record; NSString *sname; record = [records objectAtIndex:0]; sname = [record objectForKey:OCSPathRecordName]; 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++) { NSDictionary *record; NSString *sname, *spath; record = [records objectAtIndex:0]; sname = [record objectForKey:OCSPathRecordName]; 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; } - (OCSFolder *)folderAtPath:(NSString *)_path { NSMutableString *sql; NSArray *fnames, *records; NSString *ws; NSDictionary *record; if ((fnames = [self internalNamesFromPath:_path]) == nil) { [self debugWithFormat:@"got no internal names for path: '%@'", _path]; return nil; } /* generate SQL to fetch folder attributes */ ws = [self generateSQLWhereForInternalNames:fnames exactMatch:YES orDirectSubfolderMatch:NO]; sql = [NSMutableString stringWithCapacity:256]; [sql appendString:@"SELECT "]; [sql appendString:@"\"c_folder_id\", "]; [sql appendString:@"\"c_path\", "]; [sql appendString:@"\"c_location\", \"c_quick_location\", "]; [sql appendString:@"\"c_folder_type\""]; [sql appendString:@" FROM "]; [sql appendString:[self folderInfoTableName]]; [sql appendString:@" WHERE "]; [sql appendString:ws]; /* fetching */ if ((records = [self performSQL:sql]) == nil) { [self logWithFormat:@"ERROR(%s): executing SQL failed: '%@'", __PRETTY_FUNCTION__, sql]; return nil; } // TODO: need to filter on path if ([records count] != 1) { if ([records count] == 0) { [self debugWithFormat:@"found no records for path: '%@'", _path]; return nil; } [self logWithFormat:@"ERROR(%s): more than one row for path: '%@'", __PRETTY_FUNCTION__, _path]; return nil; } if ((record = [self filterRecords:records forPath:_path]) == nil) { [self debugWithFormat:@"found no record for path: '%@'", _path]; return nil; } return [self folderForRecord:record]; } - (NSException *)createFolderOfType:(NSString *)_type atPath:(NSString *)_path{ // TODO: implement folder create OCSFolderType *ftype; if ((ftype = [self folderTypeWithName:_type]) == nil) { return [NSException exceptionWithName:@"OCSMissingFolderType" reason:@"missing folder type" userInfo:nil]; } [self logWithFormat:@"create folder of type: %@", ftype]; return [NSException exceptionWithName:@"NotYetImplemented" reason:@"no money, no time, ..." userInfo:nil]; } /* folder types */ - (OCSFolderType *)folderTypeWithName:(NSString *)_name { if ([_name length] == 0) _name = OCSGenericFolderTypeName; return [self->nameToType objectForKey:[_name lowercaseString]]; } /* cache management */ - (void)reset { /* does nothing in the moment, but we need a way to signal refreshes */ } /* debugging */ - (BOOL)isDebuggingEnabled { return debugOn; } /* description */ - (NSString *)description { NSMutableString *ms; ms = [NSMutableString stringWithCapacity:256]; [ms appendFormat:@"<0x%08X[%@]:", self, NSStringFromClass([self class])]; [ms appendFormat:@" url=%@", [self->folderInfoLocation absoluteString]]; [ms appendFormat:@" channel-manager=0x%08X", [self channelManager]]; [ms appendString:@">"]; return ms; } @end /* OCSFolderManager */