1 (function (GCN) {
  2 
  3 	'use strict';
  4 
  5 	/**
  6 	 * @const
  7 	 * @type {string} All rendered content blocks come with this prefixed to
  8 	 *                their id.
  9 	 */
 10 	var CONTENT_BLOCK_PREFIX = 'GENTICS_BLOCK_';
 11 
 12 	/**
 13 	 * @type {RexExp} Will match <span id="GENTICS_block_123"></span>" but not
 14 	 *                "<node abc123>" tags.  The first backreference contains
 15 	 *                the tagname of the tag corresponding to this block.
 16 	 */
 17 	var contentBlockRegExp = new RegExp(
 18 			'<(?!node)[a-z]+\\s'        + // "<span or "<div " but not "<node "
 19 				'[^>]*?'                + // ...
 20 				'id\\s*=\\s*[\\"\\\']?' + // "id = '"
 21 				CONTENT_BLOCK_PREFIX    + // "GENTICS_block_"
 22 				'(\\w+)'                + // "123"
 23 				'[\\"\\\']?[^>]*>'      + // "' ...>"
 24 				'<\\s*\\/[a-z]+>',        // "</span>" or "</div>"
 25 			'gim'
 26 		);
 27 
 28 	/**
 29 	 * @private
 30 	 * @type {RegExp} Will match <node foo> or <node bar_123> or <node foo-bar>
 31 	 *                but not <node "blah">.
 32 	 */
 33 	var nodeNotationRegExp = /<node ([a-z0-9_\-]+?)>/gim;
 34 
 35 	/**
 36 	 * Examins a string for "<node>" tags, and for each occurance of this
 37 	 * notation, the given callback will be invoked to manipulate the string.
 38 	 *
 39 	 * @private
 40 	 * @static
 41 	 * @param {string} str The string that will be examined for "<node>" tags.
 42 	 * @param {function} onMatchFound Callback function that should receive the
 43 	 *                                following three parameters:
 44 	 *
 45 	 *                    name:string The name of the tag being notated by the
 46 	 *                                node substring.  If the `str' arguments
 47 	 *                                is "<node myTag>", then the `name' value
 48 	 *                                will be "myTag".
 49 	 *                  offset:number The offset where the node substring was
 50 	 *                                found within the examined string.
 51 	 *                     str:string The string in which the "<node *>"
 52 	 *                                substring occured.
 53 	 *
 54 	 *                                The return value of the function will
 55 	 *                                replace the entire "<node>" substring
 56 	 *                                that was passed to it within the examined
 57 	 *                                string.
 58 	 */
 59 	function replaceNodeTags(str, onMatchFound) {
 60 		var parsed = str.replace(nodeNotationRegExp, function (substr, tagname,
 61 		                                                       offset,
 62 		                                                       examined) {
 63 				return onMatchFound(tagname, offset, examined);
 64 			});
 65 
 66 		return parsed;
 67 	}
 68 
 69 	/**
 70 	 * @class
 71 	 * @name ContentObjectAPI
 72 	 */
 73 	GCN.ContentObjectAPI = GCN.defineChainback({
 74 		/** @lends ContentObjectAPI */
 75 
 76 		/**
 77 		 * @protected
 78 		 * @type {string} A string denoting a content node type.  This value is
 79 		 *                used to compose the correct REST API ajax urls.  The
 80 		 *                following are valid values: "node", "folder",
 81 		 *                "template", "page", "file", "image".
 82 		 */
 83 		_type: null,
 84 
 85 		/**
 86 		 * @protected
 87 		 * @type {object<string,*>} An internal object to store data that we
 88 		 *                          get from the server.
 89 		 */
 90 		_data: {},
 91 
 92 		/**
 93 		 * @protected
 94 		 * @type {object<string,*>} An internal object to store updates to
 95 		 *                          the content object.  Should reflect the
 96 		 *                          structural typography of the `_data'
 97 		 *                          object.
 98 		 */
 99 		_shadow: {},
100 
101 		/**
102 		 * @type {boolean} Flags whether or not data for this content object have
103 		 *                 been fetched from the server.
104 		 */
105 		_fetched: false,
106 
107 		/**
108 		 * @protected
109 		 * @type {object} will contain an objects internal settings
110 		 */
111 		_settings: null,
112 
113 		/**
114 		 * @public
115 		 * @type {Array.<string} Writeable properties for all content objects.
116 		 */
117 		WRITEABLE_PROPS: [],
118 
119 		/**
120 		 * Internal method, to fetch this object's data from the server.
121 		 *
122 		 * @protected
123 		 * @param {function(ContentObjectAPI)=} success Optional callback that
124 		 *                                              receives this object as
125 		 *                                              its only argument.
126 		 * @param {function(GCNError):boolean=} error Optional customer error
127 		 *                                            handler.
128 		 */
129 		'!_read': function (success, error) {
130 			if (this._fetched) {
131 				if (success) {
132 					success(this);
133 				}
134 
135 				return;
136 			}
137 
138 			var that = this;
139 			var id = this.id();
140 
141 			if (id === null || id === undefined) {
142 				this._getIdFromParent(function () {
143 					that._read(success, error);
144 				}, error);
145 
146 				return;
147 			}
148 
149 			var parent = this._ancestor();
150 			var channel = '';
151 
152 			if (GCN.channel()) {
153 				channel = '?nodeId=' + GCN.channel();
154 			}
155 
156 			var ajax = function () {
157 				that._authAjax({
158 					url     : GCN.settings.BACKEND_PATH + '/rest/' +
159 					          that._type + '/load/' + that.id() + channel,
160 					data    : that._loadParams(),
161 					error   : error,
162 					success : function (response) {
163 						that._processResponse(response);
164 						that._fetched = true;
165 
166 						if (success) {
167 							success(that);
168 						}
169 					}
170 				});
171 			};
172 
173 			// If this chainback object has an ancestor, then invoke that
174 			// parent's `_read()' method before fetching the data for this
175 			// chainback object.
176 			if (parent) {
177 				parent._read(ajax, error);
178 			} else {
179 				ajax();
180 			}
181 		},
182 
183 		/**
184 		 * Retrieves this object's id from its parent.  This function is used
185 		 * in order for this object to be able to fetch its data from that
186 		 * backend.
187 		 *
188 		 * @protected
189 		 * @param {function(ContentObjectAPI)=} success Optional callback that
190 		 *                                              receives this object as
191 		 *                                              its only argument.
192 		 * @param {function(GCNError):boolean=} error Optional customer error
193 		 *                                            handler.
194 		 * @throws CANNOT_GET_OBJECT_ID
195 		 */
196 		'!_getIdFromParent': function (success, error) {
197 			var parent = this._ancestor();
198 
199 			if (!parent) {
200 				var err = GCN.createError('CANNOT_GET_OBJECT_ID',
201 					'Cannot get an id for object', this);
202 
203 				GCN.handleError(err, error);
204 
205 				return;
206 			}
207 
208 			var that = this;
209 
210 			parent._read(function () {
211 				if (that._type === 'folder') {
212 					// There are 3 possible property names that an object can
213 					// hold the id of the folder that it is related to:
214 					//
215 					// "folderId": for pages, templates, files, and images.
216 					// "motherId": for folders
217 					// "nodeId":   for nodes
218 					//
219 					// We need to see which of this properties is set, the
220 					// first one we find will be our folder's id.
221 					var props = ['folderId', 'motherId', 'nodeId'];
222 					var prop = props.pop();
223 					var id;
224 
225 					while (prop) {
226 						id = parent.prop(prop);
227 
228 						if (typeof id !== 'undefined') {
229 							break;
230 						}
231 
232 						prop = props.pop();
233 					}
234 
235 					that._data.id = id;
236 				} else {
237 					that._data.id = parent.prop(that._type + 'Id');
238 				}
239 
240 				if (that._data.id === null || typeof that._data.id === 'undefined') {
241 					var err = GCN.createError('CANNOT_GET_OBJECT_ID',
242 						'Cannot get an id for object', this);
243 
244 					GCN.handleError(err, error);
245 
246 					return;
247 				}
248 
249 				that._setHash(that._data.id)._addToCache();
250 
251 				if (success) {
252 					success();
253 				}
254 			}, error);
255 		},
256 
257 		/**
258 		 * Gets this object's id. We'll return the id of the object when it has
259 		 * been loaded. This can only be a localid. Otherwise we'll return the
260 		 * id which was provided by the user. This can either be a localid or a
261 		 * globalid.
262 		 *
263 		 * @name id
264 		 * @function
265 		 * @memberOf ContentObjectAPI
266 		 * @public
267 		 * @return {number}
268 		 */
269 		'!id': function () {
270 			return this._data.id;
271 		},
272 
273 		/**
274 		 * Alias for `id()'
275 		 *
276 		 * @name id
277 		 * @function
278 		 * @memberOf ContentObjectAPI
279 		 * @protected
280 		 * @return {number}
281 		 */
282 		'!localId': function () {
283 			return this.id();
284 		},
285 
286 		/**
287 		 * Update the `_shadow' object that maintains changes to properties
288 		 * that reflected the internal `_data' object.  This shadow object is
289 		 * used to persist differential changes to a REST API object.
290 		 *
291 		 * @protected
292 		 * @param {string} path The path through the object to the property we
293 		 *                      want to modify.
294 		 * @param {*} value The value we wish to set the property to.
295 		 * @param {function=} error Custom error handler.
296 		 * @param {boolean=} force If true, no error will be thrown if `path'
297 		 *                         cannot be fully resolved against the
298 		 *                         internal `_data' object, instead, the path
299 		 *                         will be created on the shadow object.
300 		 */
301 		'!_update': function (pathStr, value, error, force) {
302 			var path = pathStr.split('.');
303 			var shadow = this._shadow;
304 			var actual = this._data;
305 			var i = 0;
306 			var j = path.length;
307 			var pathNode;
308 			// Whether or not the traversal path in `_data' and `_shadow' are
309 			// at the same position in the respective objects.
310 			var areMirrored = true;
311 
312 			while (true) {
313 				pathNode = path[i++];
314 
315 				if (areMirrored) {
316 					actual = actual[pathNode];
317 					areMirrored = jQuery.type(actual) !== 'undefined';
318 				}
319 
320 				if (i === j) {
321 					break;
322 				}
323 
324 				if (shadow[pathNode]) {
325 					shadow = shadow[pathNode];
326 				} else if (areMirrored || force) {
327 					shadow = (shadow[pathNode] = {});
328 				} else {
329 					break; // goto error
330 				}
331 			}
332 
333 			if (i === j && (areMirrored || force)) {
334 				shadow[pathNode] = value;
335 			} else {
336 				var err = GCN.createError('TYPE_ERROR', 'Object "' +
337 					path.slice(0, i).join('.') + '" does not exist',
338 					actual);
339 
340 				GCN.handleError(err, error);
341 			}
342 		},
343 
344 		/**
345 		 * Receives the response from a REST API request, and stores it in the
346 		 * internal `_data' object.
347 		 *
348 		 * @protected
349 		 * @param {object} data Parsed JSON response data.
350 		 */
351 		'!_processResponse': function (data) {
352 			jQuery.extend(this._data, data[this._type]);
353 		},
354 
355 		/**
356 		 * Specifies a list of parameters that will be added to the url when
357 		 * loading the content object from the server.
358 		 *
359 		 * @protected
360 		 * @return {object} object With parameters to be appended to the load
361 		 *                         request
362 		 */
363 		'!_loadParams': function () {},
364 
365 		/**
366 		 * Reads the proporty `property' of this content object if this
367 		 * property is among those in the WRITEABLE_PROPS array.  If a send
368 		 * argument is provided, them the property is updated with that value.
369 		 *
370 		 * @name prop
371 		 * @function
372 		 * @memberOf ContentObjectAPI
373 		 * @param {String} property Name of the property to be read or updated.
374 		 * @param {String} value Value to be set property to.
375 		 * @return {?*} Meta attribute.
376 		 * @throws UNFETCHED_OBJECT_ACCESS
377 		 * @throws READONLY_ATTRIBUTE
378 		 */
379 		'!prop': function (property, value) {
380 			if (!this._fetched) {
381 				GCN.error('UNFETCHED_OBJECT_ACCESS',
382 					'Object not fetched yet.');
383 
384 				return;
385 			}
386 
387 			if (value) {
388 				if (jQuery.inArray(property, this.WRITEABLE_PROPS) >= 0) {
389 					this._update(property, value);
390 				} else {
391 					GCN.error('READONLY_ATTRIBUTE',
392 						'Attribute "' + property + '" of ' + this._type +
393 						' is read-only. Writeable properties are: ' +
394 						this.WRITEABLE_PROPS);
395 				}
396 			}
397 
398 			return ((jQuery.type(this._shadow[property]) !== 'undefined'
399 				? this._shadow : this._data)[property]);
400 		},
401 
402 		/**
403 		 * Sends the a template string to the Aloha Servlet for rendering.
404 		 *
405 		 * @protected
406 		 * @param {string} template Template which will be rendered.
407 		 * @param {string} mode The rendering mode.  Valid values are "view",
408 		 *                      "edit", "pub."
409 		 * @param {function(object)} success callback the receives the render
410 		 *                                   response.
411 		 * @param {function(GCNError):boolean} error Error handler.
412 		 */
413 		'!_renderTemplate' : function (template, mode, success, error) {
414 			var url = GCN.settings.BACKEND_PATH + '/rest/' + this._type +
415 			          '/render/' + this.id();
416 
417 			url += '?edit=' + ('edit' === mode) + '&template=' + template;
418 
419 			this._authAjax({
420 				url     : url,
421 				error   : error,
422 				success : success
423 			});
424 		},
425 
426 		/**
427 		 * Wrapper for internal chainback _ajax method.
428 		 *
429 		 * @protected
430 		 * @param {object<string, *>} settings Settings for the ajax request.
431 		 *                                     The settings object is identical
432 		 *                                     to that of the `GCN.ajax'
433 		 *                                     method, which handles the actual
434 		 *                                     ajax transportation.
435 		 * @throws AJAX_ERROR
436 		 */
437 		'!_ajax': function (settings) {
438 			var that = this;
439 
440 			// force no cache for all API calls
441 			settings.cache = false;
442 			settings.success = (function (onSuccess, onError) {
443 				return function (data) {
444 					// Ajax calls that do not target the REST API servlet do
445 					// not response data with a `responseInfo' object.
446 					// "/CNPortletapp/alohatag" is an example.  So we cannot
447 					// just assume that it exists.
448 					if (data.responseInfo) {
449 						switch (data.responseInfo.responseCode) {
450 						case 'OK':
451 							break;
452 						case 'AUTHREQUIRED':
453 							GCN.clearSession();
454 							that._authAjax(settings);
455 							return;
456 						default:
457 							GCN.handleResponseError(data, onError);
458 							return;
459 						}
460 					}
461 
462 					if (onSuccess) {
463 						onSuccess(data);
464 					}
465 				};
466 			}(settings.success, settings.error, settings.url));
467 
468 			this._queueAjax(settings);
469 		},
470 
471 		/**
472 		 * Similar to `_ajax', except that it prefixes the ajax url with the
473 		 * current session's `sid', and will trigger an
474 		 * `authentication-required' event if the session is not authenticated.
475 		 *
476 		 * @TODO(petro): Consider simplifiying this function signature to read:
477 		 *               `_auth( url, success, error )'
478 		 *
479 		 * @protected
480 		 * @param {object<string, *>} settings Settings for the ajax request.
481 		 * @throws AUTHENTICATION_FAILED
482 		 */
483 		_authAjax: function (settings) {
484 			var that = this;
485 
486 			if (GCN.isAuthenticating) {
487 				GCN.afterNextAuthentication(function () {
488 					that._authAjax(settings);
489 				});
490 
491 				return;
492 			}
493 
494 			if (!GCN.sid) {
495 				var cancel;
496 
497 				if (settings.error) {
498 					cancel = function (error) {
499 						GCN.handleError(
500 							error || GCN.createError('AUTHENTICATION_FAILED'),
501 							settings.error
502 						);
503 					};
504 				} else {
505 					cancel = function (error) {
506 						if (error) {
507 							GCN.error(error.code, error.message, error.data);
508 						} else {
509 							GCN.error('AUTHENTICATION_FAILED');
510 						}
511 					};
512 				}
513 
514 				GCN.afterNextAuthentication(function () {
515 					that._authAjax(settings);
516 				});
517 
518 				if (GCN.usingSSO) {
519 					// First, try to automatically authenticate via
520 					// Single-SignOn
521 					GCN.loginWithSSO(GCN.onAuthenticated, function () {
522 						// ... if SSO fails, then fallback to requesting user
523 						// credentials: broadcast `authentication-required'
524 						// message.
525 						GCN.authenticate(cancel);
526 					});
527 				} else {
528 					// Trigger the `authentication-required' event to request
529 					// user credentials.
530 					GCN.authenticate(cancel);
531 				}
532 
533 				return;
534 			}
535 
536 			// Append "?sid=..." or "&sid=..." if needed.
537 
538 			var isSidInUrl = /[\?\&]sid=/.test(settings.url);
539 			if (!isSidInUrl) {
540 				var isFirstParam = (jQuery.inArray('?', settings.url) === -1);
541 
542 				settings.url += (isFirstParam ? '?' : '&') + 'sid='
543 				             +  (GCN.sid || '');
544 			}
545 
546 			this._ajax(settings);
547 		},
548 
549 		/**
550 		 * Recursively call `_continueWith()'.
551 		 *
552 		 * @protected
553 		 * @override
554 		 */
555 		'!_onContinue': function (success, error) {
556 			var that = this;
557 			this._continueWith(function () {
558 				that._read(success, error);
559 			}, error);
560 		},
561 
562 		/**
563 		 * Initializes this content object.  If a `success' callback is
564 		 * provided, it will cause this object's data to be fetched and passed
565 		 * to the callback.  This object's data will be fetched from the cache
566 		 * if is available, otherwise it will be fetched from the server.  If
567 		 * this content object API contains parent chainbacks, it will get its
568 		 * parent to fetch its own data first.
569 		 *
570 		 * You might also provide an object for initialization, to directly
571 		 * instantiate the object's data without loading it from the server.
572 		 * To do so just pass in a data object as received from the server
573 		 * instead of an id--just make sure this object has an `id' property.
574 		 *
575 		 * If an `error' handler is provided, as the third parameter, it will
576 		 * catch any errors that have occured since the invocation of this
577 		 * call.  It allows the global error handler to be intercepted before
578 		 * stopping the error or allowing it to propagate on to the global
579 		 * handler.
580 		 *
581 		 * @protected
582 		 * @param {number|string|object} id
583 		 * @param {function(ContentObjectAPI))=} success Optional success
584 		 *                                                   callback that will
585 		 *                                                   receive this
586 		 *                                                   object as its only
587 		 *                                                   argument.
588 		 * @param {function(GCNError):boolean=} error Optional custom error
589 		 *                                            handler.
590 		 * @param {object} settings Basic settings for this object.
591 		 * @throws INVALID_DATA If no id is found when providing an object for
592 		 *                      initialization.
593 		 */
594 		_init: function (id, success, error, settings) {
595 			this._settings = settings;
596 
597 			if (jQuery.type(id) === 'object') {
598 				if (!id.id) {
599 					var err = GCN.createError('INVALID_DATA',
600 						'Data not sufficient for initalization: id is missing',
601 						id);
602 
603 					GCN.handleError(err, error);
604 
605 					return;
606 				}
607 
608 				// Clear the old data and populate the _data field using the given object data
609 				this._data = {};
610 				this._data = id;
611 				this._fetched = true;
612 
613 				if (success) {
614 					success(this);
615 				}
616 			} else {
617 				// Ensure that each object has its very own `_data' and
618 				// `_shadow' objects.
619 				if (!this._fetched) {
620 					this._data = {};
621 					this._shadow = {};
622 					this._data.id = id;
623 				}
624 
625 				if (success) {
626 					this._read(success, error);
627 				}
628 			}
629 		},
630 
631 		/**
632 		 * Replaces tag blocks with appropriate "<node *>" notation in a given
633 		 * string.
634 		 *
635 		 * Given an element whose innerHTML is:
636 		 * <pre>
637 		 *		"<span id="GENTICS_BLOCK_123">My Tag</span>",
638 		 * </pre>
639 		 * `encode()' will return:
640 		 * <pre>
641 		 *		"<node 123>".
642 		 * </pre>
643 		 *
644 		 * @name encode
645 		 * @function
646 		 * @memberOf ContentObjectAPI
647 		 * @param {string} HTML string to be encoded.
648 		 * @return {string} The encoded HTML string.
649 		 */
650 		'!encode': function (html) {
651 			var that = this;
652 			var clone = jQuery('<div>' + html + '</div>');
653 
654 			// Empty all content blocks of their innerHTML.
655 			clone.find('[id^=' + CONTENT_BLOCK_PREFIX + ']').html('');
656 
657 			return clone.html().replace(contentBlockRegExp,
658 			                            function (substr, match) {
659 					var tag = that._getTagDataById(match);
660 
661 					// assert(tag)
662 					return tag ? '<node ' + tag.name + '>' : '';
663 				});
664 		},
665 
666 		/**
667 		 * For a given string, replace all occurances of "<node>" with
668 		 * appropriate HTML markup, allowing notated tags to be rendered within
669 		 * the surrounding HTML content.
670 		 *
671 		 * The `success()' handler will receives a string containing the
672 		 * contents of the `str' string with references to "<node>" having been
673 		 * inflated into their appropriate tag rendering.
674 		 *
675 		 * @name decode
676 		 * @function
677 		 * @memberOf ContentObjectAPI
678 		 * @param {string} str The content string, in which  "<node *>" tags
679 		 *                     will be inflated with their HTML rendering.
680 		 * @param {function(ContentObjectAPI))} success Success callback that
681 		 *                                              will receive the
682 		 *                                              decoded string.
683 		 * @param {function(GCNError):boolean=} error Optional custom error
684 		 *                                            handler.
685 		 */
686 		'!decode': function (str, success, error) {
687 			if (!success) {
688 				return;
689 			}
690 
691 			var prefix = 'gcn-tag-placeholder-';
692 			var toRender = [];
693 			var html = replaceNodeTags(str, function (name, offset, str) {
694 				toRender.push('<node ', name, '>');
695 				return '<div id="' + prefix + name + '"></div>';
696 			});
697 
698 			if (!toRender.length) {
699 				success(html);
700 				return;
701 			}
702 
703 			// Instead of rendering each tag individually, we render them
704 			// together in one string, and map the results back into our
705 			// original html string.  This allows us to perform one request to
706 			// the server for any number of node tags found.
707 
708 			var parsed = jQuery('<div>' + html + '</div>');
709 			var template = toRender.join('');
710 			var that = this;
711 
712 			this._renderTemplate(template, 'edit', function (data) {
713 				var content = data.content;
714 				var tag;
715 				var tags = data.tags;
716 				var j = tags.length;
717 				var rendered = jQuery('<div>' + content + '</div>');
718 
719 				var replaceTag = (function (numTags) {
720 					return function (tag) {
721 						parsed.find('#' + prefix + tag.prop('name'))
722 							.replaceWith(
723 								rendered.find('#' + tag.prop('id'))
724 							);
725 
726 						if (0 === --numTags) {
727 							success(parsed.html());
728 						}
729 					};
730 				}(j));
731 
732 				while (j) {
733 					that.tag(tags[--j], replaceTag);
734 				}
735 			}, error);
736 		},
737 
738 		/**
739 		 * Clears this object from its constructor's cache so that the next
740 		 * attempt to access this object will result in a brand new instance
741 		 * being initialized and placed in the cache.
742 		 *
743 		 * @name clear
744 		 * @function
745 		 * @memberOf ContentObjectAPI
746 		 */
747 		'!clear': function () {
748 			// Do not clear the id from the _data.
749 			var id = this._data.id;
750 			this._data = {};
751 			this._data.id = id;
752 			this._shadow = {};
753 			this._fetched = false;
754 			this._clearCache();
755 		},
756 
757 		/**
758 		 * Retreives this objects parent folder.
759 		 *
760 		 * @param {function(ContentObjectAPI)=} success Callback that will
761 		 *                                              receive the requested
762 		 *                                              object.
763 		 * @param {function(GCNError):boolean=} error Custom error handler.
764 		 * @return {ContentObjectAPI)} API object for the retrieved GCN folder.
765 		 */
766 		'!folder': function (success, error) {
767 			return this._continue(GCN.FolderAPI, this._data.folderId, success,
768 				error);
769 		},
770 
771 		/**
772 		 * Saves changes made to this content object to the backend.
773 		 *
774 		 * @param {object=} settings Optional settings to pass on to the ajax
775 		 *                           function.
776 		 * @param {function(ContentObjectAPI)=} success Optional callback that
777 		 *                                              receives this object as
778 		 *                                              its only argument.
779 		 * @param {function(GCNError):boolean=} error Optional customer error
780 		 *                                            handler.
781 		 */
782 		save: function () {
783 			var settings;
784 			var success;
785 			var error;
786 			var args = Array.prototype.slice.call(arguments);
787 			var len = args.length;
788 			var i;
789 
790 			for (i = 0; i < len; ++i) {
791 				switch (jQuery.type(args[i])) {
792 				case 'object':
793 					if (!settings) {
794 						settings = args[i];
795 					}
796 					break;
797 				case 'function':
798 					if (!success) {
799 						success = args[i];
800 					} else {
801 						error = args[i];
802 					}
803 					break;
804 				case 'undefined':
805 					break;
806 				default:
807 					var err = GCN.createError('UNKNOWN_ARGUMENT',
808 						'Don\'t know what to do with arguments[' + i + '] ' +
809 						'value: "' + args[i] + '"', args);
810 					GCN.handleError(err, error);
811 					return;
812 				}
813 			}
814 
815 			this._save(settings, success, error);
816 		},
817 
818 		/**
819 		 * Persists this object's local data onto the server.  If the object
820 		 * has not yet been fetched we need to get it first so we can update
821 		 * its internals properly...
822 		 *
823 		 * @protected
824 		 * @param {object} settings Object which will extend the basic
825 		 *                          settings of the ajax call
826 		 * @param {function(ContentObjectAPI)=} success Optional callback that
827 		 *                                              receives this object as
828 		 *                                              its only argument.
829 		 * @param {function(GCNError):boolean=} error Optional customer error
830 		 *                                            handler.
831 		 */
832 		'!_save': function (settings, success, error) {
833 			var that = this;
834 			this._continueWith(function () {
835 				that._persist(settings, success, error);
836 			}, error);
837 		},
838 
839 		/**
840 		 * Sends the current state of this content object to be stored on the
841 		 * server.
842 		 *
843 		 * @protected
844 		 * @param {function(ContentObjectAPI)=} success Optional callback that
845 		 *                                              receives this object as
846 		 *                                              its only argument.
847 		 * @param {function(GCNError):boolean=} error Optional customer error
848 		 *                                            handler.
849 		 * @throws HTTP_ERROR
850 		 */
851 		'!_persist': function (settings, success, error) {
852 			var that = this;
853 
854 			if (!this._fetched) {
855 				that._read(function () {
856 					that._persist(settings, success, error);
857 				}, error);
858 
859 				return;
860 			}
861 
862 			var json = {};
863 
864 			if (this._deletedTags.length) {
865 				json['delete'] = this._deletedTags;
866 			}
867 
868 			if (this._deletedBlocks.length) {
869 				json['delete'] = json['delete']
870 				               ? json['delete'].concat(this._deletedBlocks)
871 							   : this._deletedBlocks;
872 			}
873 
874 			json[this._type] = this._shadow;
875 			json[this._type].id = this._data.id;
876 
877 			jQuery.extend(json, settings);
878 
879 			this._authAjax({
880 				url     : GCN.settings.BACKEND_PATH + '/rest/' + that._type +
881 				          '/save/' + that.id(),
882 				type    : 'POST',
883 				error   : error,
884 				json    : json,
885 				success : function (response) {
886 					// We must not overwrite the `_data.tags' object with this
887 					// one.
888 					delete that._shadow.tags;
889 
890 					// Everything else in `_shadow' should be written over
891 					// `_data' before resetting the `_shadow' object.
892 					jQuery.extend(that._data, that._shadow);
893 					that._shadow = {};
894 
895 					// Tags have been deleted.
896 					that._deletedTags = [];
897 					that._deletedBlocks = [];
898 
899 					if (success) {
900 						success(that);
901 					}
902 				}
903 			});
904 		},
905 
906 		/**
907 		 * Deletes this content object from its containing parent.
908 		 *
909 		 * @param {function(ContentObjectAPI)=} success Optional callback that
910 		 *                                              receives this object as
911 		 *                                              its only argument.
912 		 * @param {function(GCNError):boolean=} error Optional customer error
913 		 *                                            handler.
914 		 */
915 		remove: function (success, error) {
916 			this._remove(success, error);
917 		},
918 
919 		/**
920 		 * Performs a REST API request to delete this object from the server.
921 		 *
922 		 * @protected
923 		 * @param {function(ContentObjectAPI)=} success Optional callback that
924 		 *                                              will be invoked once
925 		 *                                              this object has been
926 		 *                                              removed.
927 		 * @param {function(GCNError):boolean=} error Optional customer error
928 		 *                                            handler.
929 		 */
930 		'!_remove': function (success, error) {
931 			var that = this;
932 			this._authAjax({
933 				url     : GCN.settings.BACKEND_PATH + '/rest/' + that._type +
934 					      '/delete/' + that.id(),
935 				type    : 'POST',
936 				error   : error,
937 				success : function (response) {
938 					// Clean cache & reset object to make sure it can't be used
939 					// properly any more.
940 					that._clearCache();
941 					that._data = {};
942 					that._shadow = {};
943 
944 					// Don't forward the object to the success handler since
945 					// it's been deleted.
946 					if (success) {
947 						success();
948 					}
949 				}
950 			});
951 		}
952 
953 	});
954 
955 	/**
956 	 * Generates a factory method for chainback classes.  The method signature
957 	 * used with this factory function will match that of the target class'
958 	 * constructor.  Therefore this function is expected to be invoked with the
959 	 * follow combination of arguments ...
960 	 *
961 	 * Examples for GCN.pages api:
962 	 *
963 	 * To get an array containing 1 page:
964 	 * pages(1)
965 	 * pages(1, function () {})
966 	 *
967 	 * To get an array containing 2 pages:
968 	 * pages([1, 2])
969 	 * pages([1, 2], function () {})
970 	 *
971 	 * To get an array containing any and all pages:
972 	 * pages()
973 	 * pages(function () {})
974 	 *
975 	 * To get an array containing no pages:
976 	 * pages([])
977 	 * pages([], function () {});
978 	 *
979 	 * @param {Chainback} clazz The Chainback class we want to expose.
980 	 * @throws UNKNOWN_ARGUMENT
981 	 */
982 	GCN.exposeAPI = function (clazz) {
983 		return function () {
984 			// Convert arguments into an array
985 			// https://developer.mozilla.org/en/JavaScript/Reference/...
986 			// ...Functions_and_function_scope/arguments
987 			var args = Array.prototype.slice.call(arguments);
988 			var id;
989 			var ids;
990 			var success;
991 			var error;
992 			var settings;
993 
994 			// iterate over arguments to find id || ids, succes, error and
995 			// settings
996 			jQuery.each(args, function (i, arg) {
997 				switch (jQuery.type(arg)) {
998 				// set id
999 				case 'string':
1000 				case 'number':
1001 					if (!id && !ids) {
1002 						id = arg;
1003 					} else {
1004 						GCN.error('UNKNOWN_ARGUMENT',
1005 							'id is already set. Don\'t know what to do with ' +
1006 							'arguments[' + i + '] value: "' + arg + '"');
1007 					}
1008 					break;
1009 				// set ids
1010 				case 'array':
1011 					if (!id && !ids) {
1012 						ids = args[0];
1013 					} else {
1014 						GCN.error('UNKNOWN_ARGUMENT',
1015 							'ids is already set. Don\'t know what to do with' +
1016 							' arguments[' + i + '] value: "' + arg + '"');
1017 					}
1018 					break;
1019 				// success and error handlers
1020 				case 'function':
1021 					if (!success) {
1022 						success = arg;
1023 					} else if (success && !error) {
1024 						error = arg;
1025 					} else {
1026 						GCN.error('UNKNOWN_ARGUMENT',
1027 							'success and error handler already set. Don\'t ' +
1028 							'know what to do with arguments[' + i + ']');
1029 					}
1030 					break;
1031 				// settings
1032 				case 'object':
1033 					if (!id && !ids) {
1034 						id = arg;
1035 					} else if (!settings) {
1036 						settings = arg;
1037 					} else {
1038 						GCN.error('UNKNOWN_ARGUMENT',
1039 							'settings are already present. Don\'t know what ' +
1040 							'to do with arguments[' + i + '] value:' + ' "' +
1041 							arg + '"');
1042 					}
1043 					break;
1044 				default:
1045 					GCN.error('UNKNOWN_ARGUMENT',
1046 						'Don\'t know what to do with arguments[' + i +
1047 						'] value: "' + arg + '"');
1048 				}
1049 			});
1050 
1051 			// Prepare a new set of arguments to pass on during initialzation
1052 			// of callee object.
1053 			args = [];
1054 
1055 			// settings should always be an object, even if it's just empty
1056 			if (!settings) {
1057 				settings = {};
1058 			}
1059 
1060 			args[0] = (typeof id !== 'undefined') ? id : ids;
1061 			args[1] = success || settings.success || null;
1062 			args[2] = error || settings.error || null;
1063 			args[3] = settings;
1064 
1065 			var hash = (id || ids)
1066 			         ? clazz._makeHash(ids ? ids.sort().join(',') : id)
1067 					 : null;
1068 
1069 			return GCN.getChainback(clazz, hash, null, args);
1070 		};
1071 
1072 	};
1073 
1074 }(GCN));
1075