xmlpowershell

Modify OuterXml of a given node to keep it in file, but render node useless


I'm working on modifying xml files from an existing system using PowerShell. Our system supports using multiple nodes in one body, and depending on what the OuterXML is set as, it will bypass that value and use the other node of similar value (I suppose this may be true of all XML files but I do not want to make poor Assumptions).

Consider this XML file example. The way to modify our files to stop the use of the specific node is to prepend the node's OuterXml with an x:

<Configuration>
  <MainBlock>
    <Object NO="01" REVISION="255">
       <CODE TRANSPARENT="TRUE" />
    </Object>
    <Object NO="02" REVISION="255">
       <CODE TRANSPARENT="TRUE" />
    </Object>
    <Object NO="03" REVISION="255">
       <CODE TRANSPARENT="TRUE" />
    </Object>
    <Object NO="04" REVISION="255">
       <CODE TRANSPARENT="TRUE" />
    </Object>
    <!--Note - this line is the line I wish to modify so it is no longer in use.  The example below was edited manually-->
    <xObject NO="05" REVISION="255">
       <CODE TRANSPARENT="TRUE" />
    </xObject>
    <!--By prepending the "OuterXML" of the node above to "xObject" instead of "Object" I can keep the original values there, but use the "Object" below with a different code - this is an extremely simplified version of this concept.-->
    <Object NO="05" REVISION="255">
       <CODE TRANSPARENT="FALSE" />
    </Object>
  </MainBlock>
</Configuration>

The problem I'm having specifically is modifying the OuterXml of a given node, since OuterXml is a read-only attribute . . . When I see the nodes using $nodes = $Xml.SelectNodes to gather the Objects, I return a count of 5 (as expected with this solution), but how on earth do I modify the OuterXml of the the xObject node to read as Object and the Object Node below it to read as xObject? Here's my attempt:

  $Xml=[XML] (Get-Content -Path C:\FooBar.xml)
  
  $nodes=$Xml.SelectNodes("//Configuration/MainBlock/Object")
  $node = $nodes.Item(4).OuterXml #<< Here I can set the $node value as a STRING but not an actual XML element, but the $node element DOES contain the contents of the OuterXml Content.
  $nodes.Item(4) = $node #This does not error out but it also does NOTHING to the existing XML content
  
  $Xml.Save("C:\FooBar.xml")

Understand - there about 25 different attributes / values for each Object node within the actual XML structure I'm dealing with - this sample is just to illustrate the concept quickly/simply. While in the example above, I could easily just modify the Transparent value to "True" - that's not going feasible for me to accomplish on our files.

I know I can use the -replace command to replace the "Object" with "xObject" using PowerShell from a purely "Find and Replace" perspective, but I'm trying to use PowerShell's XML functions to their best potential.

  #While this works, it's kludgy and isn't fool proof.
  $XmlFile=(Get-Content -Path C:\FooBar.xml)
  $TextToChange=$XmlFile -replace 'xObject','fooObject'; -replace 'Object NO=\"5','xObject NO=\"5'; -replace 'fooObject','Object'
  Set-Content -Path C:\FooBar.xml -Value $TextToChange

Solution


  • Workarounds:

    # Load the XML file into an [xml] (System.Xml.XmlDocument) DOM.
    # Note: 
    #  * The form below is more robust than [xml] (Get-Content -Path C:\FooBar.xml)
    #  * Be sure to use a *full* file path in .NET methods, because .NET's working
    #    dir. usually differs from PowerShell's; use Convert-Path with a relative
    #    path to get a full one, e.g., Convert-Path FooBar.xml
    $fullFilePath = 'C:\FooBar.xml'
    ($xml = [xml]::new()).Load($fullFilePath)
    
    # Get the target elements by their 'NO' attribute value, 
    # both the <xObject> and the <Object> element.
    [array] $targetElements = 
      $xml.Configuration.MainBlock.ChildNodes |
      Where-Object NO -EQ '05'
    
    if ($targetElements.Count -eq 0) { throw "No matching element(s) found." }
    
    # Determine the parent element.
    $parentElement = $targetElements[0].ParentNode
    
    # Construct the replacement elements.
    $replacementElements = 
      $targetElements |
      ForEach-Object {
        # Change 'xObject' to 'Object' and vice versa.
        $newName = if ($_.Name -clike 'x*') { $_.Name.Substring(1) } else { 'x' + $_.Name }
        $replacementElement = $xml.CreateElement($newName, $_.NamespaceURI)
        # Move all attributes from the target node to the replacement...
        $null = foreach ($a in @($_.Attributes)) { $replacementElement.Attributes.Append($a) }
        # ... and all child elements.
        $null = foreach ($e in @($_.ChildNodes)) { $replacementElement.AppendChild($e) }
        # Output the newly constructed replacement element.
        $replacementElement
      } 
    
    # Now insert the replacement elements, each right after its original.
    $i = 0
    $null = 
      $replacementElements | ForEach-Object { 
        $parentElement.InsertAfter($_, $targetElements[$i++])
      }
    # ... and remove the original ones.
    $null = $targetElements | ForEach-Object { $parentElement.RemoveChild($_) }
    
    # Save back to the input file.
    $xml.Save($fullFilePath)