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