简体   繁体   中英

XSLT multiple repeatable child nodes

I am working with a set of records that have multiple different child node types that are repeatable. The end goal is to create a CSV mapping, and because of the nesting, each XML record can map to many CSV line items.

I understand how to use for-each to create multiple output lines, but I am having trouble because there are two different cases to loop over.

In the following example, App is the base record, SST_Interval is repeatable (1 or more), and ReplacementPart can have 0 or more PartType s.

I would like to extract a CSV that has the following format

app_id|base_vehicle_id|sst_interval_id|sst_interval_month|part_type_id

For the record shown below, the result would look like this

915152|18287|646|12|10007
915152|18287|646|12|12277
915152|18287|646|12|18159
915152|18287|32523|24|10007
915152|18287|32523|24|12277
915152|18287|32523646|24|18159

Here is the record

<App action="A" id="915152" ref="568874">
    <BaseVehicle id="18287" />
    <EngineVIN id="25" />
    <Note id="8722" vehicleattribute="no" />
    <Position id="1" />
    <MOTOR_Operation id="551841">
        <SkillCode>G</SkillCode>
        <Base_MOTOR_EWT minutes="1" />
        <SST_Interval id="646">
            <SST_IndicatorImage><![CDATA[sstgm140001]]></SST_IndicatorImage>
            <SST_IndicatorText><![CDATA[Change Engine Oil Soon]]></SST_IndicatorText>
            <SST_Frequency id="7" />
            <SST_IntervalMonth><![CDATA[12]]></SST_IntervalMonth>
            <SST_SevereService id="2080" />
            <SST_Note1 id="5117" />
        </SST_Interval>
        <SST_Interval id="32523">
            <SST_IndicatorImage><![CDATA[sstgm140001]]></SST_IndicatorImage>
            <SST_IndicatorText><![CDATA[Change Engine Oil Soon]]></SST_IndicatorText>
            <SST_Frequency id="7" />
            <SST_IntervalMonth><![CDATA[24]]></SST_IntervalMonth>
            <SST_SevereService id="2080" />
            <SST_Note1 id="5117" />
        </SST_Interval>
        <ReplacementPart>
            <PartType id="10007" servicetype_id="1" />
            <PartType id="12277" servicetype_id="1" />
            <PartType id="18159" servicetype_id="1" />
        </ReplacementPart>
    </MOTOR_Operation>
</App>

Here is the XSLT I have tried. If you can point me in the right direction I would be very grateful.

<?xml version="1.0" encoding="UTF-8" ?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:output method="text" encoding="UTF-8" omit-xml-declaration="yes"/>
  <xsl:strip-space elements="*"/>
  <xsl:template match="Header|Footer">
  </xsl:template>

  <xsl:template match="App">
    <xsl:apply-templates select="MOTOR_Operation/SST_Interval"/>
  </xsl:template>

  <xsl:template match="PartType">
    <xsl:apply-templates select="@id" mode="csv"/>
  </xsl:template>

  <xsl:template match="SST_Interval">
    <xsl:variable name="tmp">
      <xsl:apply-templates select="ancestor::App/@id" mode="csv"/>
      <xsl:apply-templates select="@id" mode="csv-nl"/>
    </xsl:variable>
    <xsl:for-each select="ancestor::App/MOTOR_Operation/ReplacementPart/PartType">
      <xsl:copy-of select="$tmp"/>
      <xsl:apply-templates select="." />
    </xsl:for-each>

  </xsl:template>

  <xsl:template match="text()|@*" mode="csv">
    <xsl:value-of select="concat(., '|')" />
  </xsl:template>

  <xsl:template match="text()|@*" mode="csv-nl">
    <xsl:value-of select="concat(., '&#xa;')" />
  </xsl:template>

</xsl:stylesheet>

I don't think you are far off. You just need to change the definition of the tmp variable to include more fields, and use mode csv for all of them, and then change the template matching PartType to use csv-nl instead.

Try this XSLT instead

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

  <xsl:template match="Header|Footer">
  </xsl:template>

  <xsl:template match="App">
    <xsl:apply-templates select="MOTOR_Operation/SST_Interval"/>
  </xsl:template>

  <xsl:template match="PartType">
    <xsl:apply-templates select="@id" mode="csv-nl"/>
  </xsl:template>

  <xsl:template match="SST_Interval">
    <xsl:variable name="tmp">
      <xsl:apply-templates select="ancestor::App/@id" mode="csv"/>
      <xsl:apply-templates select="ancestor::App/BaseVehicle/@id" mode="csv"/>
      <xsl:apply-templates select="@id" mode="csv"/>
      <xsl:apply-templates select="SST_IntervalMonth" mode="csv"/>
    </xsl:variable>
    <xsl:for-each select="ancestor::App/MOTOR_Operation/ReplacementPart/PartType">
      <xsl:copy-of select="$tmp"/>
      <xsl:apply-templates select="." />
    </xsl:for-each>
  </xsl:template>

  <xsl:template match="text()|@*" mode="csv">
    <xsl:value-of select="concat(., '|')" />
  </xsl:template>

  <xsl:template match="text()|@*" mode="csv-nl">
    <xsl:value-of select="concat(., '&#xa;')" />
  </xsl:template>
</xsl:stylesheet>

EDIT: In response to your comment, if you wanted to cope with missing nodes, I would perhaps disregard the templates with mode, and store the separator and new line characters in variables instead. Then you could do this...

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

  <xsl:variable name="sep" select="'|'" />
  <xsl:variable name="nl" select="'&#xa;'" />

  <xsl:template match="App">
    <xsl:apply-templates select="MOTOR_Operation/SST_Interval"/>
  </xsl:template>

  <xsl:template match="PartType">
    <xsl:value-of select="@id" />
    <xsl:value-of select="$nl" />
  </xsl:template>

  <xsl:template match="SST_Interval">
    <xsl:variable name="tmp">
      <xsl:value-of select="ancestor::App/@id" />
      <xsl:value-of select="$sep" />
      <xsl:value-of select="ancestor::App/BaseVehicle/@id" />
      <xsl:value-of select="$sep" />
      <xsl:value-of select="@id"/>
      <xsl:value-of select="$sep" />
      <xsl:value-of select="SST_IntervalMonth"/>
      <xsl:value-of select="$sep" />
    </xsl:variable>
    <xsl:for-each select="ancestor::App/MOTOR_Operation/ReplacementPart/PartType">
      <xsl:copy-of select="$tmp"/>
      <xsl:apply-templates select="." />
    </xsl:for-each>
  </xsl:template>
</xsl:stylesheet>

EDIT 2: In the case of there being no PartType records, try replacing the current xsl:for-each with this code

    <xsl:variable name="PartTypes" select="ancestor::App/MOTOR_Operation/ReplacementPart/PartType" />
    <xsl:for-each select="$PartTypes">
      <xsl:copy-of select="$tmp"/>
      <xsl:apply-templates select="." />
    </xsl:for-each>
    <xsl:if test="not($PartTypes)">
      <xsl:copy-of select="$tmp"/>
      <xsl:value-of select="$nl" />
    </xsl:if>

The XSLT you presented in the question works exactly as it should. The main problem is simply that you use mode csv-nl in the wrong place. Additionally, there's nothing to provide the base vehicle ID and interval month fields, but it looks pretty straightforward to add those.


Update: With respect to optional fields, although you could use XSL's conditional constructs, a more natural approach would be to simply allow the transformation of the relevant field to produce nothing. The only trick here is that for your application you must always output a delimiter, but that arises from your particular approach to inserting delimiters.

One fairly clean way to work within that framework would be to unconditionally call a named template that transforms the optional element and adds the delimiter. I have updated the stylesheet below to demonstrate this for the base vehicle id.


Allow me, however, to suggest some simplifications:

  • You don't need to omit-xml-declarations when the output method is text
  • you don't need to strip-space from the input elements because you're not actually using any text nodes; only attribute nodes are contributing to the output. Note also that whitespace stripping applies exclusively to whitespace-only text nodes
  • You don't need to suppress <Header> and <Footer> elements when there aren't any in the input, OR when the structure of the stylesheet and input result in such nodes never being transformed in the first place.
  • You're a bit verbose in providing a template for PartType when you know at the point where you select it for transformation that you want only its id attribute
  • Leveraging template parameters could make you code cleaner
  • Prefer to avoid xsl:for-each where you don't need it. Often, you can simply xsl:apply-templates to the same nodes, instead
  • As a personal style choice, I prefer to avoid the concat() function where I can instead simply provide a sequence of xsl:value-of and / or xsl:text elements

Taking all those into account, I'd suggest this variation on your stylesheet:

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

  <xsl:template match="App">
    <xsl:apply-templates select="MOTOR_Operation/SST_Interval"/>
  </xsl:template>

  <xsl:template match="SST_Interval">
    <xsl:variable name="tmp">
      <xsl:apply-templates select="ancestor::App/@id" mode="csv"/>
      <xsl:call-template name="optional-vehicle"/>
      <xsl:apply-templates select="@id" mode="csv"/>
      <xsl:apply-templates select="SST_IntervalMonth" mode="csv"/>
    </xsl:variable>
    <xsl:apply-templates select="../ReplacementPart/PartType/@id" mode="csv-nl">
      <xsl:with-param name="prefix" select="$tmp"/>
    </xsl:apply-templates>
  </xsl:template>

  <xsl:template name="optional-vehicle">
    <xsl:value-of select="ancestor::App/BaseVehicle/@id"/>
    <xsl:text>|</xsl:text>
  </xsl:template>

  <xsl:template match="node()|@*" mode="csv">
    <xsl:value-of select="." />
    <xsl:text>|</xsl:text>
  </xsl:template>

  <xsl:template match="node()|@*" mode="csv-nl">
    <xsl:param name="prefix"/>
    <xsl:value-of select="$prefix"/>
    <xsl:value-of select="." />
    <xsl:text>&#xa;</xsl:text>
  </xsl:template>

</xsl:stylesheet>

I would suggest a different approach:

XSLT 1.0

<xsl:stylesheet version="1.0" 
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text" encoding="UTF-8" />

<xsl:template match="App">
    <xsl:variable name="common1">
        <xsl:value-of select="@id"/>
        <xsl:text>|</xsl:text>
        <xsl:value-of select="BaseVehicle/@id"/>
        <xsl:text>|</xsl:text>
    </xsl:variable>
    <xsl:for-each select="MOTOR_Operation/SST_Interval">
        <xsl:variable name="common2">
            <xsl:value-of select="$common1"/>
            <xsl:value-of select="@id"/>
            <xsl:text>|</xsl:text>
            <xsl:value-of select="SST_IntervalMonth"/>
            <xsl:text>|</xsl:text>
        </xsl:variable>
        <xsl:variable name="part-types" select="../ReplacementPart/PartType" />
        <xsl:for-each select="$part-types">
            <xsl:value-of select="$common2"/>
            <xsl:value-of select="@id"/>
            <xsl:text>&#10;</xsl:text>
        </xsl:for-each>
        <xsl:if test="not($part-types)">
            <xsl:value-of select="$common2"/>
            <xsl:text>&#10;</xsl:text>
        </xsl:if>
    </xsl:for-each>
</xsl:template>

</xsl:stylesheet>

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM