views:

638

answers:

3

Short version:

Could anyone suggest or provide a sample in LINQ to XML for VB, or in an XSLT of how to change one XML element into another (without hardcoding an element-by-element copy of all the unchanged elements)?

Background:

I have an XML file, that I think is properly formed, that contains a root entry that is <collection> and multiple <dvd> elements. Within a DVD, there are Genres and Tags, as shown below. (I cut out a lot of the other elements for simplicity).

What I want to do is turn any <Tag> elements that might be present into an additional <Genre>. For example, in the entry below, I need to add <Genre>Kids</Genre>. (I realize that it is actually the NAME attribute of the TAG element that I'm looking to turn into the GENRE element, but if I could even figure out how to create a new GENRE called "Tag" I'd be much further ahead and could probably puzzle out the rest.)

I've never done anything much with XML. My understanding is that I could use an XSLT transform file and a XSLCompiledTransform or I could use LINQ to XML (I have Visual Basic 9, and would prefer to do it all inside of VB). [I'm sure there are a number of other approaches, too.]

Trouble is, I can't find any examples of XSLT or LINQ syntax that tell me how to turn one element into another. I could write out enough LINQ to copy all of the elements one by one, but there has got to be an easier way than hardcoding a copy of all the elements that don't change! (There has got to be!)

So, if someone who knows could point me to an example or give me a hand with a bit of starter code in LINQ or XSLT, I would be forever grateful (OK, maybe not forever, but at least for a long time!).

Thanks.

Sample XML:

<Collection>
  <DVD>
    <ID>0000502461</ID>
    <Title>Cirque du Soleil: Alegría</Title>
    <Released>2002-05-31</Released>
    <RunningTime>90</RunningTime>
    <Genres>
      <Genre>Family</Genre>
      <Genre>Music</Genre>
    </Genres>
    <Overview>What if anything were possible? What if ...
    </Overview>
    <Notes/>
    <Tags>
      <Tag Name="Kids" FullName="Kids"/>
    </Tags>
  </DVD>
</Collection>
A: 

Are you looking for something like this:

 <xsl:template match="Tag">
    <xsl:element name="Genre">
      <xsl:value-of select="@Name"/>           
    </xsl:element>    
  </xsl:template>
seanb
A: 

You can match any node with node(), like this:

<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="2.0"
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform"&gt;

<!-- Uncomment to remove Tags elements -->
<!-- <xsl:template match="Tags" /> -->

<xsl:template match="Genres">
    <xsl:copy>
     <xsl:apply-templates select="@*|node()" />
     <xsl:for-each select="../Tags/Tag">
      <xsl:element name="Genre">
       <xsl:value-of select="@Name" />
      </xsl:element>
     </xsl:for-each>
    </xsl:copy>
</xsl:template>

<!-- Default rule: Copy node and descend -->
<xsl:template match="@*|node()">
        <xsl:copy>
                <xsl:apply-templates select="@*|node()"/>
        </xsl:copy>
</xsl:template>

</xsl:stylesheet>
phihag
+2  A: 

Using one of the most fundamental and powerful XSLT design patterns: overriding the identity template, one will write this very simple transformation to replace every "Genres" element with a "Topics" element:

<xsl:stylesheet version="1.0"
 xmlns:xsl="http://www.w3.org/1999/XSL/Transform">

 <xsl:output omit-xml-declaration="yes"/>

    <xsl:template match="node()|@*">
      <xsl:copy>
         <xsl:apply-templates select="node()|@*"/>
      </xsl:copy>
    </xsl:template>

    <xsl:template match="Genres">
      <Topics>
         <xsl:apply-templates select="node()|@*"/>
      </Topics>
    </xsl:template>
</xsl:stylesheet>

When applied against the provided source XML document:

<Collection>
    <DVD>
     <ID>0000502461</ID>
     <Title>Cirque du Soleil: Alegría</Title>
     <Released>2002-05-31</Released>
     <RunningTime>90</RunningTime>
     <Genres>
      <Genre>Family</Genre>
      <Genre>Music</Genre>
     </Genres>
     <Overview>What if anything were possible? What if ...    </Overview>
     <Notes/>
     <Tags>
      <Tag Name="Kids" FullName="Kids"/>
     </Tags>
    </DVD>
</Collection>

The wanted result is produced:

<Collection>
    <DVD>
     <ID>0000502461</ID>
     <Title>Cirque du Soleil: Alegría</Title>
     <Released>2002-05-31</Released>
     <RunningTime>90</RunningTime>
     <Topics>
      <Genre>Family</Genre>
      <Genre>Music</Genre>
     </Topics>
     <Overview>What if anything were possible? What if ...    </Overview>
     <Notes/>
     <Tags>
      <Tag Name="Kids" FullName="Kids"/>
     </Tags>
    </DVD>
</Collection>

The first template in the stylesheet is the identity rule. It copies any matched node unchanged and recursively applies templates to its attributes or children. If no other template is present, this template creates identical copy of the source xml document, hence its name.

When there is a more specific template (specifying more specific details for the matched node, such as name and/or other conditions), it is said to "override" the more generic templates. This means that the more specific template is chosen for processing the node.

Using this extremely powerful design pattern it is trivial to implementin just a few lines such processing as:

  1. Delete all nodes that satisfy some condition.
  2. Rename all nodes that satisfy some condition.
  3. Modify the contents of all nodes that satisfy some condition

while copying all other nodes intact.

In our case, the second template is more specific and it gets selected for processing of every element named "Genres". All it does is create an element named "Topics" and inside it apply templates to all of the current node attributes and children.

Finally, the following transformation will add a new "Genre" element to the children of "Genres", for each "Tag" element:

<xsl:stylesheet version="1.0"
 xmlns:xsl="http://www.w3.org/1999/XSL/Transform">

 <xsl:output omit-xml-declaration="yes"/>

    <xsl:template match="node()|@*">
      <xsl:copy>
         <xsl:apply-templates select="node()|@*"/>
      </xsl:copy>
    </xsl:template>

    <xsl:template match="Genres">
      <xsl:copy>
         <xsl:apply-templates select="node()|@*"/>
         <xsl:apply-templates select="../Tags/Tag" mode="Gen"/>
      </xsl:copy>
    </xsl:template>

    <xsl:template match="Tag" mode="Gen">
      <Genre>
        <xsl:value-of select="@Name"/>
      </Genre>
    </xsl:template>
</xsl:stylesheet>

The result is again as required:

<Collection>
    <DVD>
     <ID>0000502461</ID>
     <Title>Cirque du Soleil: Alegría</Title>
     <Released>2002-05-31</Released>
     <RunningTime>90</RunningTime>
     <Genres>
      <Genre>Family</Genre>
      <Genre>Music</Genre>
  <Genre>Kids</Genre>
     </Genres>
     <Overview>What if anything were possible? What if ...    </Overview>
     <Notes/>
     <Tags>
      <Tag Name="Kids" FullName="Kids"/>
     </Tags>
    </DVD>
</Collection>

More code snippets using the "identity rule" pattern can be found here.

Dimitre Novatchev
Nice explanation, but the second stylesheet rule and the result (no difference to input?!) seem wrong
phihag
Please, look carefully. The result is different from the input. The "Genres"element is no longer there and it has been rplaced by a "Topics" element. This is exactly what this question asks to be accomplished: replace a specific element with another element
Dimitre Novatchev
Oh, I saw that the OP wanted also that a new "Genre" be added, corresponding to each "Tag"... :( Done!
Dimitre Novatchev