home *** CD-ROM | disk | FTP | other *** search
/ PC Format (PL) 2008 February / PC_Format_022008.iso / Internet / Mozilla Thunderbird wtyczki / lightning-0.7-tb-win.xpi / js / calRecurrenceInfo.js < prev    next >
Encoding:
JavaScript  |  2007-08-30  |  26.1 KB  |  757 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, 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.         // DTSTART/DUE is always part of the (positive) expanded set:
  367.         // the base item cannot be replaced by an exception;
  368.         // an exception can only be defined on an item resulting from an RDATE/RRULE;
  369.         // DTSTART always equals RECURRENCE-ID for items expanded from RRULE
  370.         var baseOccDate = checkIfInRange(this.mBaseItem, aRangeStart, aRangeEnd);
  371.         if (baseOccDate) {
  372.             dates.push(baseOccDate);
  373.         }
  374.  
  375.         // toss in exceptions first:
  376.         if (this.mExceptions) {
  377.             this.mExceptions.forEach(
  378.                 function(ex) {
  379.                     var occDate = checkIfInRange(ex.item, aRangeStart, aRangeEnd);
  380.                     if (occDate) {
  381.                         dates.push(aReturnRIDs ? ex.id : occDate);
  382.                     }
  383.                 });
  384.         }
  385.  
  386.         // apply positive items before negative:
  387.         var sortedRecurrenceItems = [];
  388.         for each ( var ritem in this.mRecurrenceItems ) {
  389.             if (ritem.isNegative)
  390.                 sortedRecurrenceItems.push(ritem);
  391.             else
  392.                 sortedRecurrenceItems.unshift(ritem);
  393.         }
  394.         for each (ritem in sortedRecurrenceItems) {
  395.             var cur_dates;
  396.  
  397.             // if both range start and end are specified, we ask for all of the occurrences,
  398.             // to make sure we catch all possible exceptions.  If aRangeEnd isn't specified,
  399.             // then we have to ask for aMaxCount, and hope for the best.
  400.             var maxCount;
  401.             if (aRangeStart && aRangeEnd) {
  402.                 maxCount = 0;
  403.             } else {
  404.                 maxCount = aMaxCount;
  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.                 // XXX todo: IMO a bug here,
  428.                 //           if we are asked for DTSTART dates
  429.                 //           (aReturnRIDs is false => getOccurrenceDates),
  430.                 //           then pumping in the plain expanded rule dates is wrong,
  431.                 //           we need to take the "exception'ed" DTSTART dates
  432.  
  433.                 // if positive, we just add these date to the existing set,
  434.                 // but only if they're not already there
  435.                 var datesToAdd = [];
  436.                 var rinfo = this;
  437.                 cur_dates.forEach (function (dateToAdd) {
  438.                                        if (!dates.some(function (d) { return d.compare(dateToAdd) == 0; })) {
  439.                                            dates.push(dateToAdd);
  440.                                        }
  441.                                    });
  442.             }
  443.         }
  444.  
  445.         // now sort the list
  446.         dates.sort(function (a,b) { return a.compare(b); });
  447.  
  448.         // chop anything over aMaxCount, if specified
  449.         if (aMaxCount && dates.length > aMaxCount)
  450.             dates = dates.splice(aMaxCount, dates.length - aMaxCount);
  451.  
  452.         return dates;
  453.     },
  454.  
  455.     getOccurrenceDates: function (aRangeStart, aRangeEnd,
  456.                                   aMaxCount, aCount)
  457.     {
  458.         var dates = this.calculateDates(aRangeStart, aRangeEnd, aMaxCount, false);
  459.         aCount.value = dates.length;
  460.         return dates;
  461.     },
  462.  
  463.     getOccurrences: function (aRangeStart, aRangeEnd,
  464.                               aMaxCount,
  465.                               aCount)
  466.     {
  467.         var dates = this.calculateDates(aRangeStart, aRangeEnd, aMaxCount, true);
  468.         if (dates.length == 0) {
  469.             aCount.value = 0;
  470.             return [];
  471.         }
  472.  
  473.         var count = aMaxCount;
  474.         if (!count)
  475.             count = dates.length;
  476.  
  477.         var results = [];
  478.  
  479.         for (var i = 0; i < count; i++) {
  480.             var proxy = this.getOccurrenceFor(dates[i]);
  481.             results.push(proxy);
  482.         }
  483.  
  484.         aCount.value = results.length;
  485.         return results;
  486.     },
  487.  
  488.     getOccurrenceFor: function (aRecurrenceId) {
  489.         var proxy = this.getExceptionFor(aRecurrenceId, false);
  490.         if (!proxy) {
  491.             var duration = null;
  492.             
  493.             var name = "DTEND";
  494.             if (this.mBaseItem instanceof Components.interfaces.calITodo)
  495.                 name = "DUE";
  496.                 
  497.             if (this.mBaseItem.hasProperty(name))
  498.                 duration = this.mBaseItem.duration;
  499.  
  500.             proxy = this.mBaseItem.createProxy();
  501.             proxy.recurrenceId = aRecurrenceId;
  502.             proxy.setProperty("DTSTART", aRecurrenceId.clone());
  503.             if (duration) {
  504.                 var enddate = aRecurrenceId.clone();
  505.                 enddate.addDuration(duration);
  506.                 proxy.setProperty(name, enddate);
  507.             }
  508.             if (!this.mBaseItem.isMutable)
  509.                 proxy.makeImmutable();
  510.         }
  511.         return proxy;
  512.     },
  513.  
  514.     removeOccurrenceAt: function (aRecurrenceId) {
  515.         if (!this.mBaseItem)
  516.             throw Components.results.NS_ERROR_NOT_INITIALIZED;
  517.  
  518.         if (this.mImmutable)
  519.             throw Components.results.NS_ERROR_OBJECT_IS_IMMUTABLE;
  520.  
  521.         var d = Components.classes["@mozilla.org/calendar/recurrence-date;1"].createInstance(Components.interfaces.calIRecurrenceDate);
  522.         d.isNegative = true;
  523.         d.date = aRecurrenceId.clone();
  524.  
  525.         this.removeExceptionFor(d.date);
  526.  
  527.         this.appendRecurrenceItem(d);
  528.     },
  529.  
  530.     restoreOccurrenceAt: function (aRecurrenceId) {
  531.         if (!this.mBaseItem)
  532.             throw Components.results.NS_ERROR_NOT_INITIALIZED;
  533.  
  534.         if (this.mImmutable)
  535.             throw Components.results.NS_ERROR_OBJECT_IS_IMMUTABLE;
  536.  
  537.         for (var i = 0; i < this.mRecurrenceItems.length; i++) {
  538.             if (this.mRecurrenceItems[i] instanceof Components.interfaces.calIRecurrenceDate) {
  539.                 var rd = this.mRecurrenceItems[i].QueryInterface(Components.interfaces.calIRecurrenceDate);
  540.                 if (rd.isNegative && rd.date.compare(aRecurrenceId) == 0) {
  541.                     return this.deleteRecurrenceItemAt(i);
  542.                 }
  543.             }
  544.         }
  545.  
  546.         throw Components.results.NS_ERROR_INVALID_ARG;
  547.     },
  548.  
  549.     //
  550.     // exceptions
  551.     //
  552.  
  553.     //
  554.     // Some notes:
  555.     //
  556.     // The way I read ICAL, RECURRENCE-ID is used to specify a
  557.     // particular instance of a recurring event, according to the
  558.     // RRULEs/RDATEs/etc. specified in the base event.  If one of
  559.     // these is to be changed ("an exception"), then it can be
  560.     // referenced via the UID of the original event, and a
  561.     // RECURRENCE-ID of the start time of the instance to change.
  562.     // This, to me, means that an event where one of the instances has
  563.     // changed to a different time has a RECURRENCE-ID of the original
  564.     // start time, and a DTSTART/DTEND representing the new time.
  565.     //
  566.     // ITIP, however, seems to want something different -- you're
  567.     // supposed to use UID/RECURRENCE-ID to select from the current
  568.     // set of occurrences of an event.  If you change the DTSTART for
  569.     // an instance, you're supposed to use the old (original) DTSTART
  570.     // as the RECURRENCE-ID, and put the new time as the DTSTART.
  571.     // However, after that change, to refer to that instance in the
  572.     // future, you have to use the modified DTSTART as the
  573.     // RECURRENCE-ID.  This madness is described in ITIP end of
  574.     // section 3.7.1.
  575.     // 
  576.     // This implementation does the first approach (RECURRENCE-ID will
  577.     // never change even if DTSTART for that instance changes), which
  578.     // I think is the right thing to do for CalDAV; I don't know what
  579.     // we'll do for incoming ITIP events though.
  580.     //
  581.  
  582.     mExceptions: null,
  583.  
  584.     modifyException: function (anItem) {
  585.         if (!this.mBaseItem)
  586.             throw Components.results.NS_ERROR_NOT_INITIALIZED;
  587.  
  588.         // the item must be an occurrence
  589.         if (anItem.parentItem == anItem)
  590.             throw Components.results.NS_ERROR_UNEXPECTED;
  591.  
  592.         if (anItem.parentItem.calendar != this.mBaseItem.calendar &&
  593.             anItem.parentItem.id != this.mBaseItem.id)
  594.         {
  595.             calDebug ("recurrenceInfo::addException: item parentItem != this.mBaseItem (calendar/id)!\n");
  596.             throw Components.results.NS_ERROR_INVALID_ARG;
  597.         }
  598.  
  599.         if (anItem.recurrenceId == null) {
  600.             calDebug ("recurrenceInfo::addException: item with null recurrenceId!\n");
  601.             throw Components.results.NS_ERROR_INVALID_ARG;
  602.         }
  603.  
  604.         var itemtoadd;
  605.         if (anItem.isMutable) {
  606.             itemtoadd = anItem.cloneShallow(this.mBaseItem);
  607.             itemtoadd.makeImmutable();
  608.         } else {
  609.             itemtoadd = anItem;
  610.         }
  611.  
  612.         // we're going to assume that the recurrenceId is valid here,
  613.         // because presumably the item came from one of our functions
  614.  
  615.         // remove any old one, if present
  616.         this.removeExceptionFor(anItem.recurrenceId);
  617.  
  618.         this.mExceptions.push( { id: itemtoadd.recurrenceId, item: itemtoadd } );
  619.     },
  620.  
  621.     createExceptionFor: function (aRecurrenceId) {
  622.         if (!this.mBaseItem)
  623.             throw Components.results.NS_ERROR_NOT_INITIALIZED;
  624.  
  625.         // XX should it be an error to createExceptionFor
  626.         // an already-existing recurrenceId?
  627.         var existing = this.getExceptionFor(aRecurrenceId, false);
  628.         if (existing)
  629.             return existing;
  630.  
  631.         // check if aRecurrenceId is valid.
  632.  
  633.         // this is a bit of a hack; we know that ranges are defined as [start, end),
  634.         // so we do a search on aRecurrenceId and aRecurrenceId.seconds + 1.
  635.         var rangeStart = aRecurrenceId;
  636.         var rangeEnd = aRecurrenceId.clone();
  637.         rangeEnd.second += 1;
  638.  
  639.         var dates = this.getOccurrenceDates (rangeStart, rangeEnd, 1, {});
  640.         var found = false;
  641.         for each (d in dates) {
  642.             if (d.compare(aRecurrenceId) == 0) {
  643.                 found = true;
  644.                 break;
  645.             }
  646.         }
  647.  
  648.         // not found; the recurrence id is invalid
  649.         if (!found)
  650.             throw Components.results.NS_ERROR_INVALID_ARG;
  651.  
  652.         var rid = aRecurrenceId.clone();
  653.         rid.makeImmutable();
  654.  
  655.         var newex = this.mBaseItem.createProxy();
  656.         newex.recurrenceId = rid;
  657.  
  658.         this.mExceptions.push({id: rid, item: newex});
  659.  
  660.         return newex;
  661.     },
  662.  
  663.     getExceptionFor: function (aRecurrenceId, aCreate) {
  664.         if (!this.mBaseItem)
  665.             throw Components.results.NS_ERROR_NOT_INITIALIZED;
  666.  
  667.         for each (ex in this.mExceptions) {
  668.             if (ex.id.compare(aRecurrenceId) == 0)
  669.                 return ex.item;
  670.         }
  671.  
  672.         if (aCreate) {
  673.             return this.createExceptionFor(aRecurrenceId);
  674.         }
  675.         return null;
  676.     },
  677.  
  678.     removeExceptionFor: function (aRecurrenceId) {
  679.         if (!this.mBaseItem)
  680.             throw Components.results.NS_ERROR_NOT_INITIALIZED;
  681.  
  682.         this.mExceptions = this.mExceptions.filter (function(ex) {
  683.                                                         return (ex.id.compare(aRecurrenceId) != 0);
  684.                                                     });
  685.     },
  686.  
  687.     getExceptionIds: function (aCount) {
  688.         if (!this.mBaseItem)
  689.             throw Components.results.NS_ERROR_NOT_INITIALIZED;
  690.  
  691.         var ids = this.mExceptions.map (function(ex) {
  692.                                             return ex.id;
  693.                                         });
  694.  
  695.         aCount.value = ids.length;
  696.         return ids;
  697.     },
  698.     
  699.     // changing the startdate of an item needs to take exceptions into account.
  700.     // in case we're about to modify a parentItem (aka 'folded' item), we need
  701.     // to modify the recurrenceId's of all possibly existing exceptions as well.
  702.     onStartDateChange: function (aNewStartTime, aOldStartTime) {
  703.  
  704.         // passing null for the new starttime would indicate an error condition,
  705.         // since having a recurrence without a starttime is invalid.
  706.         if (!aNewStartTime) {
  707.             throw Components.results.NS_ERROR_INVALID_ARG;
  708.         }
  709.     
  710.         // no need to check for changes if there's no previous starttime.
  711.         if (!aOldStartTime) {
  712.             return;
  713.         }
  714.     
  715.         // convert both dates to UTC since subtractDate is not timezone aware.
  716.         aOldStartTime = aOldStartTime.getInTimezone("UTC");
  717.         aNewStartTime = aNewStartTime.getInTimezone("UTC");
  718.         var timeDiff = aNewStartTime.subtractDate(aOldStartTime);
  719.         var exceptions = this.getExceptionIds({});
  720.         var modifiedExceptions = [];
  721.         for each (var exid in exceptions) {
  722.             var ex = this.getExceptionFor(exid, false);
  723.             if (ex) {
  724.                 if (!ex.isMutable) {
  725.                     ex = ex.cloneShallow(this.item);
  726.                 }
  727.                 ex.recurrenceId.addDuration(timeDiff);
  728.                 
  729.                 modifiedExceptions.push(ex);
  730.                 this.removeExceptionFor(exid);
  731.             }
  732.         }
  733.         for each (var modifiedEx in modifiedExceptions) {
  734.             this.modifyException(modifiedEx);
  735.         }
  736.  
  737.         // also take RDATE's and EXDATE's into account.
  738.         const kCalIRecurrenceDate = Components.interfaces.calIRecurrenceDate;
  739.         const kCalIRecurrenceDateSet = Components.interfaces.calIRecurrenceDateSet;
  740.         var ritems = this.getRecurrenceItems({});
  741.         for (var i in ritems) {
  742.             var ritem = ritems[i];
  743.             if (ritem instanceof kCalIRecurrenceDate) {
  744.                 ritem = ritem.QueryInterface(kCalIRecurrenceDate);
  745.                 ritem.date.addDuration(timeDiff);
  746.             } else if (ritem instanceof kCalIRecurrenceDateSet) {
  747.                 ritem = ritem.QueryInterface(kCalIRecurrenceDateSet);
  748.                 var rdates = ritem.getDates({});
  749.                 for each (var date in rdates) {
  750.                     date.addDuration(timeDiff);
  751.                 }
  752.                 ritem.setDates(rdates.length,rdates);
  753.             }
  754.         }
  755.     }
  756. };
  757.