Sunday, September 01, 2013

PowerShell Reporting With XML– Part 2

In this part I will show how to inject the XSLT into the XML document so it can be directly viewed in a browser.  This is a very simple operation and takes almost no code.

How to inject an XSLT reference into an XML file

.First we generate of load XML into an XML object:

$xmlfile="$pwd\services.xml"
$xml=gwmi win32_service | ConvertTo-Xml
$xml.Save($xmlfile)
. $xmlfile

This should open a browser and display XML assuming the browser you use is capable of this.  All currentversions of IE candispaly native XML.

You should see something like this:

image

Notice that each object is wrapped in a set of tags <Object> and eachproperty is called out in detail.  We only need two things from this.  We need the path to the object and the path to the text node of the property.

To get the objects in a list we can just query for them.  In PowerShell that query looks like this:

$xml.SelectNodes('//Objects/Object').Property|select name, '#text'

This produces a list of property names and values.  In XSLT we will do the same thing but we will do it in the browser using XSLT.

Enabling XSLT in the Browser

Here I the code that enables the XSLT in the browser:

# generate xml of objects
$xmlfile="$pwd\services.xml"
$xml=gwmi win32_service | ConvertTo-Xml
 
# inject an XSLT processing instrinction into the XML
$pi=$xml.CreateProcessingInstruction('xml-stylesheet', 'type="text/xsl" href="services.xsl"')
$xml.InsertBefore($pi,$xml.DocumentElement)
 
# save and launch in default browser
$xml.Save($xmlfile)
. $xmlfile

Of course we don’t yet have a XSL file so we will get only raw text output which is the default for a missing XSL file.

Here is a simple XSL file.  Copy and save it as “services.xsl” in the folder where you are testing this.  Currently the code uses the $pwd variable to find the current folder. Create a new folder so you can find the files more easily.  Make the new folder your current PowerShell folder.

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

This is about the most fundamental XSL we can get for generating HTML.  It has one template and one “apply-templates”  instruction.  Create the file and rerun the code snippet.  YOu can actually just rerun the command to open in the browser:

. $xmlfile

How XSL Loops Through Data

Not much different but we can now demo how the XSL loops through data and generates HTML output.

Now copy this into your “services.xsl” file by replacing all text with this text:

<?xml version="1.0"?>
<xsl:stylesheet version="1.0"
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
 
     <xsl:template match="/">
       <html>
       <body>
           <xsl:apply-templates/>
       </body>
       </html>
     </xsl:template>
     
     <xsl:template match="Objects">
         <xsl:for-each select="Object">
             <p style="color: red;">Hello from Template</p>
         </xsl:for-each>
     </xsl:template>
</xsl:stylesheet> 
 

I have added another template and inserted some output.  The template selects ‘Object’ which says that whenever a tag of that name is found we should apply this template.  Inside the template we have an XSL  “for-each” instruction which says that for each of the selected tags (Object) we should perform the contents of the “for-each” block.  Inside of the “for-each” I have placed a simple HTML tag “<P>”

<p style="color: red;">Hello from for--each</p>

The style is set so we can show that all HTML is legal here.  The output now shows one red line for each object found.

Now this is pretty useless.  We should be outputting something useful so change the <p> line to this:

<p style="color: red;">
        <xsl:value-of select="Property[@Name='DisplayName']"/
</p>

If you make a mistake in the closure the display will default back to listing all text because of a processing error in the transform so be careful to make the change without disrupting the structure.

All tags in XML must be closed.  You cannot ignore this or bad things will happen. If we are injecting HTML into a transform it also has to be closed.

<tagname/>  - self closing
<tagname></tagname> – explicit closure
<tagname>Hello<newtag>…ILLEGAL

Notice that we have now created a paragraph with the DisplayName of the service in red.

Now lets make a table.  Replace the XSL with this:

<?xml version="1.0"?>
<xsl:stylesheet version="1.0"
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
 
     <xsl:template match="/">
       <html>
       <body>
           <xsl:apply-templates/>
       </body>
       </html>
     </xsl:template>
     
     <xsl:template match="Objects">
     
       <table border="1">
         <thead>
         <tr>
           <th>Display Name</th>
         </tr>
         </thead>
 
         <xsl:for-each select="Object">
            <tr>
                <td><xsl:value-of select="Property[@Name='DisplayName']"/></td>
            </tr>
         </xsl:for-each>
         
        </table>
        
     </xsl:template>
</xsl:stylesheet> 
 

Now we have a simple table with one column.  Note that we have injected the header on entry into the template but before we enumerate the “Object” collection.  In the “for-each” we emit the table rows.

Try it:

Now place a few more items of interest into the table.  Be careful. XML and XSL are case sensitive. All tags and all XPath expressions must respect the case of the names.  “displayname” and “DisplayName” are not the same.  You must match the case of the tags and attributes as well as the match case of the values being references.

Every bit of this line is case sensitive. If you change the case of any character the line will either fail or cause a syntax error.

<xsl:value-of select="Property[@Name='DisplayName']"/>

A More Complete Table

<?xml version="1.0"?>
<xsl:stylesheet version="1.0"
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
 
     <xsl:template match="/">
       <html>
       <body>
           <xsl:apply-templates/>
       </body>
       </html>
     </xsl:template>
     
     <xsl:template match="Objects">
     
       <table border="1">
         <thead>
         <tr>
           <th>Display Name</th>
           <th>Started</th>
           <th>Start Mode</th>
           <th>Description</th>
        </tr>
         </thead>
 
         <xsl:for-each select="Object">
            <tr>
                <td><xsl:value-of select="Property[@Name='DisplayName']"/></td>
                <td><xsl:value-of select="Property[@Name='Started']"/></td>
                <td><xsl:value-of select="Property[@Name='StartMode']"/></td>
                <td><xsl:value-of select="Property[@Name='Description']"/></td>
            </tr>
         </xsl:for-each>
         
        </table>
        
     </xsl:template>
</xsl:stylesheet> 
 

Now we have four columns and good information.  Perhaps now is a good time to take a look at the source of this page.  In the browser right click on the page and select “View Source”.  What do you see?  Why is there no HTML?  Can you see the injected processing instruction?

So browsers consume XML natively. They look at it just like it and follow the instructions.  In our case the instructions are to convert the XML for display.

In ‘Part 1’ we generated HTML output from a transform.  In this part we are only generating HTML.  The browser is taking care of the rest.

Adding CSS Style

We can style this table to make it ore readable and prettier by adding one line to our XSLT.

Create a file called services.css.  Add the following content:

body{
    background-color: bisque;
}
 
thead{
    background-color: #9acd32;
}
 
table{
    border-collapse: collapse;
}

Now add this line to the XSL file.

<link href="services.css" rel="stylesheet" type="text/css"/>

Here is the complete XSL:

<?xml version="1.0"?>
<xsl:stylesheet version="1.0"
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
 
     <xsl:template match="/">
       <html>
       <link href="scripts.css" rel="stylesheet" type="text/css"/>
       <body>
           <xsl:apply-templates/>
       </body>
       </html>
     </xsl:template>
     
     <xsl:template match="Objects">
     
       <table border="1">
         <thead>
         <tr>
           <th>Display Name</th>
           <th>Started</th>
           <th>Start Mode</th>
           <th>Description</th>
        </tr>
         </thead>
 
         <xsl:for-each select="Object">
            <tr>
                <td><xsl:value-of select="Property[@Name='DisplayName']"/></td>
                <td><xsl:value-of select="Property[@Name='Started']"/></td>
                <td><xsl:value-of select="Property[@Name='StartMode']"/></td>
                <td><xsl:value-of select="Property[@Name='Description']"/></td>
            </tr>
         </xsl:for-each>
         
        </table>
        
     </xsl:template>
</xsl:stylesheet> 
 

Now you should see the XML with some basic formatting for easier reading.  We can use the CSS to do all manner of custom formatting.

That is enough for this part.  In the next part I will investigate more formatting and layout options as well as using PowerShell to add extra info such as the table name and the page title.

For all links to this series use this tag: PowerShell Reporting(XML)

No comments:

Post a Comment