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 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 numOpenCallbacks = 0; 250 that.queryCallback( callback, allitems, allmetainfo, timer ); 251 }, timeout ); 252 253 // If repositoryId or a list of repository ids, is not specified in 254 // the params object, then we will query all registered 255 // repositories 256 if ( params.repositoryId ) { 257 repositories.push( this.getRepository( params.repositoryId ) ); 258 } else { 259 repositories = this.repositories; 260 } 261 262 j = repositories.length; 263 264 var repoQueue = []; 265 266 // We need to know how many callbacks we will open before invoking 267 // the query method on each, so that as soon as the first one does 268 // callback, the correct number of open callbacks will be available 269 // to check. 270 271 for ( i = 0; i < j; ++i ) { 272 repo = repositories[ i ]; 273 274 if ( typeof repo.query === 'function' ) { 275 ++numOpenCallbacks; 276 repoQueue.push( repo ); 277 } 278 } 279 280 j = repoQueue.length; 281 282 for ( i = 0; i < j; ++i ) { 283 repo = repoQueue[ i ]; 284 repo.query( 285 params, 286 function() { 287 processResults.apply( repo, arguments ); 288 } 289 ); 290 } 291 292 // If none of the repositories implemented the query method, then 293 // don't wait for the timeout, simply report to the client 294 if ( numOpenCallbacks === 0 ) { 295 this.queryCallback( callback, allitems, allmetainfo, timer ); 296 } 297 }, 298 299 /** 300 * Passes all the results we have collected to the client through the 301 * callback it specified 302 * 303 * @param {Function} callback - Callback specified by client when 304 * invoking the query method 305 * @param {Array} items - Results, collected from all repositories 306 * @param {Object<String,Number>} metainfo - optional object containing metainfo 307 * @param {Timer} timer - We need to clear this timer 308 * @return void 309 * @hide 310 */ 311 queryCallback: function( callback, items, metainfo, timer ) { 312 if ( timer ) { 313 clearTimeout( timer ); 314 timer = undefined; 315 } 316 317 // TODO: Implement sorting based on repository specification 318 // sort items by weight 319 //items.sort( function( a, b ) { 320 // return ( b.weight || 0 ) - ( a.weight || 0 ); 321 //} ); 322 323 // prepare result data for the JSON Reader 324 var result = { 325 items : items, 326 results : items.length 327 }; 328 329 if ( metainfo ) { 330 result.numItems = metainfo.numItems; 331 result.hasMoreItems = metainfo.hasMoreItems; 332 333 } 334 335 callback.call( this, result ); 336 }, 337 338 /** 339 * @todo: This method needs to be covered with some unit tests 340 * 341 * Returns children items. (see query for an example) 342 * @param {Object<String,Mixed>} params - object with properties 343 * <div class="mdetail-params"><ul> 344 * <li><code> objectTypeFilter</code> : array (optional) <div class="sub-desc">Object types that will be returned.</div></li> 345 * <li><code> filter</code> : array (optional) <div class="sub-desc">Attributes that will be returned.</div></li> 346 * <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> 347 * <li><code> orderBy</code> : array (optional) <div class="sub-desc">ex. [{lastModificationDate:’DESC’, name:’ASC’}]</div></li> 348 * <li><code> maxItems</code> : Integer (optional) <div class="sub-desc">number items to return as result</div></li> 349 * <li><code> skipCount</code> : Integer (optional) <div class="sub-desc">This is tricky in a merged multi repository scenario</div></li> 350 * <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> 351 * </ul></div> 352 * @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. 353 * "items" is an Array of objects construced with Document/Folder. 354 * @void 355 */ 356 getChildren: function( params, callback ) { 357 var that = this, 358 repo, 359 // The marged results, collected from repository responses 360 allitems = [], 361 // The set of repositories towhich we want to delegate work 362 repositories = [], 363 // A counting semaphore (working in reverse, ie: 0 means free) 364 numOpenCallbacks = 0, 365 // When this timer times-out, whatever has been collected in 366 // allitems will be returned to the calling client, and 367 // numOpenCallbacks will be reset to 0 368 timer, 369 i, j, 370 processResults = function( items ) { 371 if ( numOpenCallbacks === 0 ) { 372 return; 373 } 374 375 if (allitems && items) { 376 jQuery.merge( allitems, items ); 377 } 378 379 if ( --numOpenCallbacks === 0 ) { 380 that.getChildrenCallback( callback, allitems, timer ); 381 } 382 }; 383 384 // If the inFolderId is the default id of 'aloha', then return all 385 // registered repositories 386 if ( params.inFolderId === 'aloha' ) { 387 var repoFilter = params.repositoryFilter, 388 hasRepoFilter = ( repoFilter && repoFilter.length ); 389 390 j = this.repositories.length; 391 392 for ( i = 0; i < j; ++i ) { 393 repo = this.repositories[ i ]; 394 if ( !hasRepoFilter || jQuery.inArray( repo.repositoryId, repoFilter ) > -1 ) { 395 repositories.push( 396 new Aloha.RepositoryFolder( { 397 id : repo.repositoryId, 398 name : repo.repositoryName, 399 repositoryId : repo.repositoryId, 400 type : 'repository', 401 hasMoreItems : true 402 } ) 403 ); 404 } 405 } 406 407 that.getChildrenCallback( callback, repositories, null ); 408 409 410 return; 411 } else { 412 repositories = this.repositories; 413 } 414 415 var timeout = parseInt( params.timeout, 10 ) || this.settings.timeout; 416 timer = window.setTimeout( function() { 417 numOpenCallbacks = 0; 418 that.getChildrenCallback( callback, allitems, timer ); 419 }, timeout ); 420 421 j = repositories.length; 422 423 for ( i = 0; i < j; ++i ) { 424 repo = repositories[ i ]; 425 426 if ( typeof repo.getChildren === 'function' ) { 427 ++numOpenCallbacks; 428 429 repo.getChildren( 430 params, 431 function() { 432 processResults.apply( repo, arguments ); 433 } 434 ); 435 } 436 } 437 438 if ( numOpenCallbacks === 0 ) { 439 this.getChildrenCallback( callback, allitems, timer ); 440 } 441 }, 442 443 /** 444 * Returns results for getChildren to calling client 445 * 446 * @return void 447 * @hide 448 */ 449 getChildrenCallback: function( callback, items, timer ) { 450 if ( timer ) { 451 clearTimeout( timer ); 452 timer = undefined; 453 } 454 455 callback.call( this, items ); 456 }, 457 458 /** 459 * @fixme: Not tested, but the code for this function does not seem to 460 * compute repository.makeClean will be undefined 461 * 462 * @todo: Rewrite this function header comment so that is clearer 463 * 464 * Pass an object, which represents an marked repository to corresponding 465 * repository, so that it can make the content clean (prepare for saving) 466 * 467 * @param {jQuery} obj - representing an editable 468 * @return void 469 */ 470 makeClean: function( obj ) { 471 // iterate through all registered repositories 472 var that = this, 473 repository = {}, 474 i = 0, 475 476 j = that.repositories.length; 477 478 // find all repository tags 479 obj.find( '[data-gentics-aloha-repository=' + this.prefix + ']' ) 480 .each( function() { 481 for ( ; i < j; ++i ) { 482 repository.makeClean( obj ); 483 } 484 console.debug( that, 485 'Passing contents of HTML Element with id { ' + 486 this.attr( 'id' ) + ' } for cleaning to repository { ' + 487 repository.repositoryId + ' }' ); 488 repository.makeClean( this ); 489 } ); 490 }, 491 492 /** 493 * Marks an object as repository of this type and with this item.id. 494 * Objects can be any DOM objects as A, SPAN, ABBR, etc. or 495 * special objects such as aloha-aloha_block elements. 496 * This method marks the target obj with two private attributes: 497 * (see http://dev.w3.org/html5/spec/elements.html#embedding-custom-non-visible-data) 498 * * data-gentics-aloha-repository: stores the repositoryId 499 * * data-gentics-aloha-object-id: stores the object.id 500 * 501 * @param {DOMObject} obj - DOM object to mark 502 * @param {Aloha.Repository.Object} item - the item which is applied to obj, 503 * if set to null, the data-GENTICS-... attributes are removed 504 * @return void 505 */ 506 markObject: function( obj, item ) { 507 if ( !obj ) { 508 return; 509 } 510 511 if ( item ) { 512 var repository = this.getRepository( item.repositoryId ); 513 514 if ( repository ) { 515 jQuery( obj ).attr( { 516 'data-gentics-aloha-repository' : item.repositoryId, 517 'data-gentics-aloha-object-id' : item.id 518 } ); 519 520 repository.markObject( obj, item ); 521 } else { 522 console.error( this, 523 'Trying to apply a repository { ' + item.name + 524 ' } to an object, but item has no repositoryId.' ); 525 } 526 } else { 527 jQuery( obj ) 528 .removeAttr( 'data-gentics-aloha-repository' ) 529 .removeAttr( 'data-gentics-aloha-object-id' ); 530 } 531 }, 532 533 /** 534 * Get the object for which the given DOM object is marked from the 535 * repository. 536 * 537 * @param {DOMObject} obj - DOM object which probably is marked 538 * @param {Function} callback - callback function 539 */ 540 getObject: function( obj, callback ) { 541 var that = this, 542 $obj = jQuery( obj ), 543 repository = this.getRepository( $obj.attr( 'data-gentics-aloha-repository' ) ), 544 itemId = $obj.attr( 'data-gentics-aloha-object-id' ); 545 546 if ( repository && itemId ) { 547 // initialize the item cache (per repository) if not already done 548 this.itemCache = this.itemCache || []; 549 this.itemCache[ repository.repositoryId ] = this.itemCache[ repository.repositoryId ] || []; 550 551 // when the item is cached, we just call the callback method 552 if ( this.itemCache[ repository.repositoryId ][ itemId ] ) { 553 callback.call( this, [ this.itemCache[ repository.repositoryId ][ itemId ] ] ); 554 } else { 555 // otherwise we get the object from the repository 556 repository.getObjectById( itemId, function( items ) { 557 // make sure the item is in the cache (for subsequent calls) 558 that.itemCache[ repository.repositoryId ][ itemId ] = items[0]; 559 callback.call( this, items ); 560 } ); 561 } 562 } 563 }, 564 565 /** 566 * @return {String} name of repository manager object 567 */ 568 toString: function() { 569 return 'repositorymanager'; 570 } 571 572 } ); 573 574 Aloha.RepositoryManager = new Aloha.RepositoryManager(); 575 576 // We return the constructor, not the instance of Aloha.RepositoryManager 577 return Aloha.RepositoryManager; 578 } ); 579