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