1 /* repositorymanager.js is part of Aloha Editor project http://aloha-editor.org 2 * 3 * Aloha Editor is a WYSIWYG HTML5 inline editing library and editor. 4 * Copyright (c) 2010-2012 Gentics Software GmbH, Vienna, Austria. 5 * Contributors http://aloha-editor.org/contribution.php 6 * 7 * Aloha Editor is free software; you can redistribute it and/or 8 * modify it under the terms of the GNU General Public License 9 * as published by the Free Software Foundation; either version 2 10 * of the License, or any later version. 11 * 12 * Aloha Editor is distributed in the hope that it will be useful, 13 * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 * GNU General Public License for more details. 16 * 17 * You should have received a copy of the GNU General Public License 18 * along with this program; if not, write to the Free Software 19 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 20 * 21 * As an additional permission to the GNU GPL version 2, you may distribute 22 * non-source (e.g., minimized or compacted) forms of the Aloha-Editor 23 * source code without the copy of the GNU GPL normally required, 24 * provided you include this license notice and a URL through which 25 * recipients can access the Corresponding Source. 26 */ 27 define(['aloha/core', 'util/class', 'jquery', 'aloha/console'], function (Aloha, Class, jQuery, console) { 28 'use strict'; 29 30 /** 31 * Repository Manager 32 * @namespace Aloha 33 * @class RepositoryManager 34 * @singleton 35 */ 36 Aloha.RepositoryManager = Class.extend({ 37 38 repositories: [], 39 settings: {}, 40 initialized: false, 41 42 /** 43 * Initialize all registered repositories 44 * Before we invoke each repositories init method, we merge the global 45 * repository settings into each repository's custom settings 46 * Warning: testing has shown that repositories are maybe not loaded yet 47 * (found that case in IE7), so don't rely on that in this init function. 48 * 49 * @todo: Write unit tests to check that global and custom settings are 50 * applied correctly 51 * 52 * @return void 53 * @hide 54 */ 55 init: function () { 56 var repositories = this.repositories; 57 58 if (Aloha.settings && Aloha.settings.repositories) { 59 this.settings = Aloha.settings.repositories; 60 } 61 62 // use the configured repository manger query timeout or 5 sec 63 this.settings.timeout = this.settings.timeout || 5000; 64 65 var count_repositories = repositories.length; 66 var i; 67 for (i = 0; i < count_repositories; ++i) { 68 var repository = repositories[i]; 69 this.initRepository(repository); 70 } 71 72 this.initialized = true; 73 }, 74 75 /** 76 * Register a Repository. 77 * 78 * @param {Aloha.Repository} repository Repository to register 79 */ 80 register: function (repository) { 81 if (!this.getRepository(repository.repositoryId)) { 82 this.repositories.push(repository); 83 84 // If we have initialized already we have to call 85 // this on our own (late-loading) 86 if (this.initialized) { 87 this.initRepository(repository); 88 } 89 } else { 90 console.warn(this, 'A repository with name { ' + repository.repositoryId + ' } already registerd. Ignoring this.'); 91 } 92 }, 93 94 /** 95 * Initializes a repository. 96 * 97 * @param {Aloha.Repository} repository Repository to initialize 98 */ 99 initRepository: function (repository) { 100 101 if (!repository.settings) { 102 repository.settings = {}; 103 } 104 105 if (this.settings[repository.repositoryId]) { 106 jQuery.extend(repository.settings, this.settings[repository.repositoryId]); 107 } 108 109 repository.init(); 110 }, 111 112 /** 113 * Returns the repository object identified by repositoryId. 114 * 115 * @param {String} repositoryId - the name of the repository 116 * @return {?Aloha.Repository} a repository or null if name not found 117 */ 118 getRepository: function (repositoryId) { 119 var repositories = this.repositories, 120 i, 121 j = repositories.length; 122 123 for (i = 0; i < j; ++i) { 124 if (repositories[i].repositoryId === repositoryId) { 125 return repositories[i]; 126 } 127 } 128 129 return null; 130 }, 131 132 /** 133 * Searches a all repositories for repositoryObjects matching query and 134 * repositoryObjectType. 135 * 136 <pre><code> 137 var params = { 138 queryString: 'hello', 139 objectTypeFilter: ['website'], 140 filter: null, 141 inFolderId: null, 142 orderBy: null, 143 maxItems: null, 144 skipCount: null, 145 renditionFilter: null, 146 repositoryId: null 147 }; 148 Aloha.RepositoryManager.query( params, function( items ) { 149 // do something with the result items 150 console.log(items); 151 }); 152 </code></pre> 153 * 154 * @param {Object <String,Mixed>} params object with properties 155 * <div class="mdetail-params"><ul> 156 * <li><code> queryString</code> : String <div class="sub-desc">The query string for full text search</div></li> 157 * <li><code> objectTypeFilter</code> : array (optional) <div class="sub-desc">Object types that will be returned.</div></li> 158 * <li><code> filter</code> : array (optional) <div class="sub-desc">Attributes that will be returned.</div></li> 159 * <li><code> inFolderId</code> : boolean (optional) <div class="sub-desc">This is indicates whether or not a candidate object is a child-object of the folder object identified by the given inFolderId (objectId).</div></li> 160 * <li><code> inTreeId</code> : boolean (optional) <div class="sub-desc">This indicates whether or not a candidate object is a descendant-object of the folder object identified by the given inTreeId (objectId).</div></li> 161 * <li><code> orderBy</code> : array (optional) <div class="sub-desc">ex. [{lastModificationDate:’DESC’, name:’ASC’}]</div></li> 162 * <li><code> maxItems</code> : Integer (optional) <div class="sub-desc">number items to return as result</div></li> 163 * <li><code> skipCount</code> : Integer (optional) <div class="sub-desc">This is tricky in a merged multi repository scenario</div></li> 164 * <li><code> renditionFilter</code> : array (optional) <div class="sub-desc">Instead of termlist an array of kind or mimetype is expected. If null or array.length == 0 all renditions are returned. See http://docs.oasis-open.org/cmis/CMIS/v1.0/cd04/cmis-spec-v1.0.html#_Ref237323310 for renditionFilter</div></li> 165 * </ul></div> 166 * @param {Function} callback - defines a callback function( items ) which will be called when all repositories returned their results or after a time out of 5sec. 167 * "items" is an Array of objects construced with Document/Folder. 168 * @void 169 */ 170 query: function (params, callback) { 171 var that = this, 172 repo, 173 // The merged results, collected from repository responses 174 allitems = [], 175 // the merge metainfo, collected from repository responses 176 allmetainfo = { 177 numItems: 0, 178 hasMoreItems: false 179 }, 180 // The set of repositories towhich we want to delegate work 181 repositories = [], 182 // A counting semaphore (working in reverse, ie: 0 means free) 183 numOpenCallbacks = 0, 184 // When this timer times-out, whatever has been collected in 185 // allitems will be returned to the calling client, and 186 // numOpenCallbacks will be reset to 0 187 timer, 188 i, 189 j, 190 /** 191 * Invoked by each repository when it wants to present its 192 * results to the manager. 193 * 194 * Collects the results from each repository, and decrements 195 * the numOpenCallbacks semaphore to indicate that there is one 196 * less repository for which we are waiting a reponse. 197 * 198 * If a repository invokes this callback after all 199 * openCallbacks have been closed (ie: numOpenCallbacks == 0), 200 * then the repository was too late ("missed the ship"), and 201 * will be ignored. 202 * 203 * If numOpenCallbacks decrements to 0 during this call, it 204 * means that the the manager is ready to report the results 205 * back to the client through the queryCallback method. 206 * 207 * nb: "this" is reference to the calling repository. 208 * 209 * @param {Array} items - Results returned by the repository 210 * @param {Object<String,Number>} metainfo - optional Metainfo returned by the repository 211 */ 212 processResults = function (items, metainfo) { 213 if (numOpenCallbacks === 0) { 214 return; 215 } 216 217 var j = items ? items.length : 0; 218 219 if (j) { 220 // Add the repositoryId for each item if a negligent 221 // repository did not do so. 222 if (!items[0].repositoryId) { 223 var repoId = this.repositoryId, 224 i; 225 for (i = 0; i < j; ++i) { 226 items[i].repositoryId = repoId; 227 } 228 } 229 230 jQuery.merge(allitems, items); 231 } 232 233 if (metainfo && allmetainfo) { 234 if (jQuery.isNumeric(metainfo.numItems) && jQuery.isNumeric(allmetainfo.numItems)) { 235 allmetainfo.numItems += metainfo.numItems; 236 } else { 237 allmetainfo.numItems = undefined; 238 } 239 240 if (typeof metainfo.hasMoreItems === 'boolean' && typeof allmetainfo.hasMoreItems === 'boolean') { 241 allmetainfo.hasMoreItems = allmetainfo.hasMoreItems || metainfo.hasMoreItems; 242 } else { 243 allmetainfo.hasMoreItems = undefined; 244 } 245 246 if (metainfo.timeout) { 247 allmetainfo.timeout = true; 248 } 249 } else { 250 // at least one repository did not return metainfo, so 251 // we have no aggregated metainfo at all 252 allmetainfo = undefined; 253 } 254 console.debug(this, "The repository " + this.repositoryId + " returned with " + j + " results."); 255 // TODO how to return the metainfo here? 256 if (--numOpenCallbacks === 0) { 257 that.queryCallback(callback, allitems, allmetainfo, timer); 258 } 259 }; 260 261 // Unless the calling client specifies otherwise, we will wait a 262 // maximum of 5 seconds for all repositories to be queried and 263 // respond. 5 seconds is deemed to be the reasonable time to wait 264 // when querying the repository manager in the context of something 265 // like autocomplete 266 var timeout = parseInt(params.timeout, 10) || this.settings.timeout; 267 timer = window.setTimeout(function () { 268 if (numOpenCallbacks > 0) { 269 console.warn(this, numOpenCallbacks + " repositories did not return before the configured timeout of " + timeout + "ms."); 270 } 271 numOpenCallbacks = 0; 272 // store in the metainfo, that a timeout occurred 273 allmetainfo = allmetainfo || {}; 274 allmetainfo.timeout = true; 275 that.queryCallback(callback, allitems, allmetainfo, timer); 276 }, timeout); 277 278 // If repositoryId or a list of repository ids, is not specified in 279 // the params object, then we will query all registered 280 // repositories 281 if (params.repositoryId) { 282 repositories.push(this.getRepository(params.repositoryId)); 283 } else { 284 repositories = this.repositories; 285 } 286 287 j = repositories.length; 288 289 var repoQueue = []; 290 291 // We need to know how many callbacks we will open before invoking 292 // the query method on each, so that as soon as the first one does 293 // callback, the correct number of open callbacks will be available 294 // to check. 295 296 for (i = 0; i < j; ++i) { 297 repo = repositories[i]; 298 299 // if no repositoryId is given query all repositories 300 // if a repositoryID is given only query if it is the right repository 301 if ((!params.repositoryId || repo.repositoryId === params.repositoryId) && typeof repo.query === 'function') { 302 ++numOpenCallbacks; 303 repoQueue.push(repo); 304 } 305 } 306 307 j = repoQueue.length; 308 309 function makeApplyRepoToProcessResults(repo) { 310 return function () { 311 processResults.apply(repo, arguments); 312 }; 313 } 314 315 for (i = 0; i < j; ++i) { 316 repo = repoQueue[i]; 317 repo.query(params, makeApplyRepoToProcessResults(repo)); 318 } 319 320 // If none of the repositories implemented the query method, then 321 // don't wait for the timeout, simply report to the client 322 if (numOpenCallbacks === 0) { 323 this.queryCallback(callback, allitems, allmetainfo, timer); 324 } 325 }, 326 327 /** 328 * Passes all the results we have collected to the client through the 329 * callback it specified 330 * 331 * @param {Function} callback - Callback specified by client when 332 * invoking the query method 333 * @param {Array} items - Results, collected from all repositories 334 * @param {Object<String,Number>} metainfo - optional object containing metainfo 335 * @param {Timer} timer - We need to clear this timer 336 * @return void 337 * @hide 338 */ 339 queryCallback: function (callback, items, metainfo, timer) { 340 if (timer) { 341 clearTimeout(timer); 342 timer = undefined; 343 } 344 345 // TODO: Implement sorting based on repository specification 346 // sort items by weight 347 //items.sort( function( a, b ) { 348 // return ( b.weight || 0 ) - ( a.weight || 0 ); 349 //} ); 350 351 // prepare result data for the JSON Reader 352 var result = { 353 items: items, 354 results: items.length 355 }; 356 357 if (metainfo) { 358 result.numItems = metainfo.numItems; 359 result.hasMoreItems = metainfo.hasMoreItems; 360 result.timeout = metainfo.timeout; 361 } 362 363 callback.call(this, result); 364 }, 365 366 /** 367 * @todo: This method needs to be covered with some unit tests 368 * 369 * Returns children items. (see query for an example) 370 * @param {Object<String,Mixed>} params - object with properties 371 * <div class="mdetail-params"><ul> 372 * <li><code> objectTypeFilter</code> : array (optional) <div class="sub-desc">Object types that will be returned.</div></li> 373 * <li><code> filter</code> : array (optional) <div class="sub-desc">Attributes that will be returned.</div></li> 374 * <li><code> inFolderId</code> : boolean (optional) <div class="sub-desc">This indicates whether or not a candidate object is a child-object of the folder object identified by the given inFolderId (objectId).</div></li> 375 * <li><code> orderBy</code> : array (optional) <div class="sub-desc">ex. [{lastModificationDate:’DESC’, name:’ASC’}]</div></li> 376 * <li><code> maxItems</code> : Integer (optional) <div class="sub-desc">number items to return as result</div></li> 377 * <li><code> skipCount</code> : Integer (optional) <div class="sub-desc">This is tricky in a merged multi repository scenario</div></li> 378 * <li><code> renditionFilter</code> : array (optional) <div class="sub-desc">Instead of termlist an array of kind or mimetype is expected. If null or array.length == 0 all renditions are returned. See http://docs.oasis-open.org/cmis/CMIS/v1.0/cd04/cmis-spec-v1.0.html#_Ref237323310 for renditionFilter</div></li> 379 * </ul></div> 380 * @param {Function} callback - defines a callback function( items ) which will be called when all repositories returned their results or after a time out of 5sec. 381 * "items" is an Array of objects construced with Document/Folder. 382 * @void 383 */ 384 getChildren: function (params, callback) { 385 var that = this, 386 repo, 387 // The marged results, collected from repository responses 388 allitems = [], 389 // The set of repositories towhich we want to delegate work 390 repositories = [], 391 // A counting semaphore (working in reverse, ie: 0 means free) 392 numOpenCallbacks = 0, 393 // When this timer times-out, whatever has been collected in 394 // allitems will be returned to the calling client, and 395 // numOpenCallbacks will be reset to 0 396 timer, 397 i, 398 j, 399 processResults = function (items) { 400 if (numOpenCallbacks === 0) { 401 return; 402 } 403 404 if (allitems && items) { 405 jQuery.merge(allitems, items); 406 } 407 408 if (--numOpenCallbacks === 0) { 409 that.getChildrenCallback(callback, allitems, timer); 410 } 411 }; 412 413 // If the inFolderId is the default id of 'aloha', then return all 414 // registered repositories 415 if (params.inFolderId === 'aloha') { 416 var repoFilter = params.repositoryFilter, 417 hasRepoFilter = (repoFilter && repoFilter.length); 418 419 j = this.repositories.length; 420 421 for (i = 0; i < j; ++i) { 422 repo = this.repositories[i]; 423 if (!hasRepoFilter || jQuery.inArray(repo.repositoryId, repoFilter) > -1) { 424 repositories.push(new Aloha.RepositoryFolder({ 425 id: repo.repositoryId, 426 name: repo.repositoryName, 427 repositoryId: repo.repositoryId, 428 type: 'repository', 429 hasMoreItems: true 430 })); 431 } 432 } 433 434 that.getChildrenCallback(callback, repositories, null); 435 436 return; 437 } 438 439 repositories = this.repositories; 440 441 var timeout = parseInt(params.timeout, 10) || this.settings.timeout; 442 timer = window.setTimeout(function () { 443 numOpenCallbacks = 0; 444 that.getChildrenCallback(callback, allitems, timer); 445 }, timeout); 446 447 j = repositories.length; 448 449 function makeApplyRepoToProcessResults(repo) { 450 return function () { 451 processResults.apply(repo, arguments); 452 }; 453 } 454 455 for (i = 0; i < j; ++i) { 456 repo = repositories[i]; 457 458 // if no repositoryId is given query all repositories 459 // if a repositoryID is given only query if it is the right repository 460 if ((!params.repositoryId || repo.repositoryId === params.repositoryId) && typeof repo.getChildren === 'function') { 461 ++numOpenCallbacks; 462 repo.getChildren(params, makeApplyRepoToProcessResults(repo)); 463 } 464 } 465 466 if (numOpenCallbacks === 0) { 467 this.getChildrenCallback(callback, allitems, timer); 468 } 469 }, 470 471 /** 472 * Returns results for getChildren to calling client 473 * 474 * @return void 475 * @hide 476 */ 477 getChildrenCallback: function (callback, items, timer) { 478 if (timer) { 479 clearTimeout(timer); 480 timer = undefined; 481 } 482 483 callback.call(this, items); 484 }, 485 486 /** 487 * @fixme: Not tested, but the code for this function does not seem to 488 * compute repository.makeClean will be undefined 489 * 490 * @todo: Rewrite this function header comment so that is clearer 491 * 492 * Pass an object, which represents an marked repository to corresponding 493 * repository, so that it can make the content clean (prepare for saving) 494 * 495 * @param {jQuery} obj - representing an editable 496 * @return void 497 */ 498 makeClean: function (obj) { 499 // iterate through all registered repositories 500 var that = this, 501 repository = {}, 502 i = 0, 503 j = that.repositories.length; 504 505 // find all repository tags 506 obj.find('[data-gentics-aloha-repository=' + this.prefix + ']').each(function () { 507 while (i < j) { 508 repository.makeClean(obj); 509 i += 1; 510 } 511 console.debug(that, 'Passing contents of HTML Element with id { ' + this.attr('id') + ' } for cleaning to repository { ' + repository.repositoryId + ' }'); 512 repository.makeClean(this); 513 }); 514 }, 515 516 /** 517 * Marks an object as repository of this type and with this item.id. 518 * Objects can be any DOM objects as A, SPAN, ABBR, etc. or 519 * special objects such as aloha-aloha_block elements. 520 * This method marks the target obj with two private attributes: 521 * (see http://dev.w3.org/html5/spec/elements.html#embedding-custom-non-visible-data) 522 * * data-gentics-aloha-repository: stores the repositoryId 523 * * data-gentics-aloha-object-id: stores the object.id 524 * 525 * @param {DOMObject} obj - DOM object to mark 526 * @param {Aloha.Repository.Object} item - the item which is applied to obj, 527 * if set to null, the data-GENTICS-... attributes are removed 528 * @return void 529 */ 530 markObject: function (obj, item) { 531 if (!obj) { 532 return; 533 } 534 535 if (item) { 536 var repository = this.getRepository(item.repositoryId); 537 538 if (repository) { 539 jQuery(obj).attr({ 540 'data-gentics-aloha-repository': item.repositoryId, 541 'data-gentics-aloha-object-id': item.id 542 }); 543 544 repository.markObject(obj, item); 545 } else { 546 console.error(this, 'Trying to apply a repository { ' + item.name + ' } to an object, but item has no repositoryId.'); 547 } 548 } else { 549 jQuery(obj).removeAttr('data-gentics-aloha-repository').removeAttr('data-gentics-aloha-object-id'); 550 } 551 }, 552 553 /** 554 * Get the object for which the given DOM object is marked from the 555 * repository. 556 * 557 * @param {DOMObject} obj - DOM object which probably is marked 558 * @param {Function} callback - callback function 559 */ 560 getObject: function (obj, callback) { 561 var that = this, 562 $obj = jQuery(obj), 563 repository = this.getRepository($obj.attr('data-gentics-aloha-repository')), 564 itemId = $obj.attr('data-gentics-aloha-object-id'); 565 566 if (repository && itemId) { 567 // initialize the item cache (per repository) if not already done 568 this.itemCache = this.itemCache || []; 569 this.itemCache[repository.repositoryId] = this.itemCache[repository.repositoryId] || []; 570 571 // when the item is cached, we just call the callback method 572 if (this.itemCache[repository.repositoryId][itemId]) { 573 callback.call(this, [this.itemCache[repository.repositoryId][itemId]]); 574 } else { 575 // otherwise we get the object from the repository 576 repository.getObjectById(itemId, function (items) { 577 // make sure the item is in the cache (for subsequent calls) 578 that.itemCache[repository.repositoryId][itemId] = items[0]; 579 callback.call(this, items); 580 }); 581 } 582 } 583 }, 584 585 /** 586 * This function gets called by the repository browser when a folder is opened. 587 * At the moment this forwards the call to the repository only. 588 * 589 * @param {object} folder object that gets forwarded 590 */ 591 folderOpened: function (folder) { 592 var repository = this.getRepository(folder.repositoryId); 593 594 if (typeof repository.folderOpened === 'function') { 595 repository.folderOpened(folder); 596 } 597 }, 598 599 /** 600 * This function gets called by the repository browser when a folder is closed. 601 * At the moment this forwards the call to the repository only. 602 * 603 * @param {object} folder object that gets forwarded 604 */ 605 folderClosed: function (folder) { 606 var repository = this.getRepository(folder.repositoryId); 607 608 if (typeof repository.folderClosed === 'function') { 609 repository.folderClosed(folder); 610 } 611 }, 612 613 /** 614 * This function gets called by the repository browser when a folder is selected. 615 * At the moment this forwards the call to the repository only. 616 * 617 * @param {object} folder object that gets forwarded 618 */ 619 folderSelected: function (folder) { 620 var repository = this.getRepository(folder.repositoryId); 621 622 if (typeof repository.folderSelected === 'function') { 623 repository.folderSelected(folder); 624 } 625 }, 626 627 /** 628 * Get the selected folder 629 * @returns selected folder or undefined 630 */ 631 getSelectedFolder: function () { 632 var i, len = this.repositories.length, 633 selected; 634 635 for (i = 0; i < len; ++i) { 636 if (typeof this.repositories[i].getSelectedFolder === 'function') { 637 selected = this.repositories[i].getSelectedFolder(); 638 if (selected) { 639 return selected; 640 } 641 } 642 } 643 }, 644 645 /** 646 * @return {String} name of repository manager object 647 */ 648 toString: function () { 649 return 'repositorymanager'; 650 } 651 652 }); 653 654 Aloha.RepositoryManager = new Aloha.RepositoryManager(); 655 656 // We return the constructor, not the instance of Aloha.RepositoryManager 657 return Aloha.RepositoryManager; 658 }); 659