Converting input XML using XSLT to other XML

2019-02-20 03:18发布

问题:

I am a beginner and want to learn XSLT. I came upon an issue converting an input XML file to another XML file using XSLT.

My input XML file:

<album>
<album_num>hi.hello</album_num>
<album_name>Cocktail</album_name>
</album>
<album>
<album_num>hey.hello</album_num>
<album_name>Mocktail</album_name>
</album>
<album>
<album_num>hey.mello</album_num>
<album_name>Monkeytail</album_name>
</album>
<album>
<album_num>hey.yellow</album_num>
<album_name>Donkeytail</album_name>
</album>
<album>
<album_num>swallow</album_num>
<album_name>abc</album_name>
</album>

I would like to get an output XML file like this:

<album>
<album_num>
<hi>
<hello>cocktail</hello>
</hi>
</album_num>
<album_num>
<hey>
<hello>MockTail</hello>
<mello>Monkeytail</mello>
<yellow>Donkeytail</yellow>
</hey>
</album_num>
<album_num>
<swallow>abc</swallow>
</album_num>
</album>

I tried the first part by creating variables, but had an issue with merging the similar elements under one element. Any code could help me learn.

My code:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="/">
<album>
<xsl:variable name="fstval" select='substring-before(//album/album_num,".")'/>
<xsl:variable name="secval" select='substring-after(//album/album_num,".")'/>
<xsl:variable name="valtoappend" select='//album/album_name'/>
<album_num>
<xsl:element name="{$fstval}">
<xsl:element name="{$secval}">
<xsl:value-of select="$valtoappend"/>
</xsl:element>
</xsl:element>
</xsl:for-each>
</album_num>
</album>
</xsl:template>
</xsl:stylesheet>

回答1:

This transformation:

<xsl:stylesheet version="1.0"
 xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
 xmlns:ext="http://exslt.org/common" exclude-result-prefixes="ext">
 <xsl:output omit-xml-declaration="yes" indent="yes"/>

 <xsl:key name="kAlbumByChildName" match="album" use="name(album_num/*[1])"/>

 <xsl:template match="/">
  <xsl:variable name="vrtfPass1">
    <xsl:apply-templates/>
  </xsl:variable>

  <xsl:apply-templates mode="pass2" select=
  "ext:node-set($vrtfPass1)/*
           [generate-id()
           =
            generate-id(key('kAlbumByChildName', name(album_num/*[1]))[1])
           ]
  "/>
 </xsl:template>

 <xsl:template match="album">
  <album>
   <album_num>
       <xsl:element name="{substring-before(album_num, '.')}">
         <xsl:element name="{substring-after(album_num, '.')}">
           <xsl:value-of select="album_name"/>
         </xsl:element>
       </xsl:element>
   </album_num>
  </album>
 </xsl:template>

 <xsl:template match="album" mode="pass2">
  <album>
   <album_num>
        <xsl:apply-templates select="*/*[1]" mode="pass2"/>
    </album_num>
  </album>
 </xsl:template>

 <xsl:template match="album_num/*" mode="pass2">
  <xsl:copy>
   <xsl:copy-of select="key('kAlbumByChildName', name())/*/*/*"/>
  </xsl:copy>
 </xsl:template>
</xsl:stylesheet>

when applied on the following document (the provided XML fragment wrapped in a single top element to make it a well-formed XML document):

<t>
    <album>
        <album_num>hi.hello</album_num>
        <album_name>Cocktail</album_name>
    </album>
    <album>
        <album_num>hey.hello</album_num>
        <album_name>Mocktail</album_name>
    </album>
    <album>
        <album_num>hey.mello</album_num>
        <album_name>Monkeytail</album_name>
    </album>
    <album>
        <album_num>hey.yellow</album_num>
        <album_name>Donkeytail</album_name>
    </album>
</t>

produces the wanted, correct result:

<album>
   <album_num>
      <hi>
         <hello>Cocktail</hello>
      </hi>
   </album_num>
</album>
<album>
   <album_num>
      <hey>
         <hello>Mocktail</hello>
         <mello>Monkeytail</mello>
         <yellow>Donkeytail</yellow>
      </hey>
   </album_num>
</album>

Explanation:

This is a two-pass transformation. The result of the first pass is:

<album>
   <album_num>
      <hi>
         <hello>Cocktail</hello>
      </hi>
   </album_num>
</album>

<album>
   <album_num>
      <hey>
         <hello>Mocktail</hello>
      </hey>
   </album_num>
</album>

<album>
   <album_num>
      <hey>
         <mello>Monkeytail</mello>
      </hey>
   </album_num>
</album>

<album>
   <album_num>
      <hey>
         <yellow>Donkeytail</yellow>
      </hey>
   </album_num>
</album>

The second pass is a standard Muenchian grouping.

Update:

Two days after asking this question and receiving a correct answer, the OP has changed the source XML document and wanted result.

This slightly modified transformation:

<xsl:stylesheet version="1.0"
     xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
     xmlns:ext="http://exslt.org/common" exclude-result-prefixes="ext">
     <xsl:output omit-xml-declaration="yes" indent="yes"/>

     <xsl:key name="kAlbumByChildName" match="album" use="name(album_num/*[1])"/>

     <xsl:template match="/">
      <xsl:variable name="vrtfPass1">
        <xsl:apply-templates/>
      </xsl:variable>

      <xsl:apply-templates mode="pass2" select=
      "ext:node-set($vrtfPass1)/*
               [generate-id()
               =
                generate-id(key('kAlbumByChildName', name(album_num/*[1]))[1])
               or
                not(album_num/*)
               ]
      "/>

     </xsl:template>

     <xsl:template match="album[contains(album_num, '.')]">
      <album>
       <album_num>
           <xsl:element name="{substring-before(album_num, '.')}">
             <xsl:element name="{substring-after(album_num, '.')}">
               <xsl:value-of select="album_name"/>
             </xsl:element>
           </xsl:element>
       </album_num>
      </album>
     </xsl:template>

     <xsl:template match="album">
      <album>
       <album_num>
             <xsl:element name="{album_num}">
               <xsl:value-of select="album_name"/>
             </xsl:element>
       </album_num>
      </album>
     </xsl:template>

     <xsl:template match="album" mode="pass2">
      <album>
       <album_num>
            <xsl:apply-templates select="*/*[1]" mode="pass2"/>
        </album_num>
      </album>
     </xsl:template>

     <xsl:template match="album_num/*" mode="pass2">
      <xsl:copy>
       <xsl:copy-of select="self::*[not(*)]/text()|key('kAlbumByChildName', name())/*/*/*"/>
      </xsl:copy>
     </xsl:template>
</xsl:stylesheet>

when applied on the new version of the XML document:

<t>
    <album>
        <album_num>hi.hello</album_num>
        <album_name>Cocktail</album_name>
    </album>
    <album>
        <album_num>hey.hello</album_num>
        <album_name>Mocktail</album_name>
    </album>
    <album>
        <album_num>hey.mello</album_num>
        <album_name>Monkeytail</album_name>
    </album>
    <album>
        <album_num>hey.yellow</album_num>
        <album_name>Donkeytail</album_name>
    </album>
    <album>
        <album_num>swallow</album_num>
        <album_name>abc</album_name>
    </album>
</t>

produces the new wanted result:

<album>
   <album_num>
      <hi>
         <hello>Cocktail</hello>
      </hi>
   </album_num>
</album>
<album>
   <album_num>
      <hey>
         <hello>Mocktail</hello>
         <mello>Monkeytail</mello>
         <yellow>Donkeytail</yellow>
      </hey>
   </album_num>
</album>
<album>
   <album_num>
      <swallow>abc</swallow>
   </album_num>
</album>