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(., '
')" />
</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(., '
')" />
</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="'
'" />
<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:
omit-xml-declarations
when the output method is text 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 <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. PartType
when you know at the point where you select it for transformation that you want only its id
attribute xsl:for-each
where you don't need it. Often, you can simply xsl:apply-templates
to the same nodes, instead 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>
</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> </xsl:text>
</xsl:for-each>
<xsl:if test="not($part-types)">
<xsl:value-of select="$common2"/>
<xsl:text> </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.