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