1 /*global jQuery:true, GCN: true */
  2 (function (GCN) {
  3 
  4 	'use strict';
  5 
  6 	/**
  7 	 * Checks whether or not a tag container instance has data for a tag of the
  8 	 * given name.
  9 	 *
 10 	 * @param {TagContainerAPI} container The container in which to look
 11 	 *                                         for the tag.
 12 	 * @param {string} tagname The name of the tag to find.
 13 	 * @return {boolean} True if the container contains a data for the tag of
 14 	 * the given name.
 15 	 */
 16 	function hasTagData(container, tagname) {
 17 		return !!container._data.tags[tagname];
 18 	}
 19 
 20 	/**
 21 	 * Extends the internal taglist with data from the given collection of tags.
 22 	 * Will overwrite the data of any tag whose name is matched in the `tags'
 23 	 * associative array.
 24 	 *
 25 	 * @param {TagContainerAPI} container The container in which to look
 26 	 *                                         for the tag.
 27 	 * @param {object} tags Associative array of tag data, mapped against the
 28 	 *                      data's corresponding tag name.
 29 	 */
 30 	function extendTags(container, tags) {
 31 		jQuery.extend(container._data.tags, tags);
 32 	}
 33 
 34 	/**
 35 	 * Gets the construct matching the given keyword.
 36 	 *
 37 	 * @param {string} keyword Construct keyword.
 38 	 * @param {NodeAPI} node The node inwhich to search for the construct.
 39 	 * @param {function(object)} success Callback function to receive the
 40 	 *                                   successfully found construct.
 41 	 * @param {function(GCNError):boolean} error Optional custom error handler.
 42 	 */
 43 	function getConstruct(keyword, node, success, error) {
 44 		node.constructs(function (constructs) {
 45 			if (constructs[keyword]) {
 46 				success(constructs[keyword]);
 47 			} else {
 48 				var err = GCN.createError(
 49 					'CONSTRUCT_NOT_FOUND',
 50 					'Cannot find construct `' + keyword + '` - Maybe the construct is not linked to this node?',
 51 					constructs
 52 				);
 53 				GCN.handleError(err, error);
 54 			}
 55 		}, error);
 56 	}
 57 
 58 	/**
 59 	 * Creates an new tag via the GCN REST-API.
 60 	 *
 61 	 * @param {TagAPI} tag A representation of the tag which will be created in
 62 	 *                     the GCN backend.
 63 	 * @param {object} data The request body that will be serialized into json.
 64 	 * @param {function(TagAPI)} success Callback function to receive the
 65 	 *                                   successfully created tag.
 66 	 * @param {function(GCNError):boolean} error Optional custom error handler.
 67 	 */
 68 	function newTag(tag, data, success, error) {
 69 		var obj = tag.parent();
 70 		var url = GCN.settings.BACKEND_PATH + '/rest/' + obj._type
 71 				+ '/newtag/' + obj._data.id + GCN._getChannelParameter(obj);
 72 		obj._authAjax({
 73 			type: 'POST',
 74 			url: url,
 75 			json: data,
 76 			error: function (xhr, status, msg) {
 77 				GCN.handleHttpError(xhr, msg, error);
 78 				tag._vacate();
 79 			},
 80 			success: function (response) {
 81 				obj._handleCreateTagResponse(tag, response, success,
 82 				                             error);
 83 			}
 84 		}, error);
 85 	}
 86 
 87 	/**
 88 	 * Creates multiple tags via the GCN REST API.
 89 	 * 
 90 	 * @param {TagContainerAPI} parent container object for the new tags
 91 	 * @param {object} tagData data object for creating tags.
 92 	 * @param {function(TagAPI)} success Optional success callback
 93 	 * @param {function(GCNError):boolean} error Optional custom error handler.
 94 	 */
 95 	function newTags(parent, tagData, success, error) {
 96 		var url = GCN.settings.BACKEND_PATH + '/rest/' + parent._type
 97 			+ '/newtags/' + parent._data.id + GCN._getChannelParameter(parent);
 98 		var id, data = {create: {}};
 99 
100 		for (id in tagData.create) {
101 			if (tagData.create.hasOwnProperty(id)) {
102 				data.create[id] = tagData.create[id].data;
103 			}
104 		}
105 
106 		parent._authAjax({
107 			type: 'POST',
108 			url: url,
109 			json: data,
110 			error: function (xhr, status, msg) {
111 				var id;
112 
113 				GCN.handleHttpError(xhr, msg, error);
114 				for (id in tagData.create) {
115 					if (tagData.create.hasOwnProperty(id)) {
116 						tagData.create[id].tag._vacate();
117 					}
118 				}
119 			},
120 			success: function (response) {
121 				var id;
122 
123 				if (GCN.getResponseCode(response) === 'OK') {
124 					tagData.response = response;
125 					for (id in response.created) {
126 						if (response.created.hasOwnProperty(id)) {
127 							var data = response.created[id].tag, tag = tagData.create[id].tag;
128 							tagData.create[id].response = response.created[id];
129 							tag._name = data.name;
130 							tag._data = data;
131 							tag._fetched = true;
132 
133 							// The tag's id is still the temporary unique id that was given
134 							// to it in _createTag().  We have to realize the tag so that
135 							// it gets the correct id. The new id changes its hash, so it
136 							// must also be removed and reinserted from the caches.
137 							tag._removeFromTempCache(tag);
138 							tag._setHash(data.id)._addToCache();
139 
140 							// Add this tag into the tag's container `_shadow' object, and
141 							// `_tagIdToNameMap hash'.
142 							parent._update('tags.' + GCN.escapePropertyName(data.name),
143 									data, error, true);
144 
145 							// TODO: We need to store the tag inside the `_data' object for
146 							// now.  A change should be made so that when containers are
147 							// saved, the data in the `_shadow' object is properly
148 							// transfered into the _data object.
149 							parent._data.tags[data.name] = data;
150 
151 							if (!parent.hasOwnProperty('_createdTagIdToNameMap')) {
152 								parent._createdTagIdToNameMap = {};
153 							}
154 
155 							parent._createdTagIdToNameMap[data.id] = data.name;
156 
157 							tag.prop('active', true);
158 						}
159 					}
160 
161 					if (success) {
162 						success();
163 					}
164 				} else {
165 					for (id in tagData) {
166 						if (tagData.hasOwnProperty(id)) {
167 							tagData[id].tag._die(GCN.getResponseCode(response));
168 						}
169 					}
170 					GCN.handleResponseError(response, error);
171 				}
172 			}
173 		}, error);
174 	}
175 
176 	/**
177 	 * Checks whether exactly one of the following combination of options is
178 	 * provided:
179 	 *
180 	 * 1. `keyword' alone
181 	 * or
182 	 * 2. `constructId' alone
183 	 * or
184 	 * 3. `sourcePageId' and `sourceTagname' together.
185 	 *
186 	 * Each of these options are mutually exclusive.
187 	 *
188 	 * @param {Object} options
189 	 * @return {boolean} True if only one combination of the possible options
190 	 *                   above is contained in the given options object.
191 	 */
192 	function isValidCreateTagOptions(options) {
193 		// If the sum is 0, it means that no options was specified.
194 		//
195 		// If the sum is greater than 0 but less than 2, it means that either
196 		// `sourcePageId' or `sourceTagname' was specified, but not both as
197 		// required.
198 		//
199 		// If the sum is greater than 2, it means that more than one
200 		// combination of settings was provided, which is one too many.
201 		return 2 === (options.sourcePageId  ? 1 : 0) +
202 		             (options.sourceTagname ? 1 : 0) +
203 		             (options.keyword       ? 2 : 0) +
204 		             (options.constructId   ? 2 : 0);
205 	}
206 
207 	/**
208 	 * Parse the arguments passed into createTag() into a normalized object.
209 	 *
210 	 * @param {Arguments} createTagArgumenents An Arguments object.
211 	 * @parma {object} Normalized map of arguments.
212 	 */
213 	function parseCreateTagArguments(createTagArguments) {
214 		var args = Array.prototype.slice.call(createTagArguments);
215 		if (0 === args.length) {
216 			return {
217 				error: '`createtag()\' requires at least one argument.  See ' +
218 				       'documentation.'
219 			};
220 		}
221 
222 		var options;
223 
224 		// The first argument must either be a string, number or an object.
225 		switch (jQuery.type(args[0])) {
226 		case 'string':
227 			options = {keyword: args[0]};
228 			break;
229 		case 'number':
230 			options = {constructId: args[0]};
231 			break;
232 		case 'object':
233 			if (!isValidCreateTagOptions(args[0])) {
234 				return {
235 					error: 'createTag() requires exactly one of the ' +
236 					       'following, mutually exclusive, settings to be' +
237 					       'used: either `keyword\', `constructId\' or a ' +
238 					       'combination of `sourcePageId\' and ' +
239 					       '`sourceTagname\'.'
240 				};
241 			}
242 			options = args[0];
243 			break;
244 		default:
245 			options = {};
246 		}
247 
248 		// Determine success() and error(): arguments 2-3.
249 		var i;
250 		for (i = 1; i < args.length; i++) {
251 			if (jQuery.type(args[i]) === 'function') {
252 				if (options.success) {
253 					options.error = args[i];
254 				} else {
255 					options.success = args[i];
256 				}
257 			}
258 		}
259 
260 		return {
261 			options: options
262 		};
263 	}
264 
265 	/**
266 	 * Given an object containing information about a tag, determines whether
267 	 * or not we should treat a tag as a editabled block.
268 	 *
269 	 * Relying on `onlyeditables' property to determine whether or not a given
270 	 * tag is a block or an editable is unreliable since it is possible to have
271 	 * a block which only contains editables:
272 	 *
273 	 * {
274 	 *  "tagname":"content",
275 	 *  "editables":[{
276 	 *    "element":"GENTICS_EDITABLE_1234",
277 	 *    "readonly":false,
278 	 *    "partname":"editablepart"
279 	 *  }],
280 	 *  "element":"GENTICS_BLOCK_1234",
281 	 *  "onlyeditables":true
282 	 *  "tagname":"tagwitheditable"
283 	 * }
284 	 *
285 	 * In the above example, even though `onlyeditable' is true the tag is
286 	 * still a block, since the tag's element and the editable's element are
287 	 * not the same.
288 	 *
289 	 * @param {object} tag A object holding the sets of blocks and editables
290 	 *                     that belong to a tag.
291 	 * @return {boolean} True if the tag
292 	 */
293 	function isBlock(tag) {
294 		if (!tag.editables || tag.editables.length > 1) {
295 			return true;
296 		}
297 		return (
298 			(1 === tag.editables.length)
299 			&&
300 			(tag.editables[0].element !== tag.element)
301 		);
302 	}
303 
304 	/**
305 	 * Given a data object received from a REST API "/rest/page/render"
306 	 * call maps the blocks and editables into a list of each.
307 	 *
308 	 * The set of blocks and the set of editables that are returned are not
309 	 * mutually exclusive--if a tag is determined to be both an editable
310 	 * and a block, it will be included in both sets.
311 	 *
312 	 * @param {object} data
313 	 * @return {object<string, Array.<object>>} A map containing a set of
314 	 *                                          editables and a set of
315 	 *                                          blocks.
316 	 */
317 	function getEditablesAndBlocks(data) {
318 		if (!data || !data.tags) {
319 			return {
320 				blocks: [],
321 				editables: []
322 			};
323 		}
324 
325 		var tag;
326 		var tags = data.tags;
327 		var blocks = [];
328 		var editables = [];
329 		var i;
330 		var j;
331 
332 		for (i = 0; i < tags.length; i++) {
333 			tag = tags[i];
334 			if (tag.editables) {
335 				for (j = 0; j < tag.editables.length; j++) {
336 					tag.editables[j].tagname = tag.tagname;
337 				}
338 				editables = editables.concat(tag.editables);
339 			}
340 			if (isBlock(tag)) {
341 				blocks.push(tag);
342 			}
343 		}
344 
345 		return {
346 			blocks: blocks,
347 			editables: editables
348 		};
349 	}
350 
351 	/**
352 	 * Abstract class that is implemented by tag containers such as
353 	 * {@link PageAPI} or {@link TemplateAPI}
354 	 * 
355 	 * @class
356 	 * @name TagContainerAPI
357 	 */
358 	GCN.TagContainerAPI = GCN.defineChainback({
359 		/** @lends TagContainerAPI */
360 
361 		/**
362 		 * @private
363 		 * @type {object<number, string>} Hash, mapping tag ids to their
364 		 *                                corresponding names.
365 		 */
366 		_tagIdToNameMap: null,
367 
368 		/**
369 		 * @private
370 		 * @type {object<number, string>} Hash, mapping tag ids to their
371 		 *                                corresponding names for newly created
372 		 *                                tags.
373 		 */
374 		_createdTagIdToNameMap: {},
375 
376 		/**
377 		 * @private
378 		 * @type {Array.<object>} A set of blocks that are are to be removed
379 		 *                        from this content object when saving it.
380 		 *                        This array is populated during the save
381 		 *                        process.  It get filled just before
382 		 *                        persisting the data to the server, and gets
383 		 *                        emptied as soon as the save operation
384 		 *                        succeeds.
385 		 */
386 		_deletedBlocks: [],
387 
388 		/**
389 		 * @private
390 		 * @type {Array.<object>} A set of tags that are are to be removed from
391 		 *                        from this content object when it is saved.
392 		 */
393 		_deletedTags: [],
394 
395 		/**
396 		 * Searching for a tag of a given id from the object structure that is
397 		 * returned by the REST API would require O(N) time.  This function,
398 		 * builds a hash that maps the tag id with its corresponding name, so
399 		 * that it can be mapped in O(1) time instead.
400 		 *
401 		 * @private
402 		 * @return {object<number,string>} A hash map where the key is the tag
403 		 *                                 id, and the value is the tag name.
404 		 */
405 		'!_mapTagIdsToNames': function () {
406 			var name;
407 			var map = {};
408 			var tags = this._data.tags;
409 			for (name in tags) {
410 				if (tags.hasOwnProperty(name)) {
411 					map[tags[name].id] = name;
412 				}
413 			}
414 			return map;
415 		},
416 
417 		/**
418 		 * Retrieves data for a tag from the internal data object.
419 		 *
420 		 * @private
421 		 * @param {string} name The name of the tag.
422 		 * @return {!object} The tag data, or null if a there if no tag
423 		 *                   matching the given name.
424 		 */
425 		'!_getTagData': function (name) {
426 			return (this._data.tags && this._data.tags[name]) ||
427 			       (this._shadow.tags && this._shadow.tags[name]);
428 		},
429 
430 		/**
431 		 * Get the tag whose id is `id'.
432 		 * Builds the `_tagIdToNameMap' hash map if it doesn't already exist.
433 		 *
434 		 * @todo: Should we deprecate this?
435 		 * @private
436 		 * @param {number} id Id of tag to retrieve.
437 		 * @return {object} The tag's data.
438 		 */
439 		'!_getTagDataById': function (id) {
440 			if (!this._tagIdToNameMap) {
441 				this._tagIdToNameMap = this._mapTagIdsToNames();
442 			}
443 			return this._getTagData(this._tagIdToNameMap[id] ||
444 				 this._createdTagIdToNameMap[id]);
445 		},
446 
447 		/**
448 		 * Extracts the editables and blocks that have been rendered from the
449 		 * REST API render call's response data.
450 		 *
451 		 * @param {object} data The response object received from the
452 		 *                      renderTemplate() call.
453 		 * @return {object} An object containing two properties: an array of
454 		 *                  blocks, and an array of editables.
455 		 */
456 		'!_processRenderedTags': getEditablesAndBlocks,
457 
458 		// !!!
459 		// WARNING adding   to folder is neccessary as jsdoc will report a
460 		// name confict otherwise
461 		// !!!
462 		/**
463 		 * Get this content object's node.
464 		 *
465 		 * @function
466 		 * @name node 
467 		 * @memberOf TagContainerAPI
468 		 * @param {funtion(NodeAPI)=} success Optional callback to receive a
469 		 *                                    {@link NodeAPI} object as the
470 		 *                                    only argument.
471 		 * @param {function(GCNError):boolean=} error Optional custom error
472 		 *                                            handler.
473 		 * @return {NodeAPI} This object's node.
474 		 */
475 		'!node': function (success, error) {
476 			return this.folder().node();
477 		},
478 
479 		/**
480 		 * Synchronizes the information about tags with the server.
481 		 *
482 		 * @private
483 		 * @param {function} callback A function that will be called after
484 		 *		reloading the tags.
485 		 */
486 		'!_syncTags': function (callback) {
487 			if (typeof callback !== 'function') {
488 				callback = function () {};
489 			}
490 
491 			var that = this;
492 			// we clear the cache first, so that tmpObject will really be a new object
493 			this._clearCache();
494 			// call _read on a temporary API object and copy the tags from the loaded object onto this
495 			// we assume that every API object, that extends TagContainerAPI has _type set to the name of the
496 			// method on GCN that will create an according instance (e.g. for PageAPI, _type is 'page' and the method would be GCN.page())
497 			var tmpObject = GCN[this._type](this._data.id);
498 			tmpObject._read(function () {
499 				that._data.tags = tmpObject._data.tags;
500 				that._invoke(callback);
501 			});
502 		},
503 
504 		// !!!
505 		// WARNING adding   to folder is neccessary as jsdoc will report a
506 		// name confict otherwise
507 		// !!!
508 		/**
509 		 * Get this content object's parent folder.
510 		 * 
511 		 * @function
512 		 * @name folder 
513 		 * @memberOf TagContainerAPI
514 		 * @param {funtion(FolderAPI)=}
515 		 *            success Optional callback to receive a {@link FolderAPI}
516 		 *            object as the only argument.
517 		 * @param {function(GCNError):boolean=}
518 		 *            error Optional custom error handler.
519 		 * @return {FolderAPI} This object's parent folder.
520 		 */
521 		'!folder': function (success, error) {
522 			var id = this._fetched ? this.prop('folderId') : null;
523 			return this._continue(GCN.FolderAPI, id, success, error);
524 		},
525 
526 		/**
527 		 * Gets a tag of the specified id, contained in this content object.
528 		 *
529 		 * @name tag
530 		 * @function
531 		 * @memberOf TagContainerAPI
532 		 * @param {number} id Id of tag to retrieve.
533 		 * @param {function} success
534 		 * @param {function} error
535 		 * @return TagAPI
536 		 */
537 		'!tag': function (id, success, error) {
538 			return this._continue(GCN.TagAPI, id, success, error);
539 		},
540 
541 		/**
542 		 * Retrieves a collection of tags from this content object.
543 		 *
544 		 * @name tags
545 		 * @function
546 		 * @memberOf TagContainerAPI
547 		 * @param {object|string|number} settings (Optional)
548 		 * @param {function} success callback
549 		 * @param {function} error (Optional)
550 		 * @return TagContainerAPI
551 		 */
552 		'!tags': function () {
553 			var args = Array.prototype.slice.call(arguments);
554 
555 			if (args.length === 0) {
556 				return;
557 			}
558 
559 			var i;
560 			var j = args.length;
561 			var filter = {};
562 			var filters;
563 			var hasFilter = false;
564 			var success;
565 			var error;
566 
567 			// Determine `success', `error', `filter'
568 			for (i = 0; i < j; ++i) {
569 				switch (jQuery.type(args[i])) {
570 				case 'function':
571 					if (success) {
572 						error = args[i];
573 					} else {
574 						success = args[i];
575 					}
576 					break;
577 				case 'number':
578 				case 'string':
579 					filters = [args[i]];
580 					break;
581 				case 'array':
582 					filters = args[i];
583 					break;
584 				}
585 			}
586 
587 			if (jQuery.type(filters) === 'array') {
588 				var k = filters.length;
589 				while (k) {
590 					filter[filters[--k]] = true;
591 				}
592 				hasFilter = true;
593 			}
594 
595 			var that = this;
596 
597 			if (success) {
598 				this._read(function () {
599 					var tags = that._data.tags;
600 					var tag;
601 					var list = [];
602 
603 					for (tag in tags) {
604 						if (tags.hasOwnProperty(tag)) {
605 							if (!hasFilter || filter[tag]) {
606 								list.push(that._continue(GCN.TagAPI, tags[tag],
607 									null, error));
608 							}
609 						}
610 					}
611 
612 					that._invoke(success, [list]);
613 				}, error);
614 			}
615 		},
616 
617 		/**
618 		 * Internal method to create a tag of a given tagtype in this content
619 		 * object.
620 		 *
621 		 * Not all tag containers allow for new tags to be created on them,
622 		 * therefore this method will only be surfaced by tag containers which
623 		 * do allow this.
624 		 *
625 		 * @private
626 		 * @param {string|number|object} construct either the keyword of the
627 		 *                               construct, or the ID of the construct
628 		 *                               or an object with the following
629 		 *                               properties
630 		 *                               <ul>
631 		 *                                <li><i>keyword</i> keyword of the construct</li>
632 		 *                                <li><i>constructId</i> ID of the construct</li>
633 		 *                                <li><i>magicValue</i> magic value to be filled into the tag</li>
634 		 *                                <li><i>sourcePageId</i> source page id</li>
635 		 *                                <li><i>sourceTagname</i> source tag name</li>
636 		 *                               </ul>
637 		 * @param {function(TagAPI)=} success Optional callback that will
638 		 *                                    receive the newly created tag as
639 		 *                                    its only argument.
640 		 * @param {function(GCNError):boolean=} error Optional custom error
641 		 *                                            handler.
642 		 * @return {TagAPI} The newly created tag.
643 		 */
644 		'!_createTag': function () {
645 			var args = parseCreateTagArguments(arguments);
646 
647 			if (args.error) {
648 				GCN.handleError(
649 					GCN.createError('INVALID_ARGUMENTS', args.error, arguments),
650 					args.error
651 				);
652 				return;
653 			}
654 
655 			var obj = this;
656 
657 			// We use a uniqueId to avoid a fetus being created.
658 			// This is to avoid the following scenario:
659 			//
660 			// var tag1 = container.createTag(...);
661 			// var tag2 = container.createTag(...);
662 			// tag1 === tag2 // is true which is wrong
663 			//
664 			// However, for all other cases, where we get an existing object,
665 			// we want this behaviour:
666 			//
667 			// var folder1 = page(1).folder(...);
668 			// var folder2 = page(1).folder(...);
669 			// folder1 === folder2 // is true which is correct
670 			//
671 			// So, createTag() is different from other chainback methods in
672 			// that each invokation must create a new instance, while other
673 			// chainback methods must return the same.
674 			//
675 			// The id will be reset as soon as the tag object is realized.
676 			// This happens below as soon as we get a success response with the
677 			// correct tag id.
678 			var newId = GCN.uniqueId('TagApi-unique-');
679 
680 			// Create a new TagAPI instance linked to this tag container.  Also
681 			// acquire a lock on the newly created tag object so that any
682 			// further operations on it will be queued until the tag object is
683 			// fully realized.
684 			var tag = obj._continue(GCN.TagAPI, newId)._procure();
685 
686 			var options = args.options;
687 			var copying = !!(options.sourcePageId && options.sourceTagname);
688 
689 			var onCopy = function () {
690 				if (options.success) {
691 					// When the newly created tag is a copy of another tag
692 					// which itselfs contains nested tags, these nested
693 					// tags were created in the CMS but this object would
694 					// only know about the containing tag.
695 					obj._syncTags(function() {
696 						// update the tag with the synchronized data
697 						tag._data = obj._data.tags[tag._name];
698 						obj._invoke(options.success, [tag]);
699 						tag._vacate();
700 					});
701 				} else {
702 					tag._vacate();
703 				}
704 			};
705 			var onCreate =  function () {
706 				if (options.success) {
707 					obj._invoke(options.success, [tag]);
708 				}
709 				tag._vacate();
710 			};
711 
712 			if (copying) {
713 				newTag(tag, {
714 					copyPageId: options.sourcePageId,
715 					copyTagname: options.sourceTagname
716 				}, onCopy, options.error);
717 			} else {
718 				if (options.constructId) {
719 					newTag(tag, {
720 						magicValue: options.magicValue,
721 						constructId: options.constructId
722 					}, onCreate, options.error);
723 				} else {
724 					// ASSERT(options.keyword)
725 					getConstruct(options.keyword, obj.node(), function (construct) {
726 						newTag(tag, {
727 							magicValue: options.magicValue,
728 							constructId: construct.constructId
729 						}, onCreate, options.error);
730 					}, options.error);
731 				}
732 			}
733 
734 			return tag;
735 		},
736 
737 		/**
738 		 * Internal method to create multiple tags at once.
739 		 * The tagData object must have a property "create" that contains a single property for each tag to be created. Each property contains
740 		 * the request "data" (consisting of constructId/keyword and magicvalue)
741 		 * 
742 		 * Each tag property will get the response object for that tag attached (when the request is successfull)
743 		 * The tagData object itself will get the whole response attached
744 		 * 
745 		 * @private
746 		 * @param {object} tagData tag data object.
747 		 * @param {function(TagAPI)=} success Optional callback success callback
748 		 * @param {function(GCNError):boolean=} error Optional custom error
749 		 *                                            handler.
750 		 */
751 		'!_createTags': function (tagData, success, error) {
752 			var obj = this, id, newId;
753 
754 			for (id in tagData.create) {
755 				if (tagData.create.hasOwnProperty(id)) {
756 					newId = GCN.uniqueId('TagApi-unique-');
757 					tagData.create[id].tag = obj._continue(GCN.TagAPI, newId);
758 				}
759 			}
760 
761 			newTags(obj, tagData, success, error);
762 		},
763 
764 		/**
765 		 * Internal helper method to handle the create tag response.
766 		 * 
767 		 * @private
768 		 * @param {TagAPI} tag
769 		 * @param {object} response response object from the REST call
770 		 * @param {function(TagContainerAPI)=} success optional success handler
771 		 * @param {function(GCNError):boolean=} error optional error handler
772 		 */
773 		'!_handleCreateTagResponse': function (tag, response, success, error) {
774 			var obj = this;
775 
776 			if (GCN.getResponseCode(response) === 'OK') {
777 				var data = response.tag;
778 				tag._name = data.name;
779 				tag._data = data;
780 				tag._fetched = true;
781 
782 				// The tag's id is still the temporary unique id that was given
783 				// to it in _createTag().  We have to realize the tag so that
784 				// it gets the correct id. The new id changes its hash, so it
785 				// must also be removed and reinserted from the caches.
786 				tag._removeFromTempCache(tag);
787 				tag._setHash(data.id)._addToCache();
788 
789 				// Add this tag into the tag's container `_shadow' object, and
790 				// `_tagIdToNameMap hash'.
791 				var shouldCreateObjectIfUndefined = true;
792 				obj._update('tags.' + GCN.escapePropertyName(data.name),
793 					data, error, shouldCreateObjectIfUndefined);
794 
795 				// TODO: We need to store the tag inside the `_data' object for
796 				// now.  A change should be made so that when containers are
797 				// saved, the data in the `_shadow' object is properly
798 				// transfered into the _data object.
799 				obj._data.tags[data.name] = data;
800 
801 				if (!obj.hasOwnProperty('_createdTagIdToNameMap')) {
802 					obj._createdTagIdToNameMap = {};
803 				}
804 
805 				obj._createdTagIdToNameMap[data.id] = data.name;
806 
807 				tag.prop('active', true);
808 
809 				if (success) {
810 					success();
811 				}
812 			} else {
813 				tag._die(GCN.getResponseCode(response));
814 				GCN.handleResponseError(response, error);
815 			}
816 		},
817 
818 		/**
819 		 * Internal method to delete the specified tag from this content
820 		 * object.
821 		 *
822 		 * @private
823 		 * @param {string} keyword The keyword of the tag to be deleted.
824 		 * @param {function(TagContainerAPI)=} success Optional callback that
825 		 *                                             receive this object as
826 		 *                                             its only argument.
827 		 * @param {function(GCNError):boolean=} error Optional custom error
828 		 *                                            handler.
829 		 */
830 		'!_removeTag': function (keyword, success, error) {
831 			this.tag(keyword).remove(success, error);
832 		},
833 
834 		/**
835 		 * Internal method to delete a set of tags from this content object.
836 		 *
837 		 * @private
838 		 * @param {Array.<string>} keywords The keywords of the set of tags to be
839 		 *                             deleted.
840 		 * @param {function(TagContainerAPI)=} success Optional callback that
841 		 *                                             receive this object as
842 		 *                                             its only argument.
843 		 * @param {function(GCNError):boolean=} error Optional custom error
844 		 *                                            handler.
845 		 */
846 		'!_removeTags': function (keywords, success, error) {
847 			var that = this;
848 			this.tags(keywords, function (tags) {
849 				var j = tags.length;
850 				while (j--) {
851 					tags[j].remove(null, error);
852 				}
853 				if (success) {
854 					that.save(success, error);
855 				}
856 			}, error);
857 		}
858 
859 	});
860 
861 	GCN.TagContainerAPI.hasTagData = hasTagData;
862 	GCN.TagContainerAPI.extendTags = extendTags;
863 	GCN.TagContainerAPI.getEditablesAndBlocks = getEditablesAndBlocks;
864 
865 }(GCN));