简体   繁体   中英

XSLT - how to get the name value of a node?

We have (perhaps unusually formatted) XML files that (very simplified) look like this:

<Contacts>
    <Contact>
        <Private>
            <Name>John Doe</Name>
            <Address>Somewhere1</Address>
            ...
        </Private>
    </Contact>
    <Contact>
        <Business>
            <Name>John Business</Name>
            <Address>Somewhere2</Address>
            <BusinessAddress>Somewhere3</BusinessAddress>
            ...
        </Business>
    </Contact>
    ...
</Contacts>

In reality, we have several dozens of different types of "Contact" nodes with many more attributes, several of which however are common between all types...

We currently use a stylesheet to format the XML file that repeats the formatting for all common attributes for each type of "Contact" node:

<!-- ... loads of prologue code -->
<xsl:for-each select="Contacts/Contact/.">
    <xsl:choose>
    <xsl:when test="Private">
        <!--    code to format each and every attribute of the current 
                "Contact" element, e.g.: -->
        <td class="header">Private Contact</td>
        <td class="detail"><xsl:value-of select="./Name"/></td>
        <!--   ...  -->
    </xsl:when>

    <xsl:when test="Business">
        <!--    code to format each and every attribute of the current 
                "Contact" element, e.g.: -->
        <td class="header">Business Contact/<td>
        <td class="detail"><xsl:value-of select="./Name"/></td>
        <!--   ...  -->
    </xsl:when>

    <!--   ...  -->

    </xsl:choose>
</xsl:for-each>

But we're getting more and more <Contact> types with more and more attributes, resulting in a mile-long stylesheet, so I want to simplify the stylesheet as follows:

<!-- ... loads of prologue code -->
<xsl:for-each select="Contacts/Contact/.">
    <!-- loads of formatting stuff common to each "Contact" type, but with 
         some wording that's "Contact" type dependant, e.g. -->
    <td class="header">**????** Contact</td>
    <td class="detail"><xsl:value-of select="./Name"/></td>
    <xsl:choose>
    <xsl:when **????** = "Private">
        <xsl:variable name="contactWhen">off</xsl:variable>
    </xsl:when>
    <xsl:when **????** = "Business">
        <xsl:variable name="contactWhen">office</xsl:variable>
    </xsl:when>
    <!-- ...  -->
    </xsl:choose>
     <td class="header">Contact/<td>
     <td class="detail">Only during $contactWhen hours</td>

    <xsl:choose>
    <xsl:when test="Private">
       <!-- code to format attributes specific for the current "Contact" -->
    </xsl:when>

    <xsl:when test="Business">
       <!-- code to format attributes specific for the current "Contact" -->
    </xsl:when>

    <!--    ...  -->

    </xsl:choose>
</xsl:for-each>

My excuses if there are any typo's or syntax errors in above code samples, but I'm not very intimate to XSLT syntax and most of above code was hand-crafted for the sake of the example...

My problem is that I'm not able to get the name value of the node under Contact , in this example Private or Business . I've tried using all variations I could think of, both of value-of select= and of simply select= (ao "", ".", "./." , Contacts/Contact , Contacts/Contact/. , [local-]name() and even Field[@name='.'] ).

Some attempts produce errors, some result in an empty string, some return the name of the parent node, ie Contact , and some return the values of all subordinate attributes (without attribute names though) as a single string... :-(

What should I code for **????** to test for the name value of the current node, and eventually assign some variable(s) a value based on the results of this test?

Thanks for any assistance,

Juul

Hi,

Thanks for the valuable input and I'll definitely consider converting this and some other similar) stylesheet(s) to use templates, but for now I'm under pressure to deliver this project.

I tried all 3 suggestions in the 'for-each' loop and just before the 'choose', but all 3 fail:

<td class="content2" colspan="2"><xsl:value-of select="ancestor::Record/child::*[1]name"/></td>

fails with XML error: expected 'EOF' found 'name'

<td class="content2" colspan="2"><xsl:value-of select="name()"/></td>

returns "Contact", not the name of the first child name

<xsl:variable name="contactType">
<xsl:choose>
    <xsl:when test="Private">Private</xsl:when>
    <xsl:when test="Business">Business</xsl:when>
</xsl:choose>
<td class="content2" colspan="2"><xsl:value-of select="$contactType"/></td>
</xsl:variable>

fails with XML error: reference to variable 'contactType' cannot be resolved. The variable may not be defined or it may not be in scope.

Can this behaviour be caused by having these constructs in inline code instead of in a template.

Please advise,

Juul

<xsl:output method="xml" indent="yes"/>
    <xsl:template match="/">
        <xsl:for-each select="Contacts/Contact">
            <xsl:for-each select="child::*[1]">
                <td class="header"><xsl:value-of select="name()"/><xsl:text> Contact</xsl:text></td>
                <td class="detail"><xsl:value-of select="ancestor::Contact/child::*[1]/Name"/></td>
            </xsl:for-each>
        </xsl:for-each>
    </xsl:template>
Try It

change your variable to:

<xsl:variable name="contactWhen">
    <xsl:choose>
        <xsl:when test="Private">off</xsl:when>
        <xsl:when test="Business">office</xsl:when>
    </xsl:choose>
</xsl:variable>

The node name of the first child of <Contact> can be accessed using the name() function. However since there are conditions to check the node name and then do something else viz. populating different value in $contactWhen or formatting attributes based on the type of contact, you will have to use the <xsl:choose><xsl:when test=""></xsl:when></xsl:choose> conditions for every unique contact.

There are different approaches to achieve the desired output and below is a template approach. The below template matches the first child of <Contact> and processes the data.

<xsl:template match="Contact/*[1]">
    <!-- fetching node name -->
    <xsl:variable name="nodeName" select="name()" />
    <td class="header"><xsl:value-of select="concat($nodeName, ' Contact')" /></td>
    <td class="detail"><xsl:value-of select="Name" /></td>

    <!-- contact hours -->
    <xsl:variable name="contactWhen">
        <xsl:choose>
            <xsl:when test="$nodeName = 'Private'">off</xsl:when>
            <xsl:when test="$nodeName = 'Business'">office</xsl:when>
        </xsl:choose>
    </xsl:variable>
    <td class="header">Contact</td>
    <td class="detail"><xsl:value-of select="concat('Only during ', $contactWhen, ' hours')" /></td>

    <!-- adding contact specific nodes and attributes -->
    <xsl:choose>
        <xsl:when test="$nodeName = 'Private'">
            <!-- code to format attributes specific for the "Private" Contact -->
            <td class="{$nodeName}">This is a Private Contact</td>
        </xsl:when>
        <xsl:when test="$nodeName = 'Business'">
            <!-- code to format attributes specific for the "Business" Contact -->
            <td class="{$nodeName}">This is a Business Contact</td>
        </xsl:when>
    </xsl:choose>
</xsl:template>

The output will look something like

<td class="header">Private Contact</td>
<td class="detail">John Doe</td>
<td class="header">Contact</td>
<td class="detail">Only during off hours</td>
<td class="Private">This is a Private Contact</td>

<td class="header">Business Contact</td>
<td class="detail">John Business</td>
<td class="header">Contact</td>
<td class="detail">Only during office hours</td>
<td class="Business">This is a Business Contact</td>

For improving code maintainability, the <xsl:choose> code snippets can be moved to different templates and then using <xsl:call-template> they can be invoked for processing. This will modularize the code and make it less cluttered.

Using xsl:choose to switch on element names is a bad code smell in XSLT: this is what template rules are for. My first step to improve this code would be to replace the code

<xsl:for-each select="Contacts/Contact/.">
    <xsl:choose>
    <xsl:when test="Private">
     ...
    <xsl:when test="Business">

with

<xsl:apply-templates select="Contacts/Contact"/>

and

<xsl:template match="Contact[Private]">
  ...
</xsl:template>

<xsl:template match="Contact[Business]">
  ...
</xsl:template>

etc.

This doesn't immediately solve your problem of reusing code at the next level down. That happens when you start to do apply-templates at that level.

<xsl:template match="Contact[Private]">
  <td class="header">Private Contact</td>
  <xsl:apply-templates select="Private/*"/>
</xsl:template>

<xsl:template match="Name | Address">
  <td class="detail"><xsl:value-of select="."/></td>
</xsl:template> 

Template rules are the way XSLT was designed to be used. This example is a great illustration of the benefits you get by using them properly.

I found the issue... Joel's answer from 2 days ago actually does the trick; I just had the reference to the variable in the declaration itself, while the reference(s) must be made AFTER the full declaration, as follows:

<xsl:variable name="contactType">
<xsl:choose>
    <xsl:when test="Private">Private</xsl:when>
    <xsl:when test="Business">Business</xsl:when>
</xsl:choose>
</xsl:variable>
<td class="content2" colspan="2"><xsl:value-of select="$contactType"/></td>

actually adds "Private" or "Business" to the formatted XML :-)

Thanks to all who contributed, also trying to improve my limited XSLT skills,

Juul

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