1 (function (GCN) {
  2 
  3 	'use strict';
  4 
  5 	/**
  6 	 * Updates the internal data of the given content object.
  7 	 *
  8 	 * This function extends and overwrites properties of the instances's
  9 	 * internal data structure.  No property is deleted on account of being
 10 	 * absent from the given `props' object.
 11 	 *
 12 	 * @param {ContentObjectAPI} obj An instance whose internal data is to be
 13 	 *                               reset.
 14 	 * @param {object} props The properties with which to replace the internal
 15 	 *                       data of the given chainback instance.
 16 	 */
 17 	function update(obj, props) {
 18 		jQuery.extend(obj._data, props);
 19 	}
 20 
 21 	/**
 22 	 * The prefix that will temporarily be applied to block tags during an
 23 	 * encode() process.
 24 	 *
 25 	 * @type {string}
 26 	 * @const
 27 	 */
 28 	var BLOCK_ENCODING_PREFIX = 'GCN_BLOCK_TMP__';
 29 
 30 	/**
 31 	 * Will match <span id="GENTICS_block_123"></span>" but not "<node abc123>"
 32 	 * tags.  The first backreference contains the tagname of the tag
 33 	 * corresponding to this block.
 34 	 *
 35 	 * @type {RexExp}
 36 	 * @const
 37 	 */
 38 	var CONTENT_BLOCK = new RegExp(
 39 			'<(?!node)[a-z]+\\s'        + // "<span or "<div " but not "<node "
 40 			    '[^>]*?'                + // ...
 41 			    'id\\s*=\\s*[\\"\\\']?' + // "id = '"
 42 			    BLOCK_ENCODING_PREFIX   + // "GCN_BLOCK_TMP__"
 43 			    '([^\\"\\\'\\s>]+)'     + // "_abc-123"
 44 			    '[\\"\\\']?[^>]*>'      + // "' ...>"
 45 			    '<\\s*\\/[a-z]+>',        // "</span>" or "</div>"
 46 			'gim'
 47 		);
 48 
 49 	/**
 50 	 * Will match <node foo> or <node bar_123> or <node foo-bar> but not
 51 	 * <node "blah">.
 52 	 *
 53 	 * @type {RegExp}
 54 	 * @const
 55 	 */
 56 	var NODE_NOTATION = /<node ([a-z0-9_\-]+?)>/gim;
 57 
 58 	/**
 59 	 * Examines a string for "<node>" tags, and for each occurance of this
 60 	 * notation, the given callback will be invoked to manipulate the string.
 61 	 *
 62 	 * @private
 63 	 * @static
 64 	 * @param {string} str The string that will be examined for "<node>" tags.
 65 	 * @param {function} onMatchFound Callback function that should receive the
 66 	 *                                following three parameters:
 67 	 *
 68 	 *                    name:string The name of the tag being notated by the
 69 	 *                                node substring.  If the `str' arguments
 70 	 *                                is "<node myTag>", then the `name' value
 71 	 *                                will be "myTag".
 72 	 *                  offset:number The offset where the node substring was
 73 	 *                                found within the examined string.
 74 	 *                     str:string The string in which the "<node *>"
 75 	 *                                substring occured.
 76 	 *
 77 	 *                                The return value of the function will
 78 	 *                                replace the entire "<node>" substring
 79 	 *                                that was passed to it within the examined
 80 	 *                                string.
 81 	 */
 82 	function replaceNodeTags(str, onMatchFound) {
 83 		var parsed = str.replace(NODE_NOTATION, function (substr, tagname,
 84 		                                                  offset, examined) {
 85 				return onMatchFound(tagname, offset, examined);
 86 			});
 87 		return parsed;
 88 	}
 89 
 90 	/*
 91 	 * have a look at _init 
 92 	 */
 93 	GCN.ContentObjectAPI = GCN.defineChainback({
 94 		/** @lends ContentObjectAPI */
 95 
 96 		/**
 97 		 * @private
 98 		 * @type {string} A string denoting a content node type.  This value is
 99 		 *                used to compose the correct REST API ajax urls.  The
100 		 *                following are valid values: "node", "folder",
101 		 *                "template", "page", "file", "image".
102 		 */
103 		_type: null,
104 
105 		/**
106 		 * @private
107 		 * @type {object<string,*>} An internal object to store data that we
108 		 *                          get from the server.
109 		 */
110 		_data: {},
111 
112 		/**
113 		 * @private
114 		 * @type {object<string,*>} An internal object to store updates to
115 		 *                          the content object.  Should reflect the
116 		 *                          structural typography of the `_data'
117 		 *                          object.
118 		 */
119 		_shadow: {},
120 
121 		/**
122 		 * @type {boolean} Flags whether or not data for this content object have
123 		 *                 been fetched from the server.
124 		 */
125 		_fetched: false,
126 
127 		/**
128 		 * @private
129 		 * @type {object} will contain an objects internal settings
130 		 */
131 		_settings: null,
132 
133 		/**
134 		 * An array of all properties of an object that can be changed by the
135 		 * user. Writeable properties for all content objects.
136 		 * 
137 		 * @public
138 		 * @type {Array.string}
139 		 */
140 		WRITEABLE_PROPS: [],
141 
142 		/**
143 		 * <p>This object can contain various contrains for writeable props. 
144 		 * Those contrains will be checked when the user tries to set/save a
145 		 * property. Currently only maxLength is beeing handled.</p>
146 		 *
147 		 * <p>Example:</p>
148 		 * <pre>WRITEABLE_PROPS_CONSTRAINTS: {
149 		 *    'name': {
150 		 *        maxLength: 255
151 		 *     } 
152 		 * }</pre>
153 		 * @type {object}
154 		 * @const
155 		 *
156 		 */
157 		WRITEABLE_PROPS_CONSTRAINTS: {},
158 
159 		/**
160 		 * Fetches this content object's data from the backend.
161 		 *
162 		 * @ignore
163 		 * @param {function(object)} success A function to receive the server
164 		 *                                   response.
165 		 * @param {function(GCNError):boolean} error Optional custrom error
166 		 *                                           handler.
167 		 */
168 		'!fetch': function (success, error, stack) {
169 			var obj = this;
170 			var ajax = function () {
171 				obj._authAjax({
172 					url: GCN.settings.BACKEND_PATH + '/rest/' + obj._type +
173 					     '/load/' + obj.id() + GCN._getChannelParameter(obj),
174 					data: obj._loadParams(),
175 					error: error,
176 					success: success
177 				});
178 			};
179 
180 			// If this chainback object has an ancestor, then invoke that
181 			// parent's `_read()' method before fetching the data for this
182 			// chainback object.
183 			if (obj._chain) {
184 				var circularReference =
185 						stack && -1 < jQuery.inArray(obj._chain, stack);
186 				if (!circularReference) {
187 					stack = stack || [];
188 					stack.push(obj._chain);
189 					obj._chain._read(ajax, error, stack);
190 					return;
191 				}
192 			}
193 
194 			ajax();
195 		},
196 
197 		/**
198 		 * Internal method, to fetch this object's data from the server.
199 		 *
200 		 * @ignore
201 		 * @private
202 		 * @param {function(ContentObjectAPI)=} success Optional callback that
203 		 *                                              receives this object as
204 		 *                                              its only argument.
205 		 * @param {function(GCNError):boolean=} error Optional customer error
206 		 *                                            handler.
207 		 */
208 		'!_read': function (success, error, stack) {
209 			var obj = this;
210 			if (obj._fetched) {
211 				if (success) {
212 					obj._invoke(success, [obj]);
213 				}
214 				return;
215 			}
216 
217 			if (obj.multichannelling) {
218 				obj.multichannelling.read(obj, success, error);
219 				return;
220 			}
221 
222 			var id = obj.id();
223 
224 			if (null === id || undefined === id) {
225 				obj._getIdFromParent(function () {
226 					obj._read(success, error, stack);
227 				}, error, stack);
228 				return;
229 			}
230 
231 			obj.fetch(function (response) {
232 				obj._processResponse(response);
233 				obj._fetched = true;
234 				if (success) {
235 					obj._invoke(success, [obj]);
236 				}
237 			}, error, stack);
238 		},
239 
240 		/**
241 		 * Retrieves this object's id from its parent.  This function is used
242 		 * in order for this object to be able to fetch its data from the
243 		 * backend.
244 		 *
245 		 * @ignore
246 		 * @private
247 		 * @param {function(ContentObjectAPI)=} success Optional callback that
248 		 *                                              receives this object as
249 		 *                                              its only argument.
250 		 * @param {function(GCNError):boolean=} error Optional customer error
251 		 *                                            handler.
252 		 * @throws CANNOT_GET_OBJECT_ID
253 		 */
254 		'!_getIdFromParent': function (success, error, stack) {
255 			var parent = this._ancestor();
256 
257 			if (!parent) {
258 				var err = GCN.createError('CANNOT_GET_OBJECT_ID',
259 					'Cannot get an id for object', this);
260 				GCN.handleError(err, error);
261 				return;
262 			}
263 
264 			var that = this;
265 
266 			parent._read(function () {
267 				if ('folder' === that._type) {
268 					// There are 3 possible property names that an object can
269 					// use to hold the id of the folder that it is related to:
270 					//
271 					// "folderId": for pages, templates, files, and images.
272 					// "motherId": for folders
273 					// "nodeId":   for nodes
274 					//
275 					// We need to see which of this properties is set, the
276 					// first one we find will be our folder's id.
277 					var props = ['folderId', 'motherId', 'nodeId'];
278 					var prop = props.pop();
279 					var id;
280 
281 					while (prop) {
282 						id = parent.prop(prop);
283 						if (typeof id !== 'undefined') {
284 							break;
285 						}
286 						prop = props.pop();
287 					}
288 
289 					that._data.id = id;
290 				} else {
291 					that._data.id = parent.prop(that._type + 'Id');
292 				}
293 
294 				if (that._data.id === null || typeof that._data.id === 'undefined') {
295 					var err = GCN.createError('CANNOT_GET_OBJECT_ID',
296 						'Cannot get an id for object', this);
297 					GCN.handleError(err, error);
298 					return;
299 				}
300 
301 				that._setHash(that._data.id)._addToCache();
302 
303 				if (success) {
304 					success();
305 				}
306 			}, error, stack);
307 		},
308 
309 		/**
310 		 * Gets this object's node id. If used in a multichannelling is enabled
311 		 * it will return the channel id or 0 if no channel was set.
312 		 * 
313 		 * @public
314 		 * @function
315 		 * @name nodeId
316 		 * @memberOf ContentObjectAPI
317 		 * @return {number} The channel to which this object is set. 0 if no
318 		 *         channel is set.
319 		 */
320 		'!nodeId': function () {
321 			return this._channel || 0;
322 		},
323 
324 		/**
325 		 * Gets this object's id. We'll return the id of the object when it has
326 		 * been loaded - this can only be a localid. Otherwise we'll return the
327 		 * id which was provided by the user. This can either be a localid or a
328 		 * globalid.
329 		 *
330 		 * @name id
331 		 * @function
332 		 * @memberOf ContentObjectAPI
333 		 * @public
334 		 * @return {number}
335 		 */
336 		'!id': function () {
337 			return this._data.id;
338 		},
339 
340 		/**
341 		 * Alias for {@link ContentObjectAPI#id}
342 		 *
343 		 * @name localId
344 		 * @function
345 		 * @memberOf ContentObjectAPI
346 		 * @private
347 		 * @return {number}
348 		 * @decprecated
349 		 */
350 		'!localId': function () {
351 			return this.id();
352 		},
353 
354 		/**
355 		 * Update the `_shadow' object that maintains changes to properties
356 		 * that reflected the internal `_data' object.  This shadow object is
357 		 * used to persist differential changes to a REST API object.
358 		 *
359 		 * @ignore
360 		 * @private
361 		 * @param {string} path The path through the object to the property we
362 		 *                      want to modify if a node in the path contains
363 		 *                      dots, then these dots should be escaped.  This
364 		 *                      can be done using the GCN.escapePropertyName()
365 		 *                      convenience function.
366 		 * @param {*} value The value we wish to set the property to.
367 		 * @param {function=} error Custom error handler.
368 		 * @param {boolean=} force If true, no error will be thrown if `path'
369 		 *                         cannot be fully resolved against the
370 		 *                         internal `_data' object, instead, the path
371 		 *                         will be created on the shadow object.
372 		 */
373 		'!_update': function (pathStr, value, error, force) {
374 			var boundary = Math.random().toString(8).substring(2);
375 			var path = pathStr.replace(/\./g, boundary)
376 			                  .replace(new RegExp('\\\\' + boundary, 'g'), '.')
377 			                  .split(boundary);
378 			var shadow = this._shadow;
379 			var actual = this._data;
380 			var i = 0;
381 			var numPathNodes = path.length;
382 			var pathNode;
383 			// Whether or not the traversal path in `_data' and `_shadow' are
384 			// at the same position in the respective objects.
385 			var areMirrored = true;
386 
387 			while (true) {
388 				pathNode = path[i++];
389 
390 				if (areMirrored) {
391 					actual = actual[pathNode];
392 					areMirrored = jQuery.type(actual) !== 'undefined';
393 				}
394 
395 				if (i === numPathNodes) {
396 					break;
397 				}
398 
399 				if (shadow[pathNode]) {
400 					shadow = shadow[pathNode];
401 				} else if (areMirrored || force) {
402 					shadow = (shadow[pathNode] = {});
403 				} else {
404 					break; // goto error
405 				}
406 			}
407 
408 			if (i === numPathNodes && (areMirrored || force)) {
409 				shadow[pathNode] = value;
410 			} else {
411 				var err = GCN.createError('TYPE_ERROR', 'Object "' +
412 					path.slice(0, i).join('.') + '" does not exist',
413 					actual);
414 				GCN.handleError(err, error);
415 			}
416 		},
417 
418 		/**
419 		 * Receives the response from a REST API request, and stores it in the
420 		 * internal `_data' object.
421 		 *
422 		 * @private
423 		 * @param {object} data Parsed JSON response data.
424 		 */
425 		'!_processResponse': function (data) {
426 			jQuery.extend(this._data, data[this._type]);
427 		},
428 
429 		/**
430 		 * Specifies a list of parameters that will be added to the url when
431 		 * loading the content object from the server.
432 		 *
433 		 * @private
434 		 * @return {object} object With parameters to be appended to the load
435 		 *                         request
436 		 */
437 		'!_loadParams': function () {},
438 
439 		/**
440 		 * Reads the property `property' of this content object if this
441 		 * property is among those in the WRITEABLE_PROPS array. If a second
442 		 * argument is provided, them the property is updated with that value.
443 		 *
444 		 * @name prop
445 		 * @function
446 		 * @memberOf ContentObjectAPI
447 		 * @param {String} property Name of the property to be read or updated.
448 		 * @param {String} value Optional value to set property to. If omitted the property will just be read.
449 		 * @param {function(GCNError):boolean=} error Custom error handler to 
450 		 *                                      stop error propagation for this
451 		 *                                      synchronous call. 
452 		 * @return {?*} Meta attribute.
453 		 * @throws UNFETCHED_OBJECT_ACCESS if the object has not been fetched from the server yet
454 		 * @throws READONLY_ATTRIBUTE whenever trying to write to an attribute that's readonly
455 		 */
456 		'!prop': function (property, value, error) {
457 			if (!this._fetched) {
458 				GCN.handleError(GCN.createError(
459 					'UNFETCHED_OBJECT_ACCESS',
460 					'Object not fetched yet.'
461 				), error);
462 				return;
463 			}
464 
465 			if (typeof value !== 'undefined') {
466 				// Check whether the property is writable
467 				if (jQuery.inArray(property, this.WRITEABLE_PROPS) >= 0) {
468 					// Check wether the property has a constraint and verify it
469 					var constraint = this.WRITEABLE_PROPS_CONSTRAINTS[property];
470 					if (constraint) {
471 						// verify maxLength
472 						if (constraint.maxLength && value.length >= constraint.maxLength) {
473 							var data = { name: property, value: value, maxLength: constraint.maxLength };
474 							var constraintError = GCN.createError('ATTRIBUTE_CONSTRAINT_VIOLATION',
475 								'Attribute "' + property + '" of ' + this._type +
476 								' is too long. The \'maxLength\' was set to {' + constraint.maxLength + '} ', data);
477 							GCN.handleError(constraintError, error);
478 							return;
479 						}
480 					}
481 					this._update(GCN.escapePropertyName(property), value);
482 				} else {
483 					GCN.handleError(GCN.createError('READONLY_ATTRIBUTE',
484 						'Attribute "' + property + '" of ' + this._type +
485 						' is read-only. Writeable properties are: ' +
486 						this.WRITEABLE_PROPS, this.WRITEABLE_PROPS), error);
487 					return;
488 				}
489 			}
490 
491 			return (
492 				(jQuery.type(this._shadow[property]) !== 'undefined'
493 					? this._shadow
494 					: this._data)[property]
495 			);
496 		},
497 
498 		/**
499 		 * Sends the a template string to the Aloha Servlet for rendering.
500 		 *
501 		 * @ignore
502 		 * @TODO: Consider making this function public.  At least one developer
503 		 *        has had need to render a custom template for a content
504 		 *        object.
505 		 *
506 		 * @private
507 		 * @param {string} template Template which will be rendered.
508 		 * @param {string} mode The rendering mode.  Valid values are "view",
509 		 *                      "edit", "pub."
510 		 * @param {function(object)} success A callback the receives the render
511 		 *                                   response.
512 		 * @param {function(GCNError):boolean} error Error handler.
513 		 */
514 		'!_renderTemplate' : function (template, mode, success, error) {
515 			var channelParam = GCN._getChannelParameter(this);
516 			var url = GCN.settings.BACKEND_PATH
517 			        + '/rest/' + this._type
518 			        + '/render/' + this.id()
519 			        + channelParam
520 			        + (channelParam ? '&' : '?')
521 			        + 'edit=' + ('edit' === mode)
522 			        + '&template=' + encodeURIComponent(template);
523 			this._authAjax({
524 				url: url,
525 				error: error,
526 				success: success
527 			});
528 		},
529 
530 		/**
531 		 * Wrapper for internal chainback _ajax method.
532 		 * 
533 		 * @ignore
534 		 * @private
535 		 * @param {object<string, *>} settings Settings for the ajax request.
536 		 *                                     The settings object is identical
537 		 *                                     to that of the `GCN.ajax'
538 		 *                                     method, which handles the actual
539 		 *                                     ajax transportation.
540 		 * @throws AJAX_ERROR
541 		 */
542 		'!_ajax': function (settings) {
543 			var that = this;
544 
545 			// force no cache for all API calls
546 			settings.cache = false;
547 			settings.success = (function (onSuccess, onError) {
548 				return function (data) {
549 					// Ajax calls that do not target the REST API servlet do
550 					// not response data with a `responseInfo' object.
551 					// "/CNPortletapp/alohatag" is an example.  So we cannot
552 					// just assume that it exists.
553 					if (data.responseInfo) {
554 						switch (data.responseInfo.responseCode) {
555 						case 'OK':
556 							break;
557 						case 'AUTHREQUIRED':
558 							GCN.clearSession();
559 							that._authAjax(settings);
560 							return;
561 						default:
562 							GCN.handleResponseError(data, onError);
563 							return;
564 						}
565 					}
566 
567 					if (onSuccess) {
568 						onSuccess(data);
569 					}
570 				};
571 			}(settings.success, settings.error, settings.url));
572 
573 			this._queueAjax(settings);
574 		},
575 
576 		/**
577 		 * Concrete implementatation of _fulfill().
578 		 *
579 		 * Resolves all promises made by this content object while ensuring
580 		 * that circularReferences, (which are completely possible, and valid)
581 		 * do not result in infinit recursion.
582 		 *
583 		 * @override
584 		 */
585 		'!_fulfill': function (success, error, stack) {
586 			var obj = this;
587 			if (obj._chain) {
588 				var circularReference =
589 						stack && -1 < jQuery.inArray(obj._chain, stack);
590 				if (!circularReference) {
591 					stack = stack || [];
592 					stack.push(obj._chain);
593 					obj._fulfill(function () {
594 						obj._read(success, error);
595 					}, error, stack);
596 					return;
597 				}
598 			}
599 			obj._read(success, error);
600 		},
601 
602 		/**
603 		 * Similar to `_ajax', except that it prefixes the ajax url with the
604 		 * current session's `sid', and will trigger an
605 		 * `authentication-required' event if the session is not authenticated.
606 		 *
607 		 * @ignore
608 		 * @TODO(petro): Consider simplifiying this function signature to read:
609 		 *               `_auth( url, success, error )'
610 		 *
611 		 * @private
612 		 * @param {object<string, *>} settings Settings for the ajax request.
613 		 * @throws AUTHENTICATION_FAILED
614 		 */
615 		_authAjax: function (settings) {
616 			var that = this;
617 
618 			if (GCN.isAuthenticating) {
619 				GCN.afterNextAuthentication(function () {
620 					that._authAjax(settings);
621 				});
622 				return;
623 			}
624 
625 			if (!GCN.sid) {
626 				var cancel;
627 
628 				if (settings.error) {
629 					/**
630 					 * @ignore
631 					 */
632 					cancel = function (error) {
633 						GCN.handleError(
634 							error || GCN.createError('AUTHENTICATION_FAILED'),
635 							settings.error
636 						);
637 					};
638 				} else {
639 					/**
640 					 * @ignore
641 					 */
642 					cancel = function (error) {
643 						if (error) {
644 							GCN.error(error.code, error.message, error.data);
645 						} else {
646 							GCN.error('AUTHENTICATION_FAILED');
647 						}
648 					};
649 				}
650 
651 				GCN.afterNextAuthentication(function () {
652 					that._authAjax(settings);
653 				});
654 
655 				if (GCN.usingSSO) {
656 					// First, try to automatically authenticate via
657 					// Single-SignOn
658 					GCN.loginWithSSO(GCN.onAuthenticated, function () {
659 						// ... if SSO fails, then fallback to requesting user
660 						// credentials: broadcast `authentication-required'
661 						// message.
662 						GCN.authenticate(cancel);
663 					});
664 				} else {
665 					// Trigger the `authentication-required' event to request
666 					// user credentials.
667 					GCN.authenticate(cancel);
668 				}
669 
670 				return;
671 			}
672 
673 			// Append "?sid=..." or "&sid=..." if needed.
674 
675 			var urlFragment = settings.url.substr(
676 				GCN.settings.BACKEND_PATH.length
677 			);
678 			var isSidInUrl = /[\?\&]sid=/.test(urlFragment);
679 			if (!isSidInUrl) {
680 				var isFirstParam = (jQuery.inArray('?',
681 					urlFragment.split('')) === -1);
682 				settings.url += (isFirstParam ? '?' : '&') + 'sid='
683 				             +  (GCN.sid || '');
684 			}
685 
686 			this._ajax(settings);
687 		},
688 
689 		/**
690 		 * Recursively call `_continueWith()'.
691 		 *
692 		 * @ignore
693 		 * @private
694 		 * @override
695 		 */
696 		'!_onContinue': function (success, error) {
697 			var that = this;
698 			this._continueWith(function () {
699 				that._read(success, error);
700 			}, error);
701 		},
702 
703 		/**
704 		 * Initializes this content object.  If a `success' callback is
705 		 * provided, it will cause this object's data to be fetched and passed
706 		 * to the callback.  This object's data will be fetched from the cache
707 		 * if is available, otherwise it will be fetched from the server.  If
708 		 * this content object API contains parent chainbacks, it will get its
709 		 * parent to fetch its own data first.
710 		 *
711 		 * <p>
712 		 * Basic content object implementation which all other content objects
713 		 * will inherit from.
714 		 * </p>
715 		 * 
716 		 * <p>
717 		 * If a `success' callback is provided,
718 		 * it will cause this object's data to be fetched and passed to the
719 		 * callback. This object's data will be fetched from the cache if is
720 		 * available, otherwise it will be fetched from the server. If this
721 		 * content object API contains parent chainbacks, it will get its parent
722 		 * to fetch its own data first.
723 		 * </p>
724 		 * 
725 		 * <p>
726 		 * You might also provide an object for initialization, to directly
727 		 * instantiate the object's data without loading it from the server. To
728 		 * do so just pass in a data object as received from the server instead
729 		 * of an id--just make sure this object has an `id' property.
730 		 * </p>
731 		 * 
732 		 * <p>
733 		 * If an `error' handler is provided, as the third parameter, it will
734 		 * catch any errors that have occured since the invocation of this call.
735 		 * It allows the global error handler to be intercepted before stopping
736 		 * the error or allowing it to propagate on to the global handler.
737 		 * </p>
738 		 * 
739 		 * @class
740 		 * @name ContentObjectAPI
741 		 * @param {number|string|object}
742 		 *            id
743 		 * @param {function(ContentObjectAPI))=}
744 		 *            success Optional success callback that will receive this
745 		 *            object as its only argument.
746 		 * @param {function(GCNError):boolean=}
747 		 *            error Optional custom error handler.
748 		 * @param {object}
749 		 *            settings Basic settings for this object - depends on the
750 		 *            ContentObjetAPI Object used.
751 		 * @throws INVALID_DATA
752 		 *             If no id is found when providing an object for
753 		 *             initialization.
754 		 */
755 		_init: function (data, success, error, settings) {
756 			this._settings = settings;
757 			var id;
758 
759 			if (jQuery.type(data) === 'object') {
760 				if (data.multichannelling) {
761 					this.multichannelling = data;
762 					// Remove the inherited object from the chain.
763 					if (this._chain) {
764 						this._chain = this._chain._chain;
765 					}
766 					id = this.multichannelling.derivedFrom.id();
767 				} else {
768 					if (!data.id) {
769 						var err = GCN.createError(
770 							'INVALID_DATA',
771 							'Data not sufficient for initalization: id is missing',
772 							data
773 						);
774 						GCN.handleError(err, error);
775 						return;
776 					}
777 					this._data = data;
778 					this._fetched = true;
779 					if (success) {
780 						this._invoke(success, [this]);
781 					}
782 					return;
783 				}
784 			} else {
785 				id = data;
786 			}
787 
788 			// Ensure that each object has its very own `_data' and `_shadow'
789 			// objects.
790 			if (!this._fetched) {
791 				this._data = {};
792 				this._shadow = {};
793 				this._data.id = id;
794 			}
795 			if (success) {
796 				this._read(success, error);
797 			}
798 		},
799 
800 		/**
801 		 * <p>Replaces tag blocks with appropriate "<node *>" notation in a given
802 		 * string. Given an element whose innerHTML is:<p>
803 		 * <pre>
804 		 *		<span id="GENTICS_BLOCK_123">My Tag</span>
805 		 * </pre>
806 		 * <p>encode() will return:</p>
807 		 * <pre>
808 		 *		<node 123>
809 		 * </pre>
810 		 *
811 		 * @name encode
812 		 * @function
813 		 * @memberOf ContentObjectAPI
814 		 * @param {!jQuery} $element
815 		 *       An element whose contents are to be encoded.
816 		 * @param {?function(!Element): string} serializeFn
817 		 *       A function that returns the serialized contents of the
818 		 *       given element as a HTML string, excluding the start and end
819 		 *       tag of the element. If not provided, jQuery.html() will
820 		 *       be used.
821 		 * @return {string} The encoded HTML string.
822 		 */
823 		'!encode': function ($element, serializeFn) {
824 			var $clone = $element.clone();
825 			var id;
826 			var $block;
827 			for (id in this._blocks) {
828 				if (this._blocks.hasOwnProperty(id)) {
829 					$block = $clone.find('#' + this._blocks[id].element);
830 					if ($block.length) {
831 						// Empty all content blocks of their innerHTML.
832 						$block.html('').attr('id', BLOCK_ENCODING_PREFIX +
833 							this._blocks[id].tagname);
834 					}
835 				}
836 			}
837 			serializeFn = serializeFn || function ($element) {
838 				return jQuery($element).html();
839 			};
840 			var html = serializeFn($clone[0]);
841 			return html.replace(CONTENT_BLOCK, function (substr, match) {
842 				return '<node ' + match + '>';
843 			});
844 		},
845 
846 		/**
847 		 * For a given string, replace all occurances of "<node>" with
848 		 * appropriate HTML markup, allowing notated tags to be rendered within
849 		 * the surrounding HTML content.
850 		 *
851 		 * The success() handler will receives a string containing the contents
852 		 * of the `str' string with references to "<node>" having been inflated
853 		 * into their appropriate tag rendering.
854 		 *
855 		 * @name decode
856 		 * @function
857 		 * @memberOf ContentObjectAPI
858 		 * @param {string} str The content string, in which  "<node *>" tags
859 		 *                     will be inflated with their HTML rendering.
860 		 * @param {function(ContentObjectAPI))} success Success callback that
861 		 *                                              will receive the
862 		 *                                              decoded string.
863 		 * @param {function(GCNError):boolean=} error Optional custom error
864 		 *                                            handler.
865 		 */
866 		'!decode': function (str, success, error) {
867 			if (!success) {
868 				return;
869 			}
870 
871 			var prefix = 'gcn-tag-placeholder-';
872 			var toRender = [];
873 			var html = replaceNodeTags(str, function (name, offset, str) {
874 				toRender.push('<node ', name, '>');
875 				return '<div id="' + prefix + name + '"></div>';
876 			});
877 
878 			if (!toRender.length) {
879 				success(html);
880 				return;
881 			}
882 
883 			// Instead of rendering each tag individually, we render them
884 			// together in one string, and map the results back into our
885 			// original html string.  This allows us to perform one request to
886 			// the server for any number of node tags found.
887 
888 			var parsed = jQuery('<div>' + html + '</div>');
889 			var template = toRender.join('');
890 			var that = this;
891 
892 			this._renderTemplate(template, 'edit', function (data) {
893 				var content = data.content;
894 				var tag;
895 				var tags = data.tags;
896 				var j = tags.length;
897 				var rendered = jQuery('<div>' + content + '</div>');
898 
899 				var replaceTag = (function (numTags) {
900 					return function (tag) {
901 						parsed.find('#' + prefix + tag.prop('name'))
902 							.replaceWith(
903 								rendered.find('#' + tag.prop('id'))
904 							);
905 
906 						if (0 === --numTags) {
907 							success(parsed.html());
908 						}
909 					};
910 				}(j));
911 
912 				while (j) {
913 					that.tag(tags[--j], replaceTag);
914 				}
915 			}, error);
916 		},
917 
918 		/**
919 		 * Clears this object from its constructor's cache so that the next
920 		 * attempt to access this object will result in a brand new instance
921 		 * being initialized and placed in the cache.
922 		 *
923 		 * @name clear
924 		 * @function
925 		 * @memberOf ContentObjectAPI
926 		 */
927 		'!clear': function () {
928 			// Do not clear the id from the _data.
929 			var id = this._data.id;
930 			this._data = {};
931 			this._data.id = id;
932 			this._shadow = {};
933 			this._fetched = false;
934 			this._clearCache();
935 		},
936 
937 		/**
938 		 * Retrieves this objects parent folder.
939 		 * 
940 		 * @name folder
941 		 * @function
942 		 * @memberOf ContentObjectAPI
943 		 * @param {function(FolderAPI)=}
944 		 *            success Callback that will receive the requested object.
945 		 * @param {function(GCNError):boolean=}
946 		 *            error Custom error handler.
947 		 * @return {FolderAPI} API object for the retrieved GCN folder.
948 		 */
949 		'!folder': function (success, error) {
950 			return this._continue(GCN.FolderAPI, this._data.folderId, success,
951 				error);
952 		},
953 
954 		/**
955 		 * Saves changes made to this content object to the backend.
956 		 * 
957 		 * @param {object=}
958 		 *            settings Optional settings to pass on to the ajax
959 		 *            function.
960 		 * @param {function(ContentObjectAPI)=}
961 		 *            success Optional callback that receives this object as its
962 		 *            only argument.
963 		 * @param {function(GCNError):boolean=}
964 		 *            error Optional customer error handler.
965 		 */
966 		save: function () {
967 			var settings;
968 			var success;
969 			var error;
970 			var args = Array.prototype.slice.call(arguments);
971 			var len = args.length;
972 			var i;
973 
974 			for (i = 0; i < len; ++i) {
975 				switch (jQuery.type(args[i])) {
976 				case 'object':
977 					if (!settings) {
978 						settings = args[i];
979 					}
980 					break;
981 				case 'function':
982 					if (!success) {
983 						success = args[i];
984 					} else {
985 						error = args[i];
986 					}
987 					break;
988 				case 'undefined':
989 					break;
990 				default:
991 					var err = GCN.createError('UNKNOWN_ARGUMENT',
992 						'Don\'t know what to do with arguments[' + i + '] ' +
993 						'value: "' + args[i] + '"', args);
994 					GCN.handleError(err, error);
995 					return;
996 				}
997 			}
998 
999 			this._save(settings, success, error);
1000 		},
1001 
1002 		/**
1003 		 * Persists this object's local data onto the server.  If the object
1004 		 * has not yet been fetched we need to get it first so we can update
1005 		 * its internals properly...
1006 		 *
1007 		 * @private
1008 		 * @param {object} settings Object which will extend the basic
1009 		 *                          settings of the ajax call
1010 		 * @param {function(ContentObjectAPI)=} success Optional callback that
1011 		 *                                              receives this object as
1012 		 *                                              its only argument.
1013 		 * @param {function(GCNError):boolean=} error Optional customer error
1014 		 *                                            handler.
1015 		 */
1016 		'!_save': function (settings, success, error) {
1017 			var that = this;
1018 			this._fulfill(function () {
1019 				that._persist(settings, success, error);
1020 			}, error);
1021 		},
1022 
1023 		/**
1024 		 * Returns the bare data structure of this content object.
1025 		 * To be used for creating the save POST body data.
1026 		 *
1027 		 * @param {object<string, *>} Plain old object representation of this
1028 		 *                            content object.
1029 		 */
1030 		'!json': function () {
1031 			var json = {};
1032 
1033 			if (this._deletedTags.length) {
1034 				json['delete'] = this._deletedTags;
1035 			}
1036 
1037 			if (this._deletedBlocks.length) {
1038 				json['delete'] = json['delete']
1039 				               ? json['delete'].concat(this._deletedBlocks)
1040 				               : this._deletedBlocks;
1041 			}
1042 
1043 			json[this._type] = this._shadow;
1044 			json[this._type].id = this._data.id;
1045 			return json;
1046 		},
1047 
1048 		/**
1049 		 * Sends the current state of this content object to be stored on the
1050 		 * server.
1051 		 *
1052 		 * @private
1053 		 * @param {function(ContentObjectAPI)=} success Optional callback that
1054 		 *                                              receives this object as
1055 		 *                                              its only argument.
1056 		 * @param {function(GCNError):boolean=} error Optional customer error
1057 		 *                                            handler.
1058 		 * @throws HTTP_ERROR
1059 		 */
1060 		'!_persist': function (settings, success, error) {
1061 			var that = this;
1062 
1063 			if (!this._fetched) {
1064 				this._read(function () {
1065 					that._persist(settings, success, error);
1066 				}, error);
1067 				return;
1068 			}
1069 
1070 			var json = this.json();
1071 			jQuery.extend(json, settings);
1072 			var tags = json[this._type].tags;
1073 			var tagname;
1074 			for (tagname in tags) {
1075 				if (tags.hasOwnProperty(tagname)) {
1076 					tags[tagname].active = true;
1077 				}
1078 			}
1079 
1080 			this._authAjax({
1081 				url   : GCN.settings.BACKEND_PATH + '/rest/'
1082 				        + this._type + '/save/' + this.id()
1083 				        + GCN._getChannelParameter(this),
1084 				type  : 'POST',
1085 				error : error,
1086 				json  : json,
1087 				success : function (response) {
1088 					// We must not overwrite the `_data.tags' object with this
1089 					// one.
1090 					delete that._shadow.tags;
1091 
1092 					// Everything else in `_shadow' should be written over to
1093 					// `_data' before resetting the `_shadow' object.
1094 					jQuery.extend(that._data, that._shadow);
1095 					that._shadow = {};
1096 					that._deletedTags = [];
1097 					that._deletedBlocks = [];
1098 
1099 					if (success) {
1100 						that._invoke(success, [that]);
1101 					}
1102 				}
1103 			});
1104 		},
1105 
1106 		/**
1107 		 * Deletes this content object from its containing parent.
1108 		 * 
1109 		 * @param {function(ContentObjectAPI)=}
1110 		 *            success Optional callback that receives this object as its
1111 		 *            only argument.
1112 		 * @param {function(GCNError):boolean=}
1113 		 *            error Optional customer error handler.
1114 		 */
1115 		remove: function (success, error) {
1116 			this._remove(success, error);
1117 		},
1118 
1119 		/**
1120 		 * Get a channel-local copy of this content object.
1121 		 *
1122 		 * @public
1123 		 * @function
1124 		 * @name localize
1125 		 * @memberOf ContentObjectAPI
1126 		 * @param {funtion(ContentObjectAPI)=} success Optional callback to
1127 		 *                                             receive this content
1128 		 *                                             object as the only
1129 		 *                                             argument.
1130 		 * @param {function(GCNError):boolean=} error Optional custom error
1131 		 *                                            handler.
1132 		 */
1133 		'!localize': function (success, error) {
1134 			if (!this._channel && !GCN.channel()) {
1135 				var err = GCN.createError(
1136 					'NO_CHANNEL_ID_SET',
1137 					'No channel is set in which to get the localized object',
1138 					GCN
1139 				);
1140 				GCN.handleError(err, error);
1141 				return false;
1142 			}
1143 			var local = this._continue(
1144 				this._constructor,
1145 				{
1146 					derivedFrom: this,
1147 					multichannelling: true,
1148 					read: GCN.multichannelling.localize
1149 				},
1150 				success,
1151 				error
1152 			);
1153 			return local;
1154 		},
1155 
1156 		/**
1157 		 * Remove this channel-local object, and delete its local copy in the
1158 		 * backend.
1159 		 *
1160 		 * @public
1161 		 * @function
1162 		 * @name unlocalize
1163 		 * @memberOf ContentObjectAPI
1164 		 * @param {funtion(ContentObjectAPI)=} success Optional callback to
1165 		 *                                             receive this content
1166 		 *                                             object as the only
1167 		 *                                             argument.
1168 		 * @param {function(GCNError):boolean=} error Optional custom error
1169 		 *                                            handler.
1170 		 */
1171 		'!unlocalize': function (success, error) {
1172 			if (!this._channel && !GCN.channel()) {
1173 				var err = GCN.createError(
1174 					'NO_CHANNEL_ID_SET',
1175 					'No channel is set in which to get the unlocalized object',
1176 					GCN
1177 				);
1178 				GCN.handleError(err, error);
1179 				return false;
1180 			}
1181 			var placeholder = {
1182 				multichannelling: {
1183 					derivedFrom: this
1184 				}
1185 			};
1186 			var that = this;
1187 			GCN.multichannelling.unlocalize(placeholder, function () {
1188 				// Clean cache & reset object to make sure it can't be used
1189 				// properly any more.
1190 				that._clearCache();
1191 				that._data = {};
1192 				that._shadow = {};
1193 				if (success) {
1194 					success();
1195 				}
1196 			}, error);
1197 		},
1198 
1199 		/**
1200 		 * Performs a REST API request to delete this object from the server.
1201 		 *
1202 		 * @private
1203 		 * @param {function()=} success Optional callback that
1204 		 *                                              will be invoked once
1205 		 *                                              this object has been
1206 		 *                                              removed.
1207 		 * @param {function(GCNError):boolean=} error Optional customer error
1208 		 *                                            handler.
1209 		 */
1210 		'!_remove': function (success, error) {
1211 			var that = this;
1212 			this._authAjax({
1213 				url     : GCN.settings.BACKEND_PATH + '/rest/'
1214 				          + this._type + '/delete/' + this.id()
1215 				          + GCN._getChannelParameter(that),
1216 				type    : 'POST',
1217 				error   : error,
1218 				success : function (response) {
1219 					// Clean cache & reset object to make sure it can't be used
1220 					// properly any more.
1221 					that._clearCache();
1222 					that._data = {};
1223 					that._shadow = {};
1224 
1225 					// Don't forward the object to the success handler since
1226 					// it's been deleted.
1227 					if (success) {
1228 						that._invoke(success);
1229 					}
1230 				}
1231 			});
1232 		},
1233 
1234 		/**
1235 		 * Removes any additionaly data stored on this objec which pertains to
1236 		 * a tag matching the given tagname.  This function will be called when
1237 		 * a tag is being removed in order to bring the content object to a
1238 		 * consistant state.
1239 		 * Should be overriden by subclasses.
1240 		 *
1241 		 * @param {string} tagid The Id of the tag whose associated data we
1242 		 *                       want we want to remove.
1243 		 */
1244 		'!_removeAssociatedTagData': function (tagname) {}
1245 
1246 	});
1247 
1248 	GCN.ContentObjectAPI.update = update;
1249 
1250 	/**
1251 	 * Generates a factory method for chainback classes.  The method signature
1252 	 * used with this factory function will match that of the target class'
1253 	 * constructor.  Therefore this function is expected to be invoked with the
1254 	 * follow combination of arguments ...
1255 	 *
1256 	 * Examples for GCN.pages api:
1257 	 *
1258 	 * To get an array containing 1 page:
1259 	 * pages(1)
1260 	 * pages(1, function () {})
1261 	 *
1262 	 * To get an array containing 2 pages:
1263 	 * pages([1, 2])
1264 	 * pages([1, 2], function () {})
1265 	 *
1266 	 * To get an array containing any and all pages:
1267 	 * pages()
1268 	 * pages(function () {})
1269 	 *
1270 	 * To get an array containing no pages:
1271 	 * pages([])
1272 	 * pages([], function () {});
1273 	 *
1274 	 * @param {Chainback} ctor The Chainback constructor we want to expose.
1275 	 * @throws UNKNOWN_ARGUMENT
1276 	 */
1277 	GCN.exposeAPI = function (ctor) {
1278 		return function () {
1279 			// Convert arguments into an array
1280 			// https://developer.mozilla.org/en/JavaScript/Reference/...
1281 			// ...Functions_and_function_scope/arguments
1282 			var args = Array.prototype.slice.call(arguments);
1283 			var id;
1284 			var ids;
1285 			var success;
1286 			var error;
1287 			var settings;
1288 
1289 			// iterate over arguments to find id || ids, succes, error and
1290 			// settings
1291 			jQuery.each(args, function (i, arg) {
1292 				switch (jQuery.type(arg)) {
1293 				// set id
1294 				case 'string':
1295 				case 'number':
1296 					if (!id && !ids) {
1297 						id = arg;
1298 					} else {
1299 						GCN.error('UNKNOWN_ARGUMENT',
1300 							'id is already set. Don\'t know what to do with ' +
1301 							'arguments[' + i + '] value: "' + arg + '"');
1302 					}
1303 					break;
1304 				// set ids
1305 				case 'array':
1306 					if (!id && !ids) {
1307 						ids = args[0];
1308 					} else {
1309 						GCN.error('UNKNOWN_ARGUMENT',
1310 							'ids is already set. Don\'t know what to do with' +
1311 							' arguments[' + i + '] value: "' + arg + '"');
1312 					}
1313 					break;
1314 				// success and error handlers
1315 				case 'function':
1316 					if (!success) {
1317 						success = arg;
1318 					} else if (success && !error) {
1319 						error = arg;
1320 					} else {
1321 						GCN.error('UNKNOWN_ARGUMENT',
1322 							'success and error handler already set. Don\'t ' +
1323 							'know what to do with arguments[' + i + ']');
1324 					}
1325 					break;
1326 				// settings
1327 				case 'object':
1328 					if (!id && !ids) {
1329 						id = arg;
1330 					} else if (!settings) {
1331 						settings = arg;
1332 					} else {
1333 						GCN.error('UNKNOWN_ARGUMENT',
1334 							'settings are already present. Don\'t know what ' +
1335 							'to do with arguments[' + i + '] value:' + ' "' +
1336 							arg + '"');
1337 					}
1338 					break;
1339 				default:
1340 					GCN.error('UNKNOWN_ARGUMENT',
1341 						'Don\'t know what to do with arguments[' + i +
1342 						'] value: "' + arg + '"');
1343 				}
1344 			});
1345 
1346 			// Prepare a new set of arguments to pass on during initialzation
1347 			// of callee object.
1348 			args = [];
1349 
1350 			// settings should always be an object, even if it's just empty
1351 			if (!settings) {
1352 				settings = {};
1353 			}
1354 
1355 			args[0] = (typeof id !== 'undefined') ? id : ids;
1356 			args[1] = success || settings.success || null;
1357 			args[2] = error || settings.error || null;
1358 			args[3] = settings;
1359 
1360 			// We either add 0 (no channel) or the channelid to the hash
1361 			var channel = GCN.settings.channel;
1362 
1363 			// Check if the value is false, and set it to 0 in this case
1364 			if (!channel) {
1365 				channel = 0;
1366 			}
1367 
1368 			var hash = (id || ids)
1369 			         ? ctor._makeHash(channel + '/' + (ids ? ids.sort().join(',') : id))
1370 			         : null;
1371 
1372 			return GCN.getChainback(ctor, hash, null, args);
1373 		};
1374 
1375 	};
1376 
1377 }(GCN));
1378