Scoubi
Scoubi

Reputation: 35

XLST + XML -> Word <field name="foo">Value</foo>

I have a XML produce with MySQL Query Browser. I'm trying to apply a XSLT to output the result into Word tables. One table for each record.

Here's a sample of my XML

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE ROOT SYSTEM "Nessus.dtd">
<ROOT>
    <row>
      <field name="Niveau">Critique</field>
      <field name="Name">Apache 2.2 &lt; 2.2.15 Multiple Vulnerabilities</field>
    </row>
    <row>
      <field name="Niveau">Critique</field>
      <field name="VulnName">Microsoft Windows 2000 Unsupported Installation Detection</field>
    </row>
    <row>
      <field name="Niveau">Haute</field>
      <field name="VulnName">CGI Generic SQL Injection</field>
    </row>
</ROOT>

For the XLST I've already found out that I need to do a for-each select

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

    <xsl:output method="xml" indent="yes"/>
    <xsl:template match="/">
<xsl:for-each select="ROOT/row">
  Niveau : <xsl:value-of select="????"/>
  Name   : <xsl:value-of select="????"/>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>

When I do this loop I see the same number of empty table as there is <row></row> in my file.

But I haven't found the way to make the right "value-of select=". I've try the following without luck.

<xsl:value-of select="@name"/>
<xsl:value-of select="name"/>
<xsl:value-of select="@row/name"/>
<xsl:value-of select="row/@name"/>
<xsl:value-of select="@ROOT/row/name"/>

And a few other that I can't remember. Any idea what I need to craft the request to get the value in my resulting file?

I've just tried with :

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

    <xsl:output method="xml" indent="yes"/>
    <xsl:template match="/">
<xsl:for-each select="ROOT/row">
  Niveau : <xsl:value-of select="field/@Niveau"/>
  Name   : <xsl:value-of select="field/@name"/>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>

And it output this :

NIVEAU :  
NAME   : name

NIVEAU :  
NAME   : Niveau

NIVEAU :  
NAME   : Niveau

I would like this output :

NIVEAU : Critique
NAME   : Apache 2.2 &lt; 2.2.15 Multiple Vulnerabilities

NIVEAU : Critique
NAME   : Microsoft Windows 2000 Unsupported Installation Detection

NIVEAU : Haute
NAME   : CGI Generic SQL Injection

Any help would be appreciated.

Thank you.

UPDATE

Now with this XSLT

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

  <xsl:output method="text"/>

  <xsl:strip-space elements="*"/>

  <xsl:template match="row">
    <xsl:text>NIVEAU : </xsl:text>
    <xsl:value-of select="field[@name = 'Niveau']"/>
    <xsl:text>&#xa;</xsl:text>
    <xsl:text>NAME   : </xsl:text>
    <xsl:value-of select="field[@name = 'Name']"/>
    <xsl:text>&#xa;&#xa;</xsl:text>
  </xsl:template>

</xsl:stylesheet>

I get this output :

<?xml version="1.0"?>
NIVEAU : 
NAME   : Apache 2.2 &lt; 2.2.15 Multiple Vulnerabilities

NIVEAU : Critique
NAME   : Microsoft Windows 2000 Unsupported Installation Detection

NIVEAU : Haute
NAME   : CGI Generic SQL Injection

As you can see the first field is empty. I could honestly live with that and fill it manually but if you see why this is happenning I'd be very happy :)

UPDATE

Using <xsl:value-of select="field[@name = 'foo']"/> gave me the value I wanted. I kept the for-each as it was easier to use (for me) inside a MS Word template.

Upvotes: 2

Views: 1340

Answers (3)

Francis Avila
Francis Avila

Reputation: 31641

for-each is generally code smell in XSLT. You most likely want a template, not a for-each loop:

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:template match="/">
    <root>
        <xsl:apply-templates/>
    </root>
</xsl:template>

<xsl:template match="field">
<xsl:value-of select="@name"/> : <xsl:value-of select="."/>
</xsl:template>

</xsl:stylesheet>

This template will produce the following output:

<root>
    
      Niveau : Critique
      name : Apache 2.2 &lt; 2.2.15 Multiple Vulnerabilities
    
    
      Niveau : Critique
      name : Microsoft Windows 2000 Unsupported Installation Detection
    
    
      Niveau : Haute
      name : CGI Generic SQL Injection
    
</root>

XSLT was designed for this pattern of use--many xsl:templates matching a small part of the source and applying other templates recursively. The most important and common template in this pattern is the identity template, which copies output. This is a good tutorial on XSLT coding patterns that you should read.

Update

Below is a complete solution that will produce text output (since that is what you seem to want, not XML output). I'm not sure xsl is the best language for this, but it works....

Notice that the only place we use for-each is for sorting to determine the length of the longest name. We use templates and pattern-matching selects to make all other loops implicit.

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
  <xsl:output method="text"/>
  <xsl:strip-space elements="*"/>

  <xsl:variable name="newline"><xsl:text>
</xsl:text></xsl:variable>

  <!-- determine the maximum length of the "name" field so we can pad with spaces for all shorter items -->
  <xsl:variable name="max_name_len">
    <xsl:for-each select="ROOT/row/field/@name">
      <xsl:sort select="string-length(.)" data-type="number" order="descending"/>
      <xsl:if test="position() = 1">
        <xsl:value-of select="string-length(.)"/>
      </xsl:if>
    </xsl:for-each>
  </xsl:variable>

  <!-- for each row, apply templates then add a blank line -->
  <xsl:template match="row">
    <xsl:apply-templates/>
    <xsl:value-of select="$newline"/>
  </xsl:template>

  <!-- for each field, apply template to name and add value, followed by a newline -->
  <xsl:template match="field">
    <xsl:apply-templates select="@name"/> : <xsl:value-of select="concat(., $newline)"/>
  </xsl:template>


  <!-- for each name, uppercase and pad with spaces to the right -->
  <xsl:template match="field/@name">
    <xsl:call-template name="padright">
      <xsl:with-param name="text">
        <xsl:call-template name="toupper">
          <xsl:with-param name="text" select="."/>
        </xsl:call-template>
      </xsl:with-param>
      <xsl:with-param name="len" select="$max_name_len"/>
    </xsl:call-template>
  </xsl:template>


  <!-- Utility function: uppercase a string -->
  <xsl:template name="toupper">
    <xsl:param name="text"/>
    <xsl:value-of select="translate($text, 'abcdefghijklmnopqrstuvwxyz', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ')"/>
  </xsl:template>

  <!-- Utility function: pad a string to desired len with spaces on the right -->
  <!-- uses a recursive solution -->
  <xsl:template name="padright">
    <xsl:param name="text"/>
    <xsl:param name="len"/>
    <xsl:choose>
      <xsl:when test="string-length($text) &lt; $len">
        <xsl:call-template name="padright">
          <xsl:with-param name="text" select="concat($text, ' ')"/>
          <xsl:with-param name="len" select="$len"/>
        </xsl:call-template>
      </xsl:when>
      <xsl:otherwise>
        <xsl:value-of select="$text"/>
      </xsl:otherwise>
    </xsl:choose>
  </xsl:template>

</xsl:stylesheet>

This stylesheet produces the following output:

NIVEAU : Critique
NAME   : Apache 2.2 < 2.2.15 Multiple Vulnerabilities

NIVEAU : Critique
NAME   : Microsoft Windows 2000 Unsupported Installation Detection

NIVEAU : Haute
NAME   : CGI Generic SQL Injection

Upvotes: 3

nine9ths
nine9ths

Reputation: 795

This stylesheet will give you exactly your desired output

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

  <xsl:output method="text"/>

  <xsl:strip-space elements="*"/>

  <xsl:template match="row">
    <xsl:text>NIVEAU : </xsl:text>
    <xsl:value-of select="field[@name eq 'Niveau']"/>
    <xsl:text>&#xa;</xsl:text>
    <xsl:text>NAME   : </xsl:text>
    <xsl:value-of select="field[@name eq 'VulnName']"/>
    <xsl:text>&#xa;&#xa;</xsl:text>
  </xsl:template>

</xsl:stylesheet>

This is very specific to your input xml and output text, for example any <field> children of <row> other than 'Niveau' or 'VulnName' will be dropped from your report. A more generic solution could look like this:

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

  <xsl:output method="text"/>

  <xsl:template match="field">
    <xsl:value-of select="concat(upper-case(@name),': ',.)"/>
  </xsl:template>

</xsl:stylesheet>

This solution though doesn't exactly match the whitespace formatting in your desired output, but it does capture all possible fields.

Upvotes: 0

Dabbler
Dabbler

Reputation: 9873

When you do <xsl:for-each select="ROOT/row">, the current context inside the loop is the rowelement. So in order to access the field name, you need to write, for example, <xsl:value-of select="field/@name"/>.

Since your XML contains several fields, you will still have to extend your XSLT file somewhat to iterate the fields, or (as Francis Avila suggested) write a template. Both methods are ok, though. The technical terms for the two approaches are "pull" and "push", respectively.

And of course, since you ultimately want to generate a word table, you will have to generate w:tr and w:tc elements, etc.

Upvotes: 1

Related Questions