I have an XML file generated with heat.exe from wix-toolset. That wraps each <File>
object inside a <Component>
.
I have to modify this with an XSLT v1.0, so that all <File>
where @Source
contains the same FileName (without extension) should be extracted to one <Component>
. Usually, @Source
only ends with ".dll" or ".config".
Additional the <File>
s ending with:
@KeyPath
to "no"@Assembly
with value ".net"Here is a sample XML I which has to be transformed:
<?xml version="1.0" encoding="utf-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
<Fragment>
<DirectoryRef Id="GAC35">
<Component Id="cmp5BC59A7DCA65D1B974894AAA758DB693" Guid="{1A2AC82E-7AD9-4CB6-BF42-4D31FAD7786E}">
<File Id="filE7FFE881A5ECF045432F46FBA78AEDD4" KeyPath="yes" Source="$(var.HarvestLoggingGac35Policy)\Policy.1.0.Logging.config" />
</Component>
<Component Id="cmp7F80B805A2BDCF92241FB8019B91FF1C" Guid="{0F88D7E9-355A-40C1-AC8C-29BBB27690FB}">
<File Id="filB35F1D68CCC038864F21E76D8A9F5977" KeyPath="yes" Source="$(var.HarvestLoggingGac35Policy)\Policy.1.0.Logging.dll" />
</Component>
<Component Id="test1" Guid="{1A2AC82E-7AD9-4CB6-BF42-4D31FAD7786E}">
<File Id="filE7FFE881A5ECF045432F46FBA78AEDD4" KeyPath="yes" Source="$(var.HarvestLoggingGac35Policy)\Policy.2.0.Logging.config" />
</Component>
<Component Id="test12" Guid="{0F88D7E9-355A-40C1-AC8C-29BBB27690FB}">
<File Id="filB35F1D68CCC038864F21E76D8A9F5977" KeyPath="yes" Source="$(var.HarvestLoggingGac35Policy)\Policy.2.0.Logging.dll" />
</Component>
</DirectoryRef>
</Fragment>
<Fragment>
<ComponentGroup Id="HeatGenerated_Gac35Policies">
<ComponentRef Id="cmp5BC59A7DCA65D1B974894AAA758DB693" />
<ComponentRef Id="cmp7F80B805A2BDCF92241FB8019B91FF1C" />
<ComponentRef Id="test1" />
<ComponentRef Id="test2" />
</ComponentGroup>
</Fragment>
</Wix>
Then this should be the expected ouput with XSL v1:
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
<Fragment>
<DirectoryRef Id="GAC35">
<Component Id="cmp5BC59A7DCA65D1B974894AAA758DB693"
Guid="{1A2AC82E-7AD9-4CB6-BF42-4D31FAD7786E}">
<File Id="filE7FFE881A5ECF045432F46FBA78AEDD4"
KeyPath="no"
Source="$(var.HarvestLoggingGac35Policy)\Policy.1.0.Logging.config"/>
<File Id="filB35F1D68CCC038864F21E76D8A9F5977"
KeyPath="yes"
Source="$(var.HarvestLoggingGac35Policy)\Policy.1.0.Logging.dll"
Assembly=".net"/>
</Component>
<Component Id="test1"
Guid="guid1">
<File Id="filE7FFE881A5ECF045432F46FBA78AEDD4"
KeyPath="no"
Source="$(var.HarvestLoggingGac35Policy)\Policy.2.0.Logging.config"/>
<File Id="filB35F1D68CCC038864F21E76D8A9F5977"
KeyPath="yes"
Source="$(var.HarvestLoggingGac35Policy)\Policy.2.0.Logging.dll"
Assembly=".net"/>
</Component>
</DirectoryRef>
</Fragment>
<Fragment>
<ComponentGroup Id="HeatGenerated_Gac35Policies">
<ComponentRef Id="cmp5BC59A7DCA65D1B974894AAA758DB693"/>
<ComponentRef Id="test1"/>
</ComponentGroup>
</Fragment>
</Wix>
EDIT: This was my own last "solution" (not complete) until I have used the answer from @michael.hor257k
<?xml version="1.0" ?>
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:wix="http://schemas.microsoft.com/wix/2006/wi"
xmlns="http://schemas.microsoft.com/wix/2006/wi">
<!-- Copy all attributes and elements to the output. -->
<xsl:output method="xml"
indent="yes"
omit-xml-declaration="yes"/>
<xsl:strip-space elements="*"/>
<xsl:template match="@* | node()">
<xsl:copy>
<xsl:apply-templates select="@* | node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="wix:Component">
<xsl:variable name="fileExtension">
<xsl:choose>
<xsl:when test="contains(wix:File/@Source, '.config')">
<xsl:value-of select="'.config'"/>
</xsl:when>
<xsl:when test="contains(wix:File/@Source, '.dll')">
<xsl:value-of select="'.dll'"/>
</xsl:when>
</xsl:choose>
</xsl:variable>
<xsl:variable name="precedingFileExtension">
<xsl:choose>
<xsl:when test="contains(preceding-sibling::wix:Component/wix:File/@Source, '.config')">
<xsl:value-of select="'.config'"/>
</xsl:when>
<xsl:when test="contains(preceding-sibling::wix:Component/wix:File/@Source, '.dll')">
<xsl:value-of select="'.dll'"/>
</xsl:when>
</xsl:choose>
</xsl:variable>
<xsl:apply-templates select="wix:File[(substring-before(substring-after(@Source,'\'), $fileExtension) = substring-before(substring-after(preceding::wix:Component/wix:File/@Source,'\'), $precedingFileExtension))]" mode="files"/>
</xsl:template>
<xsl:template match="wix:File" mode="files">
<xsl:variable name="fileExtension">
<xsl:choose>
<xsl:when test="contains(@Source, '.config')">
<xsl:value-of select="'.config'"/>
</xsl:when>
<xsl:when test="contains(@Source, '.dll')">
<xsl:value-of select="'.dll'"/>
</xsl:when>
</xsl:choose>
</xsl:variable>
<xsl:variable name="otherFileExtension">
<xsl:choose>
<xsl:when test="contains(@Source, '.config')">
<xsl:value-of select="'.dll'"/>
</xsl:when>
<xsl:when test="contains(@Source, '.dll')">
<xsl:value-of select="'.config'"/>
</xsl:when>
</xsl:choose>
</xsl:variable>
<Component>
<xsl:copy-of select="parent::wix:Component/@*" />
<xsl:copy>
<xsl:copy-of select="@*" />
</xsl:copy>
<xsl:variable name="source" select="substring-before(substring-after(@Source,'\'), $fileExtension)"/>
<xsl:apply-templates select="//wix:File[substring-before(substring-after(@Source,'\'), $otherFileExtension)=$source]" />
</Component>
</xsl:template>
<xsl:key name="policy-config-file"
match="wix:File[contains(@Source, '.config')]"
use="@Id" />
<xsl:template match="wix:File[key('policy-config-file', @Id)]" >
<xsl:element name="File">
<xsl:apply-templates select="@*" />
<xsl:attribute name="KeyPath">
<xsl:value-of select="'no'"/>
</xsl:attribute>
</xsl:element>
</xsl:template>
</xsl:stylesheet>
You cannot group nodes by something they do not have. If you want to group the components by their filename without the extension, you must start by adding such value to each component - either as an element or as an attribute. This is because removing the extension is not trivial in XSLT 1.0 and cannot be accomplished by a single XPath expression.
Once you have done that, you can proceed and apply Muenchian grouping to the interim result:
XSLT 1.0
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:wix="http://schemas.microsoft.com/wix/2006/wi"
xmlns:exsl="http://exslt.org/common"
extension-element-prefixes="exsl">
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
<xsl:strip-space elements="*"/>
<xsl:key name="k" match="wix:Component" use="key"/>
<!-- identity transform -->
<xsl:template match="@*|node()">
<xsl:copy>
<xsl:apply-templates select="@*|node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="wix:DirectoryRef">
<!-- first pass -->
<xsl:variable name="components">
<xsl:for-each select="wix:Component">
<xsl:copy>
<xsl:apply-templates select="@* | *"/>
<key>
<xsl:call-template name="remove-last-token">
<xsl:with-param name="text" select="wix:File/@Source"/>
<xsl:with-param name="delimiter" select="'.'"/>
</xsl:call-template>
</key>
</xsl:copy>
</xsl:for-each>
</xsl:variable>
<!-- output -->
<xsl:copy>
<xsl:copy-of select="@*"/>
<xsl:for-each select="exsl:node-set($components)/wix:Component[count(. | key('k', key)[1]) = 1]">
<xsl:copy>
<xsl:copy-of select="@*"/>
<xsl:copy-of select="key('k', key)/wix:File"/>
</xsl:copy>
</xsl:for-each>
</xsl:copy>
</xsl:template>
<xsl:template name="remove-last-token">
<xsl:param name="text"/>
<xsl:param name="delimiter"/>
<xsl:value-of select="substring-before($text, $delimiter)"/>
<xsl:if test="contains(substring-after($text, $delimiter), $delimiter)">
<xsl:value-of select="$delimiter"/>
<xsl:call-template name="remove-last-token">
<xsl:with-param name="text" select="substring-after($text, $delimiter)"/>
<xsl:with-param name="delimiter" select="$delimiter"/>
</xsl:call-template>
</xsl:if>
</xsl:template>
</xsl:stylesheet>
Demo: http://xsltransform.net/93dEHG9
If you can safely assume that the extension will always be either .dll
or .config
, then you could simplify this to:
XSLT 1.0
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:wix="http://schemas.microsoft.com/wix/2006/wi">
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
<xsl:strip-space elements="*"/>
<xsl:key name="k" match="wix:Component" use="substring-before(concat(substring-before(concat(wix:File/@Source, '.dll'), '.dll'), '.config'), '.config')"/>
<!-- identity transform -->
<xsl:template match="@*|node()">
<xsl:copy>
<xsl:apply-templates select="@*|node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="wix:DirectoryRef">
<xsl:copy>
<xsl:copy-of select="@*"/>
<xsl:for-each select="wix:Component[count(. | key('k', substring-before(concat(substring-before(concat(wix:File/@Source, '.dll'), '.dll'), '.config'), '.config'))[1]) = 1]">
<xsl:copy>
<xsl:copy-of select="@*"/>
<xsl:copy-of select="key('k', substring-before(concat(substring-before(concat(wix:File/@Source, '.dll'), '.dll'), '.config'), '.config'))/wix:File"/>
</xsl:copy>
</xsl:for-each>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>