/** @file PatchXml tool to modify a XML based on patch syntax @copyright Copyright (c) 2013 Intel Corporation. All rights reserved This software and associated documentation (if any) is furnished under a license and may only be used or copied in accordance with the terms of the license. Except as permitted by such license, no part of this software or documentation may be reproduced, stored in a retrieval system, or transmitted in any form or by any means without the express written consent of Intel Corporation. @version 1.0 Initial release. **/ /* Usage: cscript PatchXml.js -patch [[-patch ] ...] Options: -patch patch file with patching syntax, supports multiple files -help Remarks: It is compulsory to include input, output and at least one patch file Examples: template.xml contains: patch.p contains: # use hash sign to comment a line /root/b { @foo = "foo" append @bar = "bar" text "baz" } /root append /c delete /root/a /root delete @attrib include "anotherPatch.p" anotherPatch.p contains: /root append /d And invoked using this: cscript PatchXml.js template.xml output.xml -patch patch.p output.xml would contain: baz The detailed BNF syntax for the patch file: {} means repetition [] means optional <> means non-terminal | means choice ::= {} ::= | ::= '{' {} '}' ::= XPATH | for XPATH | '@' = | 'text' | 'append' | 'delete' | 'include' | 'import' ::= '/' [] | '@' '=' ::= XPATH | '@' ::= ::= ::= STRING | 'subst' '('STRING')' ::= NAME ::= NAME ::= STRING Terminal symbols: '' All single quoted symbols are literals. NAME Alphanumeric characters and "_" starts with alphabet or "_" STRING A quoted string with the only escape sequences: "\"" and "\n". XPATH The XPath syntax to specify a node. Currently XPATH only supports node expression with prefix of "/". XPath selector can be used to point to specific node if any node have duplicating names. Selector are enclosed in square brackets "[]" * XPath information can be refered from http://www.w3schools.com/xpath/xpath_syntax.asp http://msdn.microsoft.com/en-us/library/windows/desktop/ms256471.aspx Operators: '@' = Modify an attribute of current node. The attribute must exist. For attribute named 'value', will check for 'value_list' for valid options if present. text Modify the text in a node. Current node must contain text node. Text also can be used to append text when appending new child node. See example. append Append a node as a child of current node. The new child node can have the same name with their siblings. Also used to append an attribute to current node. The new attribute must not exist. delete Removes attribute or child node (including all descendant elements) include Parse another patch file from current patch file. This is same as specifying multiple patch files from -patch option import Import the root of another xml file and replace with current node for Perform command on all nodes matching query Definition of current node: Is the node specified by a XPath E.g.: /root/childA @attrib = "foo" | [current node] Comment line: Comments starts with the '#' character and can only be place in the beginning or the ending of a complete expression */ var exit = function (n) { WScript.Quit (n); } var log = function (s) { WScript.StdOut.Write (s + '\n'); } var debug = function (s) {}; var error = function (s) { WScript.StdErr.Write (s + '\n'); } var shell = WScript.CreateObject ("WScript.Shell"); var forReading = 1; var fso = new ActiveXObject ("Scripting.FileSystemObject"); /** Parser object is used to parse a patch file. Object can be created from main function, where this is the main patch file, or it can also be created within this parser object when "include" operation is used. **/ function Parser (f, xmlDom) { // // keeps a reference to xmlDom to use the createNode method // this.xmlDom = xmlDom; this.filename = f; this.dir = dirname (f); this.location = 0; this.line = 1; this.peekedChar = '' // // Read patch file // var file = fso.OpenTextFile (this.filename, forReading); this.text = file.ReadAll (); // // Read the first character and put in peekedChar // this.next (); } /** Parser::parsePatchXml the main function to start parsing the patch file. There are three place to call this function (1) from main, (2) from include operation in patch file without context, or (3) from include operation with context. For (1) and (2), pass in the XMLDOM, while for (3), pass in the current node **/ Parser.prototype.parsePatchXml = function (node) { debug ("parsePatchXml () started"); // // Consume comments before any statement // this.consumeAll (); while (!this.isEof ()) { // // while not eof of patch, repeat to handle multiple statement in patch file // Consume comments after each statement // this.parseExpression (node); this.consumeAll (); } debug ("parsePatchXml () ended"); } /** Parser::parseCommand Handles expression or block **/ Parser.prototype.parseCommand = function (node) { debug ("parseCommand () started"); if (this.peek () == "{") { this.parseBlock (node); } else { this.parseExpression (node); } debug ("parseCommand () ended"); } /** Parser::parseBlock Repeatedly parse expression until "}" is met **/ Parser.prototype.parseBlock = function (node) { debug ("parseBlock () started"); // // Consume "{" and the whitespace right after that // this.next (); this.consumeAll (); do { // // parse the content multiple times and consume white space after any // repeated operation // this.parseExpression (node); this.consumeAll (); } while (this.peek () != "}"); // // consume "}" // this.next (); // // consume white space right after "}" // this.consumeAll (); debug ("parseBlock () ended"); } /** Parser::parseExpression Handles more paths or operation **/ Parser.prototype.parseExpression = function (node) { debug ("parseExpression () started"); if (this.peek () == "/") { // // This is when the statement starts from a path // var path = this.parsePath (); node = selectSingleNodeSafe (node, path); // // Pass the found node // this.parseCommand (node); } else if (this.peek () == ".") { throw Error ("XPath must begin with '/'"); } else { this.parseOperation (node); } debug ("parseExpression () ended"); } /** Parser::parsePath detects the path. Characters complies with XPath syntax. **/ Parser.prototype.parsePath = function () { debug ("parsePath () started"); var xpath = ""; while (/[\/\.\[\*a-zA-Z0-9_]/.test (this.peek ())) { if (this.peek () == "[") { xpath += this.parseSelectorOpt (); } else { xpath += this.next (); } } // // Consume white space before start of grouping // this.consumeWhiteSpace (); debug ("Path: " + xpath); debug ("parsePath () ended"); return xpath; } /** Parser::parseOperation Corresponding function will be called to handle the operations. If '@' is peeked, modifyAttribute will be called, else will parse the operator **/ Parser.prototype.parseOperation = function (node) { debug ("parseOperation () started"); if (this.peek () == "@") { this.changeAttribute (node, true); } else { // // detect the first word, and find from switch case // var operator = this.parseName (); switch (operator) { case "text": this.modifyNode (node); break; case "append": this.appendOperand (node); break; case "delete": this.deleteOperand (node); break; case "include": this.includeOperand (node); break; case "import": this.importOperand (node); break; case "for": this.forStatement (node); break; default: throw Error ("No such operator: \"" + operator + "\""); } } debug ("parseOperation () ended"); } /** Perform following command for each node found by XPath query. Since we don't build AST, just re-start parser from saved location. **/ Parser.prototype.forStatement = function (node) { debug ("forStatement () started"); var path = this.parsePath (); var saveLocation = this.location; var savePeekedChar = this.peekedChar; var nodes = node.selectNodes (path); if (nodes.length == 0) { throw Error ("Cannot find any match from node \"" + node.nodeName + "\" using this path : " + path); } for (var n = 0; n < nodes.length; ++n) { this.location = saveLocation; this.peekedChar = savePeekedChar; this.parseCommand (nodes[n]); } debug ("forStatement () ended"); } /** Parser::appendOperand a child node or attribute. If append node, it will not check if the node is there or no. But if the attribute is there, it will throw error. **/ Parser.prototype.appendOperand = function (node) { debug ("appendOperand () started"); // // consume "/" or "@" // if (this.peek () == "/") { this.appendNode (node); } else if (this.peek () == "@") { this.changeAttribute (node, false); } else { throw Error ("Append operation only accepts '/' for node and '@' for attribute"); } debug ("appendOperand () ended"); } /** Parser::appendNode helper function for appendOperand **/ Parser.prototype.appendNode = function (node) { // // consume "/" // this.next (); var nodeToAppend = this.parseName (); var toAppend = this.xmlDom.createElement (nodeToAppend); // // append the node here // node.appendChild (toAppend); // // detect the grouping here. // if (this.peek () == "{") { this.parseBlock (toAppend); } } /** Parser::changeAttribute modify or append an attribute to a node. Use the modifyFlag to specify modify or append attribute **/ Parser.prototype.changeAttribute = function (node, modifyFlag) { debug ("changeAttribute () started"); // // consume "@" // this.next (); var attribToChange = this.parseName (); if (this.peek () != "=") { throw Error ("Please adhere to syntax to append/modify attribute: @attrib = value"); } // // Consume "=" // this.next (); var valueToChange = this.parseValue (); // // check the existance of the attribute // if (modifyFlag) { if (!node.getAttributeNode (attribToChange)) { throw Error ("\"" + attribToChange + "\" does not exist for modification, missing 'append'?"); } } else { if (node.getAttributeNode (attribToChange)) { throw Error ("Attribute \"" + attribToChange + "\" already exists"); } } node.setAttribute (attribToChange, valueToChange); if (attribToChange == "value") { this.checkValueList (valueToChange, node.getAttribute ("value_list")); } debug ("changeAttribute () ended"); } /** Parser::modifyNode will append a text node regardless it existed or no, and append a text to the text node. **/ Parser.prototype.modifyNode = function (node) { debug ("modifyNode () started"); var valueToModify = this.parseValue (); node.appendChild (this.xmlDom.createTextNode (valueToModify)); debug ("modifyNode () ended"); } /** Parser::deleteOperand will check if the node is there or no. If it is not, throw error. **/ Parser.prototype.deleteOperand = function (node) { debug ("deleteOperand () started"); // // detect the first "/" or "@" // if (this.peek () == "/") { var nodeToDelete = this.parsePath (); var toDelete = selectSingleNodeSafe (node, nodeToDelete); // // delete node here // toDelete.parentNode.removeChild (toDelete); } else if (this.peek () == "@") { // // Consume "@" // this.next (); var attribToDelete = this.parseName (); // // delete attrib here // if (!node.getAttributeNode (attribToDelete)) { throw Error ("\"" + attribToDelete + "\" does not exist to delete"); } node.removeAttribute (attribToDelete); } else { throw Error ("Delete operation only accepts '/' for node and '@' for attribute"); } debug ("deleteOperand () ended"); } /** Parser::includeOperand another patch file. New Parser will be created to handle this. **/ Parser.prototype.includeOperand = function (node) { debug ("includeOperand () started"); var f = this.parseString (); // // Check for existence before constructing a new parser // if (!fso.FileExists (this.dir + f)) { throw Error ("File does not exists: \"" + this.dir + f + "\""); } try { var includePatch = new Parser (this.dir + f, this.xmlDom); includePatch.parsePatchXml (node); } catch (e) { error (includePatch.filename + "(" + includePatch.line + ") : error : " + e.message); throw Error ("Included patch file faulty"); } debug ("includeOperand () ended"); } /** Parser::importOperand will import the root of another xml file to the specified node. Current node will be replaced with root of new xml file **/ Parser.prototype.importOperand = function (node) { debug ("importOperand () started"); var f = this.parseString (); var importDom = readXmlFile (this.dir + f); node.parentNode.replaceChild (importDom.documentElement, node); debug ("importOperand () ended"); } /** Parser::parseName Alphanumeric, and "_" which starts with Alphabet or "_" **/ Parser.prototype.parseName = function () { debug ("parseName () started"); this.consumeWhiteSpace (); var name = ""; // // Detect the first character and make sure it is alphabet or '_' // if (/[a-z_]/i.test (this.peek ())) { name = this.next (); } else { throw Error ("Unexpected character : \"" + this.peek () + "\""); } // // Detect the rest of the NAME characters // while (/[a-z0-9_]/i.test (this.peek ())) { name += this.next (); } this.consumeWhiteSpace (); debug ("Name: " + name + ""); debug ("parseName () ended"); return name; } /** Parser::parseString parses string quoted in double quotes. Does not return the "\"". Will replace "\n" and "\"" escape sequence found in the string. **/ Parser.prototype.parseString = function () { debug ("parseString () started"); this.consumeWhiteSpace (); var str = ""; if (this.peek () != "\"") { throw Error ("String starts with \", unexpected character: \"" + this.peek () + "\""); } // // Consume "\"" // this.next (); while (this.peek () != "\"") { if (this.peek () == "\\") { // // consume "\\" // this.next (); var c = this.peek (); // // Consume whatever it is and append the correct escape character // if (c == "\"") { // // Double quotation // this.next (); str += "\""; } else if (c == "\\") { // // Backslash // this.next (); str += "\\"; } else { // // Everything else // this.next (); str += ("\\" + c); } } else { str += this.next (); } } // // Consume "\"" // this.next (); this.consumeWhiteSpace (); debug ("String: " + str); debug ("parseString () ended"); return str; } /** Parser::parseSelectorOpt will parse the "[]". If find any "\"" in the box bracket, will call parseString to handle. Will return the "[" and "]" characters. If "[" is not found, will return empty string **/ Parser.prototype.parseSelectorOpt = function () { debug ("parseSelectorOpt () started"); var sel = ""; if (this.peek () == "[") { // // consume and append "[" // sel = this.next (); while (this.peek () != "]") { if (this.peek () == "\"") { // // need the "\"" in xpath but parseString is not returning them // sel += ("\"" + this.parseString () + "\""); } else { sel += this.next (); } } // // consume and append "]" // sel += this.next (); } debug ("Selector: " + sel); debug ("parseSelectorOpt () ended"); return sel; } /** Parser::parseValue will detect if it is a string or the "subst" operator. If "subst" operator found, will substitute the envorinment variable. Will throw error if the substitution failed. **/ Parser.prototype.parseValue = function () { debug ("parseValue () started"); this.consumeWhiteSpace (); var val = ""; if (this.peek () != "\"") { var subst = this.parseName (); if (subst != "subst") { throw Error ("Unexpected operator: \"" + subst + "\""); } if (this.next () != "(") { throw Error ("Please adhere to syntax to use environment variable substitution: subst (STRING)"); } var str = this.parseString (); val = shell.ExpandEnvironmentStrings (str); if (val == str) { throw Error ("Substitution failed for this string: \"" + str + "\""); } if (this.next () != ")") { throw Error ("Missing ')'"); } } else { val = this.parseString (); } debug ("Value : " + val); debug ("parseValue () ended"); return val; } /** Parser::consumeWhiteSpace Consume white spaces **/ Parser.prototype.consumeWhiteSpace = function () { while (/\s/.test (this.peek ())) { this.next (); } } /** Parser::consumeComment Consume multiline comments, as long as the "#" character is at the first column **/ Parser.prototype.consumeComment = function () { while (this.peek () == "#") { do { var c = this.next(); } while (!this.isEof () && c != '\n'); } } /** Parser::consumeAll Consume comments and white spaces **/ Parser.prototype.consumeAll = function () { do { this.consumeWhiteSpace (); this.consumeComment (); } while (/\s/.test (this.peek ())); } /** Parser::checkValueList user are required to determine if "value" is a valid attribute or not before calling this function. **/ Parser.prototype.checkValueList = function (value, list) { if (!list) return; var values = list.split(",,"); for (var i = 0; i < values.length; ++i) { if (values[i] == value) return; } throw Error("Value: \"" + value + "\" is not one of:\n " + values.join('\n ')); } /** Parser::peek gives the next character in the stream but do not consume it. Because the file system object does not support peek function, the next character has to be read out and stored in a variable. **/ Parser.prototype.peek = function () { return this.peekedChar; } /** Parser::next gives the next character in the stream **/ Parser.prototype.next = function () { var c = this.peekedChar; if (this.location >= this.text.length) { if (this.isEof ()) { throw Error ("Unexpected end of file"); } this.peekedChar = ""; } else { if (this.peekedChar == '\n') { ++this.line; } this.peekedChar = this.text.charAt(this.location++); } return c; } /** Parser::isEof peekedChar is checked against 0 because it will be set in next function if AtEndOfStream is set. AtEndOfStream property cannot be used here because the final character is still stored in peekedChar even after AtEndOfStream is set. **/ Parser.prototype.isEof = function () { return (this.peekedChar === ""); } /** Provides a safe method to select single node from a parent node. This will check to make sure the selected child nodes are not ambiguous **/ function selectSingleNodeSafe (parent, childPath) { debug ("selectSingleNodeSafe () started"); // // Replacing any occurance of '/' at the beginning of a path with './' . // This gives the workaround to selectNodes from xmlDom and any child nodes. // This also works to change '//foo' to './/foo' // childPath = childPath.replace (/^\//, "./"); var node = parent.selectNodes (childPath); if (!node.length) { throw Error ("Cannot find child node from parent node \"" + parent.nodeName + "\" using this path : " + childPath); } else if (node.length > 1) { throw Error ("Cannot identify non-ambiguous child from parent node \"" + parent.nodeName + "\" using this path : " + childPath); } debug ("Found child \"" + childPath + "\" from parent node \"" + parent.nodeName + "\""); debug ("selectSingleNodeSafe () ended"); return node[0]; } /** Process the input arguments and store them in array to be used. Currrently, supports one input xml, one output, but multiple patch files. **/ function processArgs (args) { var opt; var optList = { input : 0, patch : [], output : 0, debug : 0, help : 0}; for (var index = 0; index < args.length; ++index) { opt = args (index); if (opt == "-patch") { if (index < (args.length - 1)) { optList.patch.push (args (++index)); } else { throw Error ("-patch option is not complete"); } } else if (opt == "-help" || opt == "--help" || opt == "-h") { optList.help = 1; } else if (opt == "-debug") { optList.debug = 1; } else { // // For input and output file without "-" option // if (!optList.input) { optList.input = opt; } else if (!optList.output) { optList.output = opt; } else { throw Error ("Option not supported: " + opt); } } } return optList; } /** Create ActiveXObject from list. User is responsible to check for success **/ function createObjectFromList (list) { var obj; for (var i in list) { try { obj = new ActiveXObject (list[i]); break; } catch (e) {} } return obj; } /** Returns the path with trailing "/" **/ function dirname (path) { return path.replace (/\\/g,'/').replace (/[^\/]*$/, ""); } /** Open a XML file by creating XMLDOM object **/ function readXmlFile (f) { var xmlDom = createObjectFromList (["Msxml2.DOMDocument.6.0", "Msxml2.DOMDocument", "Microsoft.XMLDOM"]); if (!xmlDom) { throw Error ("Create XMLDOM object failed"); } xmlDom.async = false; xmlDom.load (f); var err = xmlDom.parseError; if (err.errorCode != 0) { error (err.url.replace ("file:///", "") + '(' + err.line + ") : error : " + err.reason + err.srcText); throw Error ("Cannot read file."); } return xmlDom; } /** This function is needed because DOMDocument could not print pretty. However, user should validate the existence of reader and writer before using this save file routine **/ function saveDomWithIndent (dom, f, rdr, wrtr) { var textStream = fso.CreateTextFile (f, true); wrtr.indent = true; wrtr.omitXMLDeclaration = (dom.firstChild.nodeTypeString != "processinginstruction"); var enc = wrtr.encoding; wrtr.encoding = "utf-8"; rdr.contentHandler = wrtr; rdr.parse (dom); textStream.Write (wrtr.output); textStream.Close (); } /** Print the usage of script **/ function usage () { log ("\n\ PatchXml Tool \n\ \n\ Usage: \n\ cscript PatchXml.js -patch [[-patch ] ...]\n\ \n\ Options: \n\ -patch patch file with patching syntax, supports multiple files \n\ -help \n\ \n\ Remarks: \n\ It is compulsory to include input, output and at least one patch file \n\ Please also refer to header comment for patch format description \n"); } /** Main is here **/ function main () { debug ("main () started"); // // Print usage if no arguments are given // if (WScript.arguments.length == 0) { usage (); return 0; } // // Process input arguments // var options = processArgs (WScript.arguments); if (options.help) { usage (); return 0; } if (options.debug) { debug = log; } debug (shell.CurrentDirectory); debug (WScript.ScriptFullName); debug (WScript.ScriptName); // // Checks if any of the arguments are not specified // if (options.input == "") { throw Error ("Input xml file is not specified"); } else if (options.patch.length == 0) { throw Error ("Patch file(s) is not specified"); } else if (options.output == "") { throw Error ("Output file is not specified"); } // // Checks for the existence of each files // if (!fso.FileExists (options.input)) { throw Error ("File does not exists: \"" + options.input + "\""); } else { for (var i in options.patch) { if (!fso.FileExists (options.patch[i])) { throw Error ("File does not exists: \"" + options.patch[i] + "\""); } } } // // Read the input xml. Only the first option given will be used // var xmlDom = readXmlFile (options.input); // // Parse the patch files. Multiple patch files can be used // debug ("Parser started"); for (var i in options.patch) { debug (options.patch[i] + " started"); try { var parser = new Parser (options.patch[i], xmlDom); parser.parsePatchXml (xmlDom); } catch (e) { error (parser.filename + "(" + parser.line + ") : error : " + e.message); throw Error ("Patch file faulty"); } debug (options.patch[i] + " ended"); } debug ("Parser ended"); // // Write the output xml. If the MXXMLWriter or SAXXMLReader are not available, // write the xml using XMLDOM object without indentation // reader = createObjectFromList (["Msxml2.SAXXMLReader.6.0", "Msxml2.SAXXMLReader"]); writer = createObjectFromList (["Msxml2.MXXMLWriter.6.0", "Msxml2.MXXMLWriter"]); if (reader && writer) { saveDomWithIndent (xmlDom, options.output, reader, writer); log ("Written XML: " + options.output); } else { xmlDom.save (options.output); log ("Written XML without indentation: " + options.output); } debug ("main () ended"); return 0; } try { exit (main()); } catch (e) { error (e.name + " : " + e.message); exit (1); }