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