XMDManager.js (17369B)
1 /** 2 * XMD (XML Mab Document Manager) 3 * 4 * @filename XMDManager.js 5 * $LastChangedDate: 2004-12-06 13:08:50 +0100 (Mon, 06 Dec 2004) $ 6 * @author Fabio Serra <faser@faser.net> 7 * @copyright Fabio Serra (The Initial Developer of the Original Code) 8 * @license Mozilla Public License Version 1.1 9 * 10 */ 11 12 /** 13 * Construct a new XMD Manager object 14 * @class This class is used to manage the XML resulting from the transformation of Amazon XML 15 * response. The XML Mab Document is the data model. 16 * @constructor 17 * @return A new XMD Manager 18 * 19 */ 20 function XMDManager() { 21 /** 22 * This flag is valid for the current session. It means that when a details (record) 23 * has been deleted from the loaded document, it can't be inserted again. In this way I 24 * prevent unuseful duplicated records 25 * @type bool 26 */ 27 this.preventDuplicateDetails = true; 28 29 /** 30 * Save all primary keys that are going to be processed 31 * @type object array 32 */ 33 this.primaryKeys = new Array(); 34 35 /** 36 * Save a reference in array of each details node present in the XML document 37 * @type DOM nodes array 38 */ 39 this.hashTable = new Array(); 40 41 /** 42 * This property is used to remember if this document was saved (file or memory is the same) 43 * @type bool 44 */ 45 this.wasSaved = false; 46 47 /** 48 * The name of the current document. 49 * @type string 50 */ 51 this.name = ""; 52 53 /** 54 * The full path including the file name and extension if the document has 55 * been saved on file system 56 * @type string 57 */ 58 this.fullPath = ""; 59 60 /** 61 * The Mab Xml Document 62 * @type XML Doc 63 */ 64 this.xmlDoc = this.make(); 65 } 66 67 68 /** 69 * Create an empty XML Document to use as a Mab model data 70 * @return A new XML Dcoument 71 * @type XMLDocument 72 */ 73 XMDManager.prototype.make = function() { 74 var today = new Date(); 75 var tsDocumentCreated = Date.parse(today); 76 77 var xmlStr = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n'+ 78 '<MABInfo xmlns="http://www.faser.net/mab/NS/MABInfo">\n' + 79 '<MABTSDocumentCreated>' + tsDocumentCreated + '</MABTSDocumentCreated>\n' + 80 '<MABDetails/>\n' + 81 '</MABInfo>'; 82 83 var xmlDoc = new DOMParser().parseFromString(xmlStr, 'text/xml'); 84 85 return xmlDoc; 86 } 87 88 /** 89 * Build the hashtable for the current document. When a document is merged the 90 * hashtable is created by default. This method should be used when an XMD document 91 * is loaded from file 92 * @return void 93 */ 94 XMDManager.prototype.buildHashTable = function() { 95 //remove all previous key 96 this.hashTable = null; 97 this.hashTable = new Array(); 98 //build 99 var details = this.xmlDoc.getElementsByTagName('Details'); 100 var pk = ""; 101 for(var i=0;i<details.length;i++) { 102 pk = this.getSingleElement(details.item(i),"MABPrimaryKey"); 103 if(pk) {this.hashTable[pk] = details.item(i);} 104 } 105 } 106 107 /** 108 * Merge the Details node of the new xml document with the current one. 109 * The last xml nodes are inserted at the top of the document 110 * @param {XMLDocument} docToMerge the new XML document 111 * @param {bool} overwrite means that the new details overwrite the old ones 112 * @return object with properties to show how many nodes was merged, duplicated 113 * and reloaded 114 * @type object 115 */ 116 XMDManager.prototype.merge = function(docToMerge,overwrite) { 117 if(typeof overwrite == "undefined") {overwrite = true;} 118 119 //Check if the new document was created by MAB or come from an Amazon search 120 var bAmazonResult = true; 121 if(this.isValidMABDoc(docToMerge)) {bAmazonResult = false;} 122 123 var mabRootDetails = this.xmlDoc.getElementsByTagName("MABDetails").item(0); 124 var details = docToMerge.getElementsByTagName('Details'); 125 126 var node, replacedNode, pk, lastUpdate, oldDetail, mabLabel; 127 128 var today = new Date(); 129 var isNewNode = false; 130 var oldNode = false; 131 132 var result = {}; 133 result.nrMergedNode = 0; 134 result.nrDuplicatedNode = 0; 135 result.nrReloadedNode = 0; 136 137 138 //LIFO - Insert new records before the old ones 139 var lifo = this.xmlDoc.getElementsByTagName("Details").item(0); 140 141 if(bAmazonResult) { 142 //Default locale if missing 143 var locale = "us"; 144 var path = "//Request/Args/Arg[@name='locale']"; 145 var xpathResult = docToMerge.evaluate(path,docToMerge,null,XPathResult.ANY_TYPE,null).iterateNext(); 146 if(xpathResult != null) { locale = xpathResult.getAttribute("value");} 147 var amazAsin = ""; 148 } 149 150 for (var i=0;i<details.length; i++) { 151 //node from the document that I have to process 152 node = details.item(i).cloneNode(true); 153 if(bAmazonResult) { 154 amazAsin = this.getSingleElement(node,"Asin"); 155 pk = amazAsin + '_' + locale; 156 //Add mandatory tags for details that come from Amazon 157 this.addMABTag(node,pk); 158 }else{ 159 pk = this.getSingleElement(node,"MABPrimaryKey") 160 } 161 162 163 //Check the details date according to the Amazon Licence Agreement (November 5, 2003) 164 //http://forums.prospero.com/n/mb/message.asp?webtag=am-assosdev&msg=91.1&ctx=0 165 166 //Price Information must be refreshed every 24 hours, the other details every 3 months 167 //So, if these info are expired remove them from the XML 168 //The user have to be update the info 169 170 lastUpdate = this.getLastUpdate(node); 171 if(lastUpdate > EXPIRED_LONG) { 172 this.removeExpiredInfo(node); 173 }else if(lastUpdate > EXPIRED_SHORT) { 174 this.removeExpiredPrice(node); 175 } 176 177 //prevent duplicate records 178 isNewNode = this.setPK(pk); 179 oldNode = this.getNodeDetail(pk); 180 181 //The record is new 182 if(isNewNode || (!this.preventDuplicateDetails && !oldNode)) { 183 if(lifo) { 184 lifo.parentNode.insertBefore(node,lifo); 185 }else{ 186 mabRootDetails.appendChild(node); 187 } 188 189 this.hashTable[pk] = node; 190 result.nrMergedNode++; 191 192 //The records is currently present in xml doc and I can overwrite it 193 //but avoid to replace MABLabel 194 }else if(overwrite && oldNode) { 195 196 mabLabel = this.getSingleElement(oldNode,"MABLabel"); 197 198 //mabRootDetails.replaceChild(node,oldNode.parentNode); 199 mabRootDetails.removeChild(oldNode); 200 replacedNode = mabRootDetails.appendChild(node); 201 this.hashTable[pk] = replacedNode; 202 203 //No comments? Create a new fake one to prevent comment request 204 var comment = this.getSingleElement(replacedNode); 205 if(!comment) { 206 var comElem = this.xmlDoc.createElement("Reviews"); 207 replacedNode.appendChild(comElem); 208 } 209 210 //Reset the old Mab Label 211 //replacedNode.getElementsByTagName("MABLabel").item(0).firstChild.nodeValue = mabLabel; 212 result.nrReloadedNode++; 213 //Skip node 214 }else{ 215 result.nrDuplicatedNode ++; 216 } 217 } 218 return result; 219 } 220 221 /** 222 * Add mandatory MAB tags to a new detail node coming from an Amazon search 223 * @param {xml node} node The node to which append the new MAB tags 224 * @param {string} pk The mab items primary key 225 * @return void 226 */ 227 XMDManager.prototype.addMABTag = function(node,pk) { 228 //Timestamp 229 var today = new Date(); 230 var ts = Date.parse(today); 231 232 //MAB Status (read | unread) 233 var MABStatus = "unread"; 234 235 //Locale 236 var locale = pk.substr((pk.length-2),pk.length); 237 238 var strMabTag = "<MABPrimaryKey>" + pk + "</MABPrimaryKey>\n" + 239 "<MABTSLastUpdate>" + ts + "</MABTSLastUpdate>\n" + 240 "<MABLabel/>\n" + 241 "<MABLocale>" + locale + "</MABLocale>\n" + 242 "<MABStatus>" + MABStatus + "</MABStatus>\n"; 243 244 //Append new tag to the passed node 245 innerXML(node,strMabTag); 246 } 247 248 /** 249 * Get all products older than an elapsed time determined by Amazon license 250 * @param {int} dayBefore Maximum days that should be passed from the last product update 251 * @return object array contains the asinList grouped by locale 252 * @type array associative 253 */ 254 XMDManager.prototype.getAllExpiredProducts = function(dayBefore) { 255 if(typeof dayBefore == "undefined") {dayBefore = EXPIRED_SHORT;} 256 var node, lastUpdate, locale, amazAsin; 257 var asinList = {}; 258 for(var d in this.hashTable) { 259 node = this.hashTable[d]; 260 lastUpdate = this.getLastUpdate(node); 261 if(lastUpdate > dayBefore) { 262 locale = this.getSingleElement(node,"MABLocale"); 263 amazAsin = this.getSingleElement(node,"Asin"); 264 if(!locale || !amazAsin) {continue;} 265 266 if(typeof asinList[locale] == "undefined") { 267 asinList[locale] = new Array(); 268 } 269 270 asinList[locale].push(amazAsin); 271 } 272 } 273 274 return asinList; 275 } 276 277 /** 278 * Count how many days are passed from the node last update 279 * @param {xml node} nodeDetail 280 * @return the elapsed days 281 * @type int 282 */ 283 XMDManager.prototype.getLastUpdate = function(nodeDetail) { 284 var today = new Date(); 285 var dayElapsed = 0; 286 var lastUpdate = this.getSingleElement(nodeDetail,"MABTSLastUpdate"); 287 if(lastUpdate) { 288 dayElapsed = Math.round((Date.parse(today) - parseInt(lastUpdate)) / 86400000); 289 } 290 291 return dayElapsed; 292 } 293 294 /** 295 * Remove all tags from a node except the most relevant 296 * @param {xml node} nodeDetail an XML node 297 * @return void 298 */ 299 XMDManager.prototype.removeExpiredInfo = function(nodeDetail) { 300 var tagExcluded = new Array("Asin","ProductName","Catalog","Authors","Artists","Directors","MABPrimaryKey","MABTSLastUpdate","MABLabel","MABLocale","MABStatus"); 301 var clonedNode = new Array(); 302 var i, cNode; 303 while(nodeDetail.hasChildNodes()) { 304 for(i=0; i<tagExcluded.length; i++) { 305 if(nodeDetail.lastChild.nodeName == tagExcluded[i]) { 306 cNode = nodeDetail.lastChild.cloneNode(true); 307 clonedNode.push(cNode); 308 } 309 } 310 311 nodeDetail.removeChild(nodeDetail.lastChild); 312 } 313 314 //Write back the excluded tag 315 for(i=0; i<clonedNode.length; i++) { 316 nodeDetail.appendChild(clonedNode[i]); 317 } 318 319 this.wasSaved = false; 320 } 321 322 /** 323 * Remove expired main prices tag 324 * @param {node xml} nodeDetail 325 * @return void 326 */ 327 XMDManager.prototype.removeExpiredPrice = function(nodeDetail) { 328 var tags = new Array("ListPrice","OurPrice","UsedPrice"); 329 var el; 330 for(var i=0; i < tags.length; i++) { 331 el = nodeDetail.getElementsByTagName(tags[i]).item(0); 332 if(el) {nodeDetail.removeChild(el);} 333 } 334 335 this.wasSaved = false; 336 } 337 338 /** 339 * Set the primary key to prevent duplicate 340 * @param {string} pk A Mab primary key 341 * @return true if a primary key has been added, false if the primary key already exist 342 * @type bool 343 */ 344 XMDManager.prototype.setPK = function(pk) { 345 var res = this.checkPK(pk); 346 if(!res) { 347 this.primaryKeys.push(pk); 348 return true; 349 }else{ 350 return false; 351 } 352 } 353 354 /** 355 * Check if the primary key has been saved 356 * @param {string} pk 357 * @return bool 358 * @type bool 359 */ 360 XMDManager.prototype.checkPK = function(pk) { 361 for(var i=0;i<this.primaryKeys.length;i++){ 362 if(this.primaryKeys[i] == pk) { 363 return true; 364 } 365 } 366 return false; 367 } 368 369 /** 370 * Check if the loaded file is a valid MAB file looking for MABInfo tag 371 * @param {XMLDocument} xmlDoc 372 * @return bool 373 * @type bool 374 */ 375 XMDManager.prototype.isValidMABDoc = function(xmlDoc) { 376 var root = xmlDoc.documentElement.nodeName; 377 var XMDRoot = "MABInfo"; 378 if(root == XMDRoot) { 379 return true; 380 }else{ 381 return false; 382 } 383 } 384 385 /** 386 *Get a Details node looking for the primary key using the hashtable 387 *@param {string} primaryKey The node primary key 388 *@return nodeDetail 389 *@type XML Node 390 */ 391 XMDManager.prototype.getNodeDetail = function(primaryKey) { 392 if(typeof this.hashTable[primaryKey] == "undefined"){return false;} 393 var nodeDetail = this.hashTable[primaryKey]; 394 if(nodeDetail) { 395 return nodeDetail; 396 }else{ 397 return false; 398 } 399 } 400 401 /** 402 * Remove a node detail from the XMD Document 403 * @param {string} primaryKey The primary key of the node to remove 404 * @return bool 405 * @type bool 406 */ 407 XMDManager.prototype.removeDetail = function(primaryKey) { 408 var nodeDetail = this.getNodeDetail(primaryKey); 409 if(nodeDetail) { 410 nodeDetail.parentNode.removeChild(nodeDetail); 411 delete this.hashTable[primaryKey]; 412 this.wasSaved = false; 413 return true; 414 }else{ 415 return false; 416 } 417 } 418 419 /** 420 * Get text from the first node element 421 * @param {node xml} nodeDetail 422 * @param {string} tagName 423 * @return The text contained inside the element 424 * @type string 425 */ 426 XMDManager.prototype.getSingleElement = function(nodeDetail,tagName) { 427 if(!nodeDetail) {return false;} 428 var el = nodeDetail.getElementsByTagName(tagName).item(0); 429 if(el && el.hasChildNodes()) { 430 return el.firstChild.nodeValue; 431 }else{ 432 return false; 433 } 434 } 435 436 /** 437 * Return an array with the text got from all elements 438 * @param {node xml} nodeDetail 439 * @param {string} tagName 440 * @return array 441 * @type array 442 */ 443 XMDManager.prototype.getArrayElement = function(nodeDetail,tagName) { 444 var elArray = new Array(); 445 if(!nodeDetail) {return elArray;} 446 var el = nodeDetail.getElementsByTagName(tagName).item(0); 447 if(el) { 448 var node = nodeDetail.getElementsByTagName(tagName); 449 var content; 450 for(var i=0;i<node.length;i++) { 451 content = node.item(i).firstChild.nodeValue; 452 if(content) {elArray[i] = content;} 453 } 454 } 455 return elArray; 456 } 457 458 /** 459 * Check if an element exists inside the xml node 460 * @param {node xml} nodeDetail 461 * @param {string} tagName 462 * @return true if tag exists 463 * @type bool 464 */ 465 XMDManager.prototype.tagExists = function(nodeDetail,tagName) { 466 if(!nodeDetail) {return false;} 467 var el = nodeDetail.getElementsByTagName(tagName).item(0); 468 if(el) { 469 return true; 470 }else{ 471 return false; 472 } 473 } 474 475 /** 476 * Set a new value text inside the provided tag only for the first child element. 477 * If newValue is "none" the child element is removed. If the child element doesn't 478 * exists a text node will be created. 479 * @param {node xml} node 480 * @param {string} tagName 481 * @param {string} newValue 482 * @return True if something has been modified 483 * @type bool 484 */ 485 XMDManager.prototype.setNodeValue = function(node,tagName,newValue) { 486 var el = node.getElementsByTagName(tagName).item(0); 487 if(el) { 488 if(newValue == "none") { 489 if(el.hasChildNodes()) {el.removeChild(el.firstChild);} 490 } else if(el.hasChildNodes()) { 491 el.firstChild.nodeValue = newValue; 492 }else{ 493 var text = document.createTextNode(newValue); 494 el.appendChild(text); 495 } 496 return true; 497 }else{ 498 return false; 499 } 500 } 501 502 /** 503 * Remove all Details node from the document 504 * @return void 505 */ 506 XMDManager.prototype.removeAllDetails = function() { 507 var root = this.xmlDoc.getElementsByTagName("MABDetails").item(0); 508 while(root.hasChildNodes()) { 509 root.removeChild(root.lastChild); 510 } 511 } 512 513 /** 514 * Sort the xml document 515 * Unfortunately this sort method is too slow, pratically unusable when there are 516 * more than 150 node. TODO (try using XSLT) 517 * @param {string} sortType The kind of value to sort (numeric | date | string) 518 * @param {string} sortTag The xml tag to use for sorting the node 519 * @param {bool} bAscending Ascending or descending sorting 520 * @return void 521 */ 522 XMDManager.prototype.sort = function(sortType,sortTag,bAscending) { 523 524 var theBody = this.xmlDoc.getElementsByTagName("MABDetails").item(0); 525 526 var numRows = 0; 527 var theSortedRows = new Array(); 528 for (var i in this.hashTable) { 529 theSortedRows[numRows] = this.hashTable[i].cloneNode(true); 530 numRows++; 531 } 532 533 theSortedRows.sort(this.sortCallBack(sortType,sortTag,bAscending)); 534 535 //Clear current xml document 536 this.removeAllDetails(); 537 538 //recreate 539 for(i=0;i<numRows;i++) { 540 theBody.appendChild(theSortedRows[i]) 541 } 542 //rebuild the hashtable 543 this.buildHashTable(); 544 } 545 546 /** 547 * Calculate how should be sorted the xml node. Actually the xml document is not re-ordered 548 * @param {string} sortType The kind of value to sort (numeric | date | string) 549 * @param {string} sortTag The xml tag to use for sorting the node 550 * @param {bool} bAscending Ascending or descending sorting 551 * @return an array with virtually sorted xml node. 552 * @type array 553 */ 554 XMDManager.prototype.virtualSort = function(sortType,sortTag,bAscending) { 555 var numRows = 0; 556 var theSortedRows = new Array(); 557 for (var i in this.hashTable) { 558 theSortedRows[numRows] = this.hashTable[i]; 559 numRows++; 560 } 561 562 theSortedRows.sort(this.sortCallBack(sortType,sortTag,bAscending)); 563 564 return theSortedRows; 565 } 566 567 /** 568 * Callback method used in the sort javascript. 569 * @private 570 * @param {string} sortType The kind of value to sort (numeric | date | string) 571 * @param {string} sortTag The xml tag to use for sorting the node 572 * @param {bool} bAscending Ascending or descending sorting 573 * @type int 574 */ 575 XMDManager.prototype.sortCallBack = function(sortType,sortTag,bAscending) { 576 577 var fTypeCast = String; 578 var asc = bAscending; 579 580 switch(sortType) { 581 case "numeric": 582 fTypeCast = toNumber; 583 break; 584 case "date": 585 fTypeCast = toYear; 586 break; 587 default: 588 fTypeCast = CaseInsensitiveString; 589 } 590 591 var me = this; 592 return function (a,b) { 593 594 var col1 = me.getSingleElement(a,sortTag); 595 var col2 = me.getSingleElement(b,sortTag); 596 597 var text1 = ""; 598 var text2 = ""; 599 600 if(col1) {text1 = col1;} 601 if(col2) {text2 = col2;} 602 603 //Sort Tag SalesRank remove decimal separator 604 if(sortTag == "SalesRank") { 605 text1 = text1.replace(/,/g,""); 606 text1 = text1.replace(/\./g,""); 607 608 text2 = text2.replace(/,/g,""); 609 text2 = text2.replace(/\./g,""); 610 } 611 612 if (fTypeCast(text1) < fTypeCast(text2)) { 613 return asc ? -1 : 1; 614 } else if (fTypeCast(text1) > fTypeCast(text2)) { 615 return asc ? 1 : -1; 616 }else{ 617 return 0; 618 } 619 }; 620 } 621