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