Home > Software engineering >  Iterate Through Multiple XPath Results - Create ONLY ONE NODE
Iterate Through Multiple XPath Results - Create ONLY ONE NODE

Time:04-22

I have the following example XML:

<Example>
        <SiteCode null="no">1ExampleSite</SiteCode>
        <SiteName null="yes"/>
        <CustomerPIN null="no">1234567</CustomerPIN>
        <CustomerLastName null="no">Test </CustomerLastName>
        <CustomerFirstName null="no">Example</CustomerFirstName>
        <CustomerMiddleName null="yes"/>
        <CustomerDOB null="no">2000-01-01 00:00:00.000</CustomerDOB> 
        <CustomerAddressLine1 null="no">1234 Easy ST</CustomerAddressLine1>
        <CustomerAddressLine2 null="yes"/>
        <CustomerCity null="no">Hartford</CustomerCity>
        <CustomerState null="no">CT</CustomerState>
        <CustomerZipCode null="no">123456</CustomerZipCode>
        <CustomerGender null="no">F</CustomerGender>
        <CustomerRaceCode null="no">White</CustomerRaceCode>
        <CustomerRaceName null="yes"/>
</Example>

What I WANT to do, is to output XML so that the Address nodes are shared under HomeAddress/Address, like this:

<?xml version="1.0" encoding="UTF-8"?>
<Information>
<PIN>1234567</PIN>
<LastName>Test </LastName>
<FirstName>Example</FirstName>
<MiddleName/>
<DateOfBirth><DateNode>2000-01-01 00:00:00.000</DateNode></DateOfBirth>
<HomeAddress>
  <Address>
   <AddressLine1>1234 Easy ST</AddressLine1>
   <AddressLine2/>
   <City>Hartford</City>
   <State>CT</State>
   <ZipCode>123456</ZipCode>
  </Address>
</HomeAddress>
<Gender><Code>F</Code></Gender>
<RaceCode>White</RaceCode>
<RaceName/>
</Information>

This is my current XSLT code:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  version="3.0">

  <xsl:template match="/Example">
    <Information>
        <xsl:for-each select="*">
            <xsl:apply-templates select=".[starts-with(local-name(), 'CustomerGender')]"/>
            <xsl:apply-templates select=".[starts-with(local-name(), 'CustomerDOB')]"/>
            <xsl:apply-templates select=".[starts-with(local-name(), 'CustomerAddress')
                      or starts-with(local-name(), 'CustomerCity')
                      or starts-with(local-name(), 'CustomerState')
                      or starts-with(local-name(), 'CustomerZipCode')][1]"/>
            <xsl:apply-templates select=".[starts-with(local-name(), 'Customer')
             and not(contains(local-name(), 'Gender'))
             and not(contains(local-name(), 'Address'))
             and not(contains(local-name(), 'City'))
             and not(contains(local-name(), 'State'))
             and not(contains(local-name(), 'ZipCode'))
             and not(contains(local-name(), 'DOB'))]"/>
            
                        
                    </xsl:for-each>
      </Information>
  </xsl:template>

  <xsl:template match=".[starts-with(local-name(), 'Customer')]">
  <xsl:element name="{replace(local-name(), 'Customer', '')}">
    <xsl:apply-templates/>
  </xsl:element>
</xsl:template>

<xsl:template match=".[starts-with(local-name(), 'CustomerGender')]">
  <xsl:element name="{replace(local-name(), 'Customer', '')}">
    <Code>
    <xsl:apply-templates/>
    </Code>
  </xsl:element>
</xsl:template>

<xsl:template match=".[starts-with(local-name(), 'CustomerDOB')]">
  <DateOfBirth>
    <DateNode>
    <xsl:apply-templates/>
    </DateNode>
  </DateOfBirth>
</xsl:template>

<xsl:template match=".[starts-with(local-name(), 'CustomerAddress')
                      or starts-with(local-name(), 'CustomerCity')
                      or starts-with(local-name(), 'CustomerState')
                      or starts-with(local-name(), 'CustomerZipCode')]">
  <HomeAddress>
    <Address>
    <xsl:for-each select=".">
    <xsl:element name="{replace(local-name(), 'Customer', '')}">
      <xsl:apply-templates/>
    </xsl:element>
    </xsl:for-each>
    </Address>
  </HomeAddress>
</xsl:template>

  <xsl:template match="@*|node()">
    <xsl:copy>
      <xsl:apply-templates select="@*|node()"/>
    </xsl:copy>
  </xsl:template>

</xsl:stylesheet>

Yet - here is my current output:

<?xml version="1.0" encoding="UTF-8"?>
<Information>
<PIN>1234567</PIN>
<LastName>Test </LastName>
<FirstName>Example</FirstName>
<MiddleName/>
<DateOfBirth><DateNode>2000-01-01 00:00:00.000</DateNode></DateOfBirth>
<Gender><Code>F</Code></Gender>
<RaceCode>White</RaceCode>
<RaceName/>
<HomeAddress><Address><AddressLine1>1234 Easy ST</AddressLine1></Address></HomeAddress><HomeAddress><Address><AddressLine2/></Address></HomeAddress>
<HomeAddress><Address><City>Hartford</City></Address></HomeAddress>
<HomeAddress><Address><State>CT</State></Address></HomeAddress>
<HomeAddress><Address><ZipCode>123456</ZipCode></Address></HomeAddress>
</Information>

How can I change this XSL logic so that I get the desired output (the shared "Address" node) instead of the current (multiple "Address" nodes)?

I'll appreciate any help on this, it's possible I overlooked a previous example, but I tried searching and couldn't find anything specific to this scenario.

EDIT: I understand that one can extract the "Address" information outside of the "for-each" I have, like this:

<xsl:for-each select="*">
            <xsl:apply-templates select=".[starts-with(local-name(), 'CustomerGender')]"/>
            <xsl:apply-templates select=".[starts-with(local-name(), 'CustomerDOB')]"/>
            
            <xsl:apply-templates select=".[starts-with(local-name(), 'Customer')
             and not(contains(local-name(), 'Gender'))
             and not(contains(local-name(), 'Address'))
             and not(contains(local-name(), 'City'))
             and not(contains(local-name(), 'State'))
             and not(contains(local-name(), 'ZipCode'))
             and not(contains(local-name(), 'DOB'))]"/>
            
                        
                    </xsl:for-each>

        <HomeAddress>
            <Address>
        <xsl:apply-templates select="*[starts-with(local-name(), 'CustomerAddress')
                      or starts-with(local-name(), 'CustomerCity')
                      or starts-with(local-name(), 'CustomerState')
                      or starts-with(local-name(), 'CustomerZipCode')]"/>
            </Address>
        </HomeAddress>

However, I want to also preserve the order of the data. If the customer address information is in the middle of the data, I want it to appear as such in the output.

CodePudding user response:

Is there a reason why you cannot do simply:

<xsl:stylesheet version="2.0" 
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
<xsl:strip-space elements="*"/>

<xsl:template match="/Example">
    <xsl:variable name="addr" select="CustomerAddressLine1 | CustomerAddressLine2 | CustomerCity | CustomerState | CustomerZipCode" />
    <Information>
        <xsl:copy-of select="* except $addr"/>
        <HomeAddress>
            <Address>
                <xsl:copy-of select="$addr"/>
            </Address>
        </HomeAddress>
    </Information>
</xsl:template>

</xsl:stylesheet>

Added:

Assuming that the address fields are always in a contiguous block, the expected output could be produced using:

<xsl:stylesheet version="1.0" 
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
<xsl:strip-space elements="*"/>

<xsl:template match="/Example">
    <xsl:variable name="addr" select="CustomerAddressLine1 | CustomerAddressLine2 | CustomerCity | CustomerState | CustomerZipCode" />
    <Information>
        <xsl:apply-templates select="$addr[1]/preceding-sibling::*[starts-with(name(), 'Customer')]"/>
        <HomeAddress>
            <Address>
                <xsl:apply-templates select="$addr" />
            </Address>
        </HomeAddress>
        <xsl:apply-templates select="$addr[last()]/following-sibling::*[starts-with(name(), 'Customer')]"/>
    </Information>
</xsl:template>

<xsl:template match="*">
    <xsl:element name="{substring-after(name(), 'Customer')}">
        <xsl:apply-templates/>
    </xsl:element>
</xsl:template> 

<xsl:template match="CustomerGender">
    <Gender>
        <Code>
            <xsl:apply-templates/>
        </Code>
  </Gender>
</xsl:template>

<xsl:template match="CustomerDOB">
    <DateOfBirth>
        <DateNode>
            <xsl:apply-templates/>
        </DateNode>
  </DateOfBirth>
</xsl:template>

</xsl:stylesheet>
  • Related