xmlxsltsap-cloud-platform

XSLT mapping. Dynamically setting xml node names in a for each loop


Hi I have duplicate node names in my xml and would like to make each of them unique. As the amount of nodes is unknown, I would like to implement a for each loop and add a counter to the name of each UserGroup node using XSLT.

I am using :

<xsl:stylesheet version="3.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">

My input xml is:

<groups>
        <UserGroup>
                <integrationKey>xxa</integrationKey>
                <uid>001</uid>]
        </UserGroup>
        <UserGroup>
                <integrationKey>xxb</integrationKey>
                <uid>002</uid>
        </UserGroup>
</groups>

How can I use XSLT to transform the xml into:

<groups>
        <UserGroup1>
                <integrationKey>xxa</integrationKey>
                <uid>001</uid>]
        </UserGroup1>
        <UserGroup2>
                <integrationKey>xxb</integrationKey>
                <uid>002</uid>
        </UserGroup2>
</groups>

EDIT Hi, the reason is I am processing a xml message payload that is built up and then needs to be converted to JSON (The groups part is attached to the main xml). The target system the payload is going to accepts a certain "format" that doesnt include UserGroup. Unfortunately the amount of UserGroups is dynamic and the mapping has limited control. I have attempted to build the Json in Groovy with:

import groovy.json.JsonSlurper
import groovy.json.JsonOutput
import groovy.json.JsonBuilder

    def body = """

{"B2BCustomer":{"integrationKey":"","customerID":"xxx","email":"xxx","name":"xxx","uid":"xxx","businessUnit":{"BU":{"integrationKey":"","code":"xxx"}},"groups":[{"UserGroup":{"integrationKey":"xxx","uid":"xxx"},"UserGroup":{"integrationKey":"yyy","uid":"yyy"}}]}}"""
 
    //Setup output JSON
    def jsonParser = new JsonSlurper();
    def jsonObject = jsonParser.parseText(body);
    body = JsonOutput.toJson(jsonObject["B2BCustomer"]);
    output = jsonParser.parseText(body);
    jsonString = jsonParser.parseText(body);
    
    //Create default b2b unit JSON
    if(output.containsKey("defaultB2BUnit")){
        output.remove("defaultB2BUnit"); 
        defaultB2BUnit = JsonOutput.toJson(jsonString["defaultB2BUnit"]);
        jsonObject = jsonParser.parseText(defaultB2BUnit);
        defaultB2BUnit =  JsonOutput.toJson(jsonObject["B2BUnit"]);
        output.put("defaultB2BUnit", defaultB2BUnit);
    }
    //Create businessUnit JSON
    if(output.containsKey("businessUnit")){
        output.remove("businessUnit"); 
        businessUnit = JsonOutput.toJson(jsonString["businessUnit"]);
        jsonObject = jsonParser.parseText(businessUnit);
        businessUnit =  JsonOutput.toJson(jsonObject["BU"]);
        output.put("businessUnit", businessUnit);
    }
    //Create groups JSON
    if(output.containsKey("groups")){
        output.remove("groups"); 
        groups = JsonOutput.toJson(jsonString["groups"]);
        jsonObject = jsonParser.parseText(groups);
        groups =  JsonOutput.toJson(jsonObject["UserGroup"]);
        output.put("groups", groups);
    }
    //Build output JSON
    def builder = new JsonBuilder();
    builder(output);
    def builderString = builder.toString().replace('"{\\', '{').replace('\\','').replace('}"','}').replace('"[','[').replace(']"',']');
    println builderString;

This works if the keys are not duplicate (It only pulls a single UserGroup out. My plan is to run this code still, but the groups part in a loop to get the desired JSON output.

If someone has a way I dont have to do string replacements that would be awesome too. Unfortunately I can't change the structure of the target systems metadata.

Current output:

{
    "integrationKey": "",
    "customerID": "xxx",
    "email": "xxx",
    "name": "xxx",
    "uid": "xxx",
    "businessUnit": {
        "integrationKey": "",
        "code": "xxx"
    },
    "groups": [{
            "integrationKey": "yyy",
            "uid": "yyy"
        }
    ]
}

Desired Output:

{
    "integrationKey": "",
    "customerID": "xxx",
    "email": "xxx",
    "name": "xxx",
    "uid": "xxx",
    "businessUnit": {
        "integrationKey": "",
        "code": "xxx"
    },
    "groups": [{
            "integrationKey": "xxx",
            "uid": "xxx"
        },
        {
            "integrationKey": "yyy",
            "uid": "yyy"
        }
    ]
}

Extra note I'm considering just storing the groups in an exhange property and Running a custom xml -> Json Groovy script to build it exactly how I want without weird effects from JSON slurper etc. Thank you for your effort and advice in advance!


Solution

  • Given XSLT 3, one way would be to use an accumulator:

      <xsl:accumulator name="UserGroupCounter" as="xs:integer" initial-value="0">
        <xsl:accumulator-rule match="groups" select="0"/>
        <xsl:accumulator-rule match="groups/UserGroup" select="$value + 1"/>
      </xsl:accumulator>
      
      <xsl:template match="UserGroup">
        <xsl:element name="{name()}{accumulator-before('UserGroupCounter')}">
          <xsl:apply-templates/>
        </xsl:element>
      </xsl:template>
    
      <xsl:mode on-no-match="shallow-copy" use-accumulators="UserGroupCounter"/>
    

    I agree with the comments made that the result format is not a good XML format. The usual advice, if you need some element index, is to not put it into the element name, but rather use an attribute e.g.

      <xsl:accumulator name="UserGroupCounter" as="xs:integer" initial-value="0">
        <xsl:accumulator-rule match="groups" select="0"/>
        <xsl:accumulator-rule match="groups/UserGroup" select="$value + 1"/>
      </xsl:accumulator>
      
      <xsl:template match="UserGroup">
        <xsl:copy>
          <xsl:attribute name="userGroupIndex" select="accumulator-before('UserGroupCounter')"/>
          <xsl:apply-templates/>
        </xsl:copy>
      </xsl:template>
    
      <xsl:mode on-no-match="shallow-copy" use-accumulators="UserGroupCounter"/>