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([ 28 'jquery', 29 'util/class', 30 'aloha/core', 31 'aloha/console', 32 'aloha/repositoryobjects' // Provides Aloha.RepositoryFolder 33 ], function ( 34 $, 35 Class, 36 Aloha, 37 Console, 38 __unused__ 39 ) { 40 'use strict'; 41 42 /** 43 * Given an input set, returns a new set which is a range of the input set 44 * that maps to the given predicate. 45 * 46 * Prefers native Array.prototype.filter() where available (after JavaScript 47 * 1.6). 48 * 49 * @param {Array} domain 50 * @param {function:boolean} predicate 51 * @return {Array} Sub set of domain 52 */ 53 var filter = (function (native) { 54 return (native 55 ? function (domain, predicate) { 56 return domain.filter(predicate); 57 } 58 : function (domain, predicate) { 59 var codomain = []; 60 var i; 61 for (i = 0; i < domain.length; i++) { 62 if (predicate(domain[i])) { 63 codomain.push(domain[i]); 64 } 65 } 66 return codomain; 67 } 68 ); 69 }(Array.prototype.hasOwnProperty('filter'))); 70 71 /** 72 * Bundles results, and meta information in preparation for the JSON Reader. 73 * 74 * Used with query(). 75 * 76 * @param {Array.<Document|Folder>} items Results, collected from all 77 * repositories. 78 * @param {object<string, number>} meta Optional object containing metainfo. 79 * @return {object} Result object. 80 */ 81 function bundle(items, meta) { 82 var result = { 83 items: items, 84 results: items.length 85 }; 86 if (meta) { 87 result.numItems = meta.numItems; 88 result.hasMoreItems = meta.hasMoreItems; 89 result.timeout = meta.timeout; 90 } 91 return result; 92 } 93 94 /** 95 * Passes all the results we have collected to the client through the 96 * callback it specified. 97 * 98 * TODO: Implement sorting based on repository specification sort 99 * items by weight. 100 * items.sort(function (a, b) { 101 * return (b.weight || 0) - (a.weight || 0); 102 * }); 103 * 104 * @param {function} callback Callback specified by client when invoking 105 * the query method. 106 * @param {Array.<Document|Folder>|object<string, number>} results 107 */ 108 function report(callback, results) { 109 callback(results); 110 } 111 112 /** 113 * Predicates; used to filter lists of repositories based on whether they 114 * implement a method or not. 115 * 116 * @type {object<string, function(Repository):boolean} 117 */ 118 var repositoryFilters = { 119 query: function (repository) { 120 return typeof repository.query === 'function'; 121 }, 122 getChildren: function (repository) { 123 return typeof repository.getChildren === 'function'; 124 }, 125 getSelectedFolder: function (repository) { 126 return typeof repository.getSelectedFolder === 'function'; 127 } 128 }; 129 130 /** 131 * Repository Manager. 132 * 133 * @namespace Aloha 134 * @class RepositoryManager 135 * @singleton 136 */ 137 var RepositoryManager = Class.extend({ 138 139 repositories: [], 140 141 settings: (Aloha.settings && Aloha.settings.repositories) || {}, 142 143 initialized: false, 144 145 /** 146 * Initializes all registered repositories. 147 * 148 * ??? 149 * | 150 * v 151 * 152 * Warning: testing has shown that repositories are maybe not loaded yet 153 * (found that case in IE7), so don't rely on that in this init 154 * function. 155 * 156 * ^ 157 * | 158 * !!! 159 */ 160 init: function () { 161 var manager = this; 162 if (typeof manager.settings.timeout === 'undefined') { 163 manager.settings.timeout = 5000; 164 } 165 var i; 166 for (i = 0; i < manager.repositories.length; i++) { 167 manager.initRepository(manager.repositories[i]); 168 } 169 manager.initialized = true; 170 }, 171 172 /** 173 * Registers a Repository. 174 * 175 * If the repositorie is registered after the Repository Manager is 176 * initialized it will be automatically initialized. 177 * 178 * @param {Repository} repository Repository to register. 179 */ 180 register: function (repository) { 181 var manager = this; 182 if (!manager.getRepository(repository.repositoryId)) { 183 manager.repositories.push(repository); 184 if (manager.initialized) { 185 manager.initRepository(repository); 186 } 187 } else { 188 Console.warn(manager, 'A repository with name "' 189 + repository.repositoryId 190 + '" already registerd. Ignoring this.'); 191 } 192 }, 193 194 /** 195 * Initializes a repository. 196 * 197 * @param {Repository} repository Repository to initialize. 198 */ 199 initRepository: function (repository) { 200 var manager = this; 201 if (!repository.settings) { 202 repository.settings = {}; 203 } 204 if (manager.settings[repository.repositoryId]) { 205 $.extend(repository.settings, 206 manager.settings[repository.repositoryId]); 207 } 208 repository.init(); 209 }, 210 211 /** 212 * Returns the repository identified by repositoryId. 213 * 214 * @param {String} id Id of repository to retrieve. 215 * @return {Repository|null} Repository or null if none with the given 216 * id is found. 217 */ 218 getRepository: function (id) { 219 var manager = this; 220 var i; 221 for (i = 0; i < manager.repositories.length; i++) { 222 if (manager.repositories[i].repositoryId === id) { 223 return manager.repositories[i]; 224 } 225 } 226 return null; 227 }, 228 229 /** 230 * Searches all repositories for repositoryObjects matching query and 231 * repositoryObjectType. 232 * 233 * <pre><code> 234 * // Example: 235 * var params = { 236 * queryString: 'hello', 237 * objectTypeFilter: ['website'], 238 * filter: null, 239 * inFolderId: null, 240 * orderBy: null, 241 * maxItems: null, 242 * skipCount: null, 243 * renditionFilter: null, 244 * repositoryId: null 245 * }; 246 * Aloha.RepositoryManager.query(params, function (items) { 247 * Console.log(items); 248 * }); 249 * </code></pre> 250 * 251 * @param {object<string, mixed>} params 252 * 253 * queryString: String The query string for full text 254 * search. 255 * objectTypeFilter: Array (optional) Object types to be retrieved. 256 * filter: Array (optional) Attributes that will be 257 * included. 258 * inFolderId: boolean (optional) Whether or not a candidate 259 * object is a child-object of the 260 * folder object identified by the 261 * given inFolderId (objectId). 262 * inTreeId: boolean (optional) This indicates whether or 263 * not a candidate object is a 264 * descendant-object of the folder 265 * object identified by the given 266 * inTreeId (objectId). 267 * orderBy: Array (optional) example: [{ 268 * lastModificationDate: 'DESC', 269 * name: 'ASC' 270 * }] 271 * maxItems: number (optional) Number of items to include in 272 * result set. 273 * skipCount: number (optional) This is tricky in a merged 274 * multi repository scenario. 275 * renditionFilter: Array (optional) Instead of termlist, an 276 * array of kind or mimetype is 277 * expected. If null or an empty 278 * set, then all renditions are 279 * returned. See 280 * http://docs.oasis-open.org/cmis/CMIS/v1.0/cd04/cmis-spec-v1.0.html#_Ref237323310 281 * for renditionFilter. 282 * 283 * @param {function(Document|Folder)} callback Function to be invoked 284 * after the repository 285 * manager has finished 286 * querying all 287 288 * repositories. 289 */ 290 query: function (params, callback) { 291 var manager = this; 292 293 var i; 294 295 // The merged results, collected from repository responses. 296 var results = []; 297 298 // The merged metainfo, collected from repository responses. 299 var allmetainfo = { 300 numItems: 0, 301 hasMoreItems: false 302 }; 303 304 // A counting semaphore (working in reverse, ie: 0 means free). 305 var numOpenQueries; 306 307 // Unless the calling client specifies otherwise, the manager will 308 // wait a maximum of 5 seconds for all repositories to be queried 309 // and respond. 5 seconds is deemed to be the reasonable time to 310 // wait when querying the repository manager in the context of 311 // something like autocomplete. 312 var timeout = (params.timeout && parseInt(params.timeout, 10)) 313 || manager.settings.timeout; 314 315 // When this timer times-out, whatever has been collected in 316 // `results' will be returned to the calling client and all further 317 // processing aborted. 318 var timer = window.setTimeout(function () { 319 // Store in metainfo that a timeout occurred. 320 allmetainfo = allmetainfo || {}; 321 allmetainfo.timeout = true; 322 323 if (numOpenQueries > 0) { 324 Console.warn(manager, numOpenQueries 325 + ' repositories did not return before the ' 326 + 'configured timeout of ' + timeout + 'ms.'); 327 numOpenQueries = 0; 328 } 329 clearTimeout(timer); 330 report(callback, bundle(results, allmetainfo)); 331 }, timeout); 332 333 /** 334 * Invoked by each repository when it wants to present its results 335 * to the manager. 336 * 337 * Collects the results from each repository, and decrements the 338 * numOpenQueries semaphore to indicate that there is one less 339 * repository for which the manager is waiting for a reponse. 340 * 341 * If a repository invokes this callback after all openCallbacks 342 * have been closed (ie: numOpenQueries == 0), then the repository 343 * was too late ("missed the ship"), and will be ignored. 344 * 345 * If numOpenQueries decrements to 0 during this call, it means that 346 * the the manager is ready to report the results back to the client 347 * through the report() method. 348 * 349 * @param {Array.<Document|Folder>} items Results returned by the 350 * repository. 351 * @param {object<string, number>} metainfo Optional Metainfo 352 * returned by some 353 * repositories. 354 */ 355 var process = function (items, metainfo) { 356 var repository = this; 357 358 if (0 === numOpenQueries) { 359 return; 360 } 361 362 if (items && items.length) { 363 364 // Because some negligent repository implementations do not 365 // set repositoryId properly. 366 if (!items[0].repositoryId) { 367 var id = repository.repositoryId; 368 var i; 369 for (i = 0; i < items.length; i++) { 370 items[i].repositoryId = id; 371 } 372 } 373 374 $.merge(results, items); 375 } 376 377 if (metainfo && allmetainfo) { 378 allmetainfo.numItems = 379 ($.isNumeric(metainfo.numItems) && 380 $.isNumeric(allmetainfo.numItems)) 381 ? allmetainfo.numItems + metainfo.numItems 382 : undefined; 383 384 allmetainfo.hasMoreItems = 385 (typeof metainfo.hasMoreItems === 'boolean' && 386 typeof allmetainfo.hasMoreItems === 'boolean') 387 ? allmetainfo.hasMoreItems || metainfo.hasMoreItems 388 : undefined; 389 390 if (metainfo.timeout) { 391 allmetainfo.timeout = true; 392 } 393 } else { 394 395 // Because if even one repository does not return metainfo, 396 // so we have no aggregated metainfo at all. 397 allmetainfo = undefined; 398 } 399 400 Console.debug(manager, 'The repository ' 401 + repository.repositoryId + 'returned with ' 402 + items.length + ' results.'); 403 404 // TODO: how to return the metainfo here? 405 if (0 === --numOpenQueries) { 406 clearTimeout(timer); 407 report(callback, bundle(results, allmetainfo)); 408 } 409 }; 410 411 var repositories = params.repositoryId 412 ? [manager.getRepository(params.repositoryId)] 413 : manager.repositories; 414 415 var queue = filter(repositories, repositoryFilters.query); 416 417 // If none of the repositories implemented the query method, then 418 // don't wait for the timeout, simply report to the client. 419 if (0 === queue.length) { 420 clearTimeout(timer); 421 report(callback, bundle(results, allmetainfo)); 422 return; 423 } 424 425 var makeProcess = function (repository) { 426 return function () { 427 process.apply(repository, arguments); 428 }; 429 }; 430 431 numOpenQueries = queue.length; 432 433 for (i = 0; i < queue.length; i++) { 434 queue[i].query(params, makeProcess(queue[i])); 435 } 436 }, 437 438 /** 439 * Retrieves children items. 440 * 441 * @param {object<string,mixed>} params Object with properties. 442 * 443 * objectTypeFilter: Array (optional) Object types to be retrieved. 444 * filter: Array (optional) Attributes to be retrieved. 445 * inFolderId: boolean (optional) This indicates whether or not 446 * a candidate object is a 447 * child-object of the folder 448 * object identified by the given 449 * inFolderId (objectId). 450 * orderBy: Array (optional) example: [{ 451 * lastModificationDate: 'DESC', 452 * name: 'ASC' 453 * }] 454 * maxItems: number (optional) number Items to return as a result. 455 * skipCount: number (optional) This is tricky in a merged 456 * multi repository scenario. 457 * renditionFilter: Array (optional) Instead of termlist an Array 458 * of kind or mimetype is 459 * expected. If null or 460 * Array.length == 0 all 461 * renditions are returned. See 462 * http://docs.oasis-open.org/cmis/CMIS/v1.0/cd04/cmis-spec-v1.0.html#_Ref237323310 463 * for renditionFilter. 464 * 465 * @param {function(Document|Folder)} callback Function to be invoked 466 * after the repository 467 * manager has finished 468 * querying all 469 * repositories. 470 */ 471 getChildren: function (params, callback) { 472 var manager = this; 473 474 var i; 475 476 // The marged results, collected from repository responses. 477 var results = []; 478 479 // A counting semaphore (working in reverse, ie: 0 means free). 480 var numOpenQueries = 0; 481 482 var timeout = (params.timeout && parseInt(params.timeout, 10)) 483 || manager.settings.timeout; 484 485 var timer = window.setTimeout(function () { 486 if (numOpenQueries > 0) { 487 Console.warn(manager, numOpenQueries 488 + ' repositories did not respond before the ' 489 + 'configured timeout of ' + timeout + 'ms.'); 490 numOpenQueries = 0; 491 } 492 clearTimeout(timer); 493 report(callback, results); 494 }, timeout); 495 496 var process = function (items) { 497 if (0 === numOpenQueries) { 498 return; 499 } 500 if (items) { 501 $.merge(results, items); 502 } 503 if (0 === --numOpenQueries) { 504 clearTimeout(timer); 505 report(callback, results); 506 } 507 }; 508 509 var repositories = params.repositoryId 510 ? [manager.getRepository(params.repositoryId)] 511 : manager.repositories; 512 513 if (params.repositoryFilter && params.repositoryFilter.length) { 514 repositories = filter(repositories, function (repository) { 515 return -1 < $.inArray(repository.repositoryId, 516 params.repositoryFilter); 517 }); 518 } 519 520 // If the inFolderId is the default id of 'aloha', then return all 521 // registered repositories as the result set. 522 if ('aloha' === params.inFolderId) { 523 var hasRepoFilter = params.repositoryFilter 524 && 0 < params.repositoryFilter.length; 525 526 for (i = 0; i < repositories.length; i++) { 527 results.push(new Aloha.RepositoryFolder({ 528 id: repositories[i].repositoryId, 529 name: repositories[i].repositoryName, 530 repositoryId: repositories[i].repositoryId, 531 type: 'repository', 532 hasMoreItems: true 533 })); 534 } 535 536 clearTimeout(timer); 537 report(callback, results); 538 return; 539 } 540 541 var queue = filter(repositories, repositoryFilters.getChildren); 542 543 if (0 === queue.length) { 544 clearTimeout(timer); 545 report(callback, results); 546 return; 547 } 548 549 numOpenQueries = queue.length; 550 551 for (i = 0; i < queue.length; i++) { 552 queue[i].getChildren(params, process); 553 } 554 }, 555 556 /** 557 * @fixme: Not tested, but the code for this function does not seem to 558 * compute repository.makeClean will be undefined 559 * 560 * @todo: Rewrite this function header comment so that is clearer 561 * 562 * Pass an object, which represents an marked repository to corresponding 563 * repository, so that it can make the content clean (prepare for saving) 564 * 565 * @param {jQuery} obj - representing an editable 566 * @return void 567 */ 568 makeClean: function (obj) { 569 // iterate through all registered repositories 570 var that = this, 571 repository = {}, 572 i = 0, 573 j = that.repositories.length; 574 575 // find all repository tags 576 obj.find('[data-gentics-aloha-repository=' + this.prefix + ']').each(function () { 577 while (i < j) { 578 repository.makeClean(obj); 579 i += 1; 580 } 581 Console.debug(that, 'Passing contents of HTML Element with id { ' + this.attr('id') + ' } for cleaning to repository { ' + repository.repositoryId + ' }'); 582 repository.makeClean(this); 583 }); 584 }, 585 586 /** 587 * Marks an object as repository of this type and with this item.id. 588 * Objects can be any DOM objects as A, SPAN, ABBR, etc. or 589 * special objects such as aloha-aloha_block elements. 590 * 591 * Marks the target obj with two private attributes: 592 * (see http://dev.w3.org/html5/spec/elements.html#embedding-custom-non-visible-data) 593 * - data-gentics-aloha-repository: stores the repositoryId 594 * - data-gentics-aloha-object-id: stores the object.id 595 * 596 * @param {HTMLElement} obj DOM object to mark. 597 * @param {Aloha.Repository.Object} item Item which is applied to obj, 598 * if set to null, the 599 * "data-GENTICS-..." attributes 600 * are removed. 601 */ 602 markObject: function (obj, item) { 603 if (!obj) { 604 return; 605 } 606 607 var manager = this; 608 609 if (item) { 610 var repository = manager.getRepository(item.repositoryId); 611 if (repository) { 612 $(obj).attr({ 613 'data-gentics-aloha-repository': item.repositoryId, 614 'data-gentics-aloha-object-id': item.id 615 }); 616 repository.markObject(obj, item); 617 } else { 618 Console.error(manager, 'Trying to apply a repository "' 619 + item.name 620 + '" to an object, but item has no repositoryId.'); 621 } 622 } else { 623 $(obj).removeAttr('data-gentics-aloha-repository') 624 .removeAttr('data-gentics-aloha-object-id'); 625 } 626 }, 627 628 /** 629 * Get the object for which the given DOM object is marked from the 630 * repository. 631 * 632 * Will initialize the item cache (per repository) if not already done. 633 * 634 * @param {HTMLElement} element DOM object which probably is marked. 635 * @param {function} callback 636 */ 637 getObject: function (element, callback) { 638 var manager = this; 639 var $element = $(element); 640 var itemId = $element.attr('data-gentics-aloha-object-id'); 641 var repositoryId = $element.attr('data-gentics-aloha-repository'); 642 var repository = manager.getRepository(repositoryId); 643 644 if (repository && itemId) { 645 if (!manager.itemCache) { 646 manager.itemCache = []; 647 } 648 649 var cache = manager.itemCache[repositoryId]; 650 if (!cache) { 651 cache = manager.itemCache[repositoryId] = []; 652 } 653 654 if (cache[itemId]) { 655 callback([cache[itemId]]); 656 } else { 657 repository.getObjectById(itemId, function (items) { 658 cache[itemId] = items[0]; 659 callback(items); 660 }); 661 } 662 } 663 }, 664 665 /** 666 * Mark a folder as opened. 667 * 668 * Called by a repository client (eg: repository browser) when a folder 669 * is opened. 670 * 671 * @param {object|Folder} folder Object with property repositoryId. 672 */ 673 folderOpened: function (folder) { 674 var repository = this.getRepository(folder.repositoryId); 675 if (typeof repository.folderOpened === 'function') { 676 repository.folderOpened(folder); 677 } 678 }, 679 680 /** 681 * Mark a folder as closed. 682 * 683 * Called by a repository client (eg: repository browser) when a folder 684 * is closed. 685 * 686 * @param {object|Folder} folder Object with property repositoryId. 687 */ 688 folderClosed: function (folder) { 689 var repository = this.getRepository(folder.repositoryId); 690 if (typeof repository.folderClosed === 'function') { 691 repository.folderClosed(folder); 692 } 693 }, 694 695 /** 696 * Mark a folder as selected. 697 * 698 * Called by a repository client (eg: repository browser) when a folder 699 * is selected. 700 * 701 * @param {object|Folder} folder Object with property repositoryId. 702 */ 703 folderSelected: function (folder) { 704 var repository = this.getRepository(folder.repositoryId); 705 if (typeof repository.folderSelected === 'function') { 706 repository.folderSelected(folder); 707 } 708 }, 709 710 /** 711 * Retrieve the selected folder. 712 * 713 * @return {Folder} Selected folder or null if it cannot be found. 714 */ 715 getSelectedFolder: function () { 716 var repositories = filter(this.repositories, 717 repositoryFilters.getSelectedFolder); 718 var i; 719 var selected; 720 for (i = 0; i < repositories.length; i++) { 721 selected = repositories[i].getSelectedFolder(); 722 if (selected) { 723 return selected; 724 } 725 } 726 return null; 727 }, 728 729 /** 730 * Human readable representation of repository manager. 731 * 732 * @return {string} 733 */ 734 toString: function () { 735 return 'repositorymanager'; 736 } 737 738 }); 739 740 Aloha.RepositoryManager = new RepositoryManager(); 741 742 return Aloha.RepositoryManager; 743 }); 744