]> err.no Git - scalable-opengroupware.org/blob - SoObjects/Appointments/SOGoAppointmentFolder.m
6772e5e940d9b9cee3e7430a3ee2a4f9af086c42
[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 #import <GDLContentStore/GCSFolder.h>
23 #import <SaxObjC/SaxObjC.h>
24 #import <NGCards/NGCards.h>
25 #import <NGObjWeb/SoObject+SoDAV.h>
26 #import <NGObjWeb/WOContext.h>
27 #import <NGObjWeb/WOMessage.h>
28 #import <NGExtensions/NGCalendarDateRange.h>
29
30 // #import <NGObjWeb/SoClassSecurityInfo.h>
31 #import <SOGo/SOGoCustomGroupFolder.h>
32 #import <SOGo/AgenorUserManager.h>
33 #import <SOGo/SOGoPermissions.h>
34 #import <SOGo/NSString+Utilities.h>
35
36 #import "common.h"
37
38 #import "SOGoAppointmentObject.h"
39 #import "SOGoTaskObject.h"
40
41 #import "SOGoAppointmentFolder.h"
42
43 #if APPLE_Foundation_LIBRARY || NeXT_Foundation_LIBRARY
44 @interface NSDate(UsedPrivates)
45 - (id)initWithTimeIntervalSince1970:(NSTimeInterval)_interval;
46 @end
47 #endif
48
49 @implementation SOGoAppointmentFolder
50
51 static NGLogger   *logger    = nil;
52 static NSNumber   *sharedYes = nil;
53
54 + (int) version
55 {
56   return [super version] + 1 /* v1 */;
57 }
58
59 + (void) initialize
60 {
61   NGLoggerManager *lm;
62   static BOOL     didInit = NO;
63 //   SoClassSecurityInfo *securityInfo;
64
65   if (didInit) return;
66   didInit = YES;
67   
68   NSAssert2([super version] == 0,
69             @"invalid superclass (%@) version %i !",
70             NSStringFromClass([self superclass]), [super version]);
71
72   lm      = [NGLoggerManager defaultLoggerManager];
73   logger  = [lm loggerForDefaultKey:@"SOGoAppointmentFolderDebugEnabled"];
74
75 //   securityInfo = [self soClassSecurityInfo];
76 //   [securityInfo declareRole: SOGoRole_Delegate
77 //                 asDefaultForPermission: SoPerm_AddDocumentsImagesAndFiles];
78 //   [securityInfo declareRole: SOGoRole_Delegate
79 //                 asDefaultForPermission: SoPerm_ChangeImagesAndFiles];
80 //   [securityInfo declareRoles: [NSArray arrayWithObjects:
81 //                                          SOGoRole_Delegate,
82 //                                        SOGoRole_Assistant, nil]
83 //                 asDefaultForPermission: SoPerm_View];
84
85   sharedYes = [[NSNumber numberWithBool:YES] retain];
86 }
87
88 - (void) dealloc
89 {
90   [self->uidToFilename release];
91   [super dealloc];
92 }
93
94 /* logging */
95
96 - (id) debugLogger
97 {
98   return logger;
99 }
100
101 /* selection */
102
103 - (NSArray *) calendarUIDs 
104 {
105   /* this is used for group calendars (this folder just returns itself) */
106   NSString *s;
107   
108   s = [[self container] nameInContainer];
109   return [s isNotNull] ? [NSArray arrayWithObjects:&s count:1] : nil;
110 }
111
112 /* name lookup */
113
114 - (BOOL) isValidAppointmentName: (NSString *)_key
115 {
116   return ([_key length] != 0);
117 }
118
119 - (id) lookupActionForCalDAVMethod: (NSString *)_key
120 {
121   SoSelectorInvocation *invocation;
122   NSString *name;
123
124   name = [NSString stringWithFormat: @"%@:", [_key davMethodToObjC]];
125
126   invocation = [[SoSelectorInvocation alloc]
127                  initWithSelectorNamed: name
128                  addContextParameter: YES];
129   [invocation autorelease];
130
131   return invocation;
132 }
133
134 - (void) appendObject: (NSDictionary *) object
135           withBaseURL: (NSString *) baseURL
136      toREPORTResponse: (WOResponse *) r
137 {
138   SOGoContentObject *ocsObject;
139   NSString *c_name, *etagLine, *calString;
140
141   c_name = [object objectForKey: @"c_name"];
142
143   ocsObject = [SOGoContentObject objectWithName: c_name
144                                  inContainer: self];
145
146   [r appendContentString: @"  <D:response>\r\n"];
147   [r appendContentString: @"    <D:href>"];
148   [r appendContentString: baseURL];
149   if (![baseURL hasSuffix: @"/"])
150     [r appendContentString: @"/"];
151   [r appendContentString: c_name];
152   [r appendContentString: @"</D:href>\r\n"];
153
154   [r appendContentString: @"    <D:propstat>\r\n"];
155   [r appendContentString: @"      <D:prop>\r\n"];
156   etagLine = [NSString stringWithFormat: @"        <D:getetag>%@</D:getetag>\r\n",
157                        [ocsObject davEntityTag]];
158   [r appendContentString: etagLine];
159   [r appendContentString: @"      </D:prop>\r\n"];
160   [r appendContentString: @"      <D:status>HTTP/1.1 200 OK</D:status>\r\n"];
161   [r appendContentString: @"    </D:propstat>\r\n"];
162   [r appendContentString: @"    <C:calendar-data>"];
163   calString = [[ocsObject contentAsString] stringByEscapingXMLString];
164   [r appendContentString: calString];
165   [r appendContentString: @"</C:calendar-data>\r\n"];
166   [r appendContentString: @"  </D:response>\r\n"];
167 }
168
169 - (void) _appendTimeRange: (id <DOMElement>) timeRangeElement
170                  toFilter: (NSMutableDictionary *) filter
171 {
172   NSCalendarDate *parsedDate;
173
174   parsedDate = [[timeRangeElement attribute: @"start"] asCalendarDate];
175   [filter setObject: parsedDate forKey: @"start"];
176   parsedDate = [[timeRangeElement attribute: @"end"] asCalendarDate];
177   [filter setObject: parsedDate forKey: @"end"];
178 }
179
180 - (NSDictionary *) _parseCalendarFilter: (id <DOMElement>) filterElement
181 {
182   NSMutableDictionary *filterData;
183   id <DOMNode> parentNode;
184   id <DOMNodeList> ranges;
185   NSString *componentName;
186
187   parentNode = [filterElement parentNode];
188   if ([[parentNode tagName] isEqualToString: @"comp-filter"]
189       && [[parentNode attribute: @"name"] isEqualToString: @"VCALENDAR"])
190     {
191       componentName = [[filterElement attribute: @"name"] lowercaseString];
192       filterData = [NSMutableDictionary new];
193       [filterData autorelease];
194       [filterData setObject: componentName forKey: @"name"];
195       ranges = [filterElement getElementsByTagName: @"time-range"];
196       if ([ranges count])
197         [self _appendTimeRange: [ranges objectAtIndex: 0]
198               toFilter: filterData];
199     }
200   else
201     filterData = nil;
202
203   return filterData;
204 }
205
206 - (NSArray *) _parseCalendarFilters: (id <DOMElement>) parentNode
207 {
208   NSEnumerator *children;
209   id<DOMElement> node;
210   NSMutableArray *filters;
211   NSDictionary *filter;
212
213   filters = [NSMutableArray new];
214
215   children = [[parentNode getElementsByTagName: @"comp-filter"] objectEnumerator];
216   node = [children nextObject];
217   while (node)
218     {
219       filter = [self _parseCalendarFilter: node];
220       if (filter)
221         [filters addObject: filter];
222       node = [children nextObject];
223     }
224
225   return filters;
226 }
227
228 - (void) _appendComponentsMatchingFilters: (NSArray *) filters
229                                toResponse: (WOResponse *) response
230                                 inContext: (WOContext *) context
231 {
232   NSArray *apts;
233   unsigned int count, max;
234   NSDictionary *currentFilter, *appointment;
235   NSEnumerator *appointments;
236   NSString *baseURL;
237
238   baseURL = [self baseURLInContext: context];
239
240   max = [filters count];
241   for (count = 0; count < max; count++)
242     {
243       currentFilter = [filters objectAtIndex: 0];
244       apts = [self fetchCoreInfosFrom: [currentFilter objectForKey: @"start"]
245                    to: [currentFilter objectForKey: @"end"]
246                    component: [currentFilter objectForKey: @"name"]];
247       appointments = [apts objectEnumerator];
248       appointment = [appointments nextObject];
249       while (appointment)
250         {
251           [self appendObject: appointment
252                 withBaseURL: baseURL
253                 toREPORTResponse: response];
254           appointment = [appointments nextObject];
255         }
256     }
257 }
258
259 - (id) davCalendarQuery: (id) context
260 {
261   WOResponse *r;
262   NSArray *filters;
263   id <DOMDocument> document;
264
265   r = [context response];
266   [r setStatus: 207];
267   [r setContentEncoding: NSUTF8StringEncoding];
268   [r setHeader: @"text/xml; charset=\"utf-8\"" forKey: @"content-type"];
269   [r setHeader: @"no-cache" forKey: @"pragma"];
270   [r setHeader: @"no-cache" forKey: @"cache-control"];
271   [r appendContentString:@"<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n"];
272   [r appendContentString: @"<D:multistatus xmlns:D=\"DAV:\""
273      @" xmlns:C=\"urn:ietf:params:xml:ns:caldav\">\r\n"];
274
275   document = [[context request] contentAsDOMDocument];
276   filters = [self _parseCalendarFilters: [document documentElement]];
277   [self _appendComponentsMatchingFilters: filters
278         toResponse: r
279         inContext: context];
280   [r appendContentString:@"</D:multistatus>\r\n"];
281
282   return r;
283 }
284
285 - (Class) objectClassForContent: (NSString *) content
286 {
287   iCalCalendar *calendar;
288   NSArray *elements;
289   NSString *firstTag;
290   Class objectClass;
291
292   objectClass = Nil;
293
294   calendar = [iCalCalendar parseSingleFromSource: content];
295   if (calendar)
296     {
297       elements = [calendar allObjects];
298       if ([elements count])
299         {
300           firstTag = [[[elements objectAtIndex: 0] tag] uppercaseString];
301           if ([firstTag isEqualToString: @"VEVENT"])
302             objectClass = [SOGoAppointmentObject class];
303           else if ([firstTag isEqualToString: @"VTODO"])
304             objectClass = [SOGoTaskObject class];
305         }
306     }
307
308   return objectClass;
309 }
310
311 - (id) deduceObjectForName: (NSString *)_key
312                  inContext: (id)_ctx
313 {
314   WORequest *request;
315   NSString *method;
316   Class objectClass;
317   id obj;
318
319   request = [_ctx request];
320   method = [request method];
321   if ([method isEqualToString: @"PUT"])
322     objectClass = [self objectClassForContent: [request contentAsString]];
323   else
324     objectClass = [self objectClassForResourceNamed: _key];
325
326   if (objectClass)
327     obj = [objectClass objectWithName: _key inContainer: self];
328   else
329     obj = nil;
330
331   return obj;
332 }
333
334 - (BOOL) requestNamedIsHandledLater: (NSString *) name
335                           inContext: (WOContext *) context
336 {
337   return [name isEqualToString: @"OPTIONS"];
338 }
339
340 - (id) lookupName: (NSString *)_key
341         inContext: (id)_ctx
342           acquire: (BOOL)_flag
343 {
344   id obj;
345   NSString *url;
346   BOOL handledLater;
347
348   /* first check attributes directly bound to the application */
349   handledLater = [self requestNamedIsHandledLater: _key inContext: _ctx];
350   if (handledLater)
351     obj = nil;
352   else
353     {
354       obj = [super lookupName:_key inContext:_ctx acquire:NO];
355       if (!obj)
356         {
357           if ([_key hasPrefix: @"{urn:ietf:params:xml:ns:caldav}"])
358             obj
359               = [self lookupActionForCalDAVMethod: [_key substringFromIndex: 31]];
360           else if ([self isValidAppointmentName:_key])
361             {
362               url = [[[_ctx request] uri] urlWithoutParameters];
363               if ([url hasSuffix: @"AsTask"])
364                 obj = [SOGoTaskObject objectWithName: _key
365                                       inContainer: self];
366               else if ([url hasSuffix: @"AsAppointment"])
367                 obj = [SOGoAppointmentObject objectWithName: _key
368                                              inContainer: self];
369               else
370                 obj = [self deduceObjectForName: _key
371                             inContext: _ctx];
372             }
373         }
374       if (!obj)
375         obj = [NSException exceptionWithHTTPStatus:404 /* Not Found */];
376     }
377
378   return obj;
379 }
380
381 - (NSArray *) davComplianceClassesInContext: (id)_ctx
382 {
383   NSMutableArray *classes;
384   NSArray *primaryClasses;
385
386   classes = [NSMutableArray new];
387   [classes autorelease];
388
389   primaryClasses = [super davComplianceClassesInContext: _ctx];
390   if (primaryClasses)
391     [classes addObjectsFromArray: primaryClasses];
392   [classes addObject: @"access-control"];
393   [classes addObject: @"calendar-access"];
394
395   return classes;
396 }
397
398 - (NSString *) groupDavResourceType
399 {
400   return @"vevent-collection";
401 }
402
403 /* vevent UID handling */
404
405 - (NSString *) resourceNameForEventUID: (NSString *)_u
406                               inFolder: (GCSFolder *)_f
407 {
408   static NSArray *nameFields = nil;
409   EOQualifier *qualifier;
410   NSArray     *records;
411   
412   if (![_u isNotNull]) return nil;
413   if (_f == nil) {
414     [self errorWithFormat:@"(%s): missing folder for fetch!",
415             __PRETTY_FUNCTION__];
416     return nil;
417   }
418   
419   if (nameFields == nil)
420     nameFields = [[NSArray alloc] initWithObjects:@"c_name", nil];
421   
422   qualifier = [EOQualifier qualifierWithQualifierFormat:@"uid = %@", _u];
423   records   = [_f fetchFields: nameFields matchingQualifier: qualifier];
424   
425   if ([records count] == 1)
426     return [[records objectAtIndex:0] valueForKey:@"c_name"];
427   if ([records count] == 0)
428     return nil;
429   
430   [self errorWithFormat:
431           @"The storage contains more than file with the same UID!"];
432   return [[records objectAtIndex:0] valueForKey:@"c_name"];
433 }
434
435 - (NSString *) resourceNameForEventUID: (NSString *) _uid
436 {
437   /* caches UIDs */
438   GCSFolder *folder;
439   NSString  *rname;
440   
441   if (![_uid isNotNull])
442     return nil;
443   if ((rname = [self->uidToFilename objectForKey:_uid]) != nil)
444     return [rname isNotNull] ? rname : nil;
445   
446   if ((folder = [self ocsFolder]) == nil) {
447     [self errorWithFormat:@"(%s): missing folder for fetch!",
448       __PRETTY_FUNCTION__];
449     return nil;
450   }
451
452   if (self->uidToFilename == nil)
453     self->uidToFilename = [[NSMutableDictionary alloc] initWithCapacity:16];
454   
455   if ((rname = [self resourceNameForEventUID:_uid inFolder:folder]) == nil)
456     [self->uidToFilename setObject:[NSNull null] forKey:_uid];
457   else
458     [self->uidToFilename setObject:rname forKey:_uid];
459   
460   return rname;
461 }
462
463 - (Class) objectClassForResourceNamed: (NSString *) c_name
464 {
465   EOQualifier *qualifier;
466   NSArray *records;
467   NSString *component;
468   Class objectClass;
469
470   qualifier = [EOQualifier qualifierWithQualifierFormat:@"c_name = %@", c_name];
471   records = [[self ocsFolder] fetchFields: [NSArray arrayWithObject: @"component"]
472                               matchingQualifier: qualifier];
473
474   if ([records count])
475     {
476       component = [[records objectAtIndex:0] valueForKey: @"component"];
477       if ([component isEqualToString: @"vevent"])
478         objectClass = [SOGoAppointmentObject class];
479       else if ([component isEqualToString: @"vtodo"])
480         objectClass = [SOGoTaskObject class];
481       else
482         objectClass = Nil;
483     }
484   else
485     objectClass = Nil;
486   
487   return objectClass;
488 }
489
490 /* fetching */
491
492 - (NSMutableDictionary *) fixupRecord: (NSDictionary *) _record
493                            fetchRange: (NGCalendarDateRange *) _r
494 {
495   NSMutableDictionary *md;
496   id tmp;
497   
498   md = [[_record mutableCopy] autorelease];
499  
500   if ((tmp = [_record objectForKey:@"startdate"])) {
501     tmp = [[NSCalendarDate alloc] initWithTimeIntervalSince1970:
502           (NSTimeInterval)[tmp unsignedIntValue]];
503     [tmp setTimeZone: [self userTimeZone]];
504     if (tmp) [md setObject:tmp forKey:@"startDate"];
505     [tmp release];
506   }
507   else
508     [self logWithFormat:@"missing 'startdate' in record?"];
509
510   if ((tmp = [_record objectForKey:@"enddate"])) {
511     tmp = [[NSCalendarDate alloc] initWithTimeIntervalSince1970:
512           (NSTimeInterval)[tmp unsignedIntValue]];
513     [tmp setTimeZone: [self userTimeZone]];
514     if (tmp) [md setObject:tmp forKey:@"endDate"];
515     [tmp release];
516   }
517   else
518     [self logWithFormat:@"missing 'enddate' in record?"];
519
520   return md;
521 }
522
523 - (NSMutableDictionary *) fixupCycleRecord: (NSDictionary *) _record
524                                 cycleRange: (NGCalendarDateRange *) _r
525 {
526   NSMutableDictionary *md;
527   id tmp;
528   
529   md = [[_record mutableCopy] autorelease];
530   
531   /* cycle is in _r */
532   tmp = [_r startDate];
533   [tmp setTimeZone:[self userTimeZone]];
534   [md setObject:tmp forKey:@"startDate"];
535   tmp = [_r endDate];
536   [tmp setTimeZone:[self userTimeZone]];
537   [md setObject:tmp forKey:@"endDate"];
538   
539   return md;
540 }
541
542 - (void) _flattenCycleRecord: (NSDictionary *) _row
543                     forRange: (NGCalendarDateRange *) _r
544                    intoArray: (NSMutableArray *) _ma
545 {
546   NSMutableDictionary *row;
547   NSDictionary        *cycleinfo;
548   NSCalendarDate      *startDate, *endDate;
549   NGCalendarDateRange *fir;
550   NSArray             *rules, *exRules, *exDates, *ranges;
551   unsigned            i, count;
552
553   cycleinfo  = [[_row objectForKey:@"cycleinfo"] propertyList];
554   if (cycleinfo == nil) {
555     [self errorWithFormat:@"cyclic record doesn't have cycleinfo -> %@", _row];
556     return;
557   }
558
559   row = [self fixupRecord:_row fetchRange: _r];
560   [row removeObjectForKey:@"cycleinfo"];
561   [row setObject:sharedYes forKey:@"isRecurrentEvent"];
562
563   startDate = [row objectForKey:@"startDate"];
564   endDate   = [row objectForKey:@"endDate"];
565   fir       = [NGCalendarDateRange calendarDateRangeWithStartDate:startDate
566                                    endDate:endDate];
567   rules     = [cycleinfo objectForKey:@"rules"];
568   exRules   = [cycleinfo objectForKey:@"exRules"];
569   exDates   = [cycleinfo objectForKey:@"exDates"];
570
571   ranges = [iCalRecurrenceCalculator recurrenceRangesWithinCalendarDateRange:_r
572                                      firstInstanceCalendarDateRange:fir
573                                      recurrenceRules:rules
574                                      exceptionRules:exRules
575                                      exceptionDates:exDates];
576   count = [ranges count];
577   for (i = 0; i < count; i++) {
578     NGCalendarDateRange *rRange;
579     id fixedRow;
580     
581     rRange   = [ranges objectAtIndex:i];
582     fixedRow = [self fixupCycleRecord:row cycleRange:rRange];
583     if (fixedRow != nil) [_ma addObject:fixedRow];
584   }
585 }
586
587 - (NSArray *) fixupRecords: (NSArray *) _records
588                 fetchRange: (NGCalendarDateRange *) _r
589 {
590   // TODO: is the result supposed to be sorted by date?
591   NSMutableArray *ma;
592   unsigned i, count;
593
594   if (_records == nil) return nil;
595   if ((count = [_records count]) == 0)
596     return _records;
597   
598   ma = [NSMutableArray arrayWithCapacity:count];
599   for (i = 0; i < count; i++) {
600     id row; // TODO: what is the type of the record?
601     
602     row = [_records objectAtIndex:i];
603     row = [self fixupRecord:row fetchRange:_r];
604     if (row != nil) [ma addObject:row];
605   }
606   return ma;
607 }
608
609 - (NSArray *) fixupCyclicRecords: (NSArray *) _records
610                       fetchRange: (NGCalendarDateRange *) _r
611 {
612   // TODO: is the result supposed to be sorted by date?
613   NSMutableArray *ma;
614   unsigned i, count;
615   
616   if (_records == nil) return nil;
617   if ((count = [_records count]) == 0)
618     return _records;
619   
620   ma = [NSMutableArray arrayWithCapacity:count];
621   for (i = 0; i < count; i++) {
622     id row; // TODO: what is the type of the record?
623     
624     row = [_records objectAtIndex:i];
625     [self _flattenCycleRecord:row forRange:_r intoArray:ma];
626   }
627   return ma;
628 }
629
630 - (NSString *) _sqlStringForComponent: (id) _component
631 {
632   NSString *sqlString;
633   NSArray *components;
634
635   if (_component)
636     {
637       if ([_component isKindOfClass: [NSArray class]])
638         components = _component;
639       else
640         components = [NSArray arrayWithObject: _component];
641
642       sqlString
643         = [NSString stringWithFormat: @" AND (component = '%@')",
644                     [components componentsJoinedByString: @"' OR component = '"]];
645     }
646   else
647     sqlString = @"";
648
649   return sqlString;
650 }
651
652 - (NSString *) _sqlStringRangeFrom: (NSCalendarDate *) _startDate
653                                 to: (NSCalendarDate *) _endDate
654 {
655   unsigned int start, end;
656
657   start = (unsigned int) [_startDate timeIntervalSince1970];
658   end = (unsigned int) [_endDate timeIntervalSince1970];
659
660   return [NSString stringWithFormat:
661                      @" AND (startdate <= %d) AND (enddate >= %d)",
662                    end, start];
663 }
664
665 - (NSArray *) fetchFields: (NSArray *) _fields
666                fromFolder: (GCSFolder *) _folder
667                      from: (NSCalendarDate *) _startDate
668                        to: (NSCalendarDate *) _endDate 
669                 component: (id) _component
670 {
671   EOQualifier *qualifier;
672   NSMutableArray *fields, *ma = nil;
673   NSArray *records;
674   NSString *sql, *dateSqlString, *componentSqlString; /* , *owner; */
675   NGCalendarDateRange *r;
676
677   if (_folder == nil) {
678     [self errorWithFormat:@"(%s): missing folder for fetch!",
679             __PRETTY_FUNCTION__];
680     return nil;
681   }
682   
683   if (_startDate && _endDate)
684     {
685       r = [NGCalendarDateRange calendarDateRangeWithStartDate: _startDate
686                                endDate: _endDate];
687       dateSqlString = [self _sqlStringRangeFrom: _startDate to: _endDate];
688     }
689   else
690     {
691       r = nil;
692       dateSqlString = @"";
693     }
694
695   componentSqlString = [self _sqlStringForComponent: _component];
696
697   /* prepare mandatory fields */
698
699   fields = [NSMutableArray arrayWithArray: _fields];
700   [fields addObject: @"uid"];
701   [fields addObject: @"startdate"];
702   [fields addObject: @"enddate"];
703
704   if (logger)
705     [self debugWithFormat:@"should fetch (%@=>%@) ...", _startDate, _endDate];
706
707   sql = [NSString stringWithFormat: @"(iscycle = 0)%@%@",
708                   dateSqlString, componentSqlString];
709
710   /* fetch non-recurrent apts first */
711   qualifier = [EOQualifier qualifierWithQualifierFormat: sql];
712
713   records = [_folder fetchFields: fields matchingQualifier: qualifier];
714   if (records)
715     {
716       if (r)
717         records = [self fixupRecords: records fetchRange: r];
718       if (logger)
719         [self debugWithFormat: @"fetched %i records: %@",
720               [records count], records];
721       ma = [NSMutableArray arrayWithArray: records];
722     }
723
724   /* fetch recurrent apts now */
725   sql = [NSString stringWithFormat: @"(iscycle = 1)%@%@",
726                   dateSqlString, componentSqlString];
727   qualifier = [EOQualifier qualifierWithQualifierFormat: sql];
728
729   [fields addObject: @"cycleinfo"];
730
731   records = [_folder fetchFields: fields matchingQualifier: qualifier];
732   if (records)
733     {
734       if (logger)
735         [self debugWithFormat: @"fetched %i cyclic records: %@",
736               [records count], records];
737       if (r)
738         records = [self fixupCyclicRecords: records fetchRange: r];
739       if (!ma)
740         ma = [NSMutableArray arrayWithCapacity: [records count]];
741
742 //       owner = [self ownerInContext: nil];
743       [ma addObjectsFromArray: records];
744     }
745   else if (!ma)
746     {
747       [self errorWithFormat: @"(%s): fetch failed!", __PRETTY_FUNCTION__];
748       return nil;
749     }
750
751   /* NOTE: why do we sort here?
752      This probably belongs to UI but cannot be achieved as fast there as
753      we can do it here because we're operating on a mutable array -
754      having the apts sorted is never a bad idea, though
755   */
756   [ma sortUsingSelector: @selector (compareAptsAscending:)];
757   if (logger)
758     [self debugWithFormat:@"returning %i records", [ma count]];
759
760 //   [ma makeObjectsPerform: @selector (setObject:forKey:)
761 //       withObject: owner
762 //       withObject: @"owner"];
763
764   return ma;
765 }
766
767 /* override this in subclasses */
768 - (NSArray *) fetchFields: (NSArray *) _fields
769                      from: (NSCalendarDate *) _startDate
770                        to: (NSCalendarDate *) _endDate 
771                 component: (id) _component
772 {
773   GCSFolder *folder;
774   
775   if ((folder = [self ocsFolder]) == nil) {
776     [self errorWithFormat:@"(%s): missing folder for fetch!",
777       __PRETTY_FUNCTION__];
778     return nil;
779   }
780
781   return [self fetchFields: _fields fromFolder: folder
782                from: _startDate to: _endDate
783                component: _component];
784 }
785
786
787 - (NSArray *) fetchFreeBusyInfosFrom: (NSCalendarDate *) _startDate
788                                   to: (NSCalendarDate *) _endDate
789 {
790   static NSArray *infos = nil; // TODO: move to a plist file
791   
792   if (!infos)
793     infos = [[NSArray alloc] initWithObjects: @"partmails", @"partstates",
794                              @"isopaque", @"status", nil];
795
796   return [self fetchFields: infos from: _startDate to: _endDate
797                component: @"vevent"];
798 }
799
800 - (NSArray *) fetchCoreInfosFrom: (NSCalendarDate *) _startDate
801                               to: (NSCalendarDate *) _endDate
802                        component: (id) _component
803 {
804   static NSArray *infos = nil; // TODO: move to a plist file
805
806   if (!infos)
807     infos = [[NSArray alloc] initWithObjects:
808                                @"c_name", @"component",
809                              @"title", @"location", @"orgmail",
810                              @"status", @"ispublic",
811                              @"isallday", @"isopaque",
812                              @"participants", @"partmails",
813                              @"partstates", @"sequence", @"priority", nil];
814
815   return [self fetchFields: infos from: _startDate to: _endDate component: _component];
816 }
817
818 - (void) deleteEntriesWithIds: (NSArray *) ids
819 {
820   Class objectClass;
821   unsigned int count, max;
822   NSString *currentId, *currentUser;
823   WOContext *context;
824   id deleteObject;
825
826   context = [[WOApplication application] context];
827   currentUser = [[context activeUser] login];
828
829   max = [ids count];
830   for (count = 0; count < max; count++)
831     {
832       currentId = [ids objectAtIndex: count];
833       objectClass
834         = [self objectClassForResourceNamed: currentId];
835       deleteObject = [objectClass objectWithName: currentId
836                                   inContainer: self];
837       [deleteObject delete];
838       [deleteObject primaryDelete];
839     }
840 }
841
842 /* URL generation */
843
844 - (NSString *) baseURLForAptWithUID: (NSString *)_uid
845                           inContext: (id)_ctx
846 {
847   // TODO: who calls this?
848   NSString *url;
849   
850   if ([_uid length] == 0)
851     return nil;
852   
853   url = [self baseURLInContext:_ctx];
854   if (![url hasSuffix:@"/"])
855     url = [url stringByAppendingString:@"/"];
856   
857   // TODO: this should run a query to determine the uid!
858   return [url stringByAppendingString:_uid];
859 }
860
861 /* folder management */
862
863 - (id) lookupHomeFolderForUID: (NSString *) _uid
864                     inContext: (id)_ctx
865 {
866   // TODO: DUP to SOGoGroupFolder
867   NSException *error = nil;
868   NSArray     *path;
869   id          ctx, result;
870
871   if (![_uid isNotNull])
872     return nil;
873   
874   if (_ctx == nil) _ctx = [[WOApplication application] context];
875   
876   /* create subcontext, so that we don't destroy our environment */
877   
878   if ((ctx = [_ctx createSubContext]) == nil) {
879     [self errorWithFormat:@"could not create SOPE subcontext!"];
880     return nil;
881   }
882   
883   /* build path */
884   
885   path = _uid != nil ? [NSArray arrayWithObjects:&_uid count:1] : nil;
886   
887   /* traverse path */
888   
889   result = [[ctx application] traversePathArray:path inContext:ctx
890                               error:&error acquire:NO];
891   if (error != nil) {
892     [self errorWithFormat:@"folder lookup failed (uid=%@): %@",
893             _uid, error];
894     return nil;
895   }
896   
897   [self debugWithFormat:@"Note: got folder for uid %@ path %@: %@",
898           _uid, [path componentsJoinedByString:@"=>"], result];
899   return result;
900 }
901
902 - (SOGoAppointmentFolder *) lookupCalendarFolderForUID: (NSString *) uid
903 {
904   SOGoFolder *upperContainer;
905   SOGoUserFolder *userFolder;
906   SOGoAppointmentFolder *calendarFolder;
907
908   upperContainer = [[self container] container];
909   userFolder = [SOGoUserFolder objectWithName: uid
910                                inContainer: upperContainer];
911   calendarFolder = [SOGoAppointmentFolder objectWithName: @"Calendar"
912                                           inContainer: userFolder];
913   [calendarFolder
914     setOCSPath: [NSString stringWithFormat: @"/Users/%@/Calendar", uid]];
915   [calendarFolder setOwner: uid];
916
917   return calendarFolder;
918 }
919
920 - (NSArray *) lookupCalendarFoldersForUIDs: (NSArray *) _uids
921                                  inContext: (id)_ctx
922 {
923   /* Note: can return NSNull objects in the array! */
924   NSMutableArray *folders;
925   NSEnumerator *e;
926   NSString     *uid;
927   
928   if ([_uids count] == 0) return nil;
929   folders = [NSMutableArray arrayWithCapacity:16];
930   e = [_uids objectEnumerator];
931   while ((uid = [e nextObject])) {
932     id folder;
933     
934     folder = [self lookupCalendarFolderForUID: uid];
935     if (![folder isNotNull])
936       [self logWithFormat:@"Note: did not find folder for uid: '%@'", uid];
937     
938     /* Note: intentionally add 'null' folders to allow a mapping */
939     [folders addObject:folder ? folder : [NSNull null]];
940   }
941   return folders;
942 }
943
944 - (NSArray *) lookupFreeBusyObjectsForUIDs: (NSArray *) _uids
945                                  inContext: (id) _ctx
946 {
947   /* Note: can return NSNull objects in the array! */
948   NSMutableArray *objs;
949   NSEnumerator   *e;
950   NSString       *uid;
951   
952   if ([_uids count] == 0) return nil;
953   objs = [NSMutableArray arrayWithCapacity:16];
954   e    = [_uids objectEnumerator];
955   while ((uid = [e nextObject])) {
956     id obj;
957     
958     obj = [self lookupHomeFolderForUID:uid inContext:nil];
959     if ([obj isNotNull]) {
960       obj = [obj lookupName:@"freebusy.ifb" inContext:nil acquire:NO];
961       if ([obj isKindOfClass:[NSException class]])
962         obj = nil;
963     }
964     if (![obj isNotNull])
965       [self logWithFormat:@"Note: did not find freebusy.ifb for uid: '%@'", uid];
966     
967     /* Note: intentionally add 'null' folders to allow a mapping */
968     [objs addObject:obj ? obj : [NSNull null]];
969   }
970   return objs;
971 }
972
973 - (NSArray *) uidsFromICalPersons: (NSArray *) _persons
974 {
975   /* Note: can return NSNull objects in the array! */
976   NSMutableArray    *uids;
977   AgenorUserManager *um;
978   unsigned          i, count;
979   
980   if (_persons == nil)
981     return nil;
982
983   count = [_persons count];
984   uids  = [NSMutableArray arrayWithCapacity:count + 1];
985   um    = [AgenorUserManager sharedUserManager];
986   
987   for (i = 0; i < count; i++) {
988     iCalPerson *person;
989     NSString   *email;
990     NSString   *uid;
991     
992     person = [_persons objectAtIndex:i];
993     email  = [person rfc822Email];
994     if ([email isNotNull]) {
995       uid = [um getUIDForEmail:email];
996     }
997     else
998       uid = nil;
999     
1000     [uids addObject:(uid != nil ? uid : (id)[NSNull null])];
1001   }
1002   return uids;
1003 }
1004
1005 - (NSArray *)lookupCalendarFoldersForICalPerson: (NSArray *) _persons
1006                                       inContext: (id) _ctx
1007 {
1008   /* Note: can return NSNull objects in the array! */
1009   NSArray *uids;
1010
1011   if ((uids = [self uidsFromICalPersons:_persons]) == nil)
1012     return nil;
1013   
1014   return [self lookupCalendarFoldersForUIDs:uids inContext:_ctx];
1015 }
1016
1017 - (id) lookupGroupFolderForUIDs: (NSArray *) _uids
1018                       inContext: (id)_ctx
1019 {
1020   SOGoCustomGroupFolder *folder;
1021   
1022   if (_uids == nil)
1023     return nil;
1024
1025   folder = [[SOGoCustomGroupFolder alloc] initWithUIDs:_uids inContainer:self];
1026   return [folder autorelease];
1027 }
1028
1029 - (id) lookupGroupCalendarFolderForUIDs: (NSArray *) _uids
1030                               inContext: (id) _ctx
1031 {
1032   SOGoCustomGroupFolder *folder;
1033   
1034   if ((folder = [self lookupGroupFolderForUIDs:_uids inContext:_ctx]) == nil)
1035     return nil;
1036   
1037   folder = [folder lookupName:@"Calendar" inContext:_ctx acquire:NO];
1038   if (![folder isNotNull])
1039     return nil;
1040   if ([folder isKindOfClass:[NSException class]]) {
1041     [self debugWithFormat:@"Note: could not lookup 'Calendar' in folder: %@",
1042             folder];
1043     return nil;
1044   }
1045   
1046   return folder;
1047 }
1048
1049 /* bulk fetches */
1050
1051 - (NSArray *) fetchAllSOGoAppointments
1052 {
1053   /* 
1054      Note: very expensive method, do not use unless absolutely required.
1055            returns an array of SOGoAppointment objects.
1056            
1057      Note that we can leave out the filenames, supposed to be stored
1058      in the 'uid' field of the iCalendar object!
1059   */
1060   NSMutableArray *events;
1061   NSDictionary *files;
1062   NSEnumerator *contents;
1063   NSString     *content;
1064   
1065   /* fetch all raw contents */
1066   
1067   files = [self fetchContentStringsAndNamesOfAllObjects];
1068   if (![files isNotNull]) return nil;
1069   if ([files isKindOfClass:[NSException class]]) return (id)files;
1070   
1071   /* transform to SOGo appointments */
1072   
1073   events   = [NSMutableArray arrayWithCapacity:[files count]];
1074   contents = [files objectEnumerator];
1075   while ((content = [contents nextObject]) != nil)
1076     [events addObject: [iCalCalendar parseSingleFromSource: content]];
1077   
1078   return events;
1079 }
1080
1081 /* folder type */
1082
1083 - (NSString *) folderType
1084 {
1085   return @"Appointment";
1086 }
1087
1088 - (NSString *) outlookFolderClass
1089 {
1090   return @"IPF.Appointment";
1091 }
1092
1093 @end /* SOGoAppointmentFolder */