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