Function: convertAdvXMLtoJSON

      +

      Goal: Recursively and generically convert advanced XML strings into JSON.

      • This function convertAdvXMLtoJSON shows how to convert advanced XML strings into JSON.

      • This function will handle items and constructs that convertXMLtoJSON will fail on 1) empty-element tags and 2) attributes.

        <ADVELEM1 adv_attrA="adv_valA" />
        
        <ADVELEM2 adv_attrA="adv_valA" adv_attrB="adv_valB">
            <SUB>
                SUBDATA
            </SUB>
        </ADVELEM2>
      • Requires Eventing Storage (or metadata collection) and a "source" collection.

      • Will operate on any mutation where the KEY or meta.id starts with "xml:".

      • Will enrich the source document with a new JSON object representing the XML data.

      • Maintains a checksum to prevent the overhead of conversion if the property in_xml is unchanged.

      • convertAdvXMLtoJSON

      • Input Data/Mutation

      • Output Data/Mutation

      // To run configure the settings for this Function, convertAdvXMLtoJSON, as follows:
      //
      // Version 7.1+
      //   "Function Scope"
      //     *.* (or try bulk.data if non-privileged)
      //   "Listen to Location"
      //     bulk.data.source
      //   "Eventing Storage"
      //     rr100.eventing.metadata
      //   Binding(s)
      //    1. "binding type", "alias name...", "bucket.scope.collection", "Access"
      //       "bucket alias", "src_col",       "bulk.data.source",        "read and write"
      //
      // Version 6.X
      //   "Source Bucket"
      //     source
      //   "MetaData Bucket"
      //     metadata
      //   Binding(s)
      //    1. "binding type", "alias name...", "bucket",     "Access"
      //       "bucket alias", "src_col",       "source",     "read and write"
      
      // ===============================================================
      // THIS IS AN APPLICATION SPECIFIC CUSTOM TRANSFOMRATION ON NEED
      function customParseFixups(key,tmp) {
          // typically we would test the key for a name where we want to transform the value
      
          // Example 1:
          //     if (key === "resultStr") {
          //        // makes an array of strings
          //        // var data_array = tmp.trim().split(' ');
          //        return data_array;
          //     }
      
          // Example 2:
          //     if (key === "resultNumStr") {
          //        // makes an array of numbers
          //        // var data_array = tmp.trim().split(' ').map(str => Number(str));
          //        return data_array;
          //     }
      
          // Do nothing this is a NOOP
          return tmp;
      }
      // ===============================================================
      
      function OnUpdate(doc, meta) {
          // filter out non XML
          if (!meta.id.startsWith("advxml:")) return;
          // The KEY started with "advxml" try to process it
          // ===========================================================
          // *** Do other  work required here on non .in_xml changes ***
          // ===========================================================
          // let's see if we need to re-create our json representation.
          var xmlchksum = crc64(doc.in_xml);
          // ===========================================================
          // Don't reprocess if the doc.in_xml has not changed this could be
          // a big performance win if the doc has other fields that mutate.
          // We do this via a checksum of the .in_xml property.
          if (doc.xmlchksum && doc.xmlchksum === xmlchksum) return;
          // Either this is the first pass, or the .in_xml property changed.
          var jsonDoc = parseAdvXmlToJson(doc.in_xml);
          log(meta.id,"1. INPUT  xml doc.in_xml :", doc.in_xml);
          log(meta.id,"2. CHECKSUM doc.in_xml   :", xmlchksum);
          log(meta.id,"3. OUTPUT doc.out_json   :", jsonDoc);
          doc.out_json = jsonDoc;
          doc.xmlchksum = xmlchksum;
          // ===========================================================
          // enrich the source collection with .out_json and .xmlchksum
          src_col[meta.id] = doc;
      }
      
      // 7.0.0 version uses String.matchAll eliminates the need to make our own MatchAll function call as parseXmlToJson(xml)
      function parseAdvXmlToJson(xml, recurs) {
      	const json = {};
      	if (!recurs) {
      		// 1st call, Fix bad closures,  Transform Example : '<tagName attrName="attrValue" >'  becomes '<tagName attrName="attrValue" />'
      		xml = xml.replace(/\"\s*>/g, '" />');
      		// 1st call, Transform Example : '<tagName attrName="attrValue" />' becomes '<tagName><attrName>attrValue</attrName></tagName>'
      		xml = xml.replace(/<\s*([^\/><\s]+)\s+(\w[^<>]*)(\s*\/>)/gm, '<$1 $2></$1>');
      	}
      	for (const res of xml.matchAll(/(?:<([\w:]*)(?:\s[^>]*)*>)((?:(?!<\1).)*)(?:<\/\1>)|<([\w:]*)(?:\s*)*\/>/gm)) {
      		// find all sest of 1..N attributes if any
      		var attrs = {};
      		for (const res1 of res[0].matchAll(/<\s*(\w[^\/><\s]*)\s+(\w[^<]*[^=<]\s*[=]\s*[\'\"][^<]+[\'\"])\s*>([^<>]*)</gm)) {
      			attrs[res1[1]] = {};
      			if (res1[3] !== "") {
      				// illegal XML: <tag1 attr1="aval1">value1</tag1>
      				log('Illegal can not have bare "' + res1[3] + '" value if we have attr(s) input:', res1[0]);
      				return null;
      			} else {
      				for (const res2 of res1[2].matchAll(/(\w+[^=<>]*)\s*[=]\s*[\"]([^\"]+)[\"]|(\w+[^=<>]*)\s*[=]\s*[\']([^\']+)[\']/gm)) {
      					if (res2[1] !== "") attrs[res1[1]][res2[1]] = res2[2];
      					if (res2[3] !== "") attrs[res1[1]][res2[3]] = res2[4];
      				}
      			}
      		}
      		const key = res[1] || res[3];
      		var value = res[2] && parseAdvXmlToJson(res[2], true);
      		if (res[2] === "" && Object.keys(attrs).length > 0) {
      			value = {};
      		}
      		if (attrs[key]) {
      			for (const p in attrs[key]) {
      				if (attrs[key][p]) {
      					value[p] = attrs[key][p];
      				}
      			}
      		}
      		attrs = {};
      		var tmp = ((value && Object.keys(value).length) ? value : res[2]) || null;
      		if (Array.isArray(json[key]) == false) {
      			if (json[key]) {
      				// we have seen this key before change from object to an array of objects
                      var old = json[key];
                      json[key] = [];
                      json[key].push(old);
                      json[key].push(tmp);
      			} else {
      			    // link to a custom function
      			    tmp = customParseFixups(key,tmp);
      				json[key] = tmp;
      			}
      		} else {
      			json[key].push(tmp);
      		}
      	}
      	return json;
      }
      
      /*
      // need this for 6.6.0 version
      function* MatchAll(str, regExp) {
        if (!regExp.global) {
          throw new TypeError('Flag /g must be set!');
        }
        const localCopy = new RegExp(regExp, regExp.flags);
        let match;
        while (match = localCopy.exec(str)) {
          yield match;
        }
      }
      
      // 6.6.0 version no String.matchAll need our own MatchAll function, call as parseXmlToJson(xml)
      function parseAdvXmlToJson(xml, recurs) {
      	const json = {};
      	if (!recurs) {
      		// 1st call, Fix bad closures,  Transform Example : '<tagName attrName="attrValue" >'  becomes '<tagName attrName="attrValue" />'
      		xml = xml.replace(/\"\s*>/g, '" />');
      		// 1st call, Transform Example : '<tagName attrName="attrValue" />' becomes '<tagName attrName="attrValue"></tagName>'
      		xml = xml.replace(/<\s*([^\/><\s]+)\s+(\w[^<>]*)(\s*\/>)/gm, '<$1 $2></$1>');
      	}
      	for (const res of MatchAll(xml, /(?:<([\w:]*)(?:\s[^>]*)*>)((?:(?!<\1).)*)(?:<\/\1>)|<([\w:]*)(?:\s*)*\/>/gm)) {
      		// find all sest of 1..N attributes if any
      		var attrs = {};
      		for (const res1 of MatchAll(res[0], /<\s*(\w[^\/><\s]*)\s+(\w[^<]*[^=<]\s*[=]\s*[\'\"][^<]+[\'\"])\s*>([^<>]*)</gm)) {
      			attrs[res1[1]] = {};
      			if (res1[3] !== "") {
      				// illegal XML: <tag1 attr1="aval1">value1</tag1>
      				log('Illegal can not have bare "' + res1[3] + '" value if we have attr(s) input:', res1[0]);
      				return null;
      			} else {
      				for (const res2 of MatchAll(res1[2], /(\w+[^=<>]*)\s*[=]\s*[\"]([^\"]+)[\"]|(\w+[^=<>]*)\s*[=]\s*[\']([^\']+)[\']/gm)) {
      					if (res2[1] !== "") attrs[res1[1]][res2[1]] = res2[2];
      					if (res2[3] !== "") attrs[res1[1]][res2[3]] = res2[4];
      				}
      			}
      		}
      		const key = res[1] || res[3];
      		var value = res[2] && parseAdvXmlToJson(res[2], true);
      		if (res[2] === "" && Object.keys(attrs).length > 0) {
      			value = {};
      		}
      		if (attrs[key]) {
      			for (const p in attrs[key]) {
      				if (attrs[key][p]) {
      					value[p] = attrs[key][p];
      				}
      			}
      		}
      		attrs = {};
      		var tmp = ((value && Object.keys(value).length) ? value : res[2]) || null;
      		if (Array.isArray(json[key]) == false) {
      			if (json[key]) {
      				// we have seen this key before change from object to an array of objects
                      var old = json[key];
                      json[key] = [];
                      json[key].push(old);
                      json[key].push(tmp);
      			} else {
      			    // link to a custom function
      			    tmp = customParseFixups(key,tmp);
      				json[key] = tmp;
      			}
      		} else {
      			json[key].push(tmp);
      		}
      	}
      	return json;
      }
      */
      INPUT: KEY advxml::1
      
      {
        "type": "advxml",
        "id": 1,
        "in_xml": "<CD><ADVELEM1 adv_attrA=\"adv_valA\"/><ADVELEM2 adv_attrA=\"adv_valA\" adv_attrB=\"adv_valB\"><SUB>SUBDATA</SUB><TITLE>EmpireBurlesque</TITLE><ARTIST>BobDylan</ARTIST><COUNTRY>USA</COUNTRY><COMPANY>Columbia</COMPANY><PRICE>10.90</PRICE><YEAR>1985</YEAR></CD>"
      }
      UPDATED/OUTPUT: KEY advxml::1
      
      {
        "type": "advxml",
        "id": 1,
        "in_xml": "<CD><ADVELEM1 adv_attrA=\"adv_valA\"/><ADVELEM2 adv_attrA=\"adv_valA\" adv_attrB=\"adv_valB\"><SUB>SUBDATA</SUB></ADVELEM2><TITLE>EmpireBurlesque</TITLE><ARTIST>BobDylan</ARTIST><COUNTRY>USA</COUNTRY><COMPANY>Columbia</COMPANY><PRICE>10.90</PRICE><YEAR>1985</YEAR></CD>",
        "out_json": {
          "CD": {
            "ADVELEM1": {
              "adv_attrA": "adv_valA"
            },
            "ADVELEM2": {
              "SUB": "SUBDATA",
              "adv_attrA": "adv_valA",
              "adv_attrB": "adv_valB"
            },
            "TITLE": "EmpireBurlesque",
            "ARTIST": "BobDylan",
            "COUNTRY": "USA",
            "COMPANY": "Columbia",
            "PRICE": "10.90",
            "YEAR": "1985"
          }
        },
        "xmlchksum": "99b252d9af646320"
      }