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