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 * Returns the repository object identified by repositoryId. 105 * 106 * @param {String} repositoryId - the name of the repository 107 * @return {?Aloha.Repository} a repository or null if name not found 108 */ 109 getRepository: function( repositoryId ) { 110 var repositories = this.repositories, 111 i = 0, 112 j = repositories.length; 113 114 for ( ; i < j; ++i ) { 115 if ( repositories[ i ].repositoryId === repositoryId ) { 116 return repositories[ i ]; 117 } 118 } 119 120 return null; 121 }, 122 123 /** 124 * Searches a all repositories for repositoryObjects matching query and 125 * repositoryObjectType. 126 * 127 <pre><code> 128 var params = { 129 queryString: 'hello', 130 objectTypeFilter: ['website'], 131 filter: null, 132 inFolderId: null, 133 orderBy: null, 134 maxItems: null, 135 skipCount: null, 136 renditionFilter: null, 137 repositoryId: null 138 }; 139 Aloha.RepositoryManager.query( params, function( items ) { 140 // do something with the result items 141 console.log(items); 142 }); 143 </code></pre> 144 * 145 * @param {Object <String,Mixed>} params object with properties 146 * <div class="mdetail-params"><ul> 147 * <li><code> queryString</code> : String <div class="sub-desc">The query string for full text search</div></li> 148 * <li><code> objectTypeFilter</code> : array (optional) <div class="sub-desc">Object types that will be returned.</div></li> 149 * <li><code> filter</code> : array (optional) <div class="sub-desc">Attributes that will be returned.</div></li> 150 * <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> 151 * <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> 152 * <li><code> orderBy</code> : array (optional) <div class="sub-desc">ex. [{lastModificationDate:’DESC’, name:’ASC’}]</div></li> 153 * <li><code> maxItems</code> : Integer (optional) <div class="sub-desc">number items to return as result</div></li> 154 * <li><code> skipCount</code> : Integer (optional) <div class="sub-desc">This is tricky in a merged multi repository scenario</div></li> 155 * <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> 156 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 * nb: "this" is reference to the calling repository. 196 * 197 * @param {Array} items - Results returned by the repository 198 * @param {Object<String,Number>} metainfo - optional Metainfo returned by the repository 199 */ 200 processResults = function( items, metainfo ) { 201 if ( numOpenCallbacks === 0 ) { 202 return; 203 } 204 205 var j = items ? items.length : 0; 206 207 if ( j ) { 208 // Add the repositoryId for each item if a negligent 209 // repository did not do so. 210 if ( !items[0].repositoryId ) { 211 var repoId = this.repositoryId, 212 i; 213 for ( i = 0; i < j; ++i ) { 214 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 237 } else { 238 // at least one repository did not return metainfo, so 239 // we have no aggregated metainfo at all 240 allmetainfo = undefined; 241 } 242 console.debug(this, "The repository " + this.repositoryId + " returned with " + j + " results."); 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 * Passes all the results we have collected to the client through the 312 * callback it specified 313 * 314 * @param {Function} callback - Callback specified by client when 315 * invoking the query method 316 * @param {Array} items - Results, collected from all repositories 317 * @param {Object<String,Number>} metainfo - optional object containing metainfo 318 * @param {Timer} timer - We need to clear this timer 319 * @return void 320 * @hide 321 */ 322 queryCallback: function( callback, items, metainfo, timer ) { 323 if ( timer ) { 324 clearTimeout( timer ); 325 timer = undefined; 326 } 327 328 // TODO: Implement sorting based on repository specification 329 // sort items by weight 330 //items.sort( function( a, b ) { 331 // return ( b.weight || 0 ) - ( a.weight || 0 ); 332 //} ); 333 334 // prepare result data for the JSON Reader 335 var result = { 336 items : items, 337 results : items.length 338 }; 339 340 if ( metainfo ) { 341 result.numItems = metainfo.numItems; 342 result.hasMoreItems = metainfo.hasMoreItems; 343 } 344 345 callback.call( this, result ); 346 }, 347 348 /** 349 * @todo: This method needs to be covered with some unit tests 350 * 351 * Returns children items. (see query for an example) 352 * @param {Object<String,Mixed>} params - object with properties 353 * <div class="mdetail-params"><ul> 354 * <li><code> objectTypeFilter</code> : array (optional) <div class="sub-desc">Object types that will be returned.</div></li> 355 * <li><code> filter</code> : array (optional) <div class="sub-desc">Attributes that will be returned.</div></li> 356 * <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> 357 * <li><code> orderBy</code> : array (optional) <div class="sub-desc">ex. [{lastModificationDate:’DESC’, name:’ASC’}]</div></li> 358 * <li><code> maxItems</code> : Integer (optional) <div class="sub-desc">number items to return as result</div></li> 359 * <li><code> skipCount</code> : Integer (optional) <div class="sub-desc">This is tricky in a merged multi repository scenario</div></li> 360 * <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> 361 * </ul></div> 362 * @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. 363 * "items" is an Array of objects construced with Document/Folder. 364 * @void 365 */ 366 getChildren: function( params, callback ) { 367 var that = this, 368 repo, 369 // The marged results, collected from repository responses 370 allitems = [], 371 // The set of repositories towhich we want to delegate work 372 repositories = [], 373 // A counting semaphore (working in reverse, ie: 0 means free) 374 numOpenCallbacks = 0, 375 // When this timer times-out, whatever has been collected in 376 // allitems will be returned to the calling client, and 377 // numOpenCallbacks will be reset to 0 378 timer, 379 i, j, 380 processResults = function( items ) { 381 if ( numOpenCallbacks === 0 ) { 382 return; 383 } 384 385 if (allitems && items) { 386 jQuery.merge( allitems, items ); 387 } 388 389 if ( --numOpenCallbacks === 0 ) { 390 that.getChildrenCallback( callback, allitems, timer ); 391 } 392 }; 393 394 // If the inFolderId is the default id of 'aloha', then return all 395 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 if ( numOpenCallbacks === 0 ) { 449 this.getChildrenCallback( callback, allitems, timer ); 450 } 451 }, 452 453 /** 454 * Returns results for getChildren to calling client 455 * 456 * @return void 457 * @hide 458 */ 459 getChildrenCallback: function( callback, items, timer ) { 460 if ( timer ) { 461 clearTimeout( timer ); 462 timer = undefined; 463 } 464 465 callback.call( this, items ); 466 }, 467 468 /** 469 * @fixme: Not tested, but the code for this function does not seem to 470 * compute repository.makeClean will be undefined 471 * 472 * @todo: Rewrite this function header comment so that is clearer 473 * 474 * Pass an object, which represents an marked repository to corresponding 475 * repository, so that it can make the content clean (prepare for saving) 476 * 477 * @param {jQuery} obj - representing an editable 478 * @return void 479 */ 480 makeClean: function( obj ) { 481 // iterate through all registered repositories 482 var that = this, 483 repository = {}, 484 i = 0, 485 j = that.repositories.length; 486 487 // find all repository tags 488 obj.find( '[data-gentics-aloha-repository=' + this.prefix + ']' ) 489 .each( function() { 490 for ( ; i < j; ++i ) { 491 repository.makeClean( obj ); 492 } 493 console.debug( that, 494 'Passing contents of HTML Element with id { ' + 495 this.attr( 'id' ) + ' } for cleaning to repository { ' + 496 repository.repositoryId + ' }' ); 497 repository.makeClean( this ); 498 } ); 499 }, 500 501 /** 502 * Marks an object as repository of this type and with this item.id. 503 * Objects can be any DOM objects as A, SPAN, ABBR, etc. or 504 * special objects such as aloha-aloha_block elements. 505 * This method marks the target obj with two private attributes: 506 * (see http://dev.w3.org/html5/spec/elements.html#embedding-custom-non-visible-data) 507 * * data-gentics-aloha-repository: stores the repositoryId 508 * * data-gentics-aloha-object-id: stores the object.id 509 * 510 * @param {DOMObject} obj - DOM object to mark 511 * @param {Aloha.Repository.Object} item - the item which is applied to obj, 512 * if set to null, the data-GENTICS-... attributes are removed 513 * @return void 514 */ 515 markObject: function( obj, item ) { 516 if ( !obj ) { 517 return; 518 } 519 520 if ( item ) { 521 var repository = this.getRepository( item.repositoryId ); 522 523 if ( repository ) { 524 jQuery( obj ).attr( { 525 'data-gentics-aloha-repository' : item.repositoryId, 526 'data-gentics-aloha-object-id' : item.id 527 } ); 528 529 repository.markObject( obj, item ); 530 } else { 531 console.error( this, 532 'Trying to apply a repository { ' + item.name + 533 ' } to an object, but item has no repositoryId.' ); 534 } 535 } else { 536 jQuery( obj ) 537 .removeAttr( 'data-gentics-aloha-repository' ) 538 .removeAttr( 'data-gentics-aloha-object-id' ); 539 } 540 }, 541 542 /** 543 * Get the object for which the given DOM object is marked from the 544 * repository. 545 * 546 * @param {DOMObject} obj - DOM object which probably is marked 547 * @param {Function} callback - callback function 548 */ 549 getObject: function( obj, callback ) { 550 var that = this, 551 $obj = jQuery( obj ), 552 repository = this.getRepository( $obj.attr( 'data-gentics-aloha-repository' ) ), 553 itemId = $obj.attr( 'data-gentics-aloha-object-id' ); 554 555 if ( repository && itemId ) { 556 // initialize the item cache (per repository) if not already done 557 this.itemCache = this.itemCache || []; 558 this.itemCache[ repository.repositoryId ] = this.itemCache[ repository.repositoryId ] || []; 559 560 // when the item is cached, we just call the callback method 561 if ( this.itemCache[ repository.repositoryId ][ itemId ] ) { 562 callback.call( this, [ this.itemCache[ repository.repositoryId ][ itemId ] ] ); 563 } else { 564 // otherwise we get the object from the repository 565 repository.getObjectById( itemId, function( items ) { 566 // make sure the item is in the cache (for subsequent calls) 567 that.itemCache[ repository.repositoryId ][ itemId ] = items[0]; 568 callback.call( this, items ); 569 } ); 570 } 571 } 572 }, 573 574 /** 575 * @return {String} name of repository manager object 576 */ 577 toString: function() { 578 return 'repositorymanager'; 579 } 580 581 } ); 582 583 Aloha.RepositoryManager = new Aloha.RepositoryManager(); 584 585 // We return the constructor, not the instance of Aloha.RepositoryManager 586 return Aloha.RepositoryManager; 587 } ); 588