]> err.no Git - scalable-opengroupware.org/blob - SoObjects/Appointments/SOGoAppointmentFolder.m
git-svn-id: http://svn.opengroupware.org/SOGo/trunk@900 d1b88da0-ebda-0310-925b-ed51d...
[scalable-opengroupware.org] / SoObjects / Appointments / SOGoAppointmentFolder.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 "SOGoAppointmentFolder.h"
23 #include <SOGo/SOGoCustomGroupFolder.h>
24 #include <SOGo/SOGoAppointment.h>
25 #include <SOGo/AgenorUserManager.h>
26 #include <GDLContentStore/GCSFolder.h>
27 #include <SaxObjC/SaxObjC.h>
28 #include <NGiCal/NGiCal.h>
29 #include <NGExtensions/NGCalendarDateRange.h>
30 #include "common.h"
31
32 #if APPLE_Foundation_LIBRARY || NeXT_Foundation_LIBRARY
33 @interface NSDate(UsedPrivates)
34 - (id)initWithTimeIntervalSince1970:(NSTimeInterval)_interval;
35 @end
36 #endif
37
38 @implementation SOGoAppointmentFolder
39
40 static NGLogger   *logger    = nil;
41 static NSTimeZone *MET       = nil;
42 static NSNumber   *sharedYes = nil;
43
44 + (int)version {
45   return [super version] + 1 /* v1 */;
46 }
47 + (void)initialize {
48   NGLoggerManager *lm;
49   static BOOL     didInit = NO;
50
51   if (didInit) return;
52   didInit = YES;
53   
54   NSAssert2([super version] == 0,
55             @"invalid superclass (%@) version %i !",
56             NSStringFromClass([self superclass]), [super version]);
57
58   lm      = [NGLoggerManager defaultLoggerManager];
59   logger  = [lm loggerForDefaultKey:@"SOGoAppointmentFolderDebugEnabled"];
60
61   MET       = [[NSTimeZone timeZoneWithAbbreviation:@"MET"] retain];
62   sharedYes = [[NSNumber numberWithBool:YES] retain];
63 }
64
65 - (void)dealloc {
66   [self->uidToFilename release];
67   [super dealloc];
68 }
69
70
71 /* logging */
72
73 - (id)debugLogger {
74   return logger;
75 }
76
77 /* selection */
78
79 - (NSArray *)calendarUIDs {
80   /* this is used for group calendars (this folder just returns itself) */
81   NSString *s;
82   
83   s = [[self container] nameInContainer];
84   return [s isNotNull] ? [NSArray arrayWithObjects:&s count:1] : nil;
85 }
86
87 /* name lookup */
88
89 - (BOOL)isValidAppointmentName:(NSString *)_key {
90   if ([_key length] == 0)
91     return NO;
92   
93   return YES;
94 }
95
96 - (id)appointmentWithName:(NSString *)_key inContext:(id)_ctx {
97   static Class aptClass = Nil;
98   id apt;
99   
100   if (aptClass == Nil)
101     aptClass = NSClassFromString(@"SOGoAppointmentObject");
102   if (aptClass == Nil) {
103     [self errorWithFormat:@"missing SOGoAppointmentObject class!"];
104     return nil;
105   }
106   
107   apt = [[aptClass alloc] initWithName:_key inContainer:self];
108   return [apt autorelease];
109 }
110
111 - (id)lookupName:(NSString *)_key inContext:(id)_ctx acquire:(BOOL)_flag {
112   id obj;
113   
114   /* first check attributes directly bound to the application */
115   if ((obj = [super lookupName:_key inContext:_ctx acquire:NO]))
116     return obj;
117   
118   if ([self isValidAppointmentName:_key])
119     return [self appointmentWithName:_key inContext:_ctx];
120   
121   /* return 404 to stop acquisition */
122   return [NSException exceptionWithHTTPStatus:404 /* Not Found */];
123 }
124
125 /* timezone */
126
127 - (NSTimeZone *)viewTimeZone {
128   // TODO: should use a cookie for configuration? we default to MET
129   return MET;
130 }
131
132 /* vevent UID handling */
133
134 - (NSString *)resourceNameForEventUID:(NSString *)_u inFolder:(GCSFolder *)_f {
135   static NSArray *nameFields = nil;
136   EOQualifier *qualifier;
137   NSArray     *records;
138   
139   if (![_u isNotNull]) return nil;
140   if (_f == nil) {
141     [self errorWithFormat:@"(%s): missing folder for fetch!",
142             __PRETTY_FUNCTION__];
143     return nil;
144   }
145   
146   if (nameFields == nil)
147     nameFields = [[NSArray alloc] initWithObjects:@"c_name", nil];
148   
149   qualifier = [EOQualifier qualifierWithQualifierFormat:@"uid = %@", _u];
150   records   = [_f fetchFields:nameFields matchingQualifier:qualifier];
151   
152   if ([records count] == 1)
153     return [[records objectAtIndex:0] valueForKey:@"c_name"];
154   if ([records count] == 0)
155     return nil;
156   
157   [self errorWithFormat:
158           @"The storage contains more than file with the same UID!"];
159   return [[records objectAtIndex:0] valueForKey:@"c_name"];
160 }
161
162 - (NSString *)resourceNameForEventUID:(NSString *)_uid {
163   /* caches UIDs */
164   GCSFolder *folder;
165   NSString  *rname;
166   
167   if (![_uid isNotNull])
168     return nil;
169   if ((rname = [self->uidToFilename objectForKey:_uid]) != nil)
170     return [rname isNotNull] ? rname : nil;
171   
172   if ((folder = [self ocsFolder]) == nil) {
173     [self errorWithFormat:@"(%s): missing folder for fetch!",
174       __PRETTY_FUNCTION__];
175     return nil;
176   }
177
178   if (self->uidToFilename == nil)
179     self->uidToFilename = [[NSMutableDictionary alloc] initWithCapacity:16];
180   
181   if ((rname = [self resourceNameForEventUID:_uid inFolder:folder]) == nil)
182     [self->uidToFilename setObject:[NSNull null] forKey:_uid];
183   else
184     [self->uidToFilename setObject:rname forKey:_uid];
185   
186   return rname;
187 }
188
189 /* fetching */
190
191 - (NSMutableDictionary *)fixupRecord:(NSDictionary *)_record
192   fetchRange:(NGCalendarDateRange *)_r
193 {
194   NSMutableDictionary *md;
195   id tmp;
196   
197   md = [[_record mutableCopy] autorelease];
198  
199   if ((tmp = [_record objectForKey:@"startdate"])) {
200     tmp = [[NSCalendarDate alloc] initWithTimeIntervalSince1970:
201           (NSTimeInterval)[tmp unsignedIntValue]];
202     [tmp setTimeZone:[self viewTimeZone]];
203     if (tmp) [md setObject:tmp forKey:@"startDate"];
204     [tmp release];
205   }
206   else
207     [self logWithFormat:@"missing 'startdate' in record?"];
208
209   if ((tmp = [_record objectForKey:@"enddate"])) {
210     tmp = [[NSCalendarDate alloc] initWithTimeIntervalSince1970:
211           (NSTimeInterval)[tmp unsignedIntValue]];
212     [tmp setTimeZone:[self viewTimeZone]];
213     if (tmp) [md setObject:tmp forKey:@"endDate"];
214     [tmp release];
215   }
216   else
217     [self logWithFormat:@"missing 'enddate' in record?"];
218
219   return md;
220 }
221
222 - (NSMutableDictionary *)fixupCycleRecord:(NSDictionary *)_record
223   cycleRange:(NGCalendarDateRange *)_r
224 {
225   NSMutableDictionary *md;
226   id tmp;
227   
228   md = [[_record mutableCopy] autorelease];
229   
230   /* cycle is in _r */
231   tmp = [_r startDate];
232   [tmp setTimeZone:[self viewTimeZone]];
233   [md setObject:tmp forKey:@"startDate"];
234   tmp = [_r endDate];
235   [tmp setTimeZone:[self viewTimeZone]];
236   [md setObject:tmp forKey:@"endDate"];
237   
238   return md;
239 }
240
241 - (void)_flattenCycleRecord:(NSDictionary *)_row
242   forRange:(NGCalendarDateRange *)_r
243   intoArray:(NSMutableArray *)_ma
244 {
245   NSMutableDictionary *row;
246   NSDictionary        *cycleinfo;
247   NSCalendarDate      *startDate, *endDate;
248   NGCalendarDateRange *fir;
249   NSArray             *rules, *exRules, *exDates, *ranges;
250   unsigned            i, count;
251
252   cycleinfo  = [[_row objectForKey:@"cycleinfo"] propertyList];
253   if (cycleinfo == nil) {
254     [self errorWithFormat:@"cyclic record doesn't have cycleinfo -> %@", _row];
255     return;
256   }
257
258   row = [self fixupRecord:_row fetchRange:_r];
259   [row removeObjectForKey:@"cycleinfo"];
260   [row setObject:sharedYes forKey:@"isRecurrentEvent"];
261
262   startDate = [row objectForKey:@"startDate"];
263   endDate   = [row objectForKey:@"endDate"];
264   fir       = [NGCalendarDateRange calendarDateRangeWithStartDate:startDate
265                                    endDate:endDate];
266   rules     = [cycleinfo objectForKey:@"rules"];
267   exRules   = [cycleinfo objectForKey:@"exRules"];
268   exDates   = [cycleinfo objectForKey:@"exDates"];
269
270   ranges = [iCalRecurrenceCalculator recurrenceRangesWithinCalendarDateRange:_r
271                                      firstInstanceCalendarDateRange:fir
272                                      recurrenceRules:rules
273                                      exceptionRules:exRules
274                                      exceptionDates:exDates];
275   count = [ranges count];
276   for (i = 0; i < count; i++) {
277     NGCalendarDateRange *rRange;
278     id fixedRow;
279     
280     rRange   = [ranges objectAtIndex:i];
281     fixedRow = [self fixupCycleRecord:row cycleRange:rRange];
282     if (fixedRow != nil) [_ma addObject:fixedRow];
283   }
284 }
285
286 - (NSArray *)fixupRecords:(NSArray *)_records
287   fetchRange:(NGCalendarDateRange *)_r
288 {
289   // TODO: is the result supposed to be sorted by date?
290   NSMutableArray *ma;
291   unsigned i, count;
292
293   if (_records == nil) return nil;
294   if ((count = [_records count]) == 0)
295     return _records;
296   
297   ma = [NSMutableArray arrayWithCapacity:count];
298   for (i = 0; i < count; i++) {
299     id row; // TODO: what is the type of the record?
300     
301     row = [_records objectAtIndex:i];
302     row = [self fixupRecord:row fetchRange:_r];
303     if (row != nil) [ma addObject:row];
304   }
305   return ma;
306 }
307
308 - (NSArray *)fixupCyclicRecords:(NSArray *)_records
309   fetchRange:(NGCalendarDateRange *)_r
310 {
311   // TODO: is the result supposed to be sorted by date?
312   NSMutableArray *ma;
313   unsigned i, count;
314   
315   if (_records == nil) return nil;
316   if ((count = [_records count]) == 0)
317     return _records;
318   
319   ma = [NSMutableArray arrayWithCapacity:count];
320   for (i = 0; i < count; i++) {
321     id row; // TODO: what is the type of the record?
322     
323     row = [_records objectAtIndex:i];
324     [self _flattenCycleRecord:row forRange:_r intoArray:ma];
325   }
326   return ma;
327 }
328
329 - (NSArray *)fetchFields:(NSArray *)_fields
330   fromFolder:(GCSFolder *)_folder
331   from:(NSCalendarDate *)_startDate
332   to:(NSCalendarDate *)_endDate 
333 {
334   EOQualifier         *qualifier;
335   NSMutableArray      *fields, *ma = nil;
336   NSArray             *records;
337   NSString            *sql;
338   NGCalendarDateRange *r;
339
340   if (_folder == nil) {
341     [self errorWithFormat:@"(%s): missing folder for fetch!",
342             __PRETTY_FUNCTION__];
343     return nil;
344   }
345   
346   r = [NGCalendarDateRange calendarDateRangeWithStartDate:_startDate
347                            endDate:_endDate];
348
349   /* prepare mandatory fields */
350
351   fields = [NSMutableArray arrayWithArray:_fields];
352   [fields addObject:@"uid"];
353   [fields addObject:@"startdate"];
354   [fields addObject:@"enddate"];
355   
356   if (logger)
357     [self debugWithFormat:@"should fetch (%@=>%@) ...", _startDate, _endDate];
358   
359   sql = [NSString stringWithFormat:@"(startdate < %d) AND (enddate > %d)"
360                                    @" AND (iscycle = 0)",
361                   (unsigned int)[_endDate   timeIntervalSince1970],
362                   (unsigned int)[_startDate timeIntervalSince1970]];
363
364   /* fetch non-recurrent apts first */
365   qualifier = [EOQualifier qualifierWithQualifierFormat:sql];
366
367   records   = [_folder fetchFields:fields matchingQualifier:qualifier];
368   if (records != nil) {
369     records = [self fixupRecords:records fetchRange:r];
370     if (logger)
371       [self debugWithFormat:@"fetched %i records: %@",[records count],records];
372     ma = [NSMutableArray arrayWithArray:records];
373   }
374   
375   /* fetch recurrent apts now */
376   sql = [NSString stringWithFormat:@"(startdate < %d) AND (cycleenddate > %d)"
377                                    @" AND (iscycle = 1)",
378                   (unsigned int)[_endDate   timeIntervalSince1970],
379                   (unsigned int)[_startDate timeIntervalSince1970]];
380   qualifier = [EOQualifier qualifierWithQualifierFormat:sql];
381
382   [fields addObject:@"cycleinfo"];
383
384   records = [_folder fetchFields:fields matchingQualifier:qualifier];
385   if (records != nil) {
386     if (logger)
387       [self debugWithFormat:@"fetched %i cyclic records: %@",
388         [records count], records];
389     records = [self fixupCyclicRecords:records fetchRange:r];
390     if (!ma) ma = [NSMutableArray arrayWithCapacity:[records count]];
391     [ma addObjectsFromArray:records];
392   }
393   else if (ma == nil) {
394     [self errorWithFormat:@"(%s): fetch failed!", __PRETTY_FUNCTION__];
395     return nil;
396   }
397   /* NOTE: why do we sort here?
398      This probably belongs to UI but cannot be achieved as fast there as
399      we can do it here because we're operating on a mutable array -
400      having the apts sorted is never a bad idea, though
401   */
402   [ma sortUsingSelector:@selector(compareAptsAscending:)];
403   if (logger)
404     [self debugWithFormat:@"returning %i records", [ma count]];
405   return ma;
406 }
407
408 /* override this in subclasses */
409 - (NSArray *)fetchFields:(NSArray *)_fields
410   from:(NSCalendarDate *)_startDate
411   to:(NSCalendarDate *)_endDate 
412 {
413   GCSFolder *folder;
414   
415   if ((folder = [self ocsFolder]) == nil) {
416     [self errorWithFormat:@"(%s): missing folder for fetch!",
417       __PRETTY_FUNCTION__];
418     return nil;
419   }
420   return [self fetchFields:_fields fromFolder:folder
421                from:_startDate to:_endDate];
422 }
423
424
425 - (NSArray *)fetchFreebusyInfosFrom:(NSCalendarDate *)_startDate
426   to:(NSCalendarDate *)_endDate
427 {
428   static NSArray *infos = nil; // TODO: move to a plist file
429   if (infos == nil) {
430     infos = [[NSArray alloc] initWithObjects:@"partmails", @"partstates", nil];
431   }
432   return [self fetchFields:infos from:_startDate to:_endDate];
433 }
434
435
436 - (NSArray *)fetchOverviewInfosFrom:(NSCalendarDate *)_startDate
437   to:(NSCalendarDate *)_endDate
438 {
439   static NSArray *infos = nil; // TODO: move to a plist file
440   if (infos == nil) {
441     infos = [[NSArray alloc] initWithObjects:
442                                @"title", 
443                                @"location", @"orgmail", @"status", @"ispublic",
444                                @"isallday", @"priority",
445                                @"partmails", @"partstates",
446                                nil];
447   }
448   return [self fetchFields:infos
449                from:_startDate
450                to:_endDate];
451 }
452
453 - (NSArray *)fetchCoreInfosFrom:(NSCalendarDate *)_startDate
454   to:(NSCalendarDate *)_endDate 
455 {
456   static NSArray *infos = nil; // TODO: move to a plist file
457   if (infos == nil) {
458     infos = [[NSArray alloc] initWithObjects:
459                                @"title", @"location", @"orgmail",
460                                @"status", @"ispublic",
461                                @"isallday", @"isopaque",
462                                @"participants", @"partmails",
463                                @"partstates", @"sequence", @"priority", nil];
464   }
465   return [self fetchFields:infos
466                from:_startDate
467                to:_endDate];
468 }
469
470 /* URL generation */
471
472 - (NSString *)baseURLForAptWithUID:(NSString *)_uid inContext:(id)_ctx {
473   // TODO: who calls this?
474   NSString *url;
475   
476   if ([_uid length] == 0)
477     return nil;
478   
479   url = [self baseURLInContext:_ctx];
480   if (![url hasSuffix:@"/"])
481     url = [url stringByAppendingString:@"/"];
482   
483   // TODO: this should run a query to determine the uid!
484   return [url stringByAppendingString:_uid];
485 }
486
487 /* folder management */
488
489 - (id)lookupHomeFolderForUID:(NSString *)_uid inContext:(id)_ctx {
490   // TODO: DUP to SOGoGroupFolder
491   NSException *error = nil;
492   NSArray     *path;
493   id          ctx, result;
494
495   if (![_uid isNotNull])
496     return nil;
497   
498   if (_ctx == nil) _ctx = [[WOApplication application] context];
499   
500   /* create subcontext, so that we don't destroy our environment */
501   
502   if ((ctx = [_ctx createSubContext]) == nil) {
503     [self errorWithFormat:@"could not create SOPE subcontext!"];
504     return nil;
505   }
506   
507   /* build path */
508   
509   path = _uid != nil ? [NSArray arrayWithObjects:&_uid count:1] : nil;
510   
511   /* traverse path */
512   
513   result = [[ctx application] traversePathArray:path inContext:ctx
514                               error:&error acquire:NO];
515   if (error != nil) {
516     [self errorWithFormat:@"folder lookup failed (uid=%@): %@",
517             _uid, error];
518     return nil;
519   }
520   
521   [self debugWithFormat:@"Note: got folder for uid %@ path %@: %@",
522           _uid, [path componentsJoinedByString:@"=>"], result];
523   return result;
524 }
525
526 - (NSArray *)lookupCalendarFoldersForUIDs:(NSArray *)_uids inContext:(id)_ctx {
527   /* Note: can return NSNull objects in the array! */
528   NSMutableArray *folders;
529   NSEnumerator *e;
530   NSString     *uid;
531   
532   if ([_uids count] == 0) return nil;
533   folders = [NSMutableArray arrayWithCapacity:16];
534   e = [_uids objectEnumerator];
535   while ((uid = [e nextObject])) {
536     id folder;
537     
538     folder = [self lookupHomeFolderForUID:uid inContext:nil];
539     if ([folder isNotNull]) {
540       folder = [folder lookupName:@"Calendar" inContext:nil acquire:NO];
541       if ([folder isKindOfClass:[NSException class]])
542         folder = nil;
543     }
544     if (![folder isNotNull])
545       [self logWithFormat:@"Note: did not find folder for uid: '%@'", uid];
546     
547     /* Note: intentionally add 'null' folders to allow a mapping */
548     [folders addObject:folder ? folder : [NSNull null]];
549   }
550   return folders;
551 }
552
553 - (NSArray *)lookupFreeBusyObjectsForUIDs:(NSArray *)_uids inContext:(id)_ctx {
554   /* Note: can return NSNull objects in the array! */
555   NSMutableArray *objs;
556   NSEnumerator   *e;
557   NSString       *uid;
558   
559   if ([_uids count] == 0) return nil;
560   objs = [NSMutableArray arrayWithCapacity:16];
561   e    = [_uids objectEnumerator];
562   while ((uid = [e nextObject])) {
563     id obj;
564     
565     obj = [self lookupHomeFolderForUID:uid inContext:nil];
566     if ([obj isNotNull]) {
567       obj = [obj lookupName:@"freebusy.ifb" inContext:nil acquire:NO];
568       if ([obj isKindOfClass:[NSException class]])
569         obj = nil;
570     }
571     if (![obj isNotNull])
572       [self logWithFormat:@"Note: did not find freebusy.ifb for uid: '%@'", uid];
573     
574     /* Note: intentionally add 'null' folders to allow a mapping */
575     [objs addObject:obj ? obj : [NSNull null]];
576   }
577   return objs;
578 }
579
580 - (NSArray *)uidsFromICalPersons:(NSArray *)_persons {
581   /* Note: can return NSNull objects in the array! */
582   NSMutableArray    *uids;
583   AgenorUserManager *um;
584   unsigned          i, count;
585   
586   if (_persons == nil)
587     return nil;
588
589   count = [_persons count];
590   uids  = [NSMutableArray arrayWithCapacity:count + 1];
591   um    = [AgenorUserManager sharedUserManager];
592   
593   for (i = 0; i < count; i++) {
594     iCalPerson *person;
595     NSString   *email;
596     NSString   *uid;
597     
598     person = [_persons objectAtIndex:i];
599     email  = [person rfc822Email];
600     if ([email isNotNull]) {
601       uid = [um getUIDForEmail:email];
602     }
603     else
604       uid = nil;
605     
606     [uids addObject:(uid != nil ? uid : (id)[NSNull null])];
607   }
608   return uids;
609 }
610
611 - (NSArray *)lookupCalendarFoldersForICalPerson:(NSArray *)_persons
612   inContext:(id)_ctx
613 {
614   /* Note: can return NSNull objects in the array! */
615   NSArray *uids;
616
617   if ((uids = [self uidsFromICalPersons:_persons]) == nil)
618     return nil;
619   
620   return [self lookupCalendarFoldersForUIDs:uids inContext:_ctx];
621 }
622
623 - (id)lookupGroupFolderForUIDs:(NSArray *)_uids inContext:(id)_ctx {
624   SOGoCustomGroupFolder *folder;
625   
626   if (_uids == nil)
627     return nil;
628
629   folder = [[SOGoCustomGroupFolder alloc] initWithUIDs:_uids inContainer:self];
630   return [folder autorelease];
631 }
632 - (id)lookupGroupCalendarFolderForUIDs:(NSArray *)_uids inContext:(id)_ctx {
633   SOGoCustomGroupFolder *folder;
634   
635   if ((folder = [self lookupGroupFolderForUIDs:_uids inContext:_ctx]) == nil)
636     return nil;
637   
638   folder = [folder lookupName:@"Calendar" inContext:_ctx acquire:NO];
639   if (![folder isNotNull])
640     return nil;
641   if ([folder isKindOfClass:[NSException class]]) {
642     [self debugWithFormat:@"Note: could not lookup 'Calendar' in folder: %@",
643             folder];
644     return nil;
645   }
646   
647   return folder;
648 }
649
650 /* bulk fetches */
651
652 - (NSArray *)fetchAllSOGoAppointments {
653   /* 
654      Note: very expensive method, do not use unless absolutely required.
655            returns an array of SOGoAppointment objects.
656            
657      Note that we can leave out the filenames, supposed to be stored
658      in the 'uid' field of the iCalendar object!
659   */
660   NSMutableArray *events;
661   NSDictionary *files;
662   NSEnumerator *contents;
663   NSString     *content;
664   
665   /* fetch all raw contents */
666   
667   files = [self fetchContentStringsAndNamesOfAllObjects];
668   if (![files isNotNull]) return nil;
669   if ([files isKindOfClass:[NSException class]]) return (id)files;
670   
671   /* transform to SOGo appointments */
672   
673   events   = [NSMutableArray arrayWithCapacity:[files count]];
674   contents = [files objectEnumerator];
675   while ((content = [contents nextObject]) != nil) {
676     SOGoAppointment *event;
677     
678     event = [[SOGoAppointment alloc] initWithICalString:content];
679     if (![event isNotNull]) {
680       [self errorWithFormat:@"(%s): could not parse an iCal file!",
681               __PRETTY_FUNCTION__];
682       continue;
683     }
684
685     [events addObject:event];
686     [event release];
687   }
688   
689   return events;
690 }
691
692 /* GET */
693
694 - (id)GETAction:(id)_ctx {
695   // TODO: I guess this should really be done by SOPE (redirect to
696   //       default method)
697   WOResponse *r;
698   NSString *uri;
699
700   uri = [[(WOContext *)_ctx request] uri];
701   if (![uri hasSuffix:@"/"]) uri = [uri stringByAppendingString:@"/"];
702   uri = [uri stringByAppendingString:@"weekoverview"];
703
704   r = [_ctx response];
705   [r setStatus:302 /* moved */];
706   [r setHeader:uri forKey:@"location"];
707   return r;
708 }
709
710 /* folder type */
711
712 - (NSString *)outlookFolderClass {
713   return @"IPF.Appointment";
714 }
715
716 @end /* SOGoAppointmentFolder */