1 /*jslint regexp: false */
  2 (function (GCN) {
  3 
  4 	'use strict';
  5 
  6 	/**
  7 	 * @private
  8 	 * @const
  9 	 * @type {object.<string, boolean>} Default page settings.
 10 	 */
 11 	var DEFAULT_SETTINGS = {
 12 		// Load folder information
 13 		folder: true,
 14 
 15 		// Lock page when loading it
 16 		update: true,
 17 
 18 		// Have language variants be included in the page response.
 19 		langvars: true,
 20 
 21 		// Have page variants be included in the page response.
 22 		pagevars: true
 23 	};
 24 
 25 	/**
 26 	 * Match URL to anchor
 27 	 *
 28 	 * @const
 29 	 * @type {RegExp}
 30 	 */
 31 	var ANCHOR_LINK = /([^#]*)#(.*)/;
 32 
 33 	/**
 34 	 * Checks whether the given tag is a magic link block.
 35 	 *
 36 	 * @param {TagAPI} tag A tag that must already have been fetched.
 37 	 * @param {Object} constructs Set of constructs.
 38 	 * @return {boolean} True if the given tag's constructId is equal to the
 39 	 *                   `MAGIC_LINK' value.
 40 	 */
 41 	function isMagicLinkTag(tag, constructs) {
 42 		return !!(constructs[GCN.settings.MAGIC_LINK]
 43 					&& (constructs[GCN.settings.MAGIC_LINK].constructId
 44 						=== tag.prop('constructId')));
 45 	}
 46 
 47 	/**
 48 	 * Given a page object, returns a jQuery set containing DOM elements for
 49 	 * each of the page's editable that is attached to the document.
 50 	 *
 51 	 * @param {PageAPI} page A page object.
 52 	 * @return {jQuery.<HTMLElement>} A jQuery set of editable DOM elements.
 53 	 */
 54 	function getEditablesInDocument(page) {
 55 		var id;
 56 		var $editables = jQuery();
 57 		var editables = page._editables;
 58 		for (id in editables) {
 59 			if (editables.hasOwnProperty(id)) {
 60 				$editables = $editables.add('#' + id);
 61 			}
 62 		}
 63 		return $editables;
 64 	}
 65 
 66 	/**
 67 	 * Returns all editables associated with the given page that have been
 68 	 * rendered in the document for editing.
 69 	 *
 70 	 * @param {PageAPI} page
 71 	 * @return {object} A set of editable objects.
 72 	 */
 73 	function getEditedEditables(page) {
 74 		return page._editables;
 75 	}
 76 
 77 	/**
 78 	 * Derives a list of the blocks that were rendered inside at least one of
 79 	 * the given page's edit()ed editables.
 80 	 *
 81 	 * @param {PageAPI} page Page object.
 82 	 * @return {Array.<object>} The set of blocks contained in any of the
 83 	 *                          page's rendered editables.
 84 	 */
 85 	function getRenderedBlocks(page) {
 86 		var editables = getEditedEditables(page);
 87 		var id;
 88 		var renderedBlocks = [];
 89 		for (id in editables) {
 90 			if (editables.hasOwnProperty(id)) {
 91 				if (editables[id]._gcnContainedBlocks) {
 92 					renderedBlocks = renderedBlocks.concat(
 93 						editables[id]._gcnContainedBlocks
 94 					);
 95 				}
 96 			}
 97 		}
 98 		return renderedBlocks;
 99 	}
100 
101 	/**
102 	 * Gets the DOM element associated with the given block.
103 	 *
104 	 * @param {object} block
105 	 * @return {?jQuery.<HTMLElement>} A jQuery unit set of the block's
106 	 *                                 corresponding DOM element, or null if no
107 	 *                                 element for the given block exists in
108 	 *                                 the document.
109 	 */
110 	function getElement(block) {
111 		var $element = jQuery('#' + block.element);
112 		return $element.length ? $element : null;
113 	}
114 
115 	/**
116 	 * Retrieves a jQuery set of all link elements that are contained in
117 	 * editables associated with the given page.
118 	 *
119 	 * @param {PageAPI} page
120 	 * @return {jQuery.<HTMLElement>} A jQuery set of link elements.
121 	 */
122 	function getEditableLinks(page) {
123 		return getEditablesInDocument(page).find('a[href]');
124 	}
125 
126 	/**
127 	 * Determines all blocks that no longer need their tags to be kept in the
128 	 * given page's tag list.
129 	 *
130 	 * @param {PageAPI} page
131 	 * @param {function(Array.<object>)} success A callback function that
132 	 *                                           receives a list of obsolete
133 	 *                                           blocks.
134 	 * @param {function(GCNError):boolean=} error Optional custom error
135 	 *                                            handler.
136 	 */
137 	function getObsoleteBlocks(page, success, error) {
138 		var blocks = getRenderedBlocks(page);
139 		if (0 === blocks.length) {
140 			success([]);
141 			return;
142 		}
143 		var $links = getEditableLinks(page);
144 		var numToProcess = blocks.length;
145 		var obsolete = [];
146 		var onSuccess = function () {
147 			if ((0 === --numToProcess) && success) {
148 				success(obsolete);
149 			}
150 		};
151 		var onError = function (GCNError) {
152 			if (error) {
153 				return error(GCNError);
154 			}
155 		};
156 		page.constructs(function (constructs) {
157 			var processTag = function (block) {
158 				page.tag(block.tagname, function (tag) {
159 					if (isMagicLinkTag(tag, constructs) && !getElement(block)) {
160 						obsolete.push(block);
161 					}
162 					onSuccess();
163 				}, onError);
164 			};
165 			var i;
166 			for (i = 0; i < blocks.length; i++) {
167 				processTag(blocks[i]);
168 			}
169 		});
170 	}
171 
172 	/**
173 	 * Checks whether or not the given block has a corresponding element in the
174 	 * document DOM.
175 	 *
176 	 * @private
177 	 * @static
178 	 * @param {object}
179 	 * @return {boolean} True if an inline element for this block exists.
180 	 */
181 	function hasInlineElement(block) {
182 		return !!getElement(block);
183 	}
184 
185 	/**
186 	 * Matches "aloha-*" class names.
187 	 *
188 	 * @const
189 	 * @type {RegExp}
190 	 */
191 	var ALOHA_CLASS_NAMES = /\baloha-[a-z0-9\-\_]*\b/gi;
192 
193 	/**
194 	 * Strips unwanted names from the given className string.
195 	 *
196 	 * All class names beginning with "aloha-block*" will be removed.
197 	 *
198 	 * @param {string} classes Space seperated list of classes.
199 	 * @return {string} Sanitized classes string.
200 	 */
201 	function cleanBlockClasses(classes) {
202 		return classes ? jQuery.trim(classes.replace(ALOHA_CLASS_NAMES, ''))
203 		               : '';
204 	}
205 
206 	/**
207 	 * Determines the backend object that was set to the given link.
208 	 *
209 	 * @param {jQuery.<HTMLElement>} $link A link in an editable.
210 	 * @return {Object} An object containing the gtxalohapagelink part keyword
211 	 *                  and value.  The keyword may be either be "url" or
212 	 *                  "fileurl" depending on the type of object linked to.
213 	 *                  The value may be a string url ("http://...") for
214 	 *                  external links or an integer for internal links.
215 	 */
216 	function getTagPartsFromLink($link) {
217 		var linkData = $link.attr('data-gentics-aloha-object-id');
218 		var href = $link.attr('href') || '';
219 		var anchorUrlMatch = href.match(ANCHOR_LINK);
220 		var tagparts = {
221 			text: $link.html(),
222 			anchor: $link.attr('data-gentics-gcn-anchor'),
223 			title: $link.attr('title'),
224 			target: $link.attr('target'),
225 			language: $link.attr('hreflang'),
226 			'class': cleanBlockClasses($link.attr('class')),
227 			channel: $link.attr('data-gcn-channelid')
228 		};
229 
230 		if (anchorUrlMatch && tagparts.anchor) {
231 			href = anchorUrlMatch[1];
232 		}
233 
234 		if (href === window.location.href) {
235 			href = '';
236 		}
237 
238 		if (linkData) {
239 			var idParts = linkData.split('.');
240 
241 			if (2 !== idParts.length) {
242 				tagparts.url = {
243 					'pageId' : linkData,
244 					'nodeId' : $link.attr('data-gcn-channelid')
245 				};
246 			} else if ('10007' === idParts[0]) {
247 				tagparts.url = {
248 					'pageId' : parseInt(idParts[1], 10),
249 					'nodeId' : $link.attr('data-gcn-channelid')
250 				};
251 				tagparts.fileurl = 0;
252 			} else if ('10008' === idParts[0] || '10011' === idParts[0]) {
253 				tagparts.url = 0;
254 				tagparts.fileurl = {
255 					'fileId' : parseInt(idParts[1], 10),
256 					'nodeId' : $link.attr('data-gcn-channelid')
257 				};
258 			} else {
259 				tagparts.url = href;
260 			}
261 		} else {
262 			// check whether the href contains links to internal pages or files
263 			var result = GCN.settings.checkForInternalLink(href);
264 
265 			tagparts.url = result.url;
266 			tagparts.fileurl = result.fileurl;
267 
268 			if (result.match) {
269 				href = '';
270 			}
271 		}
272 
273 		if (!tagparts.anchor) {
274 			tagparts.anchor = anchorUrlMatch ? anchorUrlMatch[2] : '';
275 		}
276 
277 		// Make sure the href attribute of the link is consistent with the
278 		// data fields after saving.
279 		var linkHref = href;
280 
281 		if (tagparts.anchor) {
282 			linkHref += '#' + tagparts.anchor;
283 		}
284 
285 		if (!linkHref) {
286 			linkHref = '#';
287 		}
288 
289 		$link.attr('href', linkHref);
290 		$link.attr('data-gentics-gcn-url', tagparts.url);
291 		$link.attr('data-gentics-gcn-fileurl', tagparts.fileurl);
292 		$link.attr('data-gentics-gcn-anchor', tagparts.anchor);
293 
294 		return tagparts;
295 	}
296 
297 	/**
298 	 * Checks whether a page object has a corresponding tag for a given link
299 	 * DOM element.
300 	 *
301 	 * @param {PageAPI} page The page object in which to look for the link tag.
302 	 * @param {jQuery.<HTMLElement>} $link jQuery unit set containing a link
303 	 *                                     DOM element.
304 	 * @return {boolean} True if there is a tag on the page that corresponds with
305 	 *                   the givn link.
306 	 */
307 	function hasTagForLink(page, $link) {
308 		var id = $link.attr('id');
309 		return !!(id && page._getBlockById(id));
310 	}
311 
312 	/**
313 	 * Checks whether or not the given part has a part type of the given
314 	 * name
315 	 *
316 	 * @param {TagAPI} tag
317 	 * @param {string} part Part name
318 	 * @return {boolean} True of part exists in the given tag.
319 	 */
320 	function hasTagPart(tag, part) {
321 		return !!tag._data.properties[part];
322 	}
323 
324 	/**
325 	 * Updates the parts of a tag in the page object that corresponds to the
326 	 * given link DOM element.
327 	 *
328 	 * @param {PageAPI} page
329 	 * @param {jQuery.<HTMLElement>} $link jQuery unit set containing a link
330 	 *                                     DOM element.
331 	 */
332 	function updateTagForLink(page, $link) {
333 		var block = page._getBlockById($link.attr('id'));
334 		// ASSERT(block)
335 		var tag = page.tag(block.tagname);
336 		var parts = getTagPartsFromLink($link);
337 		var part;
338 		for (part in parts) {
339 			if (parts.hasOwnProperty(part) && hasTagPart(tag, part)) {
340 				tag.part(part, parts[part]);
341 			}
342 		}
343 	}
344 
345 	/**
346 	 * Creates a new tag for the given link in the page.
347 	 *
348 	 * @param {PageAPI} page
349 	 * @param {jQuery.<HTMLElement>} $link jQuery unit set containing a link
350 	 *                                     element.
351 	 * @param {function} success Callback function that whill be called when
352 	 *                           the new tag is created.
353 	 * @param {function(GCNError):boolean=} error Optional custom error
354 	 *                                            handler.
355 	 */
356 	function createTagForLink(page, $link, success, error) {
357 		page.createTag({
358 			keyword: GCN.settings.MAGIC_LINK,
359 			magicValue: $link.html()
360 		}).edit(function (html, tag) {
361 			// Copy over the rendered id value for the link so that we can bind
362 			// the link in the DOM with the newly created block.
363 			$link.attr('id', jQuery(html).attr('id'));
364 			updateTagForLink(page, $link);
365 			success();
366 		}, error);
367 	}
368 
369 	/**
370 	 * Create tags for all new links in the page
371 	 * 
372 	 * @param {PageApi} page
373 	 * @param {jQuery.<HTMLElement>} $gcnlinks jQuery unit set containing links
374 	 * @param {function} success Callback function that will be called when the tags are created
375 	 * @param {function(GCNError)} error Optional custom error handler
376 	 */
377 	function createMissingLinkTags(page, $gcnlinks, success, error) {
378 		var $newGcnLinks = $gcnlinks.filter(function () {
379 			return !hasTagForLink(page, jQuery(this));
380 		}), linkData = {create:{}}, i = 0, id;
381 
382 		if ($newGcnLinks.length > 0) {
383 			$newGcnLinks.each(function (index) {
384 				id = 'link' + (i++);
385 				linkData.create[id] = {
386 					data: {
387 						keyword: GCN.settings.MAGIC_LINK,
388 						magicValue: jQuery(this).html()
389 					},
390 					obj: jQuery(this)
391 				};
392 			});
393 			page._createTags(linkData, function () {
394 				var id;
395 				for (id in linkData.create) {
396 					if (linkData.create.hasOwnProperty(id)) {
397 						linkData.create[id].obj.attr('id', jQuery(linkData.create[id].response.html).attr('id'));
398 					}
399 				}
400 				page._processRenderedTags(linkData.response);
401 				success();
402 			}, error);
403 		} else {
404 			success();
405 		}
406 
407 	}
408 
409 	/**
410 	 * Identifies internal GCN links in the given page's rendered editables,
411 	 * and updates their corresponding content tags, or create new tags for the
412 	 * if they are new links.
413 	 *
414 	 *  @param {PageAPI} page
415 	 *  @param {function} success
416 	 *  @param {function} error
417 	 */
418 	function processGCNLinks(page, success, error) {
419 		var $links = getEditableLinks(page);
420 		var $gcnlinks = $links.filter(function () {
421 			return this.isContentEditable;
422 		}).filter(':not(.aloha-editable)');
423 		if (0 === $gcnlinks.length) {
424 			if (success) {
425 				success();
426 			}
427 			return;
428 		}
429 		var numToProcess = $gcnlinks.length;
430 		var onSuccess = function () {
431 			if ((0 === --numToProcess) && success) {
432 				success();
433 			}
434 		};
435 		var onError = function (GCNError) {
436 			if (error) {
437 				return error(GCNError);
438 			}
439 		};
440 
441 		// When a link was copied it would result in two magic link tags
442 		// with the same ID. We remove the id attribute from such duplicates
443 		// so that hasTagForLink() will return false and create a new tag
444 		// for the copied link.
445 		var alreadyExists = {};
446 
447 		$links.each(function () {
448 			var $link = jQuery(this);
449 			var id = $link.attr('id');
450 
451 			if (id) {
452 				if (alreadyExists[id]) {
453 					$link.removeAttr('id');
454 				} else {
455 					alreadyExists[id] = true;
456 				}
457 			}
458 		});
459 
460 		createMissingLinkTags(page, $gcnlinks, function () {
461 			var i;
462 			for (i = 0; i < $gcnlinks.length; i++) {
463 				if (hasTagForLink(page, $gcnlinks.eq(i))) {
464 					updateTagForLink(page, $gcnlinks.eq(i));
465 					onSuccess();
466 				} else {
467 					onError(GCN.error('500', 'Missing Tag for Link', $gcnlinks.get(i)));
468 				}
469 			}
470 		}, onError);
471 	}
472 
473 	/**
474 	 * Adds the given blocks into the page's list of blocks that are to be
475 	 * deleted when the page is saved.
476 	 *
477 	 * @param {PageAPI} page
478 	 * @param {Array.<object>} blocks Blocks that are to be marked for deletion.
479 	 */
480 	function deleteBlocks(page, blocks) {
481 		blocks = jQuery.isArray(blocks) ? blocks : [blocks];
482 		var i;
483 		var success = function(tag) {
484 			tag.remove();
485 		};
486 		for (i = 0; i < blocks.length; i++) {
487 			if (-1 ===
488 					jQuery.inArray(blocks[i].tagname, page._deletedBlocks)) {
489 				page._deletedBlocks.push(blocks[i].tagname);
490 			}
491 			delete page._blocks[blocks[i].element];
492 
493 			page.tag(blocks[i].tagname, success);
494 		}
495 	}
496 
497 	/**
498 	 * Removes all tags on the given page which belong to links that are no
499 	 * longer present in any of the page's rendered editables.
500 	 *
501 	 * @param {PageAPI} page
502 	 * @param {function} success Callback function that will be invoked when
503 	 *                           all obsolete tags have been successfully
504 	 *                           marked for deletion.
505 	 * @param {function(GCNError):boolean=} error Optional custom error
506 	 *                                            handler.
507 	 */
508 	function deleteObsoleteLinkTags(page, success, error) {
509 		getObsoleteBlocks(page, function (obsolete) {
510 			deleteBlocks(page, obsolete);
511 			if (success) {
512 				success();
513 			}
514 		}, error);
515 	}
516 
517 	/**
518 	 * Searches for the an Aloha editable object of the given id.
519 	 *
520 	 * @TODO: Once Aloha.getEditableById() is patched to not cause an
521 	 *        JavaScript exception if the element for the given ID is not found
522 	 *        then we can deprecate this function and use Aloha's instead.
523 	 *
524 	 * @static
525 	 * @param {string} id Id of Aloha.Editable object to find.
526 	 * @return {Aloha.Editable=} The editable object, if wound; otherwise null.
527 	 */
528 	function getAlohaEditableById(id) {
529 		var Aloha = (typeof window !== 'undefined') && window.Aloha;
530 		if (!Aloha) {
531 			return null;
532 		}
533 
534 		// If the element is a textarea then route to the editable div.
535 		var element = jQuery('#' + id);
536 		if (element.length &&
537 				element[0].nodeName.toLowerCase() === 'textarea') {
538 			id += '-aloha';
539 		}
540 
541 		var editables = Aloha.editables;
542 		var j = editables.length;
543 		while (j) {
544 			if (editables[--j].getId() === id) {
545 				return editables[j];
546 			}
547 		}
548 
549 		return null;
550 	}
551 
552 	/**
553 	 * For a given list of editables and a list of blocks, determines in which
554 	 * editable each block is contained.  The result is a map of block sets.
555 	 * Each of these sets of blocks are mapped against the id of the editable
556 	 * in which they are rendered.
557 	 *
558 	 * @param {string} content The rendered content in which both the list of
559 	 *                         editables, and blocks are contained.
560 	 * @param {Array.<object>} editables A list of editables contained in the
561 	 *                                   content.
562 	 * @param {Array.<object>} blocks A list of blocks containd in the content.
563 	 * @return {object<string, Array>} A object whose keys are editable ids,
564 	 *                                  and whose values are arrays of blocks
565 	 *                                  contained in the corresponding
566 	 *                                  editable.
567 	 */
568 	function categorizeBlocksAgainstEditables(content, editables, blocks) {
569 		var i;
570 		var $content = jQuery('<div>' + content + '</div>');
571 		var sets = {};
572 		var editableId;
573 
574 		var editablesSelectors = [];
575 		for (i = 0; i < editables.length; i++) {
576 			editablesSelectors.push('#' + editables[i].element);
577 		}
578 
579 		var markerClass = 'aloha-editable-tmp-marker__';
580 		var $editables = $content.find(editablesSelectors.join(','));
581 		$editables.addClass(markerClass);
582 
583 		var $block;
584 		var $parent;
585 		for (i = 0; i < blocks.length; i++) {
586 			$block = $content.find('#' + blocks[i].element);
587 			if ($block.length) {
588 				$parent = $block.closest('.' + markerClass);
589 				if ($parent.length) {
590 					editableId = $parent.attr('id');
591 					if (editableId) {
592 						if (!sets[editableId]) {
593 							sets[editableId] = [];
594 						}
595 						sets[editableId].push(blocks[i]);
596 					}
597 				}
598 			}
599 		}
600 
601 		return sets;
602 	}
603 
604 	/**
605 	 * Causes the given editables to be tracked, so that when the content
606 	 * object is saved, these editables will be processed.
607 	 *
608 	 * @private
609 	 * @param {PageAPI} page
610 	 * @param {Array.<object>} editables A set of object representing
611 	 *                                   editable tags that have been
612 	 *                                   rendered.
613 	 */
614 	function trackEditables(page, editables) {
615 		if (!page.hasOwnProperty('_editables')) {
616 			page._editables = {};
617 		}
618 		var i;
619 		for (i = 0; i < editables.length; i++) {
620 			page._editables[editables[i].element] = editables[i];
621 		}
622 	}
623 
624 	/**
625 	 * Causes the given blocks to be tracked so that when the content object is
626 	 * saved, these editables will be processed.
627 	 *
628 	 * @private
629 	 * @param {PageAPI} page
630 	 * @param {Array.<object>} blocks An set of object representing block
631 	 *                                tags that have been rendered.
632 	 */
633 	function trackBlocks(page, blocks) {
634 		if (!page.hasOwnProperty('_blocks')) {
635 			page._blocks = {};
636 		}
637 		var i;
638 		for (i = 0; i < blocks.length; i++) {
639 			page._blocks[blocks[i].element] = blocks[i];
640 		}
641 	}
642 
643 	/**
644 	 * Associates a list of blocks with an editable so that it can later be
645 	 * determined which blocks are contained inside which editable.
646 	 *
647 	 * @param object
648 	 */
649 	function associateBlocksWithEditable(editable, blocks) {
650 		if (!jQuery.isArray(editable._gcnContainedBlocks)) {
651 			editable._gcnContainedBlocks = [];
652 		}
653 
654 		var i, j;
655 
656 		outer:
657 		for (i = 0; i < blocks.length; i++) {
658 			for (j = 0; j < editable._gcnContainedBlocks.length; j++) {
659 				if (blocks[i].element === editable._gcnContainedBlocks[j].element
660 						&& blocks[i].tagname === editable._gcnContainedBlocks[j].tagname) {
661 					// Prevent duplicates
662 					continue outer;
663 				}
664 			}
665 
666 			editable._gcnContainedBlocks.push(blocks[i]);
667 		}
668 	}
669 
670 	/**
671 	 * Extracts the editables and blocks that have been rendered from the
672 	 * REST API render call's response data, and stores them in the page.
673 	 *
674 	 * @param {PageAPI} page The page inwhich to track the incoming tags.
675 	 * @param {object} data Raw data containing editable and block tags
676 	 * information.
677 	 * @return {object} A object containing to lists: one list of blocks, and
678 	 *                  another of editables.
679 	 */
680 	function trackRenderedTags(page, data) {
681 		var tags = GCN.TagContainerAPI.getEditablesAndBlocks(data);
682 
683 		var containment = categorizeBlocksAgainstEditables(
684 			data.content,
685 			tags.editables,
686 			tags.blocks
687 		);
688 
689 		trackEditables(page, tags.editables);
690 		trackBlocks(page, tags.blocks);
691 
692 		jQuery.each(containment, function (editable, blocks) {
693 			if (page._editables[editable]) {
694 				associateBlocksWithEditable(page._editables[editable], blocks);
695 			}
696 		});
697 
698 		return tags;
699 	}
700 
701 	/**
702 	 * @private
703 	 * @const
704 	 * @type {number}
705 	 */
706 	//var TYPE_ID = 10007;
707 
708 	/**
709 	 * @private
710 	 * @const
711 	 * @type {Enum}
712 	 */
713 	var STATUS = {
714 
715 		// page was not found in the database
716 		NOTFOUND: -1,
717 
718 		// page is locally modified and not yet (re-)published
719 		MODIFIED: 0,
720 
721 		// page is marked to be published (dirty)
722 		TOPUBLISH: 1,
723 
724 		// page is published and online
725 		PUBLISHED: 2,
726 
727 		// Page is offline
728 		OFFLINE: 3,
729 
730 		// Page is in the queue (publishing of the page needs to be affirmed)
731 		QUEUE: 4,
732 
733 		// page is in timemanagement and outside of the defined timespan
734 		// (currently offline)
735 		TIMEMANAGEMENT: 5,
736 
737 		// page is to be published at a given time (not yet)
738 		TOPUBLISH_AT: 6
739 	};
740 
741 	/**
742 	 * @class
743 	 * @name PageAPI
744 	 * @extends ContentObjectAPI
745 	 * @extends TagContainerAPI
746 	 * 
747 	 * @param {number|string}
748 	 *            id of the page to be loaded
749 	 * @param {function(ContentObjectAPI))=}
750 	 *            success Optional success callback that will receive this
751 	 *            object as its only argument.
752 	 * @param {function(GCNError):boolean=}
753 	 *            error Optional custom error handler.
754 	 * @param {object}
755 	 *            settings additional settings for object creation. These
756 	 *            correspond to options available from the REST-API and will
757 	 *            extend or modify the PageAPI object.
758 	 *            <dl>
759 	 *            <dt>update: true</dt>
760 	 *            <dd>Whether the page should be locked in the backend when
761 	 *            loading it. default: true</dd>
762 	 *            <dt>template: true</dt>
763 	 *            <dd>Whether the template information should be embedded in
764 	 *            the page object. default: true</dd>
765 	 *            <dt>folder: true</dt>
766 	 *            <dd>Whether the folder information should be embedded in the
767 	 *            page object. default: true <b>WARNING</b>: do not turn this
768 	 *            option off - it will leave the API in a broken state.</dd>
769 	 *            <dt>langvars: true</dt>
770 	 *            <dd>When the language variants shall be embedded in the page
771 	 *            response. default: true</dd>
772 	 *            <dt>workflow: false</dt>
773 	 *            <dd>When the workflow information shall be embedded in the
774 	 *            page response. default: false</dd>
775 	 *            <dt>pagevars: true</dt>
776 	 *            <dd>When the page variants shall be embedded in the page
777 	 *            response. Page variants will contain folder information.
778 	 *            default: true</dd>
779 	 *            <dt>translationstatus: false</dt>
780 	 *            <dd>Will return information on the page's translation status.
781 	 *            default: false</dd>
782 	 *            </dl>
783 	 */
784 	var PageAPI = GCN.defineChainback({
785 		/** @lends PageAPI */
786 
787 		__chainbacktype__: 'PageAPI',
788 		_extends: [ GCN.TagContainerAPI, GCN.ContentObjectAPI ],
789 		_type: 'page',
790 
791 		/**
792 		 * A hash set of block tags belonging to this page.  This set grows as
793 		 * this page's tags are rendered.
794 		 *
795 		 * @private
796 		 * @type {Array.<object>}
797 		 */
798 		_blocks: {},
799 
800 		/**
801 		 * A hash set of editable tags belonging to this page.  This set grows
802 		 * as this page's tags are rendered.
803 		 *
804 		 * @private
805 		 * @type {Array.<object>}
806 		 */
807 		_editables: {},
808 
809 		/**
810 		 * Writable properties for the page object. Currently the following
811 		 * properties are writeable: cdate, description, fileName, folderId,
812 		 * name, priority, templateId. WARNING: changing the folderId might not
813 		 * work as expected.
814 		 * 
815 		 * @type {Array.string}
816 		 * @const
817 		 */
818 		WRITEABLE_PROPS: [
819 		                  'customCdate',
820 		                  'customEdate',
821 		                  'description',
822 		                  'fileName',
823 		                  'folderId', // @TODO Check if moving a page is
824 		                              //       implemented correctly.
825 		                  'name',
826 		                  'priority',
827 		                  'templateId',
828 		                  'timeManagement'
829 		                  ],
830 
831 		/**
832 		 * @type {object} Constraints for writeable props
833 		 * @const
834 		 *
835 		 */
836 		WRITEABLE_PROPS_CONSTRAINTS: {
837 			'name': {
838 				maxLength: 255
839 			}
840 		},
841 
842 		/**
843 		 * Gets all blocks that are associated with this page.
844 		 *
845 		 * It is important to note that the set of blocks in the returned array
846 		 * will only include those that are the returned by the server when
847 		 * calling  edit() on a tag that belongs to this page.
848 		 *
849 		 * @return {Array.<object>} The set of blocks that have been
850 		 *                          initialized by calling edit() on one of
851 		 *                          this page's tags.
852 		 */
853 		'!blocks': function () {
854 			return this._blocks;
855 		},
856 
857 		/**
858 		 * Retrieves a block with the given id among the blocks that are
859 		 * tracked by this page content object.
860 		 *
861 		 * @private
862 		 * @param {string} id The block's id.
863 		 * @return {?object} The block data object.
864 		 */
865 		'!_getBlockById': function (id) {
866 			return this._blocks[id];
867 		},
868 
869 		/**
870 		 * Extracts the editables and blocks that have been rendered from the
871 		 * REST API render call's response data, and stores them in the page.
872 		 *
873 		 * @override
874 		 */
875 		'!_processRenderedTags': function (data) {
876 			return trackRenderedTags(this, data);
877 		},
878 
879 		/**
880 		 * Processes this page's tags in preparation for saving.
881 		 *
882 		 * The preparation process:
883 		 *
884 		 * 1. For all editables associated with this page, determine which of
885 		 *    their blocks have been rendered into the DOM for editing so that
886 		 *    changes to the DOM can be reflected in the corresponding data
887 		 *    structures before pushing the tags to the server.
888 		 *
889 		 * 2.
890 		 *
891 		 * Processes rendered tags, and updates the `_blocks' and `_editables'
892 		 * arrays accordingly.  This function is called during pre-saving to
893 		 * update this page's editable tags.
894 		 *
895 		 * @private
896 		 */
897 		'!_prepareTagsForSaving': function (success, error) {
898 			if (!this.hasOwnProperty('_deletedBlocks')) {
899 				this._deletedBlocks = [];
900 			}
901 			var page = this;
902 			processGCNLinks(page, function () {
903 				deleteObsoleteLinkTags(page, function () {
904 					page._updateEditableBlocks();
905 					if (success) {
906 						success();
907 					}
908 				}, error);
909 			}, error);
910 		},
911 
912 		/**
913 		 * Writes the contents of editables back into their corresponding tags.
914 		 * If a corresponding tag cannot be found for an editable, a new one
915 		 * will be created for it.
916 		 *
917 		 * A reference for each editable tag is then added to the `_shadow'
918 		 * object in order that the tag will be sent with the save request.
919 		 *
920 		 * @private
921 		 */
922 		'!_updateEditableBlocks': function (filter) {
923 			var $element;
924 			var id;
925 			var editables = this._editables;
926 			var tags = this._data.tags;
927 			var tagname;
928 			var html;
929 			var alohaEditable;
930 			var $cleanElement;
931 			var customSerializer;
932 
933 			for (id in editables) {
934 				if (editables.hasOwnProperty(id)) {
935 					$element = jQuery('#' + id);
936 
937 					// Because the editable may not have have been rendered into
938 					// the document DOM.
939 					if (0 === $element.length) {
940 						continue;
941 					}
942 
943 					if (typeof filter === 'function') {
944 						if (!filter.call(this, editables[id])) {
945 							continue;
946 						}
947 					}
948 
949 					tagname = editables[id].tagname;
950 
951 					if (!tags[tagname]) {
952 						tags[tagname] = {
953 							name       : tagname,
954 							active     : true,
955 							properties : {}
956 						};
957 					} else {
958 						// Because it is sensible to assume that every editable
959 						// that was rendered for editing is intended to be an
960 						// activate tag.
961 						tags[tagname].active = true;
962 					}
963 
964 					// Because editables that have been aloha()fied, must have
965 					// their contents retrieved by getContents() in order to get
966 					// clean HTML.
967 
968 					alohaEditable = getAlohaEditableById(id);
969 
970 					if (alohaEditable) {
971 						// Avoid the unnecessary overhead of custom editable
972 						// serialization by calling html ourselves.
973 						$cleanElement = jQuery('<div>').append(
974 							alohaEditable.getContents(true)
975 						);
976 						alohaEditable.setUnmodified();
977 						// Apply the custom editable serialization as the last step.
978 						customSerializer = window.Aloha.Editable.getContentSerializer();
979 						html = this.encode($cleanElement, customSerializer);
980 					} else {
981 						html = this.encode($element);
982 					}
983 					// If the editable is backed by a parttype, that
984 					// would replace newlines by br tags while
985 					// rendering, remove all newlines before saving back
986 					if ($element.hasClass('GENTICS_parttype_text') ||
987 						$element.hasClass('GENTICS_parttype_texthtml') ||
988 						$element.hasClass('GENTICS_parttype_java_editor') ||
989 						$element.hasClass('GENTICS_parttype_texthtml_long')) {
990 						html = html.replace(/(\r\n|\n|\r)/gm,"");
991 					}
992 
993 					tags[tagname].properties[editables[id].partname] =
994 						jQuery.extend({type: 'RICHTEXT'}, tags[tagname].properties[editables[id].partname], {stringValue: html});
995 
996 					this._update('tags.' + GCN.escapePropertyName(tagname),
997 						tags[tagname]);
998 				}
999 			}
1000 		},
1001 
1002 		/**
1003 		 * @see ContentObjectAPI.!_loadParams
1004 		 */
1005 		'!_loadParams': function () {
1006 			return jQuery.extend(DEFAULT_SETTINGS, this._settings);
1007 		},
1008 
1009 		/**
1010 		 * Get this page's template.
1011 		 *
1012 		 * @public
1013 		 * @function
1014 		 * @name template
1015 		 * @memberOf PageAPI
1016 		 * @param {funtion(TemplateAPI)=} success Optional callback to receive
1017 		 *                                        a {@link TemplateAPI} object
1018 		 *                                        as the only argument.
1019 		 * @param {function(GCNError):boolean=} error Optional custom error
1020 		 *                                            handler.
1021 		 * @return {TemplateAPI} This page's parent template.
1022 		 */
1023 		'!template': function (success, error) {
1024 			var id = this._fetched ? this.prop('templateId') : null;
1025 			return this._continue(GCN.TemplateAPI, id, success, error);
1026 		},
1027 
1028 		/**
1029 		 * Cache of constructs for this page.
1030 		 * Should be cleared when page is saved.
1031 		 */
1032 		_constructs: null,
1033 
1034 		/**
1035 		 * List of success and error callbacks that need to be called
1036 		 * once the constructs are loaded
1037 		 * @private
1038 		 * @type {array.<object>}
1039 		 */
1040 		_constructLoadHandlers: null,
1041 
1042 		/**
1043 		 * Retrieve the list of constructs of the tag that are used in this
1044 		 * page.
1045 		 *
1046 		 * Note that tags that have been created on this page locally, but have
1047 		 * yet to be persisted to the server (unsaved tags), will not have their
1048 		 * constructs included in the list unless their constructs are used by
1049 		 * other saved tags.
1050 		 */
1051 		'!constructs': function (success, error) {
1052 			var page = this;
1053 			if (page._constructs) {
1054 				return success(page._constructs);
1055 			}
1056 
1057 			// if someone else is already loading the constructs, just add the callbacks
1058 			page._constructLoadHandlers = page._constructLoadHandlers || [];
1059 			if (page._constructLoadHandlers.length > 0) {
1060 				page._constructLoadHandlers.push({success: success, error: error});
1061 				return;
1062 			}
1063 
1064 			// we are the first to load the constructs, register the callbacks and
1065 			// trigger the ajax call
1066 			page._constructLoadHandlers.push({success: success, error: error});
1067 			page._authAjax({
1068 				url: GCN.settings.BACKEND_PATH
1069 				     + '/rest/construct/list.json?pageId=' + page.id(),
1070 				type: 'GET',
1071 				error: function (xhr, status, msg) {
1072 					var i;
1073 					for (i = 0; i < page._constructLoadHandlers.length; i++) {
1074 						GCN.handleHttpError(xhr, msg, page._constructLoadHandlers[i].error);
1075 					}
1076 				},
1077 				success: function (response) {
1078 					var i;
1079 					if (GCN.getResponseCode(response) === 'OK') {
1080 						page._constructs = GCN.mapConstructs(response.constructs);
1081 						for (i = 0; i < page._constructLoadHandlers.length; i++) {
1082 							page._invoke(page._constructLoadHandlers[i].success, [page._constructs]);
1083 						}
1084 					} else {
1085 						for (i = 0; i < page._constructLoadHandlers.length; i++) {
1086 							GCN.handleResponseError(response, page._constructLoadHandlers[i].error);
1087 						}
1088 					}
1089 				},
1090 
1091 				complete: function () {
1092 					page._constructLoadHandlers = [];
1093 				}
1094 			});
1095 		},
1096 
1097 		/**
1098 		 * @override
1099 		 * @see ContentObjectAPI._save
1100 		 */
1101 		'!_save': function (settings, success, error) {
1102 			var page = this;
1103 			this._fulfill(function () {
1104 				page._read(function () {
1105 					var fork = page._fork();
1106 					fork._prepareTagsForSaving(function () {
1107 						GCN.pub('page.before-saved', fork);
1108 						fork._persist(settings, function () {
1109 							if (success) {
1110 								page._constructs = null;
1111 								fork._merge(false);
1112 								page._invoke(success, [page]);
1113 								page._vacate();
1114 							} else {
1115 								fork._merge();
1116 							}
1117 						}, function () {
1118 							page._vacate();
1119 							if (error) {
1120 								error.apply(this, arguments);
1121 							}
1122 						});
1123 					}, error);
1124 				}, error);
1125 			}, error);
1126 		},
1127 
1128 		//---------------------------------------------------------------------
1129 		// Surface the tag container methods that are applicable for GCN page
1130 		// objects.
1131 		//---------------------------------------------------------------------
1132 
1133 		/**
1134 		 * Creates a tag of a given tagtype in this page.
1135 		 * The first parameter should either be the construct keyword or ID,
1136 		 * or an object containing exactly one of the following property sets:<br/>
1137 		 * <ol>
1138 		 *   <li><i>keyword</i> to create a tag based on the construct with given keyword</li>
1139 		 *   <li><i>constructId</i> to create a tag based on the construct with given ID</li>
1140 		 *   <li><i>sourcePageId</i> and <i>sourceTagname</i> to create a tag as copy of the given tag from the page</li>
1141 		 * </ol>
1142 		 *
1143 		 * Exmaple:
1144 		 * <pre>
1145 		 *  createTag('link', onSuccess, onError);
1146 		 * </pre>
1147 		 * or
1148 		 * <pre>
1149 		 *  createTag({keyword: 'link', magicValue: 'http://www.gentics.com'}, onSuccess, onError);
1150 		 * </pre>
1151 		 * or
1152 		 * <pre>
1153 		 *  createTag({sourcePageId: 4711, sourceTagname: 'link'}, onSuccess, onError);
1154 		 * </pre>
1155 		 *
1156 		 * @public
1157 		 * @function
1158 		 * @name createTag
1159 		 * @memberOf PageAPI
1160 		 * @param {string|number|object} construct either the keyword of the
1161 		 *                               construct, or the ID of the construct
1162 		 *                               or an object with the following
1163 		 *                               properties
1164 		 *                               <ul>
1165 		 *                                <li><i>keyword</i> keyword of the construct</li>
1166 		 *                                <li><i>constructId</i> ID of the construct</li>
1167 		 *                                <li><i>magicValue</i> magic value to be filled into the tag</li>
1168 		 *                                <li><i>sourcePageId</i> source page id</li>
1169 		 *                                <li><i>sourceTagname</i> source tag name</li>
1170 		 *                               </ul>
1171 		 * @param {function(TagAPI)=} success Optional callback that will
1172 		 *                                    receive the newly created tag as
1173 		 *                                    its only argument.
1174 		 * @param {function(GCNError):boolean=} error Optional custom error
1175 		 *                                            handler.
1176 		 * @return {TagAPI} The newly created tag.
1177 		 * @throws INVALID_ARGUMENTS
1178 		 */
1179 		'!createTag': function () {
1180 			return this._createTag.apply(this, arguments);
1181 		},
1182 
1183 		/**
1184 		 * Deletes the specified tag from this page.
1185 		 * You should pass a keyword here not an Id.
1186 		 * 
1187 		 * Note: Due to how the underlying RestAPI layer works,
1188 		 * the success callback will also be called if the specified tag
1189 		 * does not exist.
1190 		 * 
1191 		 * @public
1192 		 * @function
1193 		 * @memberOf PageAPI
1194 		 * @param {string}
1195 		 *            keyword The keyword of the tag to be deleted.
1196 		 * @param {function(PageAPI)=}
1197 		 *            success Optional callback that receive this object as its
1198 		 *            only argument.
1199 		 * @param {function(GCNError):boolean=}
1200 		 *            error Optional custom error handler.
1201 		 */
1202 		removeTag: function () {
1203 			this._removeTag.apply(this, arguments);
1204 		},
1205 
1206 		/**
1207 		 * Deletes a set of tags from this page.
1208 		 * 
1209 		 * @public
1210 		 * @function
1211 		 * @memberOf PageAPI
1212 		 * @param {Array.
1213 		 *            <string>} keywords The keywords of the tags to be deleted.
1214 		 * @param {function(PageAPI)=}
1215 		 *            success Optional callback that receive this object as its
1216 		 *            only argument.
1217 		 * @param {function(GCNError):boolean=}
1218 		 *            error Optional custom error handler.
1219 		 */
1220 		removeTags: function () {
1221 			this._removeTags.apply(this, arguments);
1222 		},
1223 
1224 		/**
1225 		 * Takes the page offline.
1226 		 * If instant publishing is enabled, this will take the page offline
1227 		 * immediately. Otherwise it will be taken offline during the next
1228 		 * publish run.
1229 		 *
1230 		 * @public
1231 		 * @function
1232 		 * @memberOf PageAPI
1233 		 * @param {funtion(PageAPI)=} success Optional callback to receive this
1234 		 *                                    page object as the only argument.
1235 		 * @param {function(GCNError):boolean=} error Optional custom error
1236 		 *                                            handler.
1237 		 */
1238 		takeOffline: function (success, error) {
1239 			var page = this;
1240 			page._fulfill(function () {
1241 				page._authAjax({
1242 					url: GCN.settings.BACKEND_PATH + '/rest/' + page._type +
1243 					     '/takeOffline/' + page.id(),
1244 					type: 'POST',
1245 					json: {}, // There needs to be at least empty content
1246 					          // because of a bug in Jersey.
1247 					error: error,
1248 					success: function (response) {
1249 						if (success) {
1250 							page._invoke(success, [page]);
1251 						}
1252 					}
1253 				});
1254 			});
1255 		},
1256 
1257 		/**
1258 		 * Trigger publish process for the page.
1259 		 *
1260 		 * @public
1261 		 * @function
1262 		 * @memberOf PageAPI
1263 		 * @param {funtion(PageAPI)=} success Optional callback to receive this
1264 		 *                                    page object as the only argument.
1265 		 * @param {function(GCNError):boolean=} error Optional custom error
1266 		 *                                            handler.
1267 		 */
1268 		publish: function (success, error) {
1269 			var page = this;
1270 			GCN.pub('page.before-publish', page);
1271 			this._fulfill(function () {
1272 				page._authAjax({
1273 					url: GCN.settings.BACKEND_PATH + '/rest/' + page._type +
1274 					     '/publish/' + page.id() + GCN._getChannelParameter(page),
1275 					type: 'POST',
1276 					json: {}, // There needs to be at least empty content
1277 					          // because of a bug in Jersey.
1278 					success: function (response) {
1279 						page._data.status = STATUS.PUBLISHED;
1280 						if (success) {
1281 							page._invoke(success, [page]);
1282 						}
1283 					},
1284 					error: error
1285 				});
1286 			});
1287 		},
1288 
1289 		/**
1290 		 * Renders a preview of the current page.
1291 		 * 
1292 		 * @public
1293 		 * @function
1294 		 * @memberOf PageAPI
1295 		 * @param {function(string,
1296 		 *            PageAPI)} success Callback to receive the rendered page
1297 		 *            preview as the first argument, and this page object as the
1298 		 *            second.
1299 		 * @param {function(GCNError):boolean=}
1300 		 *            error Optional custom error handler.
1301 		 */
1302 		preview: function (success, error) {
1303 			var that = this;
1304 
1305 			this._read(function () {
1306 				that._authAjax({
1307 					url: GCN.settings.BACKEND_PATH + '/rest/' + that._type +
1308 					     '/preview/' + GCN._getChannelParameter(that),
1309 					json: {
1310 						page: that._data, // @FIXME Shouldn't this a be merge of
1311 						                 //        the `_shadow' object and the
1312 										 //        `_data'.
1313 						nodeId: that.nodeId()
1314 					},
1315 					type: 'POST',
1316 					error: error,
1317 					success: function (response) {
1318 						if (success) {
1319 							GCN._handleContentRendered(response.preview, that,
1320 								function (html) {
1321 									that._invoke(success, [html, that]);
1322 								});
1323 						}
1324 					}
1325 				});
1326 			}, error);
1327 		},
1328 
1329 		/**
1330 		 * Unlocks the page when finishing editing
1331 		 * 
1332 		 * @public
1333 		 * @function
1334 		 * @memberOf PageAPI
1335 		 * @param {funtion(PageAPI)=}
1336 		 *            success Optional callback to receive this page object as
1337 		 *            the only argument.
1338 		 * @param {function(GCNError):boolean=}
1339 		 *            error Optional custom error handler.
1340 		 */
1341 		unlock: function (success, error) {
1342 			var that = this;
1343 			this._fulfill(function () {
1344 				that._authAjax({
1345 					url: GCN.settings.BACKEND_PATH + '/rest/' + that._type +
1346 					     '/cancel/' + that.id() + GCN._getChannelParameter(that),
1347 					type: 'POST',
1348 					json: {}, // There needs to be at least empty content
1349 					          // because of a bug in Jersey.
1350 					error: error,
1351 					success: function (response) {
1352 						if (success) {
1353 							that._invoke(success, [that]);
1354 						}
1355 					}
1356 				});
1357 			});
1358 		},
1359 
1360 		/**
1361 		 * @see GCN.ContentObjectAPI._processResponse
1362 		 */
1363 		'!_processResponse': function (data) {
1364 			this._data = jQuery.extend(true, {}, data[this._type], this._data);
1365 
1366 			// if data contains page variants turn them into page objects
1367 			if (this._data.pageVariants) {
1368 				var pagevars = [];
1369 				var i;
1370 				for (i = 0; i < this._data.pageVariants.length; i++) {
1371 					pagevars.push(this._continue(GCN.PageAPI,
1372 						this._data.pageVariants[i]));
1373 				}
1374 				this._data.pageVariants = pagevars;
1375 			}
1376 		},
1377 
1378 		/**
1379 		 * @override
1380 		 */
1381 		'!_removeAssociatedTagData': function (tagid) {
1382 			var block;
1383 			for (block in this._blocks) {
1384 				if (this._blocks.hasOwnProperty(block) &&
1385 						this._blocks[block].tagname === tagid) {
1386 					delete this._blocks[block];
1387 				}
1388 			}
1389 
1390 			var editable, containedBlocks, i;
1391 			for (editable in this._editables) {
1392 				if (this._editables.hasOwnProperty(editable)) {
1393 					if (this._editables[editable].tagname === tagid) {
1394 						delete this._editables[editable];
1395 					} else {
1396 						containedBlocks = this._editables[editable]._gcnContainedBlocks;
1397 						if (jQuery.isArray(containedBlocks)) {
1398 							for (i = containedBlocks.length -1; i >= 0; i--) {
1399 								if (containedBlocks[i].tagname === tagid) {
1400 									containedBlocks.splice(i, 1);
1401 								}
1402 							}
1403 						}
1404 					}
1405 				}
1406 			}
1407 		},
1408 
1409 		/**
1410 		 * Render a preview for an editable tag by POSTing the current page to the REST Endpoint /page/renderTag/{tagname}
1411 		 * 
1412 		 * @param {string} tagname name of the tag to render
1413 		 * @param {function} success success handler function
1414 		 * @param {function} error error handler function
1415 		 */
1416 		'!_previewEditableTag': function (tagname, success, error) {
1417 			var channelParam = GCN._getChannelParameter(this);
1418 			var url = GCN.settings.BACKEND_PATH +
1419 					'/rest/page/renderTag/' +
1420 					tagname +
1421 			        channelParam +
1422 			        (channelParam ? '&' : '?') +
1423 			        'links=' + encodeURIComponent(GCN.settings.linksRenderMode);
1424 			var jsonData = jQuery.extend({}, this._data);
1425 			// remove some data, we don't want to serialize and POST to the server
1426 			jsonData.pageVariants = null;
1427 			jsonData.languageVariants = null;
1428 			this._authAjax({
1429 				type: 'POST',
1430 				json: jsonData,
1431 				url: url,
1432 				error: error,
1433 				success: success
1434 			});
1435 		}
1436 
1437 	});
1438 
1439 	/**
1440 	 * Creates a new instance of PageAPI.
1441 	 * See the {@link PageAPI} constructor for detailed information.
1442 	 * 
1443 	 * @function
1444 	 * @name page
1445 	 * @memberOf GCN
1446 	 * @see PageAPI
1447 	 */
1448 	GCN.page = GCN.exposeAPI(PageAPI);
1449 	GCN.PageAPI = PageAPI;
1450 
1451 	GCN.PageAPI.trackRenderedTags = trackRenderedTags;
1452 
1453 }(GCN));
1454