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 "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>
32 #if APPLE_Foundation_LIBRARY || NeXT_Foundation_LIBRARY
33 @interface NSDate(UsedPrivates)
34 - (id)initWithTimeIntervalSince1970:(NSTimeInterval)_interval;
38 @implementation SOGoAppointmentFolder
40 static NGLogger *logger = nil;
41 static NSTimeZone *MET = nil;
42 static NSNumber *sharedYes = nil;
45 return [super version] + 1 /* v1 */;
49 static BOOL didInit = NO;
54 NSAssert2([super version] == 0,
55 @"invalid superclass (%@) version %i !",
56 NSStringFromClass([self superclass]), [super version]);
58 lm = [NGLoggerManager defaultLoggerManager];
59 logger = [lm loggerForDefaultKey:@"SOGoAppointmentFolderDebugEnabled"];
61 MET = [[NSTimeZone timeZoneWithAbbreviation:@"MET"] retain];
62 sharedYes = [[NSNumber numberWithBool:YES] retain];
66 [self->uidToFilename release];
79 - (NSArray *)calendarUIDs {
80 /* this is used for group calendars (this folder just returns itself) */
83 s = [[self container] nameInContainer];
84 return [s isNotNull] ? [NSArray arrayWithObjects:&s count:1] : nil;
89 - (BOOL)isValidAppointmentName:(NSString *)_key {
90 if ([_key length] == 0)
96 - (id)appointmentWithName:(NSString *)_key inContext:(id)_ctx {
97 static Class aptClass = Nil;
101 aptClass = NSClassFromString(@"SOGoAppointmentObject");
102 if (aptClass == Nil) {
103 [self errorWithFormat:@"missing SOGoAppointmentObject class!"];
107 apt = [[aptClass alloc] initWithName:_key inContainer:self];
108 return [apt autorelease];
111 - (id)lookupName:(NSString *)_key inContext:(id)_ctx acquire:(BOOL)_flag {
114 /* first check attributes directly bound to the application */
115 if ((obj = [super lookupName:_key inContext:_ctx acquire:NO]))
118 if ([self isValidAppointmentName:_key])
119 return [self appointmentWithName:_key inContext:_ctx];
121 /* return 404 to stop acquisition */
122 return [NSException exceptionWithHTTPStatus:404 /* Not Found */];
127 - (NSTimeZone *)viewTimeZone {
128 // TODO: should use a cookie for configuration? we default to MET
132 /* vevent UID handling */
134 - (NSString *)resourceNameForEventUID:(NSString *)_u inFolder:(GCSFolder *)_f {
135 static NSArray *nameFields = nil;
136 EOQualifier *qualifier;
139 if (![_u isNotNull]) return nil;
141 [self errorWithFormat:@"(%s): missing folder for fetch!",
142 __PRETTY_FUNCTION__];
146 if (nameFields == nil)
147 nameFields = [[NSArray alloc] initWithObjects:@"c_name", nil];
149 qualifier = [EOQualifier qualifierWithQualifierFormat:@"uid = %@", _u];
150 records = [_f fetchFields:nameFields matchingQualifier:qualifier];
152 if ([records count] == 1)
153 return [[records objectAtIndex:0] valueForKey:@"c_name"];
154 if ([records count] == 0)
157 [self errorWithFormat:
158 @"The storage contains more than file with the same UID!"];
159 return [[records objectAtIndex:0] valueForKey:@"c_name"];
162 - (NSString *)resourceNameForEventUID:(NSString *)_uid {
167 if (![_uid isNotNull])
169 if ((rname = [self->uidToFilename objectForKey:_uid]) != nil)
170 return [rname isNotNull] ? rname : nil;
172 if ((folder = [self ocsFolder]) == nil) {
173 [self errorWithFormat:@"(%s): missing folder for fetch!",
174 __PRETTY_FUNCTION__];
178 if (self->uidToFilename == nil)
179 self->uidToFilename = [[NSMutableDictionary alloc] initWithCapacity:16];
181 if ((rname = [self resourceNameForEventUID:_uid inFolder:folder]) == nil)
182 [self->uidToFilename setObject:[NSNull null] forKey:_uid];
184 [self->uidToFilename setObject:rname forKey:_uid];
191 - (NSMutableDictionary *)fixupRecord:(NSDictionary *)_record
192 fetchRange:(NGCalendarDateRange *)_r
194 NSMutableDictionary *md;
197 md = [[_record mutableCopy] autorelease];
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"];
207 [self logWithFormat:@"missing 'startdate' in record?"];
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"];
217 [self logWithFormat:@"missing 'enddate' in record?"];
222 - (NSMutableDictionary *)fixupCycleRecord:(NSDictionary *)_record
223 cycleRange:(NGCalendarDateRange *)_r
225 NSMutableDictionary *md;
228 md = [[_record mutableCopy] autorelease];
231 tmp = [_r startDate];
232 [tmp setTimeZone:[self viewTimeZone]];
233 [md setObject:tmp forKey:@"startDate"];
235 [tmp setTimeZone:[self viewTimeZone]];
236 [md setObject:tmp forKey:@"endDate"];
241 - (void)_flattenCycleRecord:(NSDictionary *)_row
242 forRange:(NGCalendarDateRange *)_r
243 intoArray:(NSMutableArray *)_ma
245 NSMutableDictionary *row;
246 NSDictionary *cycleinfo;
247 NSCalendarDate *startDate, *endDate;
248 NGCalendarDateRange *fir;
249 NSArray *rules, *exRules, *exDates, *ranges;
252 cycleinfo = [[_row objectForKey:@"cycleinfo"] propertyList];
253 if (cycleinfo == nil) {
254 [self errorWithFormat:@"cyclic record doesn't have cycleinfo -> %@", _row];
258 row = [self fixupRecord:_row fetchRange:_r];
259 [row removeObjectForKey:@"cycleinfo"];
260 [row setObject:sharedYes forKey:@"isRecurrentEvent"];
262 startDate = [row objectForKey:@"startDate"];
263 endDate = [row objectForKey:@"endDate"];
264 fir = [NGCalendarDateRange calendarDateRangeWithStartDate:startDate
266 rules = [cycleinfo objectForKey:@"rules"];
267 exRules = [cycleinfo objectForKey:@"exRules"];
268 exDates = [cycleinfo objectForKey:@"exDates"];
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;
280 rRange = [ranges objectAtIndex:i];
281 fixedRow = [self fixupCycleRecord:row cycleRange:rRange];
282 if (fixedRow != nil) [_ma addObject:fixedRow];
286 - (NSArray *)fixupRecords:(NSArray *)_records
287 fetchRange:(NGCalendarDateRange *)_r
289 // TODO: is the result supposed to be sorted by date?
293 if (_records == nil) return nil;
294 if ((count = [_records count]) == 0)
297 ma = [NSMutableArray arrayWithCapacity:count];
298 for (i = 0; i < count; i++) {
299 id row; // TODO: what is the type of the record?
301 row = [_records objectAtIndex:i];
302 row = [self fixupRecord:row fetchRange:_r];
303 if (row != nil) [ma addObject:row];
308 - (NSArray *)fixupCyclicRecords:(NSArray *)_records
309 fetchRange:(NGCalendarDateRange *)_r
311 // TODO: is the result supposed to be sorted by date?
315 if (_records == nil) return nil;
316 if ((count = [_records count]) == 0)
319 ma = [NSMutableArray arrayWithCapacity:count];
320 for (i = 0; i < count; i++) {
321 id row; // TODO: what is the type of the record?
323 row = [_records objectAtIndex:i];
324 [self _flattenCycleRecord:row forRange:_r intoArray:ma];
329 - (NSArray *)fetchFields:(NSArray *)_fields
330 fromFolder:(GCSFolder *)_folder
331 from:(NSCalendarDate *)_startDate
332 to:(NSCalendarDate *)_endDate
334 EOQualifier *qualifier;
335 NSMutableArray *fields, *ma = nil;
338 NGCalendarDateRange *r;
340 if (_folder == nil) {
341 [self errorWithFormat:@"(%s): missing folder for fetch!",
342 __PRETTY_FUNCTION__];
346 r = [NGCalendarDateRange calendarDateRangeWithStartDate:_startDate
349 /* prepare mandatory fields */
351 fields = [NSMutableArray arrayWithArray:_fields];
352 [fields addObject:@"uid"];
353 [fields addObject:@"startdate"];
354 [fields addObject:@"enddate"];
357 [self debugWithFormat:@"should fetch (%@=>%@) ...", _startDate, _endDate];
359 sql = [NSString stringWithFormat:@"(startdate < %d) AND (enddate > %d)"
360 @" AND (iscycle = 0)",
361 (unsigned int)[_endDate timeIntervalSince1970],
362 (unsigned int)[_startDate timeIntervalSince1970]];
364 /* fetch non-recurrent apts first */
365 qualifier = [EOQualifier qualifierWithQualifierFormat:sql];
367 records = [_folder fetchFields:fields matchingQualifier:qualifier];
368 if (records != nil) {
369 records = [self fixupRecords:records fetchRange:r];
371 [self debugWithFormat:@"fetched %i records: %@",[records count],records];
372 ma = [NSMutableArray arrayWithArray:records];
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];
382 [fields addObject:@"cycleinfo"];
384 records = [_folder fetchFields:fields matchingQualifier:qualifier];
385 if (records != nil) {
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];
393 else if (ma == nil) {
394 [self errorWithFormat:@"(%s): fetch failed!", __PRETTY_FUNCTION__];
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
402 [ma sortUsingSelector:@selector(compareAptsAscending:)];
404 [self debugWithFormat:@"returning %i records", [ma count]];
408 /* override this in subclasses */
409 - (NSArray *)fetchFields:(NSArray *)_fields
410 from:(NSCalendarDate *)_startDate
411 to:(NSCalendarDate *)_endDate
415 if ((folder = [self ocsFolder]) == nil) {
416 [self errorWithFormat:@"(%s): missing folder for fetch!",
417 __PRETTY_FUNCTION__];
420 return [self fetchFields:_fields fromFolder:folder
421 from:_startDate to:_endDate];
425 - (NSArray *)fetchFreebusyInfosFrom:(NSCalendarDate *)_startDate
426 to:(NSCalendarDate *)_endDate
428 static NSArray *infos = nil; // TODO: move to a plist file
430 infos = [[NSArray alloc] initWithObjects:@"partmails", @"partstates", nil];
432 return [self fetchFields:infos from:_startDate to:_endDate];
436 - (NSArray *)fetchOverviewInfosFrom:(NSCalendarDate *)_startDate
437 to:(NSCalendarDate *)_endDate
439 static NSArray *infos = nil; // TODO: move to a plist file
441 infos = [[NSArray alloc] initWithObjects:
443 @"location", @"orgmail", @"status", @"ispublic",
444 @"isallday", @"priority",
445 @"partmails", @"partstates",
448 return [self fetchFields:infos
453 - (NSArray *)fetchCoreInfosFrom:(NSCalendarDate *)_startDate
454 to:(NSCalendarDate *)_endDate
456 static NSArray *infos = nil; // TODO: move to a plist file
458 infos = [[NSArray alloc] initWithObjects:
459 @"title", @"location", @"orgmail",
460 @"status", @"ispublic",
461 @"isallday", @"isopaque",
462 @"participants", @"partmails",
463 @"partstates", @"sequence", @"priority", nil];
465 return [self fetchFields:infos
472 - (NSString *)baseURLForAptWithUID:(NSString *)_uid inContext:(id)_ctx {
473 // TODO: who calls this?
476 if ([_uid length] == 0)
479 url = [self baseURLInContext:_ctx];
480 if (![url hasSuffix:@"/"])
481 url = [url stringByAppendingString:@"/"];
483 // TODO: this should run a query to determine the uid!
484 return [url stringByAppendingString:_uid];
487 /* folder management */
489 - (id)lookupHomeFolderForUID:(NSString *)_uid inContext:(id)_ctx {
490 // TODO: DUP to SOGoGroupFolder
491 NSException *error = nil;
495 if (![_uid isNotNull])
498 if (_ctx == nil) _ctx = [[WOApplication application] context];
500 /* create subcontext, so that we don't destroy our environment */
502 if ((ctx = [_ctx createSubContext]) == nil) {
503 [self errorWithFormat:@"could not create SOPE subcontext!"];
509 path = _uid != nil ? [NSArray arrayWithObjects:&_uid count:1] : nil;
513 result = [[ctx application] traversePathArray:path inContext:ctx
514 error:&error acquire:NO];
516 [self errorWithFormat:@"folder lookup failed (uid=%@): %@",
521 [self debugWithFormat:@"Note: got folder for uid %@ path %@: %@",
522 _uid, [path componentsJoinedByString:@"=>"], result];
526 - (NSArray *)lookupCalendarFoldersForUIDs:(NSArray *)_uids inContext:(id)_ctx {
527 /* Note: can return NSNull objects in the array! */
528 NSMutableArray *folders;
532 if ([_uids count] == 0) return nil;
533 folders = [NSMutableArray arrayWithCapacity:16];
534 e = [_uids objectEnumerator];
535 while ((uid = [e nextObject])) {
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]])
544 if (![folder isNotNull])
545 [self logWithFormat:@"Note: did not find folder for uid: '%@'", uid];
547 /* Note: intentionally add 'null' folders to allow a mapping */
548 [folders addObject:folder ? folder : [NSNull null]];
553 - (NSArray *)lookupFreeBusyObjectsForUIDs:(NSArray *)_uids inContext:(id)_ctx {
554 /* Note: can return NSNull objects in the array! */
555 NSMutableArray *objs;
559 if ([_uids count] == 0) return nil;
560 objs = [NSMutableArray arrayWithCapacity:16];
561 e = [_uids objectEnumerator];
562 while ((uid = [e nextObject])) {
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]])
571 if (![obj isNotNull])
572 [self logWithFormat:@"Note: did not find freebusy.ifb for uid: '%@'", uid];
574 /* Note: intentionally add 'null' folders to allow a mapping */
575 [objs addObject:obj ? obj : [NSNull null]];
580 - (NSArray *)uidsFromICalPersons:(NSArray *)_persons {
581 /* Note: can return NSNull objects in the array! */
582 NSMutableArray *uids;
583 AgenorUserManager *um;
589 count = [_persons count];
590 uids = [NSMutableArray arrayWithCapacity:count + 1];
591 um = [AgenorUserManager sharedUserManager];
593 for (i = 0; i < count; i++) {
598 person = [_persons objectAtIndex:i];
599 email = [person rfc822Email];
600 if ([email isNotNull]) {
601 uid = [um getUIDForEmail:email];
606 [uids addObject:(uid != nil ? uid : (id)[NSNull null])];
611 - (NSArray *)lookupCalendarFoldersForICalPerson:(NSArray *)_persons
614 /* Note: can return NSNull objects in the array! */
617 if ((uids = [self uidsFromICalPersons:_persons]) == nil)
620 return [self lookupCalendarFoldersForUIDs:uids inContext:_ctx];
623 - (id)lookupGroupFolderForUIDs:(NSArray *)_uids inContext:(id)_ctx {
624 SOGoCustomGroupFolder *folder;
629 folder = [[SOGoCustomGroupFolder alloc] initWithUIDs:_uids inContainer:self];
630 return [folder autorelease];
632 - (id)lookupGroupCalendarFolderForUIDs:(NSArray *)_uids inContext:(id)_ctx {
633 SOGoCustomGroupFolder *folder;
635 if ((folder = [self lookupGroupFolderForUIDs:_uids inContext:_ctx]) == nil)
638 folder = [folder lookupName:@"Calendar" inContext:_ctx acquire:NO];
639 if (![folder isNotNull])
641 if ([folder isKindOfClass:[NSException class]]) {
642 [self debugWithFormat:@"Note: could not lookup 'Calendar' in folder: %@",
652 - (NSArray *)fetchAllSOGoAppointments {
654 Note: very expensive method, do not use unless absolutely required.
655 returns an array of SOGoAppointment objects.
657 Note that we can leave out the filenames, supposed to be stored
658 in the 'uid' field of the iCalendar object!
660 NSMutableArray *events;
662 NSEnumerator *contents;
665 /* fetch all raw contents */
667 files = [self fetchContentStringsAndNamesOfAllObjects];
668 if (![files isNotNull]) return nil;
669 if ([files isKindOfClass:[NSException class]]) return (id)files;
671 /* transform to SOGo appointments */
673 events = [NSMutableArray arrayWithCapacity:[files count]];
674 contents = [files objectEnumerator];
675 while ((content = [contents nextObject]) != nil) {
676 SOGoAppointment *event;
678 event = [[SOGoAppointment alloc] initWithICalString:content];
679 if (![event isNotNull]) {
680 [self errorWithFormat:@"(%s): could not parse an iCal file!",
681 __PRETTY_FUNCTION__];
685 [events addObject:event];
694 - (id)GETAction:(id)_ctx {
695 // TODO: I guess this should really be done by SOPE (redirect to
700 uri = [[(WOContext *)_ctx request] uri];
701 if (![uri hasSuffix:@"/"]) uri = [uri stringByAppendingString:@"/"];
702 uri = [uri stringByAppendingString:@"weekoverview"];
705 [r setStatus:302 /* moved */];
706 [r setHeader:uri forKey:@"location"];
712 - (NSString *)outlookFolderClass {
713 return @"IPF.Appointment";
716 @end /* SOGoAppointmentFolder */