1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5var EXPORTED_SYMBOLS = ["CalCompositeCalendar"];
6
7var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm");
8
9/**
10 * Calendar specific utility functions
11 */
12var calIOperationListener = Ci.calIOperationListener;
13
14function calCompositeCalendarObserverHelper(compCalendar) {
15  this.compCalendar = compCalendar;
16}
17
18calCompositeCalendarObserverHelper.prototype = {
19  QueryInterface: ChromeUtils.generateQI(["calIObserver"]),
20
21  onStartBatch(calendar) {
22    this.compCalendar.mObservers.notify("onStartBatch", [calendar]);
23  },
24
25  onEndBatch(calendar) {
26    this.compCalendar.mObservers.notify("onEndBatch", [calendar]);
27  },
28
29  onLoad(calendar) {
30    this.compCalendar.mObservers.notify("onLoad", [calendar]);
31  },
32
33  onAddItem(aItem) {
34    this.compCalendar.mObservers.notify("onAddItem", arguments);
35  },
36
37  onModifyItem(aNewItem, aOldItem) {
38    this.compCalendar.mObservers.notify("onModifyItem", arguments);
39  },
40
41  onDeleteItem(aDeletedItem) {
42    this.compCalendar.mObservers.notify("onDeleteItem", arguments);
43  },
44
45  onError(aCalendar, aErrNo, aMessage) {
46    this.compCalendar.mObservers.notify("onError", arguments);
47  },
48
49  onPropertyChanged(aCalendar, aName, aValue, aOldValue) {
50    this.compCalendar.mObservers.notify("onPropertyChanged", arguments);
51  },
52
53  onPropertyDeleting(aCalendar, aName) {
54    this.compCalendar.mObservers.notify("onPropertyDeleting", arguments);
55  },
56};
57
58function CalCompositeCalendar() {
59  this.mObserverHelper = new calCompositeCalendarObserverHelper(this);
60  this.wrappedJSObject = this;
61
62  this.mCalendars = [];
63  this.mCompositeObservers = new cal.data.ObserverSet(Ci.calICompositeObserver);
64  this.mObservers = new cal.data.ObserverSet(Ci.calIObserver);
65  this.mDefaultCalendar = null;
66  this.mStatusObserver = null;
67}
68
69var calCompositeCalendarClassID = Components.ID("{aeff788d-63b0-4996-91fb-40a7654c6224}");
70var calCompositeCalendarInterfaces = [
71  "calICalendarProvider",
72  "calICalendar",
73  "calICompositeCalendar",
74];
75CalCompositeCalendar.prototype = {
76  classID: calCompositeCalendarClassID,
77  QueryInterface: ChromeUtils.generateQI(calCompositeCalendarInterfaces),
78
79  //
80  // calICalendarProvider interface
81  //
82  get prefChromeOverlay() {
83    return null;
84  },
85
86  get displayName() {
87    return cal.l10n.getCalString("compositeName");
88  },
89
90  get shortName() {
91    return this.displayName();
92  },
93
94  createCalendar() {
95    throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
96  },
97
98  deleteCalendar(calendar, listener) {
99    // You shouldn't be able to delete from the composite calendar.
100    throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
101  },
102
103  //
104  // calICompositeCalendar interface
105  //
106
107  mCalendars: null,
108  mDefaultCalendar: null,
109  mPrefPrefix: null,
110  mDefaultPref: null,
111  mActivePref: null,
112
113  get enabledCalendars() {
114    return this.mCalendars.filter(e => !e.getProperty("disabled"));
115  },
116
117  set prefPrefix(aPrefPrefix) {
118    if (this.mPrefPrefix) {
119      for (let calendar of this.mCalendars) {
120        this.removeCalendar(calendar);
121      }
122    }
123    this.mPrefPrefix = aPrefPrefix;
124    this.mActivePref = aPrefPrefix + "-in-composite";
125    this.mDefaultPref = aPrefPrefix + "-default";
126    let mgr = cal.getCalendarManager();
127    let cals = mgr.getCalendars();
128
129    cals.forEach(function(calendar) {
130      if (calendar.getProperty(this.mActivePref)) {
131        this.addCalendar(calendar);
132      }
133      if (calendar.getProperty(this.mDefaultPref)) {
134        this.setDefaultCalendar(calendar, false);
135      }
136    }, this);
137  },
138
139  get prefPrefix() {
140    return this.mPrefPrefix;
141  },
142
143  addCalendar(aCalendar) {
144    cal.ASSERT(aCalendar.id, "calendar does not have an id!", true);
145
146    // check if the calendar already exists
147    if (this.getCalendarById(aCalendar.id)) {
148      return;
149    }
150
151    // add our observer helper
152    aCalendar.addObserver(this.mObserverHelper);
153
154    this.mCalendars.push(aCalendar);
155    if (this.mPrefPrefix) {
156      aCalendar.setProperty(this.mActivePref, true);
157    }
158    this.mCompositeObservers.notify("onCalendarAdded", [aCalendar]);
159
160    // if we have no default calendar, we need one here
161    if (this.mDefaultCalendar == null && !aCalendar.getProperty("disabled")) {
162      this.setDefaultCalendar(aCalendar, false);
163    }
164  },
165
166  removeCalendar(aCalendar) {
167    let id = aCalendar.id;
168    let newCalendars = this.mCalendars.filter(calendar => calendar.id != id);
169    if (newCalendars.length != this.mCalendars) {
170      this.mCalendars = newCalendars;
171      if (this.mPrefPrefix) {
172        aCalendar.deleteProperty(this.mActivePref);
173        aCalendar.deleteProperty(this.mDefaultPref);
174      }
175      aCalendar.removeObserver(this.mObserverHelper);
176      this.mCompositeObservers.notify("onCalendarRemoved", [aCalendar]);
177    }
178  },
179
180  getCalendarById(aId) {
181    for (let calendar of this.mCalendars) {
182      if (calendar.id == aId) {
183        return calendar;
184      }
185    }
186    return null;
187  },
188
189  getCalendars() {
190    return this.mCalendars;
191  },
192
193  get defaultCalendar() {
194    return this.mDefaultCalendar;
195  },
196
197  setDefaultCalendar(calendar, usePref) {
198    // Don't do anything if the passed calendar is the default calendar
199    if (calendar && this.mDefaultCalendar && this.mDefaultCalendar.id == calendar.id) {
200      return;
201    }
202    if (usePref && this.mPrefPrefix) {
203      if (this.mDefaultCalendar) {
204        this.mDefaultCalendar.deleteProperty(this.mDefaultPref);
205      }
206      // if not null set the new calendar as default in the preferences
207      if (calendar) {
208        calendar.setProperty(this.mDefaultPref, true);
209      }
210    }
211    this.mDefaultCalendar = calendar;
212    this.mCompositeObservers.notify("onDefaultCalendarChanged", [calendar]);
213  },
214
215  set defaultCalendar(calendar) {
216    this.setDefaultCalendar(calendar, true);
217  },
218
219  //
220  // calICalendar interface
221  //
222  // Write operations here are forwarded to either the item's
223  // parent calendar, or to the default calendar if one is set.
224  // Get operations are sent to each calendar.
225  //
226
227  get id() {
228    throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
229  },
230  set id(id) {
231    throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
232  },
233
234  get superCalendar() {
235    // There shouldn't be a superCalendar for the composite
236    return this;
237  },
238  set superCalendar(val) {
239    throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
240  },
241
242  // this could, at some point, return some kind of URI identifying
243  // all the child calendars, thus letting us create nifty calendar
244  // trees.
245  get uri() {
246    throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
247  },
248  set uri(val) {
249    throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
250  },
251
252  get readOnly() {
253    throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
254  },
255  set readOnly(bool) {
256    throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
257  },
258
259  get canRefresh() {
260    return true;
261  },
262
263  get name() {
264    throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
265  },
266  set name(val) {
267    throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
268  },
269
270  get type() {
271    return "composite";
272  },
273
274  getProperty(aName) {
275    return this.mDefaultCalendar.getProperty(aName);
276  },
277
278  get supportsScheduling() {
279    return false;
280  },
281
282  getSchedulingSupport() {
283    throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
284  },
285
286  setProperty(aName, aValue) {
287    return this.mDefaultCalendar.setProperty(aName, aValue);
288  },
289
290  deleteProperty(aName) {
291    return this.mDefaultCalendar.deleteProperty(aName);
292  },
293
294  // void addObserver( in calIObserver observer );
295  mCompositeObservers: null,
296  mObservers: null,
297  addObserver(aObserver) {
298    let wrappedCObserver = cal.wrapInstance(aObserver, Ci.calICompositeObserver);
299    if (wrappedCObserver) {
300      this.mCompositeObservers.add(wrappedCObserver);
301    }
302    this.mObservers.add(aObserver);
303  },
304
305  // void removeObserver( in calIObserver observer );
306  removeObserver(aObserver) {
307    let wrappedCObserver = cal.wrapInstance(aObserver, Ci.calICompositeObserver);
308    if (wrappedCObserver) {
309      this.mCompositeObservers.delete(wrappedCObserver);
310    }
311    this.mObservers.delete(aObserver);
312  },
313
314  refresh() {
315    if (this.mStatusObserver) {
316      this.mStatusObserver.startMeteors(
317        Ci.calIStatusObserver.DETERMINED_PROGRESS,
318        this.mCalendars.length
319      );
320    }
321    for (let calendar of this.enabledCalendars) {
322      try {
323        if (calendar.canRefresh) {
324          calendar.refresh();
325        }
326      } catch (e) {
327        cal.ASSERT(false, e);
328      }
329    }
330    // send out a single onLoad for this composite calendar,
331    // although e.g. the ics provider will trigger another
332    // onLoad asynchronously; we cannot rely on every calendar
333    // sending an onLoad:
334    this.mObservers.notify("onLoad", [this]);
335  },
336
337  // void modifyItem( in calIItemBase aNewItem, in calIItemBase aOldItem, in calIOperationListener aListener );
338  modifyItem(aNewItem, aOldItem, aListener) {
339    cal.ASSERT(aNewItem.calendar, "Composite can't modify item with null calendar", true);
340    cal.ASSERT(aNewItem.calendar != this, "Composite can't modify item with this calendar", true);
341
342    return aNewItem.calendar.modifyItem(aNewItem, aOldItem, aListener);
343  },
344
345  // void deleteItem( in string id, in calIOperationListener aListener );
346  deleteItem(aItem, aListener) {
347    cal.ASSERT(aItem.calendar, "Composite can't delete item with null calendar", true);
348    cal.ASSERT(aItem.calendar != this, "Composite can't delete item with this calendar", true);
349
350    return aItem.calendar.deleteItem(aItem, aListener);
351  },
352
353  // void addItem( in calIItemBase aItem, in calIOperationListener aListener );
354  addItem(aItem, aListener) {
355    return this.mDefaultCalendar.addItem(aItem, aListener);
356  },
357
358  // void getItem( in string aId, in calIOperationListener aListener );
359  getItem(aId, aListener) {
360    let enabledCalendars = this.enabledCalendars;
361    let cmpListener = new calCompositeGetListenerHelper(this, aListener);
362    for (let calendar of enabledCalendars) {
363      try {
364        cmpListener.opGroup.add(calendar.getItem(aId, cmpListener));
365      } catch (exc) {
366        cal.ASSERT(false, exc);
367      }
368    }
369    return cmpListener.opGroup;
370  },
371
372  // void getItems( in unsigned long aItemFilter, in unsigned long aCount,
373  //                in calIDateTime aRangeStart, in calIDateTime aRangeEnd,
374  //                in calIOperationListener aListener );
375  getItems(aItemFilter, aCount, aRangeStart, aRangeEnd, aListener) {
376    // If there are no calendars, then we just call onOperationComplete
377    let enabledCalendars = this.enabledCalendars;
378    if (enabledCalendars.length == 0) {
379      aListener.onOperationComplete(this, Cr.NS_OK, calIOperationListener.GET, null, null);
380      return null;
381    }
382    if (this.mStatusObserver) {
383      if (this.mStatusObserver.spinning == Ci.calIStatusObserver.NO_PROGRESS) {
384        this.mStatusObserver.startMeteors(Ci.calIStatusObserver.UNDETERMINED_PROGRESS, -1);
385      }
386    }
387    let cmpListener = new calCompositeGetListenerHelper(this, aListener, aCount);
388
389    for (let calendar of enabledCalendars) {
390      try {
391        cmpListener.opGroup.add(
392          calendar.getItems(aItemFilter, aCount, aRangeStart, aRangeEnd, cmpListener)
393        );
394      } catch (exc) {
395        cal.ASSERT(false, exc);
396      }
397    }
398    return cmpListener.opGroup;
399  },
400
401  startBatch() {
402    this.mCompositeObservers.notify("onStartBatch", [this]);
403  },
404  endBatch() {
405    this.mCompositeObservers.notify("onEndBatch", [this]);
406  },
407
408  get statusDisplayed() {
409    if (this.mStatusObserver) {
410      return this.mStatusObserver.spinning != Ci.calIStatusObserver.NO_PROGRESS;
411    }
412    return false;
413  },
414
415  setStatusObserver(aStatusObserver, aWindow) {
416    this.mStatusObserver = aStatusObserver;
417    if (this.mStatusObserver) {
418      this.mStatusObserver.initialize(aWindow);
419    }
420  },
421};
422
423// composite listener helper
424function calCompositeGetListenerHelper(aCompositeCalendar, aRealListener, aMaxItems) {
425  this.wrappedJSObject = this;
426  this.mCompositeCalendar = aCompositeCalendar;
427  this.mNumQueries = aCompositeCalendar.enabledCalendars.length;
428  this.mRealListener = aRealListener;
429  this.mMaxItems = aMaxItems;
430}
431
432calCompositeGetListenerHelper.prototype = {
433  QueryInterface: ChromeUtils.generateQI(["calIOperationListener"]),
434
435  mNumQueries: 0,
436  mRealListener: null,
437  mOpGroup: null,
438  mReceivedCompletes: 0,
439  mFinished: false,
440  mMaxItems: 0,
441  mItemsReceived: 0,
442
443  get opGroup() {
444    if (!this.mOpGroup) {
445      this.mOpGroup = new cal.data.OperationGroup(() => {
446        let listener = this.mRealListener;
447        this.mRealListener = null;
448        if (listener) {
449          listener.onOperationComplete(
450            this,
451            Ci.calIErrors.OPERATION_CANCELLED,
452            calIOperationListener.GET,
453            null,
454            null
455          );
456          if (this.mCompositeCalendar.statusDisplayed) {
457            this.mCompositeCalendar.mStatusObserver.stopMeteors();
458          }
459        }
460      });
461    }
462    return this.mOpGroup;
463  },
464
465  onOperationComplete(aCalendar, aStatus, aOperationType, aId, aDetail) {
466    if (!this.mRealListener) {
467      // has been cancelled, ignore any providers firing on this...
468      return;
469    }
470    if (this.mFinished) {
471      dump("+++ calCompositeGetListenerHelper.onOperationComplete: called with mFinished == true!");
472      return;
473    }
474    if (this.mCompositeCalendar.statusDisplayed) {
475      this.mCompositeCalendar.mStatusObserver.calendarCompleted(aCalendar);
476    }
477    if (!Components.isSuccessCode(aStatus)) {
478      // proxy this to a onGetResult
479      // XXX - do we want to give the real calendar? or this?
480      // XXX - get rid of iid param
481      this.mRealListener.onGetResult(aCalendar, aStatus, Ci.nsISupports, aDetail, []);
482    }
483
484    this.mReceivedCompletes++;
485    if (this.mReceivedCompletes == this.mNumQueries) {
486      if (this.mCompositeCalendar.statusDisplayed) {
487        this.mCompositeCalendar.mStatusObserver.stopMeteors();
488      }
489      // we're done here.
490      this.mFinished = true;
491      this.opGroup.notifyCompleted();
492      this.mRealListener.onOperationComplete(this, aStatus, calIOperationListener.GET, null, null);
493    }
494  },
495
496  onGetResult(aCalendar, aStatus, aItemType, aDetail, aItems) {
497    if (!this.mRealListener) {
498      // has been cancelled, ignore any providers firing on this...
499      return;
500    }
501    if (this.mFinished) {
502      dump("+++ calCompositeGetListenerHelper.onGetResult: called with mFinished == true!");
503      return;
504    }
505
506    // ignore if we have a max and we're past it
507    if (this.mMaxItems && this.mItemsReceived >= this.mMaxItems) {
508      return;
509    }
510
511    let itemsCount = aItems.length;
512
513    if (
514      Components.isSuccessCode(aStatus) &&
515      this.mMaxItems &&
516      this.mItemsReceived + itemsCount > this.mMaxItems
517    ) {
518      // this will blow past the limit
519      itemsCount = this.mMaxItems - this.mItemsReceived;
520      aItems = aItems.slice(0, itemsCount);
521    }
522
523    // send GetResults to the real listener
524    this.mRealListener.onGetResult(aCalendar, aStatus, aItemType, aDetail, aItems);
525    this.mItemsReceived += itemsCount;
526  },
527};
528