简体   繁体   中英

Using XSLT 1.0, how do I write one template to handle numbered attributes from a flat XML record?

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.

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