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 = 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 293 // If none of the repositories implemented the query method, then 294 // don't wait for the timeout, simply report to the client 295 if ( numOpenCallbacks === 0 ) { 296 this.queryCallback( callback, allitems, allmetainfo, timer ); 297 } 298 }, 299 300 /** 301 * Passes all the results we have collected to the client through the 302 * callback it specified 303 * 304 * @param {Function} callback - Callback specified by client when 305 * invoking the query method 306 * @param {Array} items - Results, collected from all repositories 307 * @param {Object<String,Number>} metainfo - optional object containing metainfo 308 * @param {Timer} timer - We need to clear this timer 309 * @return void 310 * @hide 311 */ 312 queryCallback: function( callback, items, metainfo, timer ) { 313 if ( timer ) { 314 clearTimeout( timer ); 315 timer = undefined; 316 } 317 318 // TODO: Implement sorting based on repository specification 319 // sort items by weight 320 //items.sort( function( a, b ) { 321 // return ( b.weight || 0 ) - ( a.weight || 0 ); 322 //} ); 323 324 // prepare result data for the JSON Reader 325 var result = { 326 items : items, 327 results : items.length 328 }; 329 330 if ( metainfo ) { 331 result.numItems = metainfo.numItems; 332 result.hasMoreItems = metainfo.hasMoreItems; 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 jQuery.merge( allitems, items ); 376 377 if ( --numOpenCallbacks === 0 ) { 378 that.getChildrenCallback( callback, allitems, timer ); 379 } 380 }; 381 382 // If the inFolderId is the default id of 'aloha', then return all 383 // registered repositories 384 if ( params.inFolderId === 'aloha' ) { 385 var repoFilter = params.repositoryFilter, 386 hasRepoFilter = ( repoFilter && repoFilter.length ); 387 388 j = this.repositories.length; 389 390 391 for ( i = 0; i < j; ++i ) { 392 repo = this.repositories[ i ]; 393 if ( !hasRepoFilter || jQuery.inArray( repo.repositoryId, repoFilter ) > -1 ) { 394 repositories.push( 395 new Aloha.RepositoryFolder( { 396 id : repo.repositoryId, 397 name : repo.repositoryName, 398 repositoryId : repo.repositoryId, 399 type : 'repository', 400 hasMoreItems : true 401 } ) 402 ); 403 } 404 } 405 406 that.getChildrenCallback( callback, repositories, null ); 407 408 return; 409 } else { 410 repositories = this.repositories; 411 } 412 413 var timeout = parseInt( params.timeout, 10 ) || this.settings.timeout; 414 timer = setTimeout( function() { 415 numOpenCallbacks = 0; 416 that.getChildrenCallback( callback, allitems, timer ); 417 }, timeout ); 418 419 j = repositories.length; 420 421 for ( i = 0; i < j; ++i ) { 422 repo = repositories[ i ]; 423 424 if ( typeof repo.getChildren === 'function' ) { 425 ++numOpenCallbacks; 426 427 repo.getChildren( 428 params, 429 function() { 430 processResults.apply( repo, arguments ); 431 } 432 ); 433 } 434 } 435 436 if ( numOpenCallbacks === 0 ) { 437 this.getChildrenCallback( callback, allitems, timer ); 438 } 439 }, 440 441 /** 442 * Returns results for getChildren to calling client 443 * 444 * @return void 445 * @hide 446 */ 447 getChildrenCallback: function( callback, items, timer ) { 448 if ( timer ) { 449 clearTimeout( timer ); 450 timer = undefined; 451 } 452 453 callback.call( this, items ); 454 }, 455 456 /** 457 * @fixme: Not tested, but the code for this function does not seem to 458 * compute repository.makeClean will be undefined 459 * 460 * @todo: Rewrite this function header comment so that is clearer 461 * 462 * Pass an object, which represents an marked repository to corresponding 463 * repository, so that it can make the content clean (prepare for saving) 464 * 465 * @param {jQuery} obj - representing an editable 466 * @return void 467 */ 468 makeClean: function( obj ) { 469 // iterate through all registered repositories 470 var that = this, 471 repository = {}, 472 i = 0, 473 j = that.repositories.length; 474 475 // find all repository tags 476 obj.find( '[data-gentics-aloha-repository=' + this.prefix + ']' ) 477 .each( function() { 478 for ( ; i < j; ++i ) { 479 repository.makeClean( obj ); 480 } 481 console.debug( that, 482 'Passing contents of HTML Element with id { ' + 483 this.attr( 'id' ) + ' } for cleaning to repository { ' + 484 repository.repositoryId + ' }' ); 485 repository.makeClean( this ); 486 } ); 487 }, 488 489 /** 490 * Marks an object as repository of this type and with this item.id. 491 * Objects can be any DOM objects as A, SPAN, ABBR, etc. or 492 * special objects such as aloha-aloha_block elements. 493 * This method marks the target obj with two private attributes: 494 * (see http://dev.w3.org/html5/spec/elements.html#embedding-custom-non-visible-data) 495 * * data-gentics-aloha-repository: stores the repositoryId 496 * * data-gentics-aloha-object-id: stores the object.id 497 * 498 * @param {DOMObject} obj - DOM object to mark 499 * @param {Aloha.Repository.Object} item - the item which is applied to obj, 500 * if set to null, the data-GENTICS-... attributes are removed 501 * @return void 502 */ 503 markObject: function( obj, item ) { 504 if ( !obj ) { 505 return; 506 } 507 508 if ( item ) { 509 var repository = this.getRepository( item.repositoryId ); 510 511 if ( repository ) { 512 jQuery( obj ).attr( { 513 'data-gentics-aloha-repository' : item.repositoryId, 514 'data-gentics-aloha-object-id' : item.id 515 } ); 516 517 repository.markObject( obj, item ); 518 } else { 519 console.error( this, 520 'Trying to apply a repository { ' + item.name + 521 ' } to an object, but item has no repositoryId.' ); 522 } 523 } else { 524 jQuery( obj ) 525 .removeAttr( 'data-gentics-aloha-repository' ) 526 .removeAttr( 'data-gentics-aloha-object-id' ); 527 } 528 }, 529 530 /** 531 * Get the object for which the given DOM object is marked from the 532 * repository. 533 * 534 * @param {DOMObject} obj - DOM object which probably is marked 535 * @param {Function} callback - callback function 536 */ 537 getObject: function( obj, callback ) { 538 var that = this, 539 $obj = jQuery( obj ), 540 repository = this.getRepository( $obj.attr( 'data-gentics-aloha-repository' ) ), 541 itemId = $obj.attr( 'data-gentics-aloha-object-id' ); 542 543 if ( repository && itemId ) { 544 // initialize the item cache (per repository) if not already done 545 this.itemCache = this.itemCache || []; 546 this.itemCache[ repository.repositoryId ] = this.itemCache[ repository.repositoryId ] || []; 547 548 // when the item is cached, we just call the callback method 549 if ( this.itemCache[ repository.repositoryId ][ itemId ] ) { 550 callback.call( this, [ this.itemCache[ repository.repositoryId ][ itemId ] ] ); 551 } else { 552 // otherwise we get the object from the repository 553 repository.getObjectById( itemId, function( items ) { 554 // make sure the item is in the cache (for subsequent calls) 555 that.itemCache[ repository.repositoryId ][ itemId ] = items[0]; 556 callback.call( this, items ); 557 } ); 558 } 559 } 560 }, 561 562 /** 563 * @return {String} name of repository manager object 564 */ 565 toString: function() { 566 return 'repositorymanager'; 567 } 568 569 } ); 570 571 Aloha.RepositoryManager = new Aloha.RepositoryManager(); 572 573 // We return the constructor, not the instance of Aloha.RepositoryManager 574 return Aloha.RepositoryManager; 575 } ); 576