Skip to content

Instantly share code, notes, and snippets.

@Warrenn
Last active June 22, 2022 06:37
Show Gist options
  • Save Warrenn/2fefcee7277580567b48 to your computer and use it in GitHub Desktop.
Save Warrenn/2fefcee7277580567b48 to your computer and use it in GitHub Desktop.
Powershell script to create a XML-Document-Transform. This auto generates a transform file by comparing a production file to the development file and figuring out what the transform needs to look like in order to transform the development file to the production equivalent
function Create-Xdt(
[string] $prodConfig,
[string] $devConfig,
[string] $xdtFile,
[string[]] $keepNodes = @(),
[hashtable] $findReplace = @{},
[string[]] $keyAttributes = @("name", "key", "path", "contract")
)
{
if([System.String]::IsNullOrEmpty($prodConfig) -or
[System.String]::IsNullOrEmpty($devConfig) -or
-not(Test-Path($prodConfig)) -or
-not(Test-Path($devConfig))){
return
}
function recurse($node, $path, $parent, $xmlDoc){
if($node.NodeType -eq [System.Xml.XmlNodeType]::Element){
$selectedAttr = $null
$node.Attributes |
%{
if((rating -item $_.Name -itemColl $keyAttributes) -gt
(rating -item $selectedAttr.Name -itemColl $keyAttributes))
{
$selectedAttr = @{
Name = $_.Name
Value = $_.Value
}
}
}
if(($xmlDoc.SelectNodes($path).Count -eq 1) -and
($keyAttributes -notcontains $selectedAttr.Name)){
$selectedAttr = $null
}
if($selectedAttr -ne $null){
$path = "$($path)[@$($selectedAttr.Name)='$($selectedAttr.Value)']"
}
$path
$node.Attributes |
%{
if($_.Name -notlike $selectedAttr.Name){
"$($path)/@$($_.Name)"
}
}
$selectedAttr = $null
}
$node.ChildNodes |
%{recurse -node $_ -path "$($path)/$($_.LocalName)" -parent $path -xmlDoc $xmlDoc }
}
function ignorePath([string]$xpath, [string[]]$ignorePaths){
if([string]::IsNullOrEmpty($xpath)){
return $true
}
foreach($ignore in $ignorePaths){
if([string]::IsNullOrEmpty($ignore)){
continue
}
if($xpath.StartsWith($ignore)){
return $true
}
}
return $false
}
function rating([string]$item, [string[]] $itemColl){
if([string]::IsNullOrEmpty($item) -or ($itemColl.Count -eq 0)){
return -1;
}
for($i = 0; $i -lt $itemColl.Count; $i++){
if($itemColl[$i] -eq $item){
return $i + 1;
}
}
return 0;
}
function addAttributes([System.Xml.XmlAttributeCollection] $attrs, [string] $xpath, [hashtable] $dict){
foreach($attr in $attrs){
$attrPath = "$($xpath)/@$($attr.Name)"
if(-not $dict.ContainsKey($attrPath)){
$dict.Add($attrPath, $attr.Value)
}
}
}
[xml]$prodXml = (gc $prodConfig) -replace "xmlns\s*=\s*""[^""]*""",""
[xml]$devXml = (gc $devConfig) -replace "xmlns\s*=\s*""[^""]*""",""
$prodNodes = recurse -node $prodXml -path "" -parent "" -xmlDoc $prodXml
$devNodes = recurse -node $devXml -path "" -parent "" -xmlDoc $devXml
$missingElms = @{}
$missingAttrs = @{}
$wrongElmValues = @{}
$wrongAttrValues = @{}
$ignoreList = New-Object System.Collections.ArrayList -ArgumentList @(,$keepNodes)
foreach($xpath in $prodNodes)
{
if(ignorePath -xpath $xpath -ignorePaths $ignoreList){
continue
}
$devNode = $devXml.SelectSingleNode($xpath)
$prodNode = $prodXml.SelectSingleNode($xpath)
if($devNode -eq $null){
[void]$ignoreList.Add($xpath)
if(($prodNode.NodeType -eq [System.Xml.XmlNodeType]::Attribute)){
$missingAttrs.Add($xpath, $prodNode.Value)
}
else{
$missingElms.Add($xpath, $prodNode.OuterXml)
}
continue
}
if(($devNode.NodeType -eq [System.Xml.XmlNodeType]::Attribute) -and
($devNode.Value -notlike $prodNode.Value)){
$wrongAttrValues.Set_Item($xpath, $prodNode.Value)
continue
}
if($devNode.HasChildNodes -ne $prodNode.HasChildNodes)
{
addAttributes -attrs $prodNode.Attributes -xpath $xpath -dict $wrongAttrValues
addAttributes -attrs $devNode.Attributes -xpath $xpath -dict $wrongAttrValues
[void]$ignoreList.Add($xpath)
$wrongElmValues.Add($xpath, $prodNode.InnerXml)
continue
}
if($devNode.'#text' -like $prodNode.'#text'){
continue
}
addAttributes -attrs $prodNode.Attributes -xpath $xpath -dict $wrongAttrValues
addAttributes -attrs $devNode.Attributes -xpath $xpath -dict $wrongAttrValues
$wrongElmValues.Add($xpath, $prodNode.InnerXml)
}
foreach($xpath in $devNodes)
{
if(($prodNodes -contains $xpath) -or
(ignorePath -xpath $xpath -ignorePaths $ignoreList)){
continue
}
$node = $devXml.SelectSingleNode($xpath)
$mustChange = $false
$nodeValue = ""
if($node.NodeType -eq [System.Xml.XmlNodeType]::Attribute){
$nodeValue = $node.Value
}
else{
$nodeValue = $node.InnerXml
}
$findReplace.GetEnumerator() |
%{
if($nodeValue -match $_.Name ){
$mustChange = $true
$newValue = $_.Value
for($i = 0;$i -lt $Matches.Count; $i++){
$newValue = $newValue -replace "\{\\$($i)\}",$Matches[$i]
}
}
}
if(-not $mustChange){
continue
}
if($node.NodeType -eq [System.Xml.XmlNodeType]::Attribute){
$missingAttrs.Set_Item($xpath, $newValue)
continue
}
addAttributes -attrs $node.Attributes -xpath $xpath -dict $wrongAttrValues
$wrongElmValues.Set_Item($xpath, $newValue)
}
foreach($xpath in $keepNodes)
{
$node = $prodXml.SelectSingleNode($xpath)
if($node.NodeType -eq [System.Xml.XmlNodeType]::Attribute){
$wrongAttrValues.Add($xpath, $node.Value)
continue
}
addAttributes -attrs $node.Attributes -xpath $xpath -dict $wrongAttrValues
$wrongElmValues.Add($xpath, $node.InnerXml)
}
$tempOut = $null
if(-not[string]::IsNullOrEmpty($xdtFile)){
[xml]$tempOut = New-Object System.XML.XMLDocument
[System.Xml.XmlNamespaceManager] $nsMgr = $tempOut.NameTable
$nsMgr.AddNamespace("xdt","http://schemas.microsoft.com/XML-Document-Transform")
$decl = $tempOut.CreateXmlDeclaration("1.0","utf-8",$null)
[void]$tempOut.InsertBefore($decl, $tempOut.DocumentElement)
}
function reportProgress($items,$text,$transformType)
{
if($items.Count -gt 0 ){
Write-Output $text
}
$items.GetEnumerator() |
%{
Write-Output $_.Name
createXdtDoc -doc $tempOut -xpath $_.Name -value $_.Value -transformType $transformType
}
}
reportProgress -items $missingElms -text "Missing Elements" -transformType "Insert"
reportProgress -items $missingAttrs -text "Missing Attributes" -transformType "SetAttributes"
reportProgress -items $wrongAttrValues -text "Wrong Attribute Values" -transformType "SetAttributes"
reportProgress -items $wrongElmValues -text "Wrong Element Values" -transformType "Replace"
if($tempOut -ne $null)
{
if(($tempOut.DocumentElement -ne $null) -and
($tempOut.DocumentElement.GetAttribute("xmlns:xdt") -ne "http://schemas.microsoft.com/XML-Document-Transform"))
{
$tempOut.DocumentElement.SetAttribute("xmlns:xdt", "http://schemas.microsoft.com/XML-Document-Transform")
}
$tempOut.Save($xdtFile)
}
}
function addXdt([xml]$doc, [System.Xml.XmlElement]$node,[string] $xdtType, [string]$xdtValue){
$anode = $doc.CreateAttribute("xdt:$($xdtType)","http://schemas.microsoft.com/XML-Document-Transform")
$anode.Value = $xdtValue
[void]$node.Attributes.Append($anode)
}
function createXdtDoc([xml]$doc, [string]$xpath, [string]$value, [string]$transformType)
{
if($doc -eq $null)
{
return
}
$node=[xml]$doc
foreach ($part in [regex]::Split($xpath, "(/)(?=(?:[^']|'[^']*')*$)", [System.Text.RegularExpressions.RegexOptions]::ExplicitCapture) )
{
if([string]::IsNullOrEmpty($part))
{
continue
}
$child=$node.SelectSingleNode($part)
if ($child -ne $null)
{
$node = $child
continue
}
if ($part.StartsWith("@"))
{
$anode = $doc.CreateAttribute($part.Substring(1));
$anode.Value = $value
[void]$node.Attributes.Append($anode);
$attrList = ""
$identifier = ""
$hasReplace = $false
foreach($atr in $node.Attributes)
{
if($atr.Name -eq "xdt:Locator")
{
[void]($atr.Value -match "Match\((.*)\)")
$identifier = $Matches[1]
continue
}
if(($atr.Name -eq "xdt:Transform") -and
(($atr.Value -eq "Replace") -or
($atr.Value -eq "Insert")))
{
$hasReplace = $true
}
if($atr.Name.Contains(":"))
{
continue
}
if($atr.Name -eq $identifier)
{
continue
}
$attrList = "$($attrList),$($atr.Name)"
}
$attrList = $attrList.Trim(',')
if(-not([string]::IsNullOrEmpty($attrList)) -and (-not $hasReplace))
{
addXdt -doc $doc -node $node -xdtType "Transform" -xdtValue "SetAttributes($($attrList))"
}
return
}
$attrib = $null
$elmName = $part
if ($part.Contains("["))
{
$attrDecl = $part.Split('[',2)
$elmName = $attrDecl[0]
$attrParts = $attrDecl[1].Split('=',2)
$attrib = @{
Name = $attrParts[0].TrimStart('@')
Value = $attrParts[1].Trim(" ']".ToCharArray())
}
}
$child = $doc.CreateElement($elmName)
if ($attrib -ne $null)
{
addXdt -doc $doc -node $child -xdtType "Locator" -xdtValue "Match($($attrib.Name))"
$anode=$doc.CreateAttribute($attrib.Name)
$anode.Value=$attrib.Value
[void]$child.Attributes.Append($anode)
}
[void]$node.AppendChild($child)
$node = $child
}
if($transformType -eq "Insert")
{
$outerXml = [xml]$value
foreach($atr in $outerXml.DocumentElement.Attributes)
{
$anode = $doc.CreateAttribute($atr.Name)
$anode.Value = $atr.Value
[void]$node.Attributes.Append($anode)
}
addXdt -doc $doc -node $node -xdtType "Transform" -xdtValue $transformType
$node.InnerXml = $outerXml.DocumentElement.InnerXml
}
if($transformType -eq "Replace")
{
addXdt -doc $doc -node $node -xdtType "Transform" -xdtValue $transformType
$node.InnerXml = $value
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment