]> err.no Git - sope/blob - sope-gdl1/GDLContentStore/GCSFolderManager.m
fixed gcc 4.0 warnings
[sope] / sope-gdl1 / GDLContentStore / GCSFolderManager.m
1 /*
2   Copyright (C) 2004-2005 SKYRIX Software AG
3
4   This file is part of OpenGroupware.org.
5
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
9   later version.
10
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.
15
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
19   02111-1307, USA.
20 */
21
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"
28 #include "common.h"
29 #include <GDLAccess/EOAdaptorChannel.h>
30
31 /*
32   Required database schema:
33   
34     <arbitary table>
35       c_path
36       c_path1, path2, path3... [quickPathCount times]
37       c_foldername
38   
39   TODO:
40   - add a local cache?
41 */
42
43 @implementation GCSFolderManager
44
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;
51 #if 0
52 static NSString   *GCSPathColumnName        = @"c_path";
53 static NSString   *GCSTypeColumnName        = @"c_folder_type";
54 static NSString   *GCSTypeRecordName        = @"cFolderType";
55 #endif
56 static NSString   *GCSPathRecordName        = @"cPath";
57 static NSString   *GCSGenericFolderTypeName = @"Container";
58 static const char *GCSPathColumnPattern     = "c_path%i";
59
60 + (void)initialize {
61   NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
62   
63   debugOn     = [ud boolForKey:@"GCSFolderManagerDebugEnabled"];
64   debugSQLGen = [ud boolForKey:@"GCSFolderManagerSQLDebugEnabled"];
65   emptyArray  = [[NSArray alloc] init];
66 }
67
68 + (id)defaultFolderManager {
69   NSString *s;
70   NSURL    *url;
71   if (fm) return fm;
72   
73   s = [[NSUserDefaults standardUserDefaults] stringForKey:@"OCSFolderInfoURL"];
74   if ([s length] == 0) {
75     NSLog(@"ERROR(%s): default 'OCSFolderInfoURL' is not configured.",
76           __PRETTY_FUNCTION__);
77     return nil;
78   }
79   if ((url = [NSURL URLWithString:s]) == nil) {
80     NSLog(@"ERROR(%s): default 'OCSFolderInfoURL' is not a valid URL: '%@'",
81           __PRETTY_FUNCTION__, s);
82     return nil;
83   }
84   if ((fm = [[self alloc] initWithFolderInfoLocation:url]) == nil) {
85     NSLog(@"ERROR(%s): could not create folder manager with URL: '%@'",
86           __PRETTY_FUNCTION__, [url absoluteString]);
87     return nil;
88   }
89   
90   NSLog(@"Note: setup default manager at: %@", url);
91   return fm;
92 }
93
94 - (id)initWithFolderInfoLocation:(NSURL *)_url {
95   if (_url == nil) {
96     [self logWithFormat:@"ERROR(%s): missing folder info url!", 
97             __PRETTY_FUNCTION__];
98     [self release];
99     return nil;
100   }
101   if ((self = [super init])) {
102     GCSFolderType *cal, *contact;
103     
104     self->channelManager = [[GCSChannelManager defaultChannelManager] retain];
105     self->folderInfoLocation = [_url retain];
106
107     if ([[self folderInfoTableName] length] == 0) {
108       [self logWithFormat:@"ERROR(%s): missing tablename in URL: %@", 
109             __PRETTY_FUNCTION__, [_url absoluteString]];
110       [self release];
111       return nil;
112     }
113     
114     /* register default folder types */
115     
116     cal     = [[GCSFolderType alloc] initWithFolderTypeName:@"appointment"];
117     contact = [[GCSFolderType alloc] initWithFolderTypeName:@"contact"];
118     self->nameToType = [[NSDictionary alloc] initWithObjectsAndKeys:
119                                                cal,     @"appointment", 
120                                                contact, @"contact", 
121                                              nil];
122     [cal     release]; cal     = nil;
123     [contact release]; contact = nil;
124   }
125   return self;
126 }
127
128 - (void)dealloc {
129   [self->nameToType         release];
130   [self->folderInfoLocation release];
131   [self->channelManager     release];
132   [super dealloc];
133 }
134
135 /* accessors */
136
137 - (NSURL *)folderInfoLocation {
138   return self->folderInfoLocation;
139 }
140
141 - (NSString *)folderInfoTableName {
142   return [[self folderInfoLocation] gcsTableName];
143 }
144
145 /* connection */
146
147 - (GCSChannelManager *)channelManager {
148   return self->channelManager;
149 }
150
151 - (EOAdaptorChannel *)acquireOpenChannel {
152   EOAdaptorChannel *ch;
153   
154   ch = [[self channelManager] acquireOpenChannelForURL:
155               [self folderInfoLocation]];
156   return ch;
157 }
158 - (void)releaseChannel:(EOAdaptorChannel *)_channel {
159   [[self channelManager] releaseChannel:_channel];
160   if (debugOn) [self debugWithFormat:@"released channel: %@", _channel];
161 }
162
163 - (BOOL)canConnect {
164   return [[self channelManager] canConnect:[self folderInfoLocation]];
165 }
166
167 - (NSArray *)performSQL:(NSString *)_sql {
168   EOAdaptorChannel *channel;
169   NSException    *ex;
170   NSMutableArray *rows;
171   NSDictionary   *row;
172   NSArray        *attrs;
173   
174   /* acquire channel */
175   
176   if ((channel = [self acquireOpenChannel]) == nil) {
177     if (debugOn) [self debugWithFormat:@"could not acquire channel!"];
178     return nil;
179   }
180   if (debugOn) [self debugWithFormat:@"acquired channel: %@", channel];
181   
182   /* run SQL */
183   
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];
188     return nil;
189   }
190   
191   /* fetch results */
192   
193   attrs = [channel describeResults];
194   rows = [NSMutableArray arrayWithCapacity:16];
195   while ((row = [channel fetchAttributes:attrs withZone:NULL]) != nil)
196     [rows addObject:row];
197   
198   [self releaseChannel:channel];
199   return rows;
200 }
201
202 /* row factory */
203
204 - (GCSFolder *)folderForRecord:(NSDictionary *)_record {
205   GCSFolder     *folder;
206   GCSFolderType *folderType;
207   NSString      *folderTypeName, *locationString, *folderName, *path;
208   NSNumber      *folderId;
209   NSURL         *location, *quickLocation;
210   
211   if (_record == nil) return nil;
212   
213   folderTypeName = [_record objectForKey:@"cFolderType"];
214   if (![folderTypeName isNotNull]) {
215     [self logWithFormat:@"ERROR(%s): missing type in folder: %@",
216             __PRETTY_FUNCTION__, _record];
217     return nil;
218   }
219   if ((folderType = [self folderTypeWithName:folderTypeName]) == nil) {
220     [self logWithFormat:
221             @"ERROR(%s): could not resolve type '%@' of folder: %@",
222             __PRETTY_FUNCTION__,
223             folderTypeName, [_record valueForKey:@"cPath"]];
224     return nil;
225   }
226   
227   folderId   = [_record objectForKey:@"cFolderId"];
228   folderName = [_record objectForKey:@"cPath"];
229   path       = [self pathFromInternalName:folderName];
230   
231   locationString = [_record objectForKey:@"cLocation"];
232   location = [locationString isNotNull] 
233     ? [NSURL URLWithString:locationString]
234     : nil;
235   if (location == nil) {
236     [self logWithFormat:@"ERROR(%s): missing folder location in record: %@", 
237             __PRETTY_FUNCTION__, _record];
238     return nil;
239   }
240   
241   locationString = [_record objectForKey:@"cQuickLocation"];
242   quickLocation = [locationString isNotNull] 
243     ? [NSURL URLWithString:locationString]
244     : nil;
245
246   if (quickLocation == nil) {
247     [self logWithFormat:@"WARNING(%s): missing quick location in record: %@", 
248             __PRETTY_FUNCTION__, _record];
249   }
250   
251   folder = [[GCSFolder alloc] initWithPath:path primaryKey:folderId
252                               folderTypeName:folderTypeName 
253                               folderType:folderType
254                               location:location quickLocation:quickLocation
255                               folderManager:self];
256   return [folder autorelease];
257 }
258
259 /* path SQL */
260
261 - (NSString *)generateSQLWhereForInternalNames:(NSArray *)_names
262   exactMatch:(BOOL)_beExact orDirectSubfolderMatch:(BOOL)_directSubs
263 {
264   /* generates a WHERE qualifier for matching the "quick" entries */
265   NSMutableString *sql;
266   unsigned i, count;
267   
268   if ((count = [_names count]) == 0) {
269     [self debugWithFormat:@"WARNING(%s): passed in empty name array!",
270             __PRETTY_FUNCTION__];
271     return @"1 = 2";
272   }
273   
274   sql = [NSMutableString stringWithCapacity:(count * 8)];
275   for (i = 0; i < quickPathCount; i++) {
276     NSString *pathColumn;
277     char buf[32];
278     
279     sprintf(buf, GCSPathColumnPattern, (i + 1));
280     pathColumn = [[NSString alloc] initWithCString:buf];
281     
282     /* Note: the AND addition must be inside the if's for non-exact stuff */
283     
284     if (i < count) {
285       /* exact match, regular column */
286       if ([sql length] > 0) [sql appendString:@" AND "];
287       [sql appendString:pathColumn];
288       [sql appendFormat:@" = '%@'", [_names objectAtIndex:i]];
289     }
290     else if (_beExact) {
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"];
296     }
297     else if (_directSubs) {
298       /* fetch immediate subfolders */
299       if ([sql length] > 0) [sql appendString:@" AND "];
300       [sql appendString:pathColumn];
301       if (i == count) {
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"];
306       }
307       else {
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"];
312       }
313     }
314     
315     [pathColumn release];
316   }
317   
318   if (_beExact && (count > quickPathCount)) {
319     [sql appendString:@" AND c_foldername = '"];
320     [sql appendString:[_names lastObject]];
321     [sql appendString:@"'"];
322   }
323   
324   return sql;
325 }
326
327 - (NSString *)generateSQLPathFetchForInternalNames:(NSArray *)_names
328   exactMatch:(BOOL)_beExact orDirectSubfolderMatch:(BOOL)_directSubs
329 {
330   /* fetches the 'path' subset for a given quick-names */
331   NSMutableString *sql;
332   NSString *ws;
333   
334   ws = [self generateSQLWhereForInternalNames:_names 
335              exactMatch:_beExact orDirectSubfolderMatch:_directSubs];
336   if ([ws length] == 0)
337     return nil;
338   
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];
345   return sql;
346 }
347
348 /* handling folder names */
349
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;
355   return YES;
356 }
357
358 - (NSString *)internalNameFromPath:(NSString *)_path {
359   // TODO: ensure proper path and SQL escaping!
360   
361   if (![self _isStandardizedPath:_path]) {
362     [self debugWithFormat:@"%s: not a standardized path: '%@'", 
363             __PRETTY_FUNCTION__, _path];
364     return nil;
365   }
366   
367   if ([_path hasSuffix:@"/"] && [_path length] > 1)
368     _path = [_path substringToIndex:([_path length] - 1)];
369   
370   return _path;
371 }
372 - (NSArray *)internalNamesFromPath:(NSString *)_path {
373   NSString *fname;
374   NSArray  *fnames;
375   
376   if ((fname = [self internalNameFromPath:_path]) == nil)
377     return nil;
378   
379   if ([fname hasPrefix:@"/"])
380     fname = [fname substringFromIndex:1];
381   
382   fnames = [fname componentsSeparatedByString:@"/"];
383   if ([fnames count] == 0)
384     return nil;
385   
386   return fnames;
387 }
388 - (NSString *)pathFromInternalName:(NSString *)_name {
389   /* for incomplete pathes, like '/Users/helge/' */
390   return _name;
391 }
392 - (NSString *)pathPartFromInternalName:(NSString *)_name {
393   /* for incomplete pathes, like 'Users/' */
394   return _name;
395 }
396
397 - (NSDictionary *)filterRecords:(NSArray *)_records forPath:(NSString *)_path {
398   unsigned i, count;
399   NSString *name;
400   
401   if (_records == nil) return nil;
402   if ((name = [self internalNameFromPath:_path]) == nil) return nil;
403   
404   for (i = 0, count = [_records count]; i < count; i++) {
405     NSDictionary *record;
406     NSString     *recName;
407     
408     record  = [_records objectAtIndex:i];
409     recName = [record objectForKey:GCSPathRecordName];
410 #if 0
411     [self logWithFormat:@"check '%@' vs '%@' (%@)...", 
412           name, recName, [_records objectAtIndex:i]];
413 #endif
414     
415     if ([name isEqualToString:recName])
416       return [_records objectAtIndex:i];
417   }
418   return nil;
419 }
420
421 - (BOOL)folderExistsAtPath:(NSString *)_path {
422   NSString *fname;
423   NSArray  *fnames, *records;
424   NSString *sql;
425   unsigned count;
426   
427   if ((fnames = [self internalNamesFromPath:_path]) == nil) {
428     [self debugWithFormat:@"got no internal names for path: '%@'", _path];
429     return NO;
430   }
431   
432   sql = [self generateSQLPathFetchForInternalNames:fnames 
433               exactMatch:YES orDirectSubfolderMatch:NO];
434   if ([sql length] == 0) {
435     [self debugWithFormat:@"got no SQL for names: %@", fnames];
436     return NO;
437   }
438   
439   if ((records = [self performSQL:sql]) == nil) {
440     [self logWithFormat:@"ERROR(%s): executing SQL failed: '%@'", 
441             __PRETTY_FUNCTION__, sql];
442     return NO;
443   }
444   
445   if ((count = [records count]) == 0)
446     return NO;
447   
448   fname = [self internalNameFromPath:_path];
449   if (count == 1) {
450     NSDictionary *record;
451     NSString *sname;
452     
453     record = [records objectAtIndex:0];
454     sname  = [record objectForKey:GCSPathRecordName];
455     return [fname isEqualToString:sname];
456   }
457   
458   [self logWithFormat:@"records: %@", records];
459   
460   return NO;
461 }
462
463 - (NSArray *)listSubFoldersAtPath:(NSString *)_path recursive:(BOOL)_recursive{
464   NSMutableArray *result;
465   NSString *fname;
466   NSArray  *fnames, *records;
467   NSString *sql;
468   unsigned i, count;
469   
470   if ((fnames = [self internalNamesFromPath:_path]) == nil) {
471     [self debugWithFormat:@"got no internal names for path: '%@'", _path];
472     return nil;
473   }
474   
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];
479     return nil;
480   }
481   
482   if ((records = [self performSQL:sql]) == nil) {
483     [self logWithFormat:@"ERROR(%s): executing SQL failed: '%@'", 
484             __PRETTY_FUNCTION__, sql];
485     return nil;
486   }
487   
488   if ((count = [records count]) == 0)
489     return emptyArray;
490
491   result = [NSMutableArray arrayWithCapacity:(count > 128 ? 128 : count)];
492   
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;
498     
499     record = [records objectAtIndex:i];
500     sname  = [record objectForKey:GCSPathRecordName];
501     if (![sname hasPrefix:fname]) /* does not match at all ... */
502       continue;
503     
504     /* strip prefix and following slash */
505     sname = [sname substringFromIndex:[fname length]];
506     spath = [self pathPartFromInternalName:sname];
507     
508     if (_recursive) {
509       if ([spath length] > 0) [result addObject:spath];
510     }
511     else {
512       /* direct children only, so exclude everything with a slash */
513       if ([sname rangeOfString:@"/"].length == 0 && [spath length] > 0)
514         [result addObject:spath];
515     }
516   }
517   
518   return result;
519 }
520
521 - (GCSFolder *)folderAtPath:(NSString *)_path {
522   NSMutableString *sql;
523   NSArray      *fnames, *records;
524   NSString     *ws;
525   NSDictionary *record;
526   
527   if ((fnames = [self internalNamesFromPath:_path]) == nil) {
528     [self debugWithFormat:@"got no internal names for path: '%@'", _path];
529     return nil;
530   }
531   
532   /* generate SQL to fetch folder attributes */
533   
534   ws = [self generateSQLWhereForInternalNames:fnames 
535              exactMatch:YES orDirectSubfolderMatch:NO];
536   
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];
547   
548   /* fetching */
549   
550   if ((records = [self performSQL:sql]) == nil) {
551     [self logWithFormat:@"ERROR(%s): executing SQL failed: '%@'", 
552             __PRETTY_FUNCTION__, sql];
553     return nil;
554   }
555   
556   // TODO: need to filter on path
557   //         required when we start to have deeper hierarchies
558   //       => isn't that already done below?
559   
560   if ([records count] != 1) {
561     if ([records count] == 0) {
562       [self debugWithFormat:@"found no records for path: '%@'", _path];
563       return nil;
564     }
565     
566     [self logWithFormat:@"ERROR(%s): more than one row for path: '%@'", 
567             __PRETTY_FUNCTION__, _path];
568     return nil;
569   }
570   
571   if ((record = [self filterRecords:records forPath:_path]) == nil) {
572     [self debugWithFormat:@"found no record for path: '%@'", _path];
573     return nil;
574   }
575   
576   return [self folderForRecord:record];
577 }
578
579 - (NSException *)createFolderOfType:(NSString *)_type atPath:(NSString *)_path{
580   // TODO: implement folder create
581   GCSFolderType *ftype;
582   
583   if ((ftype = [self folderTypeWithName:_type]) == nil) {
584     return [NSException exceptionWithName:@"GCSMissingFolderType"
585                         reason:@"missing folder type"
586                         userInfo:nil];
587   }
588   
589   [self logWithFormat:@"create folder of type: %@", ftype];
590   
591   return [NSException exceptionWithName:@"NotYetImplemented"
592                       reason:@"no money, no time, ..."
593                       userInfo:nil];
594 }
595
596 /* folder types */
597
598 - (GCSFolderType *)folderTypeWithName:(NSString *)_name {
599   if ([_name length] == 0)
600     _name = GCSGenericFolderTypeName;
601   
602   return [self->nameToType objectForKey:[_name lowercaseString]];
603 }
604
605 /* cache management */
606
607 - (void)reset {
608   /* does nothing in the moment, but we need a way to signal refreshes */
609 }
610
611 /* debugging */
612
613 - (BOOL)isDebuggingEnabled {
614   return debugOn;
615 }
616
617 /* description */
618
619 - (NSString *)description {
620   NSMutableString *ms;
621
622   ms = [NSMutableString stringWithCapacity:256];
623   [ms appendFormat:@"<0x%08X[%@]:", self, NSStringFromClass([self class])];
624   
625   [ms appendFormat:@" url=%@", [self->folderInfoLocation absoluteString]];
626   [ms appendFormat:@" channel-manager=0x%08X", [self channelManager]];
627   
628   [ms appendString:@">"];
629   return ms;
630 }
631
632 @end /* GCSFolderManager */