1 (function (GCN) {
  2 
  3 	'use strict';
  4 
  5 	// One needs to be cautious when using channel-inherited ids to load content
  6 	// objects.  The object data that is loaded varies depending on whether or
  7 	// not you are in a channel, and whether or not a channel local copy of the
  8 	// inherited object exists in a given channel.
  9 	//
 10 	// Consider working in a channel (with a node id 2 for example).  Further
 11 	// consider that this channel inherits a page with the id 123, and that at
 12 	// some point in time a channel-local copy of this page is made with the id
 13 	// 456.  Now consider the following statements:
 14 	//
 15 	// GCN.channel(2);
 16 	// var p1 = GCN.page(123, function () {});
 17 	// var p2 = GCN.page(456, function () {});
 18 	// GCN.channel(false);
 19 	// var p3 = GCN.page(123, function () {});
 20 	// var p4 = GCN.page(456, function () {});
 21 	//
 22 	// The fact that page 456 is a channel-local copy of page 123 in the
 23 	// channel 2, means that p1, and p2 will be two difference objects on
 24 	// the client which reference the exact same object on the backend.
 25 	//
 26 	// If client changes were made to p1 and p2, and both objects are saved(),
 27 	// one set of changes will be overridden, with the last changes to reach
 28 	// the server clobbering those of the first.
 29 	//
 30 	// In this senario out library cache would contain the following entries:
 31 	//
 32 	// "PageAPI:0/123" = @clientobject1 => @serverobject1
 33 	// "PageAPI:0/456" = @clientobject2 => @serverobject2
 34 	// "PageAPI:2/123" = @clientobject3 => @serverobject2
 35 	// "PageAPI:2/456" = @clientobject4 => @serverobject2
 36 	//
 37 	// What we should do in the future is have object p1, and p2 be references
 38 	// to the same instance on the client side.  This would make it symmetrical
 39 	// with the REST-API.  The challenge in the above scenario is that 2
 40 	// requests will be made to the server for 2 different id, and not until
 41 	// the requests complete will be know whether a channel-local copy was
 42 	// returned.
 43 	//
 44 	// What we should do is that in _processResponse() we should
 45 	// check if there exists any object matching any of the values of
 46 	// getCacheKeyVariations(), and if so, we should through away the incoming
 47 	// data and use the data that is previously loaded.  We would need to make
 48 	// sure that _clearCache() does in fact clear references for a given
 49 	// instance.
 50 	//
 51 	// Example:
 52 	// !_processResponse': function (data) {
 53 	//    var keys = GCN.cache.getCacheKeyVariations(this, data[this._type]);
 54 	//    GCN.cache.addCacheReferences(keys, this);
 55 	//    ...
 56 
 57 	// === multichannelling ===================================================
 58 
 59 	/**
 60 	 * Constructs the nodeId query parameter for REST-API calls.
 61 	 *
 62 	 * @param {ContentObjectAPI} obj A content object instance.
 63 	 * @param {string=} delimiter Optional delimiter character.
 64 	 * @return {string} Query parameter string.
 65 	 */
 66 	function getChannelParameter(obj, delimiter) {
 67 		if (false === obj._channel) {
 68 			return '';
 69 		}
 70 		return (delimiter || '?') + 'nodeId=' + obj._channel;
 71 	}
 72 
 73 	/**
 74 	 * Adds the given instance to its constructor's cache.
 75 	 *
 76 	 * @TODO: Should be a factory method GCN.Cache.addToConstructorCache()
 77 	 * @param {ChainbackAPI} instance
 78 	 * @param {function} ctor The constructor of `instance`.
 79 	 */
 80 	function addToConstructorCache(instance, ctor) {
 81 		ctor.__gcncache__[instance.__gcnhash__] = instance;
 82 	}
 83 
 84 	/**
 85 	 * Removes the given instance from its ancestor's temporary cache.
 86 	 *
 87 	 * @TODO: Should be a factory method GCN.Cache.removeFromAncestorCache()
 88 	 * @param {ChainbackAPI} instance
 89 	 * @param {ChainbackAPI} ancestor Ancestor object of `instance`.
 90 	 */
 91 	function removeFromAncestorCache(instance, ancestor) {
 92 		var hash;
 93 		var cache = ancestor.__gcntempcache__;
 94 		for (hash in cache) {
 95 			if (cache.hasOwnProperty(hash) && instance === cache[hash]) {
 96 				delete cache[hash];
 97 			}
 98 		}
 99 	}
100 
101 	/**
102 	 * Initializes a placeholder content object by populating it with its
103 	 * fetched data, assigning it a hash, and tranferring it from its ancestor's
104 	 * cache into its constructor's cache.
105 	 *
106 	 * The localized object must mask the original object from which the
107 	 * channel-local copy was derived.  All further attempts to access the
108 	 * original object should now return the channel-local copy.  This is done
109 	 * by assigning the channel-local instance the identical id and overriding
110 	 * it in the cache.
111 	 *
112 	 * @param {Chainback} localized A placeholder for localized content object.
113 	 * @param {object} data The data of the localized object that was returned
114 	 *                      by the "localize/" REST-API call.
115 	 */
116 	function initialize(localized, data) {
117 		var original = localized.multichannelling.derivedFrom;
118 
119 		// Are tags localizable? If they are not then this is not needed
120 		localized._name = data.name;
121 
122 		localized._data = data;
123 		localized._fetched = true;
124 		localized._setHash(original.id());
125 
126 		// Because the cache entry that contained `original` will be overwritten
127 		// with `localized`, the data in `original` is therefore stale.  Any
128 		// further attemtps to access an object using the id of `original` will
129 		// return `localized`.
130 		original.clear();
131 
132 		removeFromAncestorCache(localized, original);
133 		addToConstructorCache(localized, localized._constructor);
134 	}
135 
136 	/**
137 	 * Fetches the channel-local data represented by the given placeholder object.
138 	 *
139 	 * @param {Chainback} obj A placeholder for a localized content object.
140 	 * @param {function(ContentObjectAPI)} success A callback function to
141 	 *                                             receive the channel-local
142 	 *                                             data.
143 	 * @param {function(GCNError)=} error Optional custom error handler.
144 	 */
145 	function fetch(obj, success, error) {
146 		obj._authAjax({
147 			url: GCN.settings.BACKEND_PATH + '/rest/' + obj._type + '/load/'
148 			     + obj.id() + getChannelParameter(obj),
149 			data: obj.multichannelling.derivedFrom._loadParams(),
150 			error: error,
151 			success: function (response) {
152 				if (GCN.getResponseCode(response) !== 'OK') {
153 					GCN.handleResponseError(response, error);
154 				} else {
155 					success(response);
156 				}
157 			}
158 		});
159 	}
160 
161 	/**
162 	 * Create a channel-local version of a content object represented by `obj`.
163 	 *
164 	 * @param {ContentObjectAPI} obj Place holder for a localized content
165 	 *                               object.
166 	 * @param {function(ContentObjectAPI)} success A function invoked the
167 	 *                                             object is successfully
168 	 *                                             localized.
169 	 * @param {function(gcnerror)=} error Optional custom error handler.
170 	 */
171 	function createLocalizedVersion(obj, success, error) {
172 		var derived = obj.multichannelling.derivedFrom;
173 		obj._authAjax({
174 			url: GCN.settings.BACKEND_PATH + '/rest/' + derived._type
175 			     + '/localize/' + derived.id(),
176 			type: 'POST',
177 			json: { channelId: derived._channel },
178 			error: error,
179 			success: function (response) {
180 				if (GCN.getResponseCode(response) !== 'OK') {
181 					GCN.handleResponseError(response, error);
182 				} else {
183 					success();
184 				}
185 			}
186 		});
187 	}
188 
189 	/**
190 	 * Delete the localized version of a content object.
191 	 *
192 	 * @param {ContentObjectAPI} obj Content Object
193 	 * @param {function(ContentObjectAPI)} success a callback function when the
194 	 *                                             object is successfully
195 	 *                                             localized.
196 	 * @param {function(gcnerror)=} error optional custom error handler.
197 	 */
198 	function deleteLocalizedVersion(obj, success, error) {
199 		var derived = obj.multichannelling.derivedFrom;
200 		derived._authAjax({
201 			url: GCN.settings.BACKEND_PATH + '/rest/' + derived._type
202 			     + '/unlocalize/' + derived.id(),
203 			type: 'POST',
204 			json: { channelId: derived._channel },
205 			error: error,
206 			success: function (response) {
207 				if (GCN.getResponseCode(response) !== 'OK') {
208 					GCN.handleResponseError(response, error);
209 				} else {
210 					success();
211 				}
212 			}
213 		});
214 	}
215 
216 	/**
217 	 * Fetches the contents of the object from which our placeholder content
218 	 * object is derived from.
219 	 *
220 	 * @TODO: rename to fetchOriginal()
221 	 *
222 	 * @param {ContentObjectAPI} obj A placeholder content object waiting for
223 	 *                               data.
224 	 * @param {function(Chainback)} success A callback function that will
225 	 *                                      receive the content object when it
226 	 *                                      has been successfully read from the
227 	 *                                      backend.
228 	 * @param {function(GCNError)=} error Optional custom error handler.
229 	 */
230 	function fetchDerivedObject(obj, success, error) {
231 		obj.multichannelling.derivedFrom._read(success, error);
232 	}
233 
234 	/**
235 	 * Poplates a placeholder that represents an localized content object with
236 	 * with its data to such that it becomes a fully fetched object.
237 	 *
238 	 * Will first cause the inherited object from which this placeholder is
239 	 * derived to be localized.
240 	 *
241 	 * @TODO: If fetching the object that is to be localized results in an
242 	 *        object with a different id being returned, then the returned
243 	 *        object is the channel-local version of the object we wanted to
244 	 *        localize.  The object should not be re-localized therefore, rather
245 	 *        the channel-local copy should simply be returned.
246 	 *        See isLocalizedData() at bottom of file.
247 	 *
248 	 * @param {Chainback} obj A placholder content object what is waiting for
249 	 *                        data.
250 	 * @param {function(Chainback)} success A callback function that will
251 	 *                                      receive the content object when it
252 	 *                                      has been successfully read from the
253 	 *                                      backend.
254 	 * @param {function(GCNError)=} error Optional custom error handler.
255 	 */
256 	function localize(obj, success, error) {
257 		fetchDerivedObject(obj, function (derived) {
258 			if (!derived.prop('inherited')) {
259 				var err = GCN.createError(
260 					'CANNOT_LOCALIZE',
261 					'Cannot localize an object which is not inherited',
262 					obj
263 				);
264 				GCN.handleError(err, error);
265 				return;
266 			}
267 			createLocalizedVersion(obj, function () {
268 				fetch(obj, function (response) {
269 					initialize(obj, response[obj._type]);
270 					success(obj);
271 				}, error);
272 			}, error);
273 		}, error);
274 	}
275 
276 	/**
277 	 * Poplates a placeholder that represents an inherited content object with
278 	 * with its data to such that it becomes a fully fetched object.
279 	 *
280 	 * Will first cause the local object from which this placeholder is derived
281 	 * to be deleted so that the inherited object data is re-exposed.
282 	 *
283 	 * @param {Chainback} obj A placholder content object what is waiting for
284 	 *                        data.
285 	 * @param {function(Chainback)} success A callback function that will
286 	 *                                      receive the content object when it
287 	 *                                      has been successfully read from the
288 	 *                                      backend.
289 	 * @param {function(GCNError)=} error Optional custom error handler.
290 	 */
291 	function unlocalize(obj, success, error) {
292 		fetchDerivedObject(obj, function (derived) {
293 			if (derived.prop('inherited')) {
294 				var err = GCN.createError(
295 					'CANNONT_UNLOCALIZE',
296 					'Cannot unlocalize an object that was not first localized',
297 					obj
298 				);
299 				GCN.handleError(err, error);
300 				return false;
301 			}
302 			deleteLocalizedVersion(obj, success, error);
303 		}, error);
304 	}
305 
306 	GCN.multichannelling =  {
307 		localize: localize,
308 		unlocalize: unlocalize
309 	};
310 
311 	// === caching ============================================================
312 
313 	/**
314 	 * Determines whether the incoming data is that of a multi-channel
315 	 * localized copy.
316 	 *
317 	 * When loading content objects in multi-channel nodes, there exists the
318 	 * possibility that the object data that is returned does not match the id
319 	 * of the one being requested.  This is the case when the request is made
320 	 * with a channel specified, and the requested id is of an inherited page
321 	 * which has previously been localized.  In such situations, the response
322 	 * will contain the data for the local copy and not the original inherited
323 	 * data.
324 	 *
325 	 * @param {ContentObjectAPI} obj The content object whose data was
326 	 *                               requested.
327 	 * @param {object} data The content object data from the server response.
328 	 * @return {boolean} True if the data object contains data for the local
329 	 *                   copy of this content object.
330 	 */
331 	function isLocalizedData(obj, data) {
332 		return obj._channel && (data.id !== obj.id());
333 	}
334 
335 	/**
336 	 * Generates a hash from the given parameters.
337 	 *
338 	 * @param {?string} prefix
339 	 * @param {string} type The Chainback object type.
340 	 * @param {number} channel A node id.
341 	 * @param {number} id An object id.
342 	 * @return {string} A hash key.
343 	 */
344 	function makeCacheHash(prefix, type, channel, id) {
345 		return (prefix ? prefix + '::' : '') + type + ':' + channel + '/' + id;
346 	}
347 
348 	/**
349 	 * Generates a list of hash keys which should map to the given obj. 
350 	 *
351 	 * @param {Chainback} obj A content object instance.
352 	 * @param {object} data The object's data received from the REST-API.
353 	 * @param {Array.<string>} An array of hash keys.
354 	 */
355 	function getCacheKeyVariations(obj, data) {
356 		var ctor = obj._constructor;
357 		var channel = obj._channel;
358 		var idFromObj = obj.id();
359 		var idFromData = data.id;
360 		var type = ctor.__chainbacktype__;
361 		var prefix = (ctor._needsChainedHash && obj._chain)
362 		           ? obj._chain.__gcnhash__
363 		           : '';
364 		var keys = [];
365 		keys.push(makeCacheHash(prefix, type, channel, idFromObj));
366 		if (isLocalizedData(obj, data)) {
367 			keys.push(makeCacheHash(prefix, type, 0, idFromData));
368 			keys.push(makeCacheHash(prefix, type, channel, idFromData));
369 		}
370 		return keys;
371 	}
372 
373 	/**
374 	 * Maps an obj into its constructor's cache against a list of hash keys.
375 	 *
376 	 * @param {Array.<string>} keys A set of hash keys.
377 	 * @param {Chainback} obj A chainback instance which the given keys should
378 	 *                        should be mapped to. 
379 	 */
380 	function addCacheReferences(keys, obj) {
381 		var cache = obj._constructor.__gcncache__;
382 		var i;
383 		for (i = 0; i < keys.length; i++) {
384 			cache[keys[i]] = obj;
385 		}
386 	}
387 
388 	GCN.cache = {
389 		getKeyVariations: getCacheKeyVariations,
390 		addReferences: addCacheReferences
391 	};
392 
393 }(GCN));
394