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