2 Copyright (C) 2004-2005 SKYRIX Software AG
4 This file is part of OpenGroupware.org.
6 OGo is free software; you can redistribute it and/or modify it under
7 the terms of the GNU Lesser General Public License as published by the
8 Free Software Foundation; either version 2, or (at your option) any
11 OGo is distributed in the hope that it will be useful, but WITHOUT ANY
12 WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
14 License for more details.
16 You should have received a copy of the GNU Lesser General Public
17 License along with OGo; see the file COPYING. If not, write to the
18 Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA
22 #include "GCSFolderManager.h"
23 #include "GCSChannelManager.h"
24 #include "GCSFolderType.h"
25 #include "GCSFolder.h"
26 #include "NSURL+GCS.h"
27 #include "EOAdaptorChannel+GCS.h"
29 #include <GDLAccess/EOAdaptorChannel.h>
32 Required database schema:
36 c_path1, path2, path3... [quickPathCount times]
43 @implementation GCSFolderManager
45 static GCSFolderManager *fm = nil;
46 static BOOL debugOn = NO;
47 static BOOL debugSQLGen = NO;
48 static BOOL debugPathTraversal = NO;
49 static int quickPathCount = 4;
50 static NSArray *emptyArray = nil;
52 static NSString *GCSPathColumnName = @"c_path";
53 static NSString *GCSTypeColumnName = @"c_folder_type";
54 static NSString *GCSTypeRecordName = @"cFolderType";
56 static NSString *GCSPathRecordName = @"cPath";
57 static NSString *GCSGenericFolderTypeName = @"Container";
58 static const char *GCSPathColumnPattern = "c_path%i";
61 NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
63 debugOn = [ud boolForKey:@"GCSFolderManagerDebugEnabled"];
64 debugSQLGen = [ud boolForKey:@"GCSFolderManagerSQLDebugEnabled"];
65 emptyArray = [[NSArray alloc] init];
68 + (id)defaultFolderManager {
73 s = [[NSUserDefaults standardUserDefaults] stringForKey:@"OCSFolderInfoURL"];
74 if ([s length] == 0) {
75 NSLog(@"ERROR(%s): default 'OCSFolderInfoURL' is not configured.",
79 if ((url = [NSURL URLWithString:s]) == nil) {
80 NSLog(@"ERROR(%s): default 'OCSFolderInfoURL' is not a valid URL: '%@'",
81 __PRETTY_FUNCTION__, s);
84 if ((fm = [[self alloc] initWithFolderInfoLocation:url]) == nil) {
85 NSLog(@"ERROR(%s): could not create folder manager with URL: '%@'",
86 __PRETTY_FUNCTION__, [url absoluteString]);
90 NSLog(@"Note: setup default manager at: %@", url);
94 - (id)initWithFolderInfoLocation:(NSURL *)_url {
96 [self logWithFormat:@"ERROR(%s): missing folder info url!",
101 if ((self = [super init])) {
102 GCSFolderType *cal, *contact;
104 self->channelManager = [[GCSChannelManager defaultChannelManager] retain];
105 self->folderInfoLocation = [_url retain];
107 if ([[self folderInfoTableName] length] == 0) {
108 [self logWithFormat:@"ERROR(%s): missing tablename in URL: %@",
109 __PRETTY_FUNCTION__, [_url absoluteString]];
114 /* register default folder types */
116 cal = [[GCSFolderType alloc] initWithFolderTypeName:@"appointment"];
117 contact = [[GCSFolderType alloc] initWithFolderTypeName:@"contact"];
118 self->nameToType = [[NSDictionary alloc] initWithObjectsAndKeys:
122 [cal release]; cal = nil;
123 [contact release]; contact = nil;
129 [self->nameToType release];
130 [self->folderInfoLocation release];
131 [self->channelManager release];
137 - (NSURL *)folderInfoLocation {
138 return self->folderInfoLocation;
141 - (NSString *)folderInfoTableName {
142 return [[self folderInfoLocation] gcsTableName];
147 - (GCSChannelManager *)channelManager {
148 return self->channelManager;
151 - (EOAdaptorChannel *)acquireOpenChannel {
152 EOAdaptorChannel *ch;
154 ch = [[self channelManager] acquireOpenChannelForURL:
155 [self folderInfoLocation]];
158 - (void)releaseChannel:(EOAdaptorChannel *)_channel {
159 [[self channelManager] releaseChannel:_channel];
160 if (debugOn) [self debugWithFormat:@"released channel: %@", _channel];
164 return [[self channelManager] canConnect:[self folderInfoLocation]];
167 - (NSArray *)performSQL:(NSString *)_sql {
168 EOAdaptorChannel *channel;
170 NSMutableArray *rows;
174 /* acquire channel */
176 if ((channel = [self acquireOpenChannel]) == nil) {
177 if (debugOn) [self debugWithFormat:@"could not acquire channel!"];
180 if (debugOn) [self debugWithFormat:@"acquired channel: %@", channel];
184 if ((ex = [channel evaluateExpressionX:_sql]) != nil) {
185 [self logWithFormat:@"ERROR(%s): cannot execute\n SQL '%@':\n %@",
186 __PRETTY_FUNCTION__, _sql, ex];
187 [self releaseChannel:channel];
193 attrs = [channel describeResults];
194 rows = [NSMutableArray arrayWithCapacity:16];
195 while ((row = [channel fetchAttributes:attrs withZone:NULL]) != nil)
196 [rows addObject:row];
198 [self releaseChannel:channel];
204 - (GCSFolder *)folderForRecord:(NSDictionary *)_record {
206 GCSFolderType *folderType;
207 NSString *folderTypeName, *locationString, *folderName, *path;
209 NSURL *location, *quickLocation;
211 if (_record == nil) return nil;
213 folderTypeName = [_record objectForKey:@"cFolderType"];
214 if (![folderTypeName isNotNull]) {
215 [self logWithFormat:@"ERROR(%s): missing type in folder: %@",
216 __PRETTY_FUNCTION__, _record];
219 if ((folderType = [self folderTypeWithName:folderTypeName]) == nil) {
221 @"ERROR(%s): could not resolve type '%@' of folder: %@",
223 folderTypeName, [_record valueForKey:@"cPath"]];
227 folderId = [_record objectForKey:@"cFolderId"];
228 folderName = [_record objectForKey:@"cPath"];
229 path = [self pathFromInternalName:folderName];
231 locationString = [_record objectForKey:@"cLocation"];
232 location = [locationString isNotNull]
233 ? [NSURL URLWithString:locationString]
235 if (location == nil) {
236 [self logWithFormat:@"ERROR(%s): missing folder location in record: %@",
237 __PRETTY_FUNCTION__, _record];
241 locationString = [_record objectForKey:@"cQuickLocation"];
242 quickLocation = [locationString isNotNull]
243 ? [NSURL URLWithString:locationString]
246 if (quickLocation == nil) {
247 [self logWithFormat:@"WARNING(%s): missing quick location in record: %@",
248 __PRETTY_FUNCTION__, _record];
251 folder = [[GCSFolder alloc] initWithPath:path primaryKey:folderId
252 folderTypeName:folderTypeName
253 folderType:folderType
254 location:location quickLocation:quickLocation
256 return [folder autorelease];
261 - (NSString *)generateSQLWhereForInternalNames:(NSArray *)_names
262 exactMatch:(BOOL)_beExact orDirectSubfolderMatch:(BOOL)_directSubs
264 /* generates a WHERE qualifier for matching the "quick" entries */
265 NSMutableString *sql;
268 if ((count = [_names count]) == 0) {
269 [self debugWithFormat:@"WARNING(%s): passed in empty name array!",
270 __PRETTY_FUNCTION__];
274 sql = [NSMutableString stringWithCapacity:(count * 8)];
275 for (i = 0; i < quickPathCount; i++) {
276 NSString *pathColumn;
277 unsigned char buf[32];
279 sprintf(buf, GCSPathColumnPattern, (i + 1));
280 pathColumn = [[NSString alloc] initWithCString:buf];
282 /* Note: the AND addition must be inside the if's for non-exact stuff */
285 /* exact match, regular column */
286 if ([sql length] > 0) [sql appendString:@" AND "];
287 [sql appendString:pathColumn];
288 [sql appendFormat:@" = '%@'", [_names objectAtIndex:i]];
291 /* exact match, ensure that all additional quick-cols are NULL */
292 if ([sql length] > 0) [sql appendString:@" AND "];
293 [sql appendString:pathColumn];
294 [sql appendString:@" IS NULL"];
295 if (debugPathTraversal) [self logWithFormat:@"BE EXACT, NULL columns"];
297 else if (_directSubs) {
298 /* fetch immediate subfolders */
299 if ([sql length] > 0) [sql appendString:@" AND "];
300 [sql appendString:pathColumn];
302 /* if it is a direct subfolder, the next path cannot be empty */
303 [sql appendString:@" IS NOT NULL"];
304 if (debugPathTraversal)
305 [self logWithFormat:@"DIRECT SUBS, first level"];
308 /* but for 'direct' subfolders, all following things must be empty */
309 [sql appendString:@" IS NULL"];
310 if (debugPathTraversal)
311 [self logWithFormat:@"DIRECT SUBS, lower level"];
315 [pathColumn release];
318 if (_beExact && (count > quickPathCount)) {
319 [sql appendString:@" AND c_foldername = '"];
320 [sql appendString:[_names lastObject]];
321 [sql appendString:@"'"];
327 - (NSString *)generateSQLPathFetchForInternalNames:(NSArray *)_names
328 exactMatch:(BOOL)_beExact orDirectSubfolderMatch:(BOOL)_directSubs
330 /* fetches the 'path' subset for a given quick-names */
331 NSMutableString *sql;
334 ws = [self generateSQLWhereForInternalNames:_names
335 exactMatch:_beExact orDirectSubfolderMatch:_directSubs];
336 if ([ws length] == 0)
339 sql = [NSMutableString stringWithCapacity:256];
340 [sql appendString:@"SELECT c_path FROM "];
341 [sql appendString:[self folderInfoTableName]];
342 [sql appendString:@" WHERE "];
343 [sql appendString:ws];
344 if (debugSQLGen) [self logWithFormat:@"PathFetch-SQL: %@", sql];
348 /* handling folder names */
350 - (BOOL)_isStandardizedPath:(NSString *)_path {
351 if (![_path isAbsolutePath]) return NO;
352 if ([_path rangeOfString:@".."].length > 0) return NO;
353 if ([_path rangeOfString:@"~"].length > 0) return NO;
354 if ([_path rangeOfString:@"//"].length > 0) return NO;
358 - (NSString *)internalNameFromPath:(NSString *)_path {
359 // TODO: ensure proper path and SQL escaping!
361 if (![self _isStandardizedPath:_path]) {
362 [self debugWithFormat:@"%s: not a standardized path: '%@'",
363 __PRETTY_FUNCTION__, _path];
367 if ([_path hasSuffix:@"/"] && [_path length] > 1)
368 _path = [_path substringToIndex:([_path length] - 1)];
372 - (NSArray *)internalNamesFromPath:(NSString *)_path {
376 if ((fname = [self internalNameFromPath:_path]) == nil)
379 if ([fname hasPrefix:@"/"])
380 fname = [fname substringFromIndex:1];
382 fnames = [fname componentsSeparatedByString:@"/"];
383 if ([fnames count] == 0)
388 - (NSString *)pathFromInternalName:(NSString *)_name {
389 /* for incomplete pathes, like '/Users/helge/' */
392 - (NSString *)pathPartFromInternalName:(NSString *)_name {
393 /* for incomplete pathes, like 'Users/' */
397 - (NSDictionary *)filterRecords:(NSArray *)_records forPath:(NSString *)_path {
401 if (_records == nil) return nil;
402 if ((name = [self internalNameFromPath:_path]) == nil) return nil;
404 for (i = 0, count = [_records count]; i < count; i++) {
405 NSDictionary *record;
408 record = [_records objectAtIndex:i];
409 recName = [record objectForKey:GCSPathRecordName];
411 [self logWithFormat:@"check '%@' vs '%@' (%@)...",
412 name, recName, [_records objectAtIndex:i]];
415 if ([name isEqualToString:recName])
416 return [_records objectAtIndex:i];
421 - (BOOL)folderExistsAtPath:(NSString *)_path {
423 NSArray *fnames, *records;
427 if ((fnames = [self internalNamesFromPath:_path]) == nil) {
428 [self debugWithFormat:@"got no internal names for path: '%@'", _path];
432 sql = [self generateSQLPathFetchForInternalNames:fnames
433 exactMatch:YES orDirectSubfolderMatch:NO];
434 if ([sql length] == 0) {
435 [self debugWithFormat:@"got no SQL for names: %@", fnames];
439 if ((records = [self performSQL:sql]) == nil) {
440 [self logWithFormat:@"ERROR(%s): executing SQL failed: '%@'",
441 __PRETTY_FUNCTION__, sql];
445 if ((count = [records count]) == 0)
448 fname = [self internalNameFromPath:_path];
450 NSDictionary *record;
453 record = [records objectAtIndex:0];
454 sname = [record objectForKey:GCSPathRecordName];
455 return [fname isEqualToString:sname];
458 [self logWithFormat:@"records: %@", records];
463 - (NSArray *)listSubFoldersAtPath:(NSString *)_path recursive:(BOOL)_recursive{
464 NSMutableArray *result;
466 NSArray *fnames, *records;
470 if ((fnames = [self internalNamesFromPath:_path]) == nil) {
471 [self debugWithFormat:@"got no internal names for path: '%@'", _path];
475 sql = [self generateSQLPathFetchForInternalNames:fnames
476 exactMatch:NO orDirectSubfolderMatch:(_recursive ? NO : YES)];
477 if ([sql length] == 0) {
478 [self debugWithFormat:@"got no SQL for names: %@", fnames];
482 if ((records = [self performSQL:sql]) == nil) {
483 [self logWithFormat:@"ERROR(%s): executing SQL failed: '%@'",
484 __PRETTY_FUNCTION__, sql];
488 if ((count = [records count]) == 0)
491 result = [NSMutableArray arrayWithCapacity:(count > 128 ? 128 : count)];
493 fname = [self internalNameFromPath:_path];
494 fname = [fname stringByAppendingString:@"/"]; /* add slash */
495 for (i = 0; i < count; i++) {
496 NSDictionary *record;
497 NSString *sname, *spath;
499 record = [records objectAtIndex:i];
500 sname = [record objectForKey:GCSPathRecordName];
501 if (![sname hasPrefix:fname]) /* does not match at all ... */
504 /* strip prefix and following slash */
505 sname = [sname substringFromIndex:[fname length]];
506 spath = [self pathPartFromInternalName:sname];
509 if ([spath length] > 0) [result addObject:spath];
512 /* direct children only, so exclude everything with a slash */
513 if ([sname rangeOfString:@"/"].length == 0 && [spath length] > 0)
514 [result addObject:spath];
521 - (GCSFolder *)folderAtPath:(NSString *)_path {
522 NSMutableString *sql;
523 NSArray *fnames, *records;
525 NSDictionary *record;
527 if ((fnames = [self internalNamesFromPath:_path]) == nil) {
528 [self debugWithFormat:@"got no internal names for path: '%@'", _path];
532 /* generate SQL to fetch folder attributes */
534 ws = [self generateSQLWhereForInternalNames:fnames
535 exactMatch:YES orDirectSubfolderMatch:NO];
537 sql = [NSMutableString stringWithCapacity:256];
538 [sql appendString:@"SELECT "];
539 [sql appendString:@"c_folder_id, "];
540 [sql appendString:@"c_path, "];
541 [sql appendString:@"c_location, c_quick_location, "];
542 [sql appendString:@"c_folder_type"];
543 [sql appendString:@" FROM "];
544 [sql appendString:[self folderInfoTableName]];
545 [sql appendString:@" WHERE "];
546 [sql appendString:ws];
550 if ((records = [self performSQL:sql]) == nil) {
551 [self logWithFormat:@"ERROR(%s): executing SQL failed: '%@'",
552 __PRETTY_FUNCTION__, sql];
556 // TODO: need to filter on path
557 // required when we start to have deeper hierarchies
558 // => isn't that already done below?
560 if ([records count] != 1) {
561 if ([records count] == 0) {
562 [self debugWithFormat:@"found no records for path: '%@'", _path];
566 [self logWithFormat:@"ERROR(%s): more than one row for path: '%@'",
567 __PRETTY_FUNCTION__, _path];
571 if ((record = [self filterRecords:records forPath:_path]) == nil) {
572 [self debugWithFormat:@"found no record for path: '%@'", _path];
576 return [self folderForRecord:record];
579 - (NSException *)createFolderOfType:(NSString *)_type atPath:(NSString *)_path{
580 // TODO: implement folder create
581 GCSFolderType *ftype;
583 if ((ftype = [self folderTypeWithName:_type]) == nil) {
584 return [NSException exceptionWithName:@"GCSMissingFolderType"
585 reason:@"missing folder type"
589 [self logWithFormat:@"create folder of type: %@", ftype];
591 return [NSException exceptionWithName:@"NotYetImplemented"
592 reason:@"no money, no time, ..."
598 - (GCSFolderType *)folderTypeWithName:(NSString *)_name {
599 if ([_name length] == 0)
600 _name = GCSGenericFolderTypeName;
602 return [self->nameToType objectForKey:[_name lowercaseString]];
605 /* cache management */
608 /* does nothing in the moment, but we need a way to signal refreshes */
613 - (BOOL)isDebuggingEnabled {
619 - (NSString *)description {
622 ms = [NSMutableString stringWithCapacity:256];
623 [ms appendFormat:@"<0x%08X[%@]:", self, NSStringFromClass([self class])];
625 [ms appendFormat:@" url=%@", [self->folderInfoLocation absoluteString]];
626 [ms appendFormat:@" channel-manager=0x%08X", [self channelManager]];
628 [ms appendString:@">"];
632 @end /* GCSFolderManager */