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