I have an xml document with a structure like this.
<Document>
<record>
<field name="Dep1FirstName">Frank</field>
<field name="Dep1MiddleName"/>
<field name="Dep1LastName">Billings</field>
<field name="Dep1DoB">1952-01-20</field>
<field name="Dep1Gender"/>
<field name="Dep2Prefix"/>
<field name="Dep2FirstName"/>
<field name="Dep2MiddleName"/>
<field name="Dep2LastName"/>
</record>
<record>
<field name="Date_of_Birth">1978-09-20</field>
<field name="Dep1FirstName"/>
<field name="Dep1MiddleName"/>
<field name="Dep1LastName"/>
<!-- many more -->
</record>
</Document>
The elements (each number representing one dependent) can go up to ten, so I really want to write one template to handle each grouping (number) for the dependents. If there's no data for the group, I won't copy it over (Person may only have two dependents and not ten). In example, I would only be using Dep1. So far, I have come up with something like this:
<xsl:template match="ns:Document">
<div class="container">
<xsl:apply-templates select="ns:Content"/>
</div>
</xsl:template>
<xsl:template match="ns:record">
<div class="page">
<div>
<xsl:apply-templates/>
</div>
</div>
</xsl:template>
<xsl:template match="ns:field[@name='Dep1FirstName' and text()]">
<div class="dependents_info">
<xsl:apply-templates select="../ns:field[contains(@name,'Dep') and contains(@name,'1')" mode="secondary"/>
</div>
</xsl:template>
<!-- make per dependent template (can be up to ten per schema) -->
<xsl:template match="ns:field[contains(@name,'Dep') and contains(@name,'$NUMBER') and contains(@name,'FirstName')]" mode="secondary">
<div class="dependent">
<xsl:value-of select="."/>
<xsl:value-of select="../ns:field[@name='Dep$NUMBERLastName']" />
...
</div>
</xsl:template>
The $NUMBER would need to be updated for each of the 10 (assuming that the dependent existed). Is there a clean way to do this other than writing one template for each number which violates DRY (don't repeat yourself)?
EDIT: I have updated the structure of the source doc with a lot more detail, because the answers to the questions have gone with answers that rely on globals like xsl:key, and thus, the rest of the doc structure is more relevant than I originally thought.
When I see crazy ways of abusing XML like this, my instinct is to first write a transformation that turns it into something sane. That is, turn
<e>
<field name="Dep1FirstName"/>
<field name="Dep1MiddleName"/>
<field name="Dep1LastName"/>
<field name="Dep1DoB"/>
<field name="Dep1Gender"/>
<field name="Dep2Prefix"/>
<field name="Dep2FirstName"/>
<field name="Dep2MiddleName"/>
<field name="Dep2LastName"/>
</e>
into
<e>
<dep nr="1">
<FirstName/>
<MiddleName/>
<LastName/>
<DoB/>
<Gender/
</dep>
<dep nr="2">
<Prefix/>
<FirstName/>
<MiddleName/>
<LastName/>
</dep>
</e>
Once you've done that, everything else is plain sailing.
This of course is a grouping problem, where the grouping key is the 4th character of the name attribute ( substring(@name, 4, 1)
). I don't do XSLT 1.0 grouping for people, it's much easier to download an XSLT 2.0 processor which makes the task trivial. In XSLT 2.0 it's:
<xsl:for-each-group select="field"
group-adjacent="substring(@name, 4, 1)">
<xsl:element name="{substring(@name, 3, 1)}">
<xsl:attribute name="nr" select="{current-grouping-key()}"/>
<xsl:for-each select="current-group()">
<xsl:element name="{substring(@name, 5)}">
<xsl:copy-of select="node()"/>
</xsl:element>
</xsl:for-each>
</xsl:element>
</xsl:for-each-group>
The elements can go up to ten, so I really want to write one template to handle each grouping (number).
I think you mean that you want to write a template with which to process groups of ns:field
elements, where the elements are grouped according to the decimal number that follows 'Dep' in their name
attributes. I furthermore suppose that in your real data, the elements are non-empty. Additionally, from the structure of the stylesheet you presented, I deduce that you are willing to assume that every grouping will contain a field having a name
attribute of the form 'DepXFirstName'.
I think you are focusing a bit too much on the Number. XSLT 1.0 does not have C-style for
loops. On the other hand, it can be pretty flexible about handling cases such as when there is a gap in the group numbers -- maybe even at number 1.
Since you are willing to assume that each group will contain a 'FirstName' element, you can use those as representative elements of each group. You can therefore apply a template to those particular elements that achieves the transformation you want. You can furthermore associate the other related elements via the prefix that precedes 'FirstName'. And in case there is a risk of the elements or groups being presented out of numeric order, yes, you can pick out the number and sort by it.
Here is a stylesheet that does all that:
<?xml version="1.0"?>
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:ns="urn:x:ns">
<xsl:template match = 'ns:root'>
<xsl:if test="ns:field[starts-with(substring-before(@name, 'FirstName'), 'Dep')]">
<div class="dependents_info">
<xsl:for-each select="ns:field[starts-with(substring-before(@name, 'FirstName'), 'Dep')]">
<xsl:sort select="substring-after(substring-before(@name, 'FirstName'), 'Dep')"
data-type="number" />
<xsl:variable name="prefix" select="substring-before(@name, 'FirstName')"/>
<div class="dependent">
<xsl:apply-templates
select="../ns:field[@name = concat($prefix, 'Prefix')]"
mode="name" />
<xsl:apply-templates
select="../ns:field[@name = concat($prefix, 'FirstName')]"
mode="name" />
<xsl:apply-templates
select="../ns:field[@name = concat($prefix, 'MiddleName')]"
mode="name" />
<xsl:apply-templates
select="../ns:field[@name = concat($prefix, 'LastName')]"
mode="name" />
<xsl:apply-templates
select="../ns:field[@name = concat($prefix, 'Gender')]"
mode="paren-name" />
<xsl:apply-templates
select="../ns:field[@name = concat($prefix, 'DoB')]"
mode="name" />
</div>
</xsl:for-each>
</div>
</xsl:if>
</xsl:template>
<xsl:template match="ns:field" mode="name">
<xsl:value-of select='.'/><xsl:text> </xsl:text>
</xsl:template>
<xsl:template match="ns:field" mode="paren-name">(<xsl:value-of select='.'/>)<xsl:text> </xsl:text>
</xsl:template>
</xsl:stylesheet>
Using that stylesheet and this data ...
<?xml version="1.0"?>
<root xmlns="urn:x:ns">
<field name="Dep1MiddleName">Jane</field>
<field name="Dep1LastName">Smith</field>
<field name="Dep1DoB">2/21/64</field>
<field name="Dep1Gender">F</field>
<field name="Dep2Prefix">Mr.</field>
<field name="Dep2FirstName">John</field>
<field name="Dep2LastName">Smith</field>
<field name="Dep1FirstName">Sarah</field>
</root>
... xsltproc produces this output:
<?xml version="1.0"?>
<div class="dependents_info"><div class="dependent">Sarah Jane Smith (F) 2/21/64 </div><div class="dependent">Mr. John Smith </div></div>
The output is not exactly pretty, but you can put in whatever additional markup and formatting you like. Note that:
I use an xsl:if
instead of a separate template to determine whether there are any dependent groups. This is a style choice, but it has the advantage that it works just fine if dependent numbering happens to start at a number different from 1.
I use substring-before()
to pick out the 'DepX' prefix from the 'DepXFirstName' elements that we are assuming can be used as representative members of their groups.
I use starts-with()
rather than contains()
to check the Dep
part. This is more precise.
I pick out the number of each group so as to xsl:sort
on it, but otherwise it is unneeded. I instead form the names of the other members of each group by concatenating the discovered prefix with the appropriate tail. And note that the output demonstrates that the sorting works.
I do use templates to transform the elements of each group, because the example data seem to indicate that you cannot rely on all fields being present in every group. Applying a template to an empty node list does not affect the output tree.
Assuming, you intend grouping on the first four characters of @name for Dep1 , Dep2 , etc. groupings, consider the Muenchian Method grouping in XSLT 1.0:
Input
<root>
<field name="Dep1FirstName"/>
<field name="Dep1MiddleName"/>
<field name="Dep1LastName"/>
<field name="Dep1DoB"/>
<field name="Dep1Gender"/>
<field name="Dep2Prefix"/>
<field name="Dep2FirstName"/>
<field name="Dep2MiddleName"/>
<field name="Dep2LastName"/>
</root>
XSLT Script
<xsl:transform xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:output version="1.0" encoding="UTF-8" indent="yes" omit-xml-declaration="yes" />
<xsl:strip-space elements="*"/>
<xsl:key name="depnum" match="field" use="substring(@name, 1, 4)" />
<xsl:template match="root">
<div class="dependents_info">
<xsl:for-each select="field[generate-id() =
generate-id(key('depnum', substring(@name, 1, 4))[1])]">
<div class="dependent">
<xsl:for-each select="key('depnum', substring(@name, 1, 4))">
<field><xsl:copy-of select="@name"/></field>
</xsl:for-each>
</div>
</xsl:for-each>
</div>
</xsl:template>
</xsl:transform>
Output
<?xml version='1.0' encoding='UTF-8'?>
<div class="dependents_info">
<div class="dependent">
<field name="Dep1FirstName"/>
<field name="Dep1MiddleName"/>
<field name="Dep1LastName"/>
<field name="Dep1DoB"/>
<field name="Dep1Gender"/>
</div>
<div class="dependent">
<field name="Dep2Prefix"/>
<field name="Dep2FirstName"/>
<field name="Dep2MiddleName"/>
<field name="Dep2LastName"/>
</div>
</div>
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.