Calendar

<<  October 2017  >>
MonTueWedThuFriSatSun
2526272829301
2345678
9101112131415
16171819202122
23242526272829
303112345

View posts in large calendar

RecentComments

None

 
 
     
 

One would think this should be easy. Just put Powershell into the Language of an msxsl:script element. But it doesn't work :-D

ScriptingLanguageNotSupported

Pity.  I rooted around and the language attribute can be any of the CodeDomProviders available on the system.

PS> [System.CodeDom.Compiler.CodeDomProvider]::GetAllCompilerInfo()

CodeDomProviderType IsCodeDomProviderTypeValid
------------------- --------------------------
Microsoft.CSharp.CSharpCodeProvider True
Microsoft.VisualBasic.VBCodeProvider True
Microsoft.JScript.JScriptCodeProvider True
Microsoft.VisualC.CppCodeProvider True

According to thisthere won't be one for Powershell soon. Never mind, I still want to use Powershell from Xslt. So I tried this.

function T-AddChecksum{
    param ($inxml)
    begin{
       . pslib:\xml\invoke-transform.ps1
       [xml]$xslt = @'
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:msxsl="urn:schemas-microsoft-com:xslt"
xmlns:cjb="cjb"
>

  <xsl:template match="node()|@*">
    <xsl:copy>
      <xsl:apply-templates select="@*|node()"/>
    </xsl:copy>
  </xsl:template>
  <xsl:template match="*[local-name()='file']">
    <xsl:variable name="fname"><xsl:call-template name="get-path" /></xsl:variable>
    <xsl:copy>
      <xsl:apply-templates select="@*|node()"/>
      <xsl:attribute name="Checksum"><xsl:value-of select="cjb:GetChecksum(string($fname))" /></xsl:attribute>
    </xsl:copy>
  </xsl:template>
  <xsl:template name="get-path">
    <xsl:for-each select="ancestor-or-self::*[not(@Root)]">
      <xsl:value-of select="@Base" /><xsl:text>\</xsl:text><xsl:value-of select="@Name" />
    </xsl:for-each>
  </xsl:template>
</xsl:stylesheet>
'@

$func = @'
    function GetChecksum([string]$file){
        $stream = [System.IO.File]::OpenRead($file)
        $sha256 = new-object System.Security.Cryptography.SHA256Managed
        $checksum = $sha256.ComputeHash($stream)
        [System.BitConverter]::ToString($checksum).Replace("-", [system.String]::Empty)
    }
'@
       $pso = new-object psobject
       $pso = add-member -inputobject $pso -membertype scriptmethod -name GetChecksum -value {$func} -PassThru
    }
    process{
        if ($_ -is [xml]){
            [xml](invoke-transform -inxml $_ -inxsl $xslt -extensionobjects @{"cjb"=$pso})
        }
    }
    end{
        if ($inxml -is [xml]){
            [xml](invoke-transform -inxml $inxml -inxsl $xslt -extensionobjects @{"cjb"=$pso})
        }
    }
}

But that doesn't work. It is so long since I wrote it I can't remember if it worked, worked under version 1.0, worked in the CTP, seemed like a good idea that should work but didn't or it didn't work at all. Anyway it still looks like a good idea to me.

This is the object created with C# in the previous example.

PS > $cs|gm
TypeName: Checksum
Name MemberType Definition
---- ---------- ----------
Equals Method bool Equals(System.Object obj)
GetChecksum Method string GetChecksum(string file)
GetHashCode Method int GetHashCode()
GetType Method type GetType()
ToString Method string ToString()

This is the Powershell object created with PSObject

PS D:\powershell\temp> $pso|gm
TypeName: System.Management.Automation.PSCustomObject
Name MemberType Definition
---- ---------- ----------
Equals Method bool Equals(System.Object obj)
GetHashCode Method int GetHashCode()
GetType Method type GetType()
ToString Method string ToString()
GetChecksum ScriptMethod System.Object GetChecksum();

So it is not a valid Xslt extension object?

Right, so we can call C# from Xslt via an extension object and we can call Powershell from C# so all we have to do is write a C# wrapper around the Powershell script and we can call Powershell from Xslt.

function T-AddChecksum{
    param ($inxml)
    begin{
        . pslib:\xml\invoke-transform.ps1
        [xml]$xslt = @'
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:msxsl="urn:schemas-microsoft-com:xslt"
xmlns:cjb="cjb"
>
  <xsl:template match="node()|@*">
    <xsl:copy>
      <xsl:apply-templates select="@*|node()"/>
    </xsl:copy>
  </xsl:template>
  <xsl:template match="*[local-name()='file']">
    <xsl:variable name="fname"><xsl:call-template name="get-path" /></xsl:variable>
    <xsl:copy>
      <xsl:apply-templates select="@*|node()"/>
      <xsl:attribute name="Checksum"><xsl:value-of select="cjb:GetChecksum(string($fname))" /></xsl:attribute>
    </xsl:copy>
  </xsl:template>
  <xsl:template name="get-path">
    <xsl:for-each select="ancestor-or-self::*[not(@Root)]">
      <xsl:value-of select="@Parent" /><xsl:text>\</xsl:text><xsl:value-of select="@Name" />
    </xsl:for-each>
  </xsl:template>
</xsl:stylesheet>
'@
        $code = @'
using System;
using System.Management.Automation;

public partial class PSCaller
{
    ScriptBlock _script;

    public PSCaller(ScriptBlock script) { Script = script; }
    public ScriptBlock Script
    {
        get { return _script; }
        set { _script = value; }
    }
    public String GetChecksum(String file)
    {
        try {
            return (string)Script.Invoke(file)[0].BaseObject;
        } catch(Exception ex) {
            throw new InvalidOperationException("Script failed***!", ex);
        }
    }
}
'@
       Add-Type -TypeDefinition $code
       $psco = new-object PSCaller({
            $file = $args[0]
            $stream = [System.IO.File]::OpenRead($file)
            $sha256 = new-object System.Security.Cryptography.SHA256Managed
            $checksum = $sha256.ComputeHash($stream)
            [System.BitConverter]::ToString($checksum).Replace("-", [system.String]::Empty)
       })
    }
    process{
        [xml](invoke-transform -inxml $_ -inxsl $xslt -extensionobjects @{"cjb"=$psco})
    }
    end{
        [xml](invoke-transform -inxml $inxml -inxsl $xslt -extensionobjects @{"cjb"=$psco})
    }
}

This works but it is a bit clunky. The name and signature 'String GetChecksum(String file)' is in the C# script and is seperate from the actual code that is in the Powershell script. A right mashup :-)

What we need is a way of describing the signature and Powershell script in one place. I started looking at DSLs. Here is a simple class/method DSL

$dsl = {
    psclass public PSCaller{
        method String GetChecksum([String]){
            $file = $args[0]
            $stream = [System.IO.File]::OpenRead($file)
            $sha256 = new-object System.Security.Cryptography.SHA256Managed
            $checksum = $sha256.ComputeHash($stream)
            [System.BitConverter]::ToString($checksum).Replace("-", [system.String]::Empty)
        }
    }
}

I was using 'class' as the class keyword but it is now a Powershell reserved word so I am using 'psclass'. It can handle multiple methods with multiple arguments and the arguments can be any type i.e. [System.Xml.XmlElement]. This example code defines 1 method with the signature 'String GetChecksum([String])' followed by the script itself. It produces the following C#

using System;
using System.Collections;
using System.Management.Automation;
using System.Xml;

public class PSCaller
{
    ScriptBlock script0;

    public PSCaller(ScriptBlock Script0)
    {
        script0 = Script0;
    }
    public String GetChecksum(string p0)
    {
        ScriptBlock Script = script0;
        try
        {
            return (String)Script.Invoke(p0)[0].BaseObject;
        }
        catch (Exception ex)
        {
            throw new InvalidOperationException("Script failed***!", ex);
        }
    }
}

and an array of ScriptBlocks. It then compiles the code and passes the array of ScriptBlocks to the new object constructor.

[string]$code = &$dsl
Add-Type -TypeDefinition $code -ReferencedAssemblies System.xml
$NewObject = new-object PSCaller($scriptArray)

This is what the object looks like.

PS > $newobject|gm
TypeName: PSCaller2
Name MemberType Definition
---- ---------- ----------
Equals Method bool Equals(System.Object obj)
GetChecksum Method string GetChecksum(string p0)
GetDifferentChecksum Method string GetDifferentChecksum(string p0)
GetHashCode Method int GetHashCode()
GetType Method type GetType()
ToString Method string ToString()

So this is what a Translet might look like

function T-AddChecksum{
    param ($inxml)
    begin{
       . pslib:\xml\invoke-transform.ps1
       . pslib:\xml\Create-ObjectFromDSL.ps1
        [xml]$xslt = @'
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:msxsl="urn:schemas-microsoft-com:xslt"
xmlns:cjb="cjb"
>
  <xsl:template match="node()|@*">
    <xsl:copy>
      <xsl:apply-templates select="@*|node()"/>
    </xsl:copy>
  </xsl:template>
  <xsl:template match="*[local-name()='file']">
    <xsl:variable name="fname"><xsl:call-template name="get-path" /></xsl:variable>
    <xsl:copy>
      <xsl:apply-templates select="@*|node()"/>
      <xsl:attribute name="Checksum"><xsl:value-of select="cjb:GetChecksum(string($fname))" /></xsl:attribute>
    </xsl:copy>
  </xsl:template>
  <xsl:template name="get-path">
    <xsl:for-each select="ancestor-or-self::*[not(@Root)]">
      <xsl:value-of select="@Parent" /><xsl:text>\</xsl:text><xsl:value-of select="@Name" />
    </xsl:for-each>
  </xsl:template>
</xsl:stylesheet>
'@
       
        $dsl = {
            psclass public PSCaller{
                method String GetChecksum([String]){
                    $file = $args[0]
                    $stream = [System.IO.File]::OpenRead($file)
                    $sha256 = new-object System.Security.Cryptography.SHA256Managed
                    $checksum = $sha256.ComputeHash($stream)
                    [System.BitConverter]::ToString($checksum).Replace("-", [system.String]::Empty)
                }
                method String GetDifferentChecksum([String]){
                    $file = $args[0] #different
                    $stream = [System.IO.File]::OpenRead($file)
                    $sha256 = new-object System.Security.Cryptography.SHA256Managed
                    $checksum = $sha256.ComputeHash($stream)
                    [System.BitConverter]::ToString($checksum).Replace("-", [system.String]::Empty)
                }
            }
        }
        $psco = Create-ObjectFromDSL $dsl
    }
    process{
        if ($_ -is [xml]){
            [xml](invoke-transform -inxml $_ -inxsl $xslt -extensionobjects @{"cjb"=$psco})
        }
    }
    end{
        if ($inxml -is [xml]){
            [xml](invoke-transform -inxml $inxml -inxsl $xslt -extensionobjects @{"cjb"=$psco})
        }
    }
}

To use it you need the Create-ObjectFromDSL.ps1 script. Here it is

Create-ObjectFromDSL.zip (953.00 bytes)

Why you would want to do this and what you would use it for is up to you. Most people would use Java, JavaScript or C# to extend Xslt. In most cases I would generate Xml from the things that Powershell can do well and pass that into a transform. But this just shows that is is partly possible to use Powershell as an Xslt scripting language and it adds another arrow to your quiver.

Since we have to do the Create-ObjectFromDSL step anyway we could change the Xslt to something like this

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:cjbxsl
="urn:schemas-bayes-co-uk:xslt"
xmlns:ucjb
="urn:schemas-bayes-co-uk:extension-object" version="1.0">
   
<cjbxsl:script language="Powershell" implements-prefix="ucjb">
       
<cjbxsl:method name="GetChecksum" returns="string" args="string">
            <![CDATA[
                $file = $args[0]
                $stream = [System.IO.File]::OpenRead($file)
                $sha256 = new-object System.Security.Cryptography.SHA256Managed
                $checksum = $sha256.ComputeHash($stream)
                [System.BitConverter]::ToString($checksum).Replace("-", [system.String]::Empty)
            ]]>
       
</cjbxsl:method>
   
</cjbxsl:script>

   
<xsl:template match="node()|@*">
       
<xsl:copy>
           
<xsl:apply-templates select="@*|node()" />
       
</xsl:copy>
   
</xsl:template>
   
<xsl:template match="*[local-name()='file']">
       
<xsl:variable name="fname">
           
<xsl:call-template name="get-path" />
       
</xsl:variable>
       
<xsl:copy>
           
<xsl:apply-templates select="@*|node()" />
           
<xsl:attribute name="Checksum">
               
<xsl:value-of select="ucjb:GetChecksum(string($fname))" />
           
</xsl:attribute>
       
</xsl:copy>
   
</xsl:template>
   
<xsl:template name="get-path">
       
<xsl:for-each select="ancestor-or-self::*[not(@Root)]">
           
<xsl:value-of select="@Parent" />
           
<xsl:text>\</xsl:text>
           
<xsl:value-of select="@Name" />
       
</xsl:for-each>
   
</xsl:template>
</xsl:stylesheet>

then we can pass the stylesheet through another translet to create the C# and the extension object :-D. The output from that translet would be the extension object that is actually passed as the extension object to the original stylesheet and used in the pipeline. Oh dizzy!

function T-CreateObjectFromPSScriptXslt{
    param ($inxml)
    begin{
        . pslib:\xml\invoke-transform.ps1
        $xslt = New-Object Xml.Xmldocument
        $xslt.PSBase.PreserveWhitespace = $true
        $xslt.LoadXml(@'
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:cjbxsl
="urn:schemas-bayes-co-uk:xslt" version="1.0">
   
<xsl:output method="text" />
   
<xsl:template match="cjbxsl:script">
$code=@"
        using System;
        using System.Collections;
        using System.Management.Automation;
        using System.Xml;

        public class
<xsl:value-of select="@assembly-name" />
        {
       
<xsl:apply-templates select="cjbxsl:method" mode="declarations" />

        public
<xsl:value-of select="@assembly-name" />(
       
<xsl:apply-templates select="cjbxsl:method" mode="constructor-args" />
        )
        {
       
<xsl:apply-templates select="cjbxsl:method" mode="constructor-body" />
        }
       
<xsl:apply-templates select="cjbxsl:method" mode="methods" />
        }
"@
Add-Type -TypeDefinition $code -ReferencedAssemblies System.xml
$NewObject = New-Object
<xsl:value-of select="@assembly-name" />(<xsl:apply-templates select="cjbxsl:method" mode="new-object" />)
$NewObject
   
</xsl:template>
   
<xsl:template match="cjbxsl:method" mode="declarations">
        public ScriptBlock script
<xsl:value-of select="count(preceding::cjbxsl:method)" />;
   
</xsl:template>
   
<xsl:template match="cjbxsl:method" mode="constructor-args">
        ScriptBlock Script
<xsl:value-of select="count(preceding::cjbxsl:method)" />
       
<xsl:if test="not(position()=last())">, </xsl:if>
   
</xsl:template>
   
<xsl:template match="cjbxsl:method" mode="constructor-body">
        script
<xsl:value-of select="count(preceding::cjbxsl:method)" /> = Script<xsl:value-of select="count(preceding::cjbxsl:method)" />;
   
</xsl:template>
   
<xsl:template match="cjbxsl:method" mode="methods">
        public
<xsl:value-of select="@returns" /> <xsl:text> 
       
</xsl:text> <xsl:value-of select="@name" />(<xsl:value-of select="@args" />)
        {
        ScriptBlock Script = script
<xsl:value-of select="count(preceding::cjbxsl:method)" />;
        try
        {
        return (String)Script.Invoke(p0)[0].BaseObject;
        }
        catch (Exception ex)
        {
        throw new InvalidOperationException("Script failed***!", ex);
        }
        }
   
</xsl:template>
   
<xsl:template match="cjbxsl:method" mode="new-object">
        {
<xsl:value-of select="." />}<xsl:if test="not(position()=last())">,</xsl:if>
   
</xsl:template>
   
<xsl:template match="@* | node()">
       
<xsl:apply-templates select="@* | node()" />
   
</xsl:template>
</xsl:stylesheet>
'@)
    }
    process{
        if ($_ -is [xml]){
            invoke-transform -inxml $_ -inxsl $xslt
        }
    }
    end{
        if ($inxml -is [xml]){
            invoke-transform -inxml $inxml -inxsl $xslt
        }
    }
}

And here is a stylesheet that will use it

function T-AddChecksum{
    param ($inxml)
    begin{
        . pslib:\xml\xslt\T-CreateObjectFromPSScriptXslt.ps1
        . pslib:\xml\invoke-transform.ps1
        [xml]$xslt = @'
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:cjbxsl="urn:schemas-bayes-co-uk:xslt" xmlns:ucjb="urn:cjb" version="1.0">
   
<cjbxsl:script language="Powershell" implements-prefix="ucjb" assembly-name="ChecksumSHA3">
       
<cjbxsl:method name="GetChecksum" returns="string" args="string p0">
            <![CDATA[
                $file = $args[0]
                $stream = [System.IO.File]::OpenRead($file)
                $sha256 = new-object System.Security.Cryptography.SHA256Managed
                $checksum = $sha256.ComputeHash($stream)
                [System.BitConverter]::ToString($checksum).Replace("-", [system.String]::Empty)
            ]]>
       
</cjbxsl:method>
       
<cjbxsl:method name="GetChecksum384" returns="string" args="string p0">
            <![CDATA[
                $file = $args[0]
                $stream = [System.IO.File]::OpenRead($file)
                $sha384 = new-object System.Security.Cryptography.SHA384Managed
                $checksum = $sha384.ComputeHash($stream)
                [System.BitConverter]::ToString($checksum).Replace("-", [system.String]::Empty)
            ]]>
       
</cjbxsl:method>
   
</cjbxsl:script>

   
<xsl:template match="node()|@*">
       
<xsl:copy>
           
<xsl:apply-templates select="@*|node()" />
       
</xsl:copy>
   
</xsl:template>
   
<xsl:template match="*[local-name()='file']">
       
<xsl:variable name="fname">
           
<xsl:call-template name="get-path" />
       
</xsl:variable>
       
<xsl:copy>
           
<xsl:apply-templates select="@*|node()" />
           
<xsl:attribute name="Checksum">
               
<xsl:value-of select="ucjb:GetChecksum(string($fname))" />
           
</xsl:attribute>
       
</xsl:copy>
   
</xsl:template>
   
<xsl:template name="get-path">
       
<xsl:for-each select="ancestor-or-self::*[not(@Root)]">
           
<xsl:value-of select="@Parent" />
           
<xsl:text>\</xsl:text>
           
<xsl:value-of select="@Name" />
       
</xsl:for-each>
   
</xsl:template>
</xsl:stylesheet>
'@
        $psco = Invoke-Expression (T-CreateObjectFromPSScriptXslt $xslt)
    }
    process{
        if ($_ -is [xml]){
            [xml](invoke-transform -inxml $_ -inxsl $xslt -extensionObjects @{ucjb=$psco})
        }
    }
    end{
        if ($inxml -is [xml]){
            [xml](invoke-transform -inxml $inxml -inxsl $xslt -extensionObjects @{ucjb=$psco})
        }
    }
}

Voila!!! Powershell as an xslt scripting language. happyrabbit2

Digg It!DZone It!StumbleUponTechnoratiRedditDel.icio.usNewsVineFurlBlinkList

Add comment