0

Well, i need to move xml node position inside same xml file

I've got this xml:

<METATRANSCRIPT:METATRANSCRIPT xmlns:METATRANSCRIPT="http://www.mpi.nl/IMDI/Schema/IMDI" xmlns="http://www.mpi.nl/IMDI/Schema/IMDI" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"  ArchiveHandle="hdl:2196/00-0000-0000-0013-2CB3-2" Date="2012-02-08" FormatId="IMDI 3.0"  Originator="Editor - Profile:local/SESSION.Profile.xml" Type="SESSION" Version="3"  xsi:schemaLocation="http://www.mpi.nl/IMDI/Schema/IMDI ./IMDI_3.0.xsd">
  <Session>
  <Name>Acknowledgement</Name>
  <Title>Acknowledgement written by Maria Alzira Roque Gameira in her Master's Thesis</Title>
  <Date>2009-09-16</Date>
  <Description LanguageId="ISO639-3:eng" Link="">Maria Alzira Roque Gameiro (MD009) reads aloud the acknowledgement she wrote in Minderico for her Master Thesis on museology. </Description>
  <MDGroup>
     <Actors>
           <Name>Sabine Wurm</Name>
           <FullName>Sabine Wurm</FullName>
        </Actor>
      </Actors>
    </MDGroup>
   </METATRANSCRIPT:METATRANSCRIPT>
    <imdi xmlns="http://www.mpi.nl/IMDI/Schema/IMDI" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" FormatId="IMDI 3.03" Originator="" Type="SESSION" Version="1" xsi:schemaLocation="http://www.mpi.nl/IMDI/Schema/IMDI">
     <Session>      
     <MDGroup>
     <Actors>
        <Actor>
           <Name/>
           <FullName/>
        </Actor>
        <Actor>
           <Name/>
           <FullName/>
        </Actor>
        <Actor>
           <Name/>
           <FullName/>
        </Actor>
        <Actor>
           <Name/>
           <FullName/>
        </Actor>
     </Actors>
  </MDGroup>      
  </Session>
  </imdi>

The ideia is to move each Actor inside imdi, and insert it after the first Actor inside METATRANSCRIPT:METATRANSCRIPT. This way i will have all actors in the same parent (METATRANSCRIPT)

My code:

Get-ChildItem -Path 'C:\Scripts\Source\' -Recurse -Include "*.xml" -File| ForEach-Object{

[xml]$xml = Get-Content $_.FullName;
$xml.imdi.Session.MDGroup.Actors
...
...
$xml.Save($_.FullName)}

My issue is :

how can i select and move each node, before using insertafter() method ? Thanks a lot for any help on this. I don't know how to start.

Expected Output:

<METATRANSCRIPT:METATRANSCRIPT xmlns:METATRANSCRIPT="http://www.mpi.nl/IMDI/Schema/IMDI" xmlns="http://www.mpi.nl/IMDI/Schema/IMDI" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"  ArchiveHandle="hdl:2196/00-0000-0000-0013-2CB3-2" Date="2012-02-08" FormatId="IMDI 3.0"  Originator="Editor - Profile:local/SESSION.Profile.xml" Type="SESSION" Version="3"  xsi:schemaLocation="http://www.mpi.nl/IMDI/Schema/IMDI ./IMDI_3.0.xsd">
 <Session>
 <Name>Acknowledgement</Name>
 <Title>Acknowledgement written by Maria Alzira Roque Gameira in her Master's Thesis</Title>
 <Date>2009-09-16</Date>
 <Description LanguageId="ISO639-3:eng" Link="">Maria Alzira Roque Gameiro (MD009) reads aloud the acknowledgement she wrote in Minderico for her Master Thesis on museology. </Description>
 <MDGroup>
 <Actors>
   <Actor>
       <Name>Sabine Wurm</Name>
       <FullName>Sabine Wurm</FullName>
    </Actor>
       <Actor>
       <Name/>
       <FullName/>
    </Actor>
    <Actor>
       <Name/>
       <FullName/>
    </Actor>
    <Actor>
       <Name/>
       <FullName/>
    </Actor>
    <Actor>
       <Name/>
       <FullName/>
    </Actor>
  </Actors>
 </MDGroup>
 </Session>
 </METATRANSCRIPT:METATRANSCRIPT>
Paulo
  • 331
  • 2
  • 11

2 Answers2

1

EDIT:

I'm going to leave the answer below so the children of the future will know what not to do, but here's the update:

TIL that cut-and-paste does exist - see dialog with @mklement0.

Unrelated to the issue of cut-and-paste, if you look at @mklement0's answer, you'll note that he uses dot notation to select elements within the xml document, while I use xpath. In certain situations where elements are buried 10 (or even more) layers deep under <root>, xpath may be a better tool; compare $xml.Root.imdi.Session.MDGroup.Actors.Actor with $xml.SelectNodes("//imdi//Actor"). I know that some (many?) people downright loath xpath, but I personally feel more mentally comfortable using it.

So in @mklement0's honor, here's his solution, translated to xpath:

Replace the long foreach below with:

$nodes | ForEach-Object { 
  $destination.AppendChild($_)   
}

and it's done!

Original answer - the first paragraph has proven wrong....

Unfortunately, there's no "cut and paste" method in PS (that I know of), so you have to do it the long way - find the targets, copy them, insert them at the destination and then go back to the source and delete them.

Something along the lines of:

$source = $xml.SelectSingleNode("//imdi")
$nodes = $xml.SelectNodes("//imdi//Actor")
$destination = $xml.SelectSingleNode(".//*[name()='Session']//*[name()='Actors']")

foreach ($node in $nodes)
{

$actor = $xml.CreateElement("Actor")

$name = $xml.CreateElement('Name')
$fullname = $xml.CreateElement('FullName')

$nam = $node.SelectSingleNode('./Name')
$fnam = $node.SelectSingleNode('./FullName')

$actor.AppendChild($name).set_InnerText($nam.InnerText)
$actor.AppendChild($fullname).set_InnerText($fnam.InnerText)
$destination.AppendChild($actor)
}

$xml.doc.RemoveChild($source)

This should output what you need.

Jack Fleeting
  • 24,385
  • 6
  • 23
  • 45
1

PowerShell's helpful adaptation of the XML DOM, which reflects child elements and attributes as properties of [xml](System.Xml.XmlDocument) documents and their nodes, still also allows you access to the native members of the underlying .NET types:

That is, you can drill down to the desired element(s) using the convenient dot notation (e.g., $xml.Root.METATRANSCRIPT.Session.MDGroup.Actors) while still being able to call the methods of the underlying System.Xml.XmlNode type such as .AppendChild().

Note that intra-document use of .AppendChild() implicitly moves the node being appended from its original location, so this call is all that is needed, except for potentially using .RemoveNode() on then-childless parent nodes.

The following assumes that your real XML document has a document root element named <Root> (your input XML is an invalid XML fragment):

Get-ChildItem 'C:\Scripts\Source\' -Recurse -File -Filter *.xml | ForEach-Object{

  # Read the file into an XML DOM.
  [xml] $xml = Get-Content -Raw $_.FullName

  # Move all <Actor> elements from below <imdi> to below <METATRANSCRIPT>
  $xml.Root.imdi.Session.MDGroup.Actors.Actor | ForEach-Object { 
    $xml.Root.METATRANSCRIPT.Session.MDGroup.Actors.AppendChild($_) 
  }

  # Remove the now virtually empty <imdi> element, if desired.
  $xml.Root.RemoveChild($xml.Root.imdi)

  # Save the modified document.
  $xml.Save($_.FullName)
}

Note that $xml.Root.imdi.Session.MDGroup.Actors.Actor returns an array of all child elements named Actor, all elements of which are then one by one appended as children to the target element via the .AppendChild() method.

mklement0
  • 382,024
  • 64
  • 607
  • 775
  • Thanks, @Jack. Totally optional, but if you wanted to contrast our answers better in order to guide future readers, you could highlight that you're using XPath to select the target nodes, and perhaps why that is (situationally) preferable. – mklement0 Oct 30 '20 at 14:20
  • 1
    Done; now I'm going to actually spend some time to better understand the information in the first paragraphs of your answer. – Jack Fleeting Oct 30 '20 at 14:37