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