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 * Initializes a placeholder content object by giving to it its fetched 75 * data, assigning it a hash, and moving it from the temporary cache into 76 * the main cache. 77 * 78 * @param {Chainback} obj A placeholder content object. 79 * @param {object} data The data of the localized object that was returned 80 * by the "localize/" REST-API call. 81 */ 82 function initialize(obj, data) { 83 var objId = obj.id(); 84 obj._name = data.name; 85 obj._data = data; 86 obj._fetched = true; 87 obj.multichannelling.derivedFrom._removeFromTempCache(obj); 88 obj.multichannelling.derivedFrom.clear(); 89 obj._setHash(objId); 90 obj._addToCache(); 91 } 92 93 /** 94 * Fetches the object data with which to populate the given placeholder 95 * content object. 96 * 97 * @param {Chainback} obj A placeholder content object. 98 * @param {function(ContentObjectAPI)} success A callback function to 99 * receive the localized 100 * content object when it is 101 * successfully initialized. 102 * @param {function(GCNError)=} error Optional custom error handler. 103 */ 104 function fetch(obj, success, error) { 105 obj._authAjax({ 106 url: GCN.settings.BACKEND_PATH + '/rest/' + obj._type + '/load/' 107 + obj.id() + getChannelParameter(obj), 108 data: obj.multichannelling.derivedFrom._loadParams(), 109 error: error, 110 success: function (response) { 111 if (GCN.getResponseCode(response) !== 'OK') { 112 GCN.handleResponseError(response, error); 113 } else { 114 success(response); 115 } 116 } 117 }); 118 } 119 120 /** 121 * Create a localized version of a content object. 122 * 123 * @param {ContentObjectAPI} obj Content Object 124 * @param {function(ContentObjectAPI)} success a callback function when the 125 * object is successfully 126 * localized. 127 * @param {function(gcnerror)=} error optional custom error handler. 128 */ 129 function createLocalizedVersion(obj, success, error) { 130 var derived = obj.multichannelling.derivedFrom; 131 obj._authAjax({ 132 url: GCN.settings.BACKEND_PATH + '/rest/' + derived._type 133 + '/localize/' + derived.id(), 134 type: 'POST', 135 json: { channelId: derived._channel }, 136 error: error, 137 success: function (response) { 138 if (GCN.getResponseCode(response) !== 'OK') { 139 GCN.handleResponseError(response, error); 140 } else { 141 success(); 142 } 143 } 144 }); 145 } 146 147 /** 148 * Delete the localized version of a content object. 149 * 150 * @param {ContentObjectAPI} obj Content Object 151 * @param {function(ContentObjectAPI)} success a callback function when the 152 * object is successfully 153 * localized. 154 * @param {function(gcnerror)=} error optional custom error handler. 155 */ 156 function deleteLocalizedVersion(obj, success, error) { 157 var derived = obj.multichannelling.derivedFrom; 158 derived._authAjax({ 159 url: GCN.settings.BACKEND_PATH + '/rest/' + derived._type 160 + '/unlocalize/' + derived.id(), 161 type: 'POST', 162 json: { channelId: derived._channel }, 163 error: error, 164 success: function (response) { 165 if (GCN.getResponseCode(response) !== 'OK') { 166 GCN.handleResponseError(response, error); 167 } else { 168 success(); 169 } 170 } 171 }); 172 } 173 174 /** 175 * Fetches the contents of the object from which our placeholder content 176 * object is derived from. 177 * 178 * @param {ContentObjectAPI} obj A placeholder content object waiting for 179 * data. 180 * @param {function(Chainback)} success A callback function that will 181 * receive the content object when it 182 * has been successfully read from the 183 * backend. 184 * @param {function(GCNError)=} error Optional custom error handler. 185 */ 186 function fetchDerivedObject(obj, success, error) { 187 obj.multichannelling.derivedFrom._read(success, error); 188 } 189 190 /** 191 * Poplates a placeholder that represents an localized content object with 192 * with its data to such that it becomes a fully fetched object. 193 * 194 * Will first cause the inherited object from which this placeholder is 195 * derived to be localized. 196 * 197 * @param {Chainback} obj A placholder content object what is waiting for 198 * data. 199 * @param {function(Chainback)} success A callback function that will 200 * receive the content object when it 201 * has been successfully read from the 202 * backend. 203 * @param {function(GCNError)=} error Optional custom error handler. 204 */ 205 function localize(obj, success, error) { 206 fetchDerivedObject(obj, function (derived) { 207 if (!derived.prop('inherited')) { 208 var err = GCN.createError( 209 'CANNOT_LOCALIZE', 210 'Cannot localize an object which is not inherited', 211 obj 212 ); 213 GCN.handleError(err, error); 214 return; 215 } 216 createLocalizedVersion(obj, function () { 217 fetch(obj, function (response) { 218 initialize(obj, response[obj._type]); 219 success(obj); 220 }, error); 221 }, error); 222 }, error); 223 } 224 225 /** 226 * Poplates a placeholder that represents an inherited content object with 227 * with its data to such that it becomes a fully fetched object. 228 * 229 * Will first cause the local object from which this placeholder is derived 230 * to be deleted so that the inherited object data is re-exposed. 231 * 232 * @param {Chainback} obj A placholder content object what is waiting for 233 * data. 234 * @param {function(Chainback)} success A callback function that will 235 * receive the content object when it 236 * has been successfully read from the 237 * backend. 238 * @param {function(GCNError)=} error Optional custom error handler. 239 */ 240 function unlocalize(obj, success, error) { 241 fetchDerivedObject(obj, function (derived) { 242 if (derived.prop('inherited')) { 243 var err = GCN.createError( 244 'CANNONT_UNLOCALIZE', 245 'Cannot unlocalize an object that was not first localized', 246 obj 247 ); 248 GCN.handleError(err, error); 249 return false; 250 } 251 deleteLocalizedVersion(obj, success, error); 252 }, error); 253 } 254 255 GCN.multichannelling = { 256 localize: localize, 257 unlocalize: unlocalize 258 }; 259 260 // === caching ============================================================ 261 262 /** 263 * Determines whether the incoming data is that of a multi-channel 264 * localized copy. 265 * 266 * When loading content objects in multi-channel nodes, there exists the 267 * possibility that the object data that is returned does not match the id 268 * of the one being requested. This is the case when the request is made 269 * with a channel specified, and the requested id is of an inherited page 270 * which has previously been localized. In such situations, the response 271 * will contain the data for the local copy and not the original inherited 272 * data. 273 * 274 * @param {ContentObjectAPI} obj The content object whose data was 275 * requested. 276 * @param {object} data The content object data from the server response. 277 * @return {boolean} True if the data object contains data for the local 278 * copy of this content object. 279 */ 280 function isLocalizedData(obj, data) { 281 return obj._channel && (data.id !== obj.id()); 282 } 283 284 /** 285 * Generates a hash from the given parameters. 286 * 287 * @param {?string} prefix 288 * @param {string} type The Chainback object type. 289 * @param {number} channel A node id. 290 * @param {number} id An object id. 291 * @return {string} A hash key. 292 */ 293 function makeCacheHash(prefix, type, channel, id) { 294 return (prefix ? prefix + '::' : '') + type + ':' + channel + '/' + id; 295 } 296 297 /** 298 * Generates a list of hash keys which should map to the given obj. 299 * 300 * @param {Chainback} obj A content object instance. 301 * @param {object} data The object's data received from the REST-API. 302 * @param {Array.<string>} An array of hash keys. 303 */ 304 function getCacheKeyVariations(obj, data) { 305 var ctor = obj._constructor; 306 var channel = obj._channel; 307 var idFromObj = obj.id(); 308 var idFromData = data.id; 309 var type = ctor.__chainbacktype__; 310 var prefix = (ctor._needsChainedHash && obj._chain) 311 ? obj._chain.__gcnhash__ 312 : ''; 313 var keys = []; 314 keys.push(makeCacheHash(prefix, type, channel, idFromObj)); 315 if (isLocalizedData(obj, data)) { 316 keys.push(makeCacheHash(prefix, type, 0, idFromData)); 317 keys.push(makeCacheHash(prefix, type, channel, idFromData)); 318 } 319 return keys; 320 } 321 322 /** 323 * Maps an obj into its constructor's cache against a list of hash keys. 324 * 325 * @param {Array.<string>} keys A set of hash keys. 326 * @param {Chainback} obj A chainback instance which the given keys should 327 * should be mapped to. 328 */ 329 function addCacheReferences(keys, obj) { 330 var cache = obj._constructor.__gcncache__; 331 var i; 332 for (i = 0; i < keys.length; i++) { 333 cache[keys[i]] = obj; 334 } 335 } 336 337 GCN.cache = { 338 getKeyVariations: getCacheKeyVariations, 339 addReferences: addCacheReferences 340 }; 341 342 }(GCN)); 343