/* Copyright (C) 2004-2005 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. */ #include "GCSFolderManager.h" #include "GCSChannelManager.h" #include "GCSFolderType.h" #include "GCSFolder.h" #include "NSURL+GCS.h" #include "EOAdaptorChannel+GCS.h" #include "common.h" #include #include /* Required database schema: c_path c_path1, path2, path3... [quickPathCount times] c_foldername TODO: - add a local cache? */ @implementation GCSFolderManager static GCSFolderManager *fm = nil; static BOOL debugOn = NO; static BOOL debugSQLGen = NO; static BOOL debugPathTraversal = NO; static int quickPathCount = 4; static NSArray *emptyArray = nil; #if 0 static NSString *GCSPathColumnName = @"c_path"; static NSString *GCSTypeColumnName = @"c_folder_type"; static NSString *GCSTypeRecordName = @"c_folder_type"; #endif static NSString *GCSPathRecordName = @"c_path"; static NSString *GCSGenericFolderTypeName = @"Container"; static const char *GCSPathColumnPattern = "c_path%i"; static NSCharacterSet *asciiAlphaNumericCS = nil; + (void)initialize { NSUserDefaults *ud = [NSUserDefaults standardUserDefaults]; debugOn = [ud boolForKey:@"GCSFolderManagerDebugEnabled"]; debugSQLGen = [ud boolForKey:@"GCSFolderManagerSQLDebugEnabled"]; emptyArray = [[NSArray alloc] init]; if (!asciiAlphaNumericCS) { asciiAlphaNumericCS = [NSCharacterSet characterSetWithCharactersInString: @"0123456789" @"ABCDEFGHIJKLMNOPQRSTUVWXYZ" @"abcdefghijklmnopqrstuvwxyz"]; [asciiAlphaNumericCS retain]; } } + (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; } - (NSDictionary *)loadDefaultFolderTypes { NSMutableDictionary *typeMap; NSArray *types; unsigned i, count; types = [[GCSFolderType resourceLocator] lookupAllFilesWithExtension:@"ocs" doReturnFullPath:NO]; if ((count = [types count]) == 0) { [self logWithFormat:@"Note: no GCS folder types found."]; return nil; } typeMap = [NSMutableDictionary dictionaryWithCapacity:count]; [self logWithFormat:@"Note: loading %d GCS folder types:", count]; for (i = 0, count = [types count]; i < count; i++) { NSString *type; GCSFolderType *typeObject; type = [[types objectAtIndex:i] stringByDeletingPathExtension]; typeObject = [[GCSFolderType alloc] initWithFolderTypeName:type]; [self logWithFormat:@" %@: %s", type, [typeObject isNotNull] ? "OK" : "FAIL"]; [typeMap setObject:typeObject forKey:type]; [typeObject release]; } return typeMap; } - (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])) { self->channelManager = [[GCSChannelManager defaultChannelManager] retain]; self->folderInfoLocation = [_url retain]; self->folderNamePrefix = nil; 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 */ self->nameToType = [[self loadDefaultFolderTypes] copy]; } return self; } - (void)dealloc { [self->nameToType release]; [self->folderInfoLocation release]; [self->channelManager release]; [self->folderNamePrefix release]; [super dealloc]; } /* accessors */ - (void) setFolderNamePrefix:(NSString *)_folderNamePrefix { ASSIGN(self->folderNamePrefix, _folderNamePrefix); } - (NSString *) folderNamePrefix { return self->folderNamePrefix; } - (NSURL *)folderInfoLocation { return self->folderInfoLocation; } - (NSString *)folderInfoTableName { return [[self folderInfoLocation] gcsTableName]; } /* connection */ - (GCSChannelManager *)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\n SQL '%@':\n %@", __PRETTY_FUNCTION__, _sql, ex]; [self releaseChannel:channel]; return nil; } /* fetch results */ attrs = [channel describeResults:NO /* do not beautify names */]; rows = [NSMutableArray arrayWithCapacity:16]; while ((row = [channel fetchAttributes:attrs withZone:NULL]) != nil) [rows addObject:row]; [self releaseChannel:channel]; return rows; } /* row factory */ - (GCSFolder *)folderForRecord:(NSDictionary *)_record { GCSFolder *folder; GCSFolderType *folderType; NSString *folderTypeName, *locationString, *folderName, *path; NSNumber *folderId; NSURL *location, *quickLocation, *aclLocation; if (_record == nil) return nil; folderTypeName = [_record objectForKey:@"c_folder_type"]; if (![folderTypeName isNotNull]) { [self logWithFormat:@"ERROR(%s): missing type in folder: %@", __PRETTY_FUNCTION__, _record]; return nil; } if ((folderType = [self folderTypeWithName:folderTypeName]) == nil) { [self logWithFormat: @"ERROR(%s): could not resolve type '%@' of folder: %@", __PRETTY_FUNCTION__, folderTypeName, [_record valueForKey:@"c_path"]]; return nil; } folderId = [_record objectForKey:@"c_folder_id"]; folderName = [_record objectForKey:@"c_path"]; path = [self pathFromInternalName:folderName]; locationString = [_record objectForKey:@"c_location"]; 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:@"c_quick_location"]; quickLocation = [locationString isNotNull] ? [NSURL URLWithString:locationString] : nil; if (quickLocation == nil) { [self logWithFormat:@"WARNING(%s): missing quick location in record: %@", __PRETTY_FUNCTION__, _record]; } locationString = [_record objectForKey:@"c_acl_location"]; aclLocation = [locationString isNotNull] ? [NSURL URLWithString:locationString] : nil; folder = [[GCSFolder alloc] initWithPath:path primaryKey:folderId folderTypeName:folderTypeName folderType:folderType location:location quickLocation:quickLocation aclLocation:aclLocation 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; char buf[32]; sprintf(buf, GCSPathColumnPattern, (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:GCSPathRecordName]; #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:GCSPathRecordName]; 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:i]; sname = [record objectForKey:GCSPathRecordName]; 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; } - (GCSFolder *)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, c_acl_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 // required when we start to have deeper hierarchies // => isn't that already done below? 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]; } - (NSString *)sqlCreateWithTableName: (NSString *)_tabName { return [NSString stringWithFormat: @"CREATE TABLE %@ (\n" @" c_name VARCHAR (256) NOT NULL,\n" @" c_content VARCHAR (100000) NOT NULL,\n" @" c_creationdate INT4 NOT NULL,\n" @" c_lastmodified INT4 NOT NULL,\n" @" c_version INT4 NOT NULL\n" @")", _tabName]; } - (NSString *)sqlAclCreateWithTableName: (NSString *)_tabName { return [NSString stringWithFormat: @"CREATE TABLE %@ (\n" @" c_uid VARCHAR (256) NOT NULL,\n" @" c_object VARCHAR (256) NOT NULL,\n" @" c_role VARCHAR (80) NOT NULL\n" @")", _tabName]; } - (NSString *)baseTableNameForFolderAtPath:(NSString *)_path { NSMutableString *fixedPath; unsigned int count, max; unichar currentChar; fixedPath = [NSMutableString new]; [fixedPath autorelease]; if (self->folderNamePrefix != nil) [fixedPath appendString: self->folderNamePrefix]; max = [_path length]; for (count = 0; count < max; count++) { currentChar = [_path characterAtIndex: count]; if ([asciiAlphaNumericCS characterIsMember: currentChar]) [fixedPath appendFormat: @"%Lc", currentChar]; else [fixedPath appendString: @"_"]; } return (([fixedPath length] < 49) ? (NSString *)fixedPath : [fixedPath substringToIndex: 49]); } - (NSString *)finalizedTableNameForBaseName:(NSString *)_baseName atBaseURL:(NSString *)_baseURL withChannel:(EOAdaptorChannel *)_channel { NSString *potentialName, *sqlTestFormat, *sqlTest; unsigned int count; potentialName = _baseName; sqlTestFormat = [NSString stringWithFormat: @"SELECT * FROM %@" @" WHERE c_location = '%@/%%@'" @" OR c_quick_location = '%@/%%@_quick'" @" OR c_acl_location = '%@/%%@_acl'", [self folderInfoTableName], _baseURL, _baseURL, _baseURL]; sqlTest = [NSString stringWithFormat: sqlTestFormat, potentialName, potentialName, potentialName]; count = 0; while ([[self performSQL: sqlTest] count] > 0) { count++; potentialName = [NSString stringWithFormat: @"%@%d", _baseName, count]; sqlTest = [NSString stringWithFormat: sqlTestFormat, potentialName, potentialName, potentialName]; } return potentialName; } - (NSException *)createFolderOfType:(NSString *)_type atPath:(NSString *)_path{ GCSFolderType *ftype; NSString *tableName, *quickTableName, *aclTableName; NSString *baseURL; EOAdaptorChannel *channel; NSMutableArray *paths; if ([[self performSQL: [NSString stringWithFormat: @"SELECT * FROM %@" @" WHERE c_path = '%@'", [self folderInfoTableName], _path]] count] > 0) { return [NSException exceptionWithName:@"GCSExitingFolder" reason:@"a folder already exists at that path" userInfo:nil]; } if ((ftype = [self folderTypeWithName:_type]) == nil) { return [NSException exceptionWithName:@"GCSMissingFolderType" reason:@"missing folder type"userInfo:nil]; } if ((channel = [self acquireOpenChannel]) == nil) { return [NSException exceptionWithName:@"GCSNoChannel" reason:@"could not open channel" userInfo:nil]; } tableName = [self baseTableNameForFolderAtPath: _path]; baseURL = [[folderInfoLocation absoluteString] stringByDeletingLastPathComponent]; tableName = [self finalizedTableNameForBaseName: tableName atBaseURL: baseURL withChannel: channel]; quickTableName = [NSString stringWithFormat: @"%@_quick", tableName]; aclTableName = [NSString stringWithFormat: @"%@_acl", tableName]; [channel evaluateExpressionX: [NSString stringWithFormat: @"DROP TABLE %@", tableName]]; [channel evaluateExpressionX: [NSString stringWithFormat: @"DROP TABLE %@", quickTableName]]; [channel evaluateExpressionX: [NSString stringWithFormat: @"DROP TABLE %@", aclTableName]]; [channel evaluateExpressionX: [self sqlCreateWithTableName: tableName]]; [channel evaluateExpressionX: [ftype sqlQuickCreateWithTableName: quickTableName]]; [channel evaluateExpressionX: [self sqlAclCreateWithTableName: aclTableName]]; paths = [NSMutableArray arrayWithArray: [_path componentsSeparatedByString: @"/"]]; while ([paths count] < 5) [paths addObject: @""]; [channel evaluateExpressionX: [NSString stringWithFormat: @"INSERT INTO %@" @" (c_path, c_path1, c_path2, c_path3, c_path4," @" c_foldername, c_location, c_quick_location," @" c_acl_location, c_folder_type)" @" VALUES ('%@', '%@', '%@', '%@', '%@', '%@', '%@/%@'," @" '%@/%@', '%@/%@', '%@')", [self folderInfoTableName], _path, [paths objectAtIndex: 1], [paths objectAtIndex: 2], [paths objectAtIndex: 3], [paths objectAtIndex: 4], [_path lastPathComponent], baseURL, tableName, baseURL, quickTableName, baseURL, aclTableName, _type]]; [self releaseChannel: channel]; return nil; } - (NSException *)deleteFolderAtPath:(NSString *)_path { GCSFolder *folder; NSArray *fnames; NSString *sql, *ws; EOAdaptorChannel *channel; NSException *ex; if ((folder = [self folderAtPath:_path]) == nil) { return [NSException exceptionWithName:@"GCSMissingFolder" reason:@"missing folder" userInfo:nil]; } if ((fnames = [self internalNamesFromPath:_path]) == nil) { [self debugWithFormat:@"got no internal names for path: '%@'", _path]; return nil; } ws = [self generateSQLWhereForInternalNames:fnames exactMatch:YES orDirectSubfolderMatch:NO]; sql = [NSString stringWithFormat: @"DELETE FROM %@ WHERE %@", [self folderInfoTableName], ws]; if ((channel = [self acquireOpenChannel]) == nil) { return [NSException exceptionWithName:@"GCSNoChannel" reason:@"could not " userInfo:nil]; } if ((ex = [channel evaluateExpressionX:sql]) != nil) { [self releaseChannel:channel]; return ex; } [self releaseChannel:channel]; return [folder deleteFolder]; } /* folder types */ - (GCSFolderType *)folderTypeWithName:(NSString *)_name { if ([_name length] == 0) _name = GCSGenericFolderTypeName; 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%p[%@]:", self, NSStringFromClass([self class])]; [ms appendFormat:@" url=%@", [self->folderInfoLocation absoluteString]]; [ms appendFormat:@" channel-manager=0x%p", [self channelManager]]; [ms appendString:@">"]; return ms; } @end /* GCSFolderManager */