home *** CD-ROM | disk | FTP | other *** search
/ Personal Computer World 2007 September / PCWSEP07.iso / Software / Linux / Linux Mint 3.0 Light / LinuxMint-3.0-Light.iso / casper / filesystem.squashfs / usr / lib / sunbird / js / calRecurrenceInfo.js < prev    next >
Encoding:
JavaScript  |  2007-05-23  |  27.6 KB  |  778 lines

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