1 /* -*- Mode: javascript; tab-width: 20; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
2 /* ***** BEGIN LICENSE BLOCK *****
3 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
4 *
5 * The contents of this file are subject to the Mozilla Public License Version
6 * 1.1 (the "License"); you may not use this file except in compliance with
7 * the License. You may obtain a copy of the License at
8 * http://www.mozilla.org/MPL/
9 *
10 * Software distributed under the License is distributed on an "AS IS" basis,
11 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
12 * for the specific language governing rights and limitations under the
13 * License.
14 *
15 * The Original Code is lightning code.
16 *
17 * The Initial Developer of the Original Code is
18 * Oracle Corporation
19 * Portions created by the Initial Developer are Copyright (C) 2005
20 * the Initial Developer. All Rights Reserved.
21 *
22 * Contributor(s):
23 * Vladimir Vukicevic <vladimir.vukicevic@oracle.com>
24 * Matthew Willis <lilmatt@mozilla.com>
25 * Daniel Boelzle <daniel.boelzle@sun.com>
26 *
27 * Alternatively, the contents of this file may be used under the terms of
28 * either the GNU General Public License Version 2 or later (the "GPL"), or
29 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
30 * in which case the provisions of the GPL or the LGPL are applicable instead
31 * of those above. If you wish to allow use of your version of this file only
32 * under the terms of either the GPL or the LGPL, and not to allow others to
33 * use your version of this file under the terms of the MPL, indicate your
34 * decision by deleting the provisions above and replace them with the notice
35 * and other provisions required by the GPL or the LGPL. If you do not delete
36 * the provisions above, a recipient may use your version of this file under
37 * the terms of any one of the MPL, the GPL or the LGPL.
38 *
39 * ***** END LICENSE BLOCK ***** */
40
calRecurrenceInfo
41 function calRecurrenceInfo() {
42 this.mRecurrenceItems = new Array();
43 this.mExceptions = new Array();
44 }
45
calDebug
46 function calDebug() {
47 dump.apply(null, arguments);
48 }
49
50 var calRecurrenceInfoClassInfo = {
getInterfaces
51 getInterfaces: function (count) {
52 var ifaces = [
53 Components.interfaces.nsISupports,
54 Components.interfaces.calIRecurrenceInfo,
55 Components.interfaces.nsIClassInfo
56 ];
57 count.value = ifaces.length;
58 return ifaces;
59 },
60
getHelperForLanguage
61 getHelperForLanguage: function (language) {
62 return null;
63 },
64
65 contractID: "@mozilla.org/calendar/recurrence-info;1",
66 classDescription: "Calendar Recurrence Info",
67 classID: Components.ID("{04027036-5884-4a30-b4af-f2cad79f6edf}"),
68 implementationLanguage: Components.interfaces.nsIProgrammingLanguage.JAVASCRIPT,
69 flags: 0
70 };
71
72 calRecurrenceInfo.prototype = {
73 // QI with CI
QueryInterface
74 QueryInterface: function(aIID) {
75 if (aIID.equals(Components.interfaces.nsISupports) ||
76 aIID.equals(Components.interfaces.calIRecurrenceInfo))
77 return this;
78
79 if (aIID.equals(Components.interfaces.nsIClassInfo))
80 return calRecurrenceInfoClassInfo;
81
82 throw Components.results.NS_ERROR_NO_INTERFACE;
83 },
84
85 //
86 // Mutability bits
87 //
88 mImmutable: false,
get_isMutable
89 get isMutable() { return !this.mImmutable; },
makeImmutable
90 makeImmutable: function() {
91 if (this.mImmutable)
92 return;
93
94 for each (ritem in this.mRecurrenceItems) {
95 if (ritem.isMutable)
96 ritem.makeImmutable();
97 }
98
99 for each (ex in this.mExceptions) {
100 if (ex.item.isMutable)
101 ex.item.makeImmutable();
102 }
103
104 this.mImmutable = true;
105 },
106
clone
107 clone: function() {
108 var cloned = new calRecurrenceInfo();
109 cloned.mBaseItem = this.mBaseItem;
110
111 var clonedItems = [];
112 for each (ritem in this.mRecurrenceItems)
113 clonedItems.push(ritem.clone());
114 cloned.mRecurrenceItems = clonedItems;
115
116 var clonedExceptions = [];
117 for each (exitem in this.mExceptions) {
118 var c = exitem.item.cloneShallow(this.mBaseItem);
119 clonedExceptions.push( { id: exitem.id, item: c } );
120 }
121 cloned.mExceptions = clonedExceptions;
122
123 return cloned;
124 },
125
126 //
127 // calIRecurrenceInfo impl
128 //
129 mBaseItem: null,
130
get_item
131 get item() {
132 return this.mBaseItem;
133 },
134
set_item
135 set item(value) {
136 if (this.mImmutable)
137 throw Components.results.NS_ERROR_OBJECT_IS_IMMUTABLE;
138
139 this.mBaseItem = value;
140 // patch exception's parentItem:
141 for each (exitem in this.mExceptions) {
142 exitem.item.parentItem = value;
143 }
144 },
145
146 mRecurrenceItems: null,
147
get_isFinite
148 get isFinite() {
149 if (!this.mBaseItem)
150 throw Components.results.NS_ERROR_NOT_INITIALIZED;
151
152 for each (ritem in this.mRecurrenceItems) {
153 if (!ritem.isFinite)
154 return false;
155 }
156
157 return true;
158 },
159
getRecurrenceItems
160 getRecurrenceItems: function(aCount) {
161 if (!this.mBaseItem)
162 throw Components.results.NS_ERROR_NOT_INITIALIZED;
163
164 aCount.value = this.mRecurrenceItems.length;
165 return this.mRecurrenceItems;
166 },
167
setRecurrenceItems
168 setRecurrenceItems: function(aCount, aItems) {
169 if (!this.mBaseItem)
170 throw Components.results.NS_ERROR_NOT_INITIALIZED;
171
172 if (this.mImmutable)
173 throw Components.results.NS_ERROR_OBJECT_IS_IMMUTABLE;
174
175 // should we clone these?
176 this.mRecurrenceItems = aItems;
177 },
178
countRecurrenceItems
179 countRecurrenceItems: function() {
180 if (!this.mBaseItem)
181 throw Components.results.NS_ERROR_NOT_INITIALIZED;
182
183 return this.mRecurrenceItems.length;
184 },
185
getRecurrenceItemAt
186 getRecurrenceItemAt: function(aIndex) {
187 if (!this.mBaseItem)
188 throw Components.results.NS_ERROR_NOT_INITIALIZED;
189
190 if (aIndex < 0 || aIndex >= this.mRecurrenceItems.length)
191 throw Components.results.NS_ERROR_INVALID_ARG;
192
193 return this.mRecurrenceItems[aIndex];
194 },
195
appendRecurrenceItem
196 appendRecurrenceItem: function(aItem) {
197 if (!this.mBaseItem)
198 throw Components.results.NS_ERROR_NOT_INITIALIZED;
199
200 if (this.mImmutable)
201 throw Components.results.NS_ERROR_OBJECT_IS_IMMUTABLE;
202
203 this.mRecurrenceItems.push(aItem);
204 },
205
deleteRecurrenceItemAt
206 deleteRecurrenceItemAt: function(aIndex) {
207 if (!this.mBaseItem)
208 throw Components.results.NS_ERROR_NOT_INITIALIZED;
209
210 if (this.mImmutable)
211 throw Components.results.NS_ERROR_OBJECT_IS_IMMUTABLE;
212
213 if (aIndex < 0 || aIndex >= this.mRecurrenceItems.length)
214 throw Components.results.NS_ERROR_INVALID_ARG;
215
216 this.mRecurrenceItems.splice(aIndex, 1);
217 },
218
deleteRecurrenceItem
219 deleteRecurrenceItem: function(aItem) {
220 if (!this.mBaseItem)
221 throw Components.results.NS_ERROR_NOT_INITIALIZED;
222
223 if (this.mImmutable)
224 throw Components.results.NS_ERROR_OBJECT_IS_IMMUTABLE;
225
226 // Because xpcom objects can be wrapped in various ways, testing for
227 // mere == sometimes returns false even when it should be true. Use
228 // the interface pointer returned by sip to avoid that problem.
229 var sip1 = Components.classes["@mozilla.org/supports-interface-pointer;1"]
230 .createInstance(Components.interfaces.nsISupportsInterfacePointer);
231 sip1.data = aItem;
232 sip1.dataIID = Components.interfaces.calIRecurrenceItem;
233 for (var i = 0; i < this.mRecurrenceItems.length; i++) {
234 if (this.mRecurrenceItems[i] == sip1.data) {
235 this.deleteRecurrenceItemAt(i);
236 return;
237 }
238 }
239
240 throw Components.results.NS_ERROR_INVALID_ARG;
241 },
242
insertRecurrenceItemAt
243 insertRecurrenceItemAt: function(aItem, aIndex) {
244 if (!this.mBaseItem)
245 throw Components.results.NS_ERROR_NOT_INITIALIZED;
246
247 if (this.mImmutable)
248 throw Components.results.NS_ERROR_OBJECT_IS_IMMUTABLE;
249
250 if (aIndex < 0 || aIndex > this.mRecurrenceItems.length)
251 throw Components.results.NS_ERROR_INVALID_ARG;
252
253 this.mRecurrenceItems.splice(aIndex, 0, aItem);
254 },
255
clearRecurrenceItems
256 clearRecurrenceItems: function() {
257 if (!this.mBaseItem)
258 throw Components.results.NS_ERROR_NOT_INITIALIZED;
259
260 if (this.mImmutable)
261 throw Components.results.NS_ERROR_OBJECT_IS_IMMUTABLE;
262
263 this.mRecurrenceItems = new Array();
264 },
265
266 //
267 // calculations
268 //
269
getNextOccurrenceDate
270 getNextOccurrenceDate: function (aTime) {
271 if (!this.mBaseItem)
272 throw Components.results.NS_ERROR_NOT_INITIALIZED;
273
274 var startDate = this.mBaseItem.recurrenceStartDate;
275 var dates = [];
276
277 for each (ritem in this.mRecurrenceItems) {
278 var date = ritem.getNextOccurrence(startDate, aTime);
279 if (!date)
280 continue;
281
282 if (ritem.isNegative)
283 dates = dates.filter(function (d) { return (d.compare(date) != 0); });
284 else
285 dates.push(date);
286 }
287
288 // if no dates, there's no next
289 if (dates.length == 0)
290 return null;
291
292 // find the earliest date
293 var earliestDate = dates[0];
294 dates.forEach(function (d) { if (d.compare(earliestDate) < 0) earliestDate = d; });
295
296 return earliestDate;
297 },
298
getNextOccurrence
299 getNextOccurrence: function (aTime) {
300 var earliestDate = this.getNextOccurrenceDate (aTime);
301 if (!earliestDate)
302 return null;
303
304 if (this.mExceptions) {
305 // scan exceptions for any dates earlier than
306 // earliestDate (but still after aTime)
307 this.mExceptions.forEach (function (ex) {
308 var dtstart = ex.item.getProperty("DTSTART");
309 if (aTime.compare(dtstart) <= 0 &&
310 earliestDate.compare(dtstart) > 0)
311 {
312 earliestDate = dtstart;
313 }
314 });
315 }
316
317 var startDate = earliestDate.clone();
318 var endDate = null;
319
320 if (this.mBaseItem.hasProperty("DTEND")) {
321 endDate = earliestDate.clone();
322 endDate.addDuration(this.mBaseItem.duration);
323 }
324
325 var proxy = this.mBaseItem.createProxy();
326 proxy.recurrenceId = earliestDate.clone();
327
328 proxy.setProperty("DTSTART", startDate);
329 if (endDate)
330 proxy.setProperty("DTEND", endDate);
331
332 return proxy;
333 },
334
335 // internal helper function;
calculateDates
336 calculateDates: function (aRangeStart, aRangeEnd,
337 aMaxCount, aReturnRIDs)
338 {
339 if (!this.mBaseItem)
340 throw Components.results.NS_ERROR_NOT_INITIALIZED;
341
342 // workaround for UTC- timezones
343 var rangeStart = ensureDateTime(aRangeStart);
344 var rangeEnd = ensureDateTime(aRangeEnd);
345
346 // If aRangeStart falls in the middle of an occurrence, libical will
347 // not return that occurrence when we go and ask for an
348 // icalrecur_iterator_new. This actually seems fairly rational, so
349 // instead of hacking libical, I'm going to move aRangeStart back far
350 // enough to make sure we get the occurrences we might miss.
351 var searchStart = rangeStart.clone();
352 var baseDuration = null;
353 try {
354 baseDuration = this.mBaseItem.duration;
355 var duration = baseDuration.clone();
356 duration.isNegative = true;
357 searchStart.addDuration(duration);
358 } catch(ex) {
359 dump("recurrence tweaking exception:"+ex+'\n');
360 }
361
362 var startDate = this.mBaseItem.recurrenceStartDate;
363 var dates = [];
364
365 // DTSTART/DUE is always part of the (positive) expanded set:
366 // the base item cannot be replaced by an exception;
367 // an exception can only be defined on an item resulting from an RDATE/RRULE;
368 // DTSTART always equals RECURRENCE-ID for items expanded from RRULE
369 var baseOccDate = checkIfInRange(this.mBaseItem, aRangeStart, aRangeEnd, true);
370 if (baseOccDate) {
371 dates.push(baseOccDate);
372 }
373
374 // toss in exceptions first:
375 if (this.mExceptions) {
376 this.mExceptions.forEach(
377 function(ex) {
378 var occDate = checkIfInRange(ex.item, aRangeStart, aRangeEnd, true);
379 if (occDate) {
380 dates.push(aReturnRIDs ? ex.id : occDate);
381 }
382 });
383 }
384
385 // if both range start and end are specified, we ask for all of the occurrences,
386 // to make sure we catch all possible exceptions. If aRangeEnd isn't specified,
387 // then we have to ask for aMaxCount, and hope for the best.
388 var maxCount;
389 if (rangeStart && rangeEnd) {
390 maxCount = 0;
391 } else {
392 maxCount = aMaxCount;
393 }
394
395 // apply positive items before negative:
396 var sortedRecurrenceItems = [];
397 for each ( var ritem in this.mRecurrenceItems ) {
398 if (ritem.isNegative)
399 sortedRecurrenceItems.push(ritem);
400 else
401 sortedRecurrenceItems.unshift(ritem);
402 }
403 for each (ritem in sortedRecurrenceItems) {
404 var cur_dates;
405
406 cur_dates = ritem.getOccurrences(startDate,
407 searchStart,
408 rangeEnd,
409 maxCount, {});
410
411 if (cur_dates.length == 0)
412 continue;
413
414 if (ritem.isNegative) {
415 // if this is negative, we look for any of the given dates
416 // in the existing set, and remove them if they're
417 // present.
418
419 // XXX: i'm pretty sure negative dates can't really have exceptions
420 // (like, you can't make a date "real" by defining an RECURRENCE-ID which
421 // is an EXDATE, and then giving it a real DTSTART) -- so we don't
422 // check exceptions here
423 cur_dates.forEach (function (dateToRemove) {
424 dates = dates.filter(function (d) { return d.compare(dateToRemove) != 0; });
425 });
426 } else {
427 // if positive, we just add these date to the existing set,
428 // but only if they're not already there
429
430 var index = 0;
431 const len = cur_dates.length;
432
433 // skip items before rangeStart due to searchStart libical hack:
434 if (rangeStart && baseDuration) {
435 for (; index < len; ++index) {
436 var date = cur_dates[index].clone();
437 date.addDuration(baseDuration);
438 if (rangeStart.compare(date) < 0) {
439 break;
440 }
441 }
442 }
443 for (; index < len; ++index) {
444 var dateToAdd = cur_dates[index];
445 if (!dates.some(function (d) { return d.compare(dateToAdd) == 0; })) {
446 dates.push(dateToAdd);
447 }
448 }
449 }
450 }
451
452 // now sort the list
453 dates.sort(function (a,b) { return a.compare(b); });
454
455 // chop anything over aMaxCount, if specified
456 if (aMaxCount && dates.length > aMaxCount)
457 dates = dates.splice(aMaxCount, dates.length - aMaxCount);
458
459 return dates;
460 },
461
getOccurrenceDates
462 getOccurrenceDates: function (aRangeStart, aRangeEnd,
463 aMaxCount, aCount)
464 {
465 var dates = this.calculateDates(aRangeStart, aRangeEnd, aMaxCount, false);
466 aCount.value = dates.length;
467 return dates;
468 },
469
getOccurrences
470 getOccurrences: function (aRangeStart, aRangeEnd,
471 aMaxCount,
472 aCount)
473 {
474 var dates = this.calculateDates(aRangeStart, aRangeEnd, aMaxCount, true);
475 if (dates.length == 0) {
476 aCount.value = 0;
477 return [];
478 }
479
480 var count = aMaxCount;
481 if (!count)
482 count = dates.length;
483
484 var results = [];
485
486 for (var i = 0; i < count; i++) {
487 var proxy = this.getOccurrenceFor(dates[i]);
488 results.push(proxy);
489 }
490
491 aCount.value = results.length;
492 return results;
493 },
494
getOccurrenceFor
495 getOccurrenceFor: function (aRecurrenceId) {
496 var proxy = this.getExceptionFor(aRecurrenceId, false);
497 if (!proxy) {
498 var duration = null;
499
500 var name = "DTEND";
501 if (this.mBaseItem instanceof Components.interfaces.calITodo)
502 name = "DUE";
503
504 if (this.mBaseItem.hasProperty(name))
505 duration = this.mBaseItem.duration;
506
507 proxy = this.mBaseItem.createProxy();
508 proxy.recurrenceId = aRecurrenceId;
509 proxy.setProperty("DTSTART", aRecurrenceId.clone());
510 if (duration) {
511 var enddate = aRecurrenceId.clone();
512 enddate.addDuration(duration);
513 proxy.setProperty(name, enddate);
514 }
515 if (!this.mBaseItem.isMutable)
516 proxy.makeImmutable();
517 }
518 return proxy;
519 },
520
removeOccurrenceAt
521 removeOccurrenceAt: function (aRecurrenceId) {
522 if (!this.mBaseItem)
523 throw Components.results.NS_ERROR_NOT_INITIALIZED;
524
525 if (this.mImmutable)
526 throw Components.results.NS_ERROR_OBJECT_IS_IMMUTABLE;
527
528 var d = Components.classes["@mozilla.org/calendar/recurrence-date;1"].createInstance(Components.interfaces.calIRecurrenceDate);
529 d.isNegative = true;
530 d.date = aRecurrenceId.clone();
531
532 this.removeExceptionFor(d.date);
533
534 this.appendRecurrenceItem(d);
535 },
536
restoreOccurrenceAt
537 restoreOccurrenceAt: function (aRecurrenceId) {
538 if (!this.mBaseItem)
539 throw Components.results.NS_ERROR_NOT_INITIALIZED;
540
541 if (this.mImmutable)
542 throw Components.results.NS_ERROR_OBJECT_IS_IMMUTABLE;
543
544 for (var i = 0; i < this.mRecurrenceItems.length; i++) {
545 if (this.mRecurrenceItems[i] instanceof Components.interfaces.calIRecurrenceDate) {
546 var rd = this.mRecurrenceItems[i].QueryInterface(Components.interfaces.calIRecurrenceDate);
547 if (rd.isNegative && rd.date.compare(aRecurrenceId) == 0) {
548 return this.deleteRecurrenceItemAt(i);
549 }
550 }
551 }
552
553 throw Components.results.NS_ERROR_INVALID_ARG;
554 },
555
556 //
557 // exceptions
558 //
559
560 //
561 // Some notes:
562 //
563 // The way I read ICAL, RECURRENCE-ID is used to specify a
564 // particular instance of a recurring event, according to the
565 // RRULEs/RDATEs/etc. specified in the base event. If one of
566 // these is to be changed ("an exception"), then it can be
567 // referenced via the UID of the original event, and a
568 // RECURRENCE-ID of the start time of the instance to change.
569 // This, to me, means that an event where one of the instances has
570 // changed to a different time has a RECURRENCE-ID of the original
571 // start time, and a DTSTART/DTEND representing the new time.
572 //
573 // ITIP, however, seems to want something different -- you're
574 // supposed to use UID/RECURRENCE-ID to select from the current
575 // set of occurrences of an event. If you change the DTSTART for
576 // an instance, you're supposed to use the old (original) DTSTART
577 // as the RECURRENCE-ID, and put the new time as the DTSTART.
578 // However, after that change, to refer to that instance in the
579 // future, you have to use the modified DTSTART as the
580 // RECURRENCE-ID. This madness is described in ITIP end of
581 // section 3.7.1.
582 //
583 // This implementation does the first approach (RECURRENCE-ID will
584 // never change even if DTSTART for that instance changes), which
585 // I think is the right thing to do for CalDAV; I don't know what
586 // we'll do for incoming ITIP events though.
587 //
588
589 mExceptions: null,
590
modifyException
591 modifyException: function (anItem) {
592 if (!this.mBaseItem)
593 throw Components.results.NS_ERROR_NOT_INITIALIZED;
594
595 // the item must be an occurrence
596 if (anItem.parentItem == anItem)
597 throw Components.results.NS_ERROR_UNEXPECTED;
598
599 if (anItem.parentItem.calendar != this.mBaseItem.calendar &&
600 anItem.parentItem.id != this.mBaseItem.id)
601 {
602 calDebug ("recurrenceInfo::addException: item parentItem != this.mBaseItem (calendar/id)!\n");
603 throw Components.results.NS_ERROR_INVALID_ARG;
604 }
605
606 if (anItem.recurrenceId == null) {
607 calDebug ("recurrenceInfo::addException: item with null recurrenceId!\n");
608 throw Components.results.NS_ERROR_INVALID_ARG;
609 }
610
611 var itemtoadd;
612 if (anItem.isMutable) {
613 itemtoadd = anItem.cloneShallow(this.mBaseItem);
614 itemtoadd.makeImmutable();
615 } else {
616 itemtoadd = anItem;
617 }
618
619 // we're going to assume that the recurrenceId is valid here,
620 // because presumably the item came from one of our functions
621
622 // remove any old one, if present
623 this.removeExceptionFor(anItem.recurrenceId);
624
625 this.mExceptions.push( { id: itemtoadd.recurrenceId, item: itemtoadd } );
626 },
627
createExceptionFor
628 createExceptionFor: function (aRecurrenceId) {
629 if (!this.mBaseItem)
630 throw Components.results.NS_ERROR_NOT_INITIALIZED;
631
632 // XX should it be an error to createExceptionFor
633 // an already-existing recurrenceId?
634 var existing = this.getExceptionFor(aRecurrenceId, false);
635 if (existing)
636 return existing;
637
638 // check if aRecurrenceId is valid.
639
640 // this is a bit of a hack; we know that ranges are defined as [start, end),
641 // so we do a search on aRecurrenceId and aRecurrenceId.seconds + 1.
642 var rangeStart = aRecurrenceId;
643 var rangeEnd = aRecurrenceId.clone();
644 rangeEnd.second += 1;
645
646 var dates = this.getOccurrenceDates (rangeStart, rangeEnd, 1, {});
647 var found = false;
648 for each (d in dates) {
649 if (d.compare(aRecurrenceId) == 0) {
650 found = true;
651 break;
652 }
653 }
654
655 // not found; the recurrence id is invalid
656 if (!found)
657 throw Components.results.NS_ERROR_INVALID_ARG;
658
659 var rid = aRecurrenceId.clone();
660 rid.makeImmutable();
661
662 var newex = this.mBaseItem.createProxy();
663 newex.recurrenceId = rid;
664
665 this.mExceptions.push({id: rid, item: newex});
666
667 return newex;
668 },
669
getExceptionFor
670 getExceptionFor: function (aRecurrenceId, aCreate) {
671 if (!this.mBaseItem)
672 throw Components.results.NS_ERROR_NOT_INITIALIZED;
673
674 for each (ex in this.mExceptions) {
675 if (ex.id.compare(aRecurrenceId) == 0)
676 return ex.item;
677 }
678
679 if (aCreate) {
680 return this.createExceptionFor(aRecurrenceId);
681 }
682 return null;
683 },
684
removeExceptionFor
685 removeExceptionFor: function (aRecurrenceId) {
686 if (!this.mBaseItem)
687 throw Components.results.NS_ERROR_NOT_INITIALIZED;
688
689 this.mExceptions = this.mExceptions.filter (function(ex) {
690 return (ex.id.compare(aRecurrenceId) != 0);
691 });
692 },
693
getExceptionIds
694 getExceptionIds: function (aCount) {
695 if (!this.mBaseItem)
696 throw Components.results.NS_ERROR_NOT_INITIALIZED;
697
698 var ids = this.mExceptions.map (function(ex) {
699 return ex.id;
700 });
701
702 aCount.value = ids.length;
703 return ids;
704 },
705
706 // changing the startdate of an item needs to take exceptions into account.
707 // in case we're about to modify a parentItem (aka 'folded' item), we need
708 // to modify the recurrenceId's of all possibly existing exceptions as well.
onStartDateChange
709 onStartDateChange: function (aNewStartTime, aOldStartTime) {
710
711 // passing null for the new starttime would indicate an error condition,
712 // since having a recurrence without a starttime is invalid.
713 if (!aNewStartTime) {
714 throw Components.results.NS_ERROR_INVALID_ARG;
715 }
716
717 // no need to check for changes if there's no previous starttime.
718 if (!aOldStartTime) {
719 return;
720 }
721
722 // convert both dates to UTC since subtractDate is not timezone aware.
723 aOldStartTime = aOldStartTime.getInTimezone(UTC());
724 aNewStartTime = aNewStartTime.getInTimezone(UTC());
725 var timeDiff = aNewStartTime.subtractDate(aOldStartTime);
726 var exceptions = this.getExceptionIds({});
727 var modifiedExceptions = [];
728 for each (var exid in exceptions) {
729 var ex = this.getExceptionFor(exid, false);
730 if (ex) {
731 if (!ex.isMutable) {
732 ex = ex.cloneShallow(this.item);
733 }
734 ex.recurrenceId.addDuration(timeDiff);
735
736 modifiedExceptions.push(ex);
737 this.removeExceptionFor(exid);
738 }
739 }
740 for each (var modifiedEx in modifiedExceptions) {
741 this.modifyException(modifiedEx);
742 }
743
744 // also take RDATE's and EXDATE's into account.
745 const kCalIRecurrenceDate = Components.interfaces.calIRecurrenceDate;
746 const kCalIRecurrenceDateSet = Components.interfaces.calIRecurrenceDateSet;
747 var ritems = this.getRecurrenceItems({});
748 for (var i in ritems) {
749 var ritem = ritems[i];
750 if (ritem instanceof kCalIRecurrenceDate) {
751 ritem = ritem.QueryInterface(kCalIRecurrenceDate);
752 ritem.date.addDuration(timeDiff);
753 } else if (ritem instanceof kCalIRecurrenceDateSet) {
754 ritem = ritem.QueryInterface(kCalIRecurrenceDateSet);
755 var rdates = ritem.getDates({});
756 for each (var date in rdates) {
757 date.addDuration(timeDiff);
758 }
759 ritem.setDates(rdates.length,rdates);
760 }
761 }
762 }
763 };