0

Im trying to add a new XML Node to a existing XML which is a API Response. But unfortunately i cannot get it to work trying many tutorials and examples. I cannot tell you which Options i tried but the last one is included...

I have this XML (Which is a API Response):

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<AccessPermissions xmlns="http://localhost/RESTApi/v1">
  <AccessProfilePermissions/>
  <ReaderSpecialPermissions/>
  <RoomZoneSpecialPermissions>
    <RoomZoneSpecialPermission>
      <RoomZoneNumber>6</RoomZoneNumber>
      <AccessWeeklyProfileNumber>1</AccessWeeklyProfileNumber>
      <ValidFrom>2023-07-13</ValidFrom>
    </RoomZoneSpecialPermission>
</AccessPermissions>

Now i want to add another <RoomZoneSpecialPermission> into the XML to send it back to the API Endpoint. I expect something like that:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<AccessPermissions xmlns="http://localhost/RESTApi/v1">
  <AccessProfilePermissions/>
  <ReaderSpecialPermissions/>
  <RoomZoneSpecialPermissions>
    <RoomZoneSpecialPermission>
      <RoomZoneNumber>6</RoomZoneNumber>
      <AccessWeeklyProfileNumber>1</AccessWeeklyProfileNumber>
      <ValidFrom>2023-07-13</ValidFrom>
    </RoomZoneSpecialPermission>
    <RoomZoneSpecialPermission>
      <RoomZoneNumber>7</RoomZoneNumber>
      <AccessWeeklyProfileNumber>1</AccessWeeklyProfileNumber>
      <ValidFrom>2023-07-31</ValidFrom>
    </RoomZoneSpecialPermission>
</AccessPermissions>

My Script (Without API Call, since that works perfectly fine):

# API Response to XML Object
$xmlObject = [xml]$apiresponse

# New Permission in XML Format and conver to XML Object
$newxml = "<RoomZoneSpecialPermission><RoomZoneNumber>7</RoomZoneNumber><AccessWeeklyProfileNumber>1</AccessWeeklyProfileNumber><ValidFrom>2023-07-31</ValidFrom></RoomZoneSpecialPermission>"
$newPermissionObject = [xml]$newxml

# Select RoomZoneSpecialPermissions Node
$roomZoneSpecialPermissions = $xmlObject.SelectSingleNode("//a:RoomZoneSpecialPermissions", $xmlObject.DocumentElement.NamespaceURI)

# New RoomZoneSpecialPermission Node
$newPermissionElement = $xmlObject.CreateElement("RoomZoneSpecialPermission", "http://localhost/RESTApi/v1")

# Add Elements to RoomZoneSpecialPermission Node generated above
$newPermissionElement.AppendChild($xmlObject.CreateElement("RoomZoneNumber", "http://localhost/RESTApi/v1")).InnerText = "7"
$newPermissionElement.AppendChild($xmlObject.CreateElement("AccessWeeklyProfileNumber", "http://localhost/RESTApi/v1")).InnerText = "1"
$newPermissionElement.AppendChild($xmlObject.CreateElement("ValidFrom", "http://localhost/RESTApi/v1")).InnerText = "2023-07-31"


$roomZoneSpecialPermissions.Trust.FrameworkPolicy.ClaimsProviders.AppendChild($newPermissionElement)

$modifiedxml = $xmlObject.OuterXml
Write-Output $modifiedxml

With the many tutoritals i tried came different Errors like:

  • Exception callind "AppendChild" with "1" arguments "The node to be inserted is from a different document context"

  • Cannot find an overload for "SelectSingleNode" and the argument count 2

Could you please tell me how to accomplish this task? Im pretty devastated right now since none of the explanations worked so far.

Nico
  • 323
  • 4
  • 14
  • 1
    Doesn't `$roomZoneSpecialPermissions.AppendChild($newPermissionElement)` instead of `$roomZoneSpecialPermissions.Trust.FrameworkPolicy.ClaimsProviders.AppendChild($newPermissionElement)` work? You are not using the part commented as `# New Permission in XML Format and conver to XML Object` so I would ditch that. – Martin Honnen Jul 31 '23 at 15:25
  • 1
    Are you sure the code is exactly as you've posted it? You define `$newPermissionObject` but then never use it... – Mathias R. Jessen Jul 31 '23 at 15:30
  • @MathiasR.Jessen, thats a fagment from other tries getting it to work. – Nico Aug 01 '23 at 12:28

3 Answers3

1

Your sample xml is not well formed, but assuming you fix that, try using the following and see if it works:

$newPermissionObject = $xmlObject.CreateDocumentFragment()
$newPermissionObject.InnerXML=$newxml

$destination=$xmlObject.SelectSingleNode('//*[local-name()="RoomZoneSpecialPermissions"]')
$destination.AppendChild($newPermissionObject)
Jack Fleeting
  • 24,385
  • 6
  • 23
  • 45
  • I dont get it. Why are you using $xmlObject to generate $newPermissionObject. Could you please explain that? I dont know how to use this code in my script :( – Nico Aug 01 '23 at 12:30
  • @Nico Not sure I understand the question - `$newPermissionObject` is a fragment to be inserted into `$xmlObject`. If you don't do that, you get the `node to be inserted is from a different document context` error mentioned in your question. – Jack Fleeting Aug 01 '23 at 12:55
  • Could you please explain me this Line: $newPermissionObject = $xmlObject.CreateDocumentFragment() $xmlObject in my Code is the XML Object of my API Response. Why does the $newPermissionObject Variable get "filled" with it then when it should contain the new Permissions to be inserted? I guess thats what i dont understand. – Nico Aug 01 '23 at 13:15
  • I tried your Solution and it does add the new correctly unter Node. But unfortunately the complete top part of the XML ( ) is missing. – Nico Aug 01 '23 at 13:34
  • @Nico I hare no idea why it happens on your system! It works for me with your (well formed) sample xml.... You may want to post it as a separate question, per SO rules.. – Jack Fleeting Aug 01 '23 at 13:59
  • I got it. The trick is to not use $destination. Of course i have to use $xmlObject and then the xml is complete with the new xml Node. Thanks! – Nico Aug 01 '23 at 14:12
  • @Nico Good to hear! If we're done, don't forget to accept the answer please – Jack Fleeting Aug 01 '23 at 14:25
0

You have a namespace issue. Try Xml Linq

using assembly System.Xml.Linq

$inputFilename = 'c:\temp\test.xml'
$outputFilename = 'c:\temp\test1.xml'

$doc = [System.Xml.Linq.XDocument]::Load($inputFilename)
$ns = $doc.Root.GetDefaultNamespace()

$roomZoneSpecialPermissions = $doc.Descendants($ns + 'RoomZoneSpecialPermissions')

$roomZoneSpecialPermission = [System.Xml.Linq.XElement]::new($ns + [System.Xml.Linq.XName]::Get('RoomZoneSpecialPermission'))

$roomZoneNumber = [System.Xml.Linq.XElement]::new($ns + [System.Xml.Linq.XName]::Get('RoomZoneNumber'), 7)
$roomZoneSpecialPermission.Add($roomZoneNumber)

$accessWeeklyProfileNumber = [System.Xml.Linq.XElement]::new($ns + [System.Xml.Linq.XName]::Get('AccessWeeklyProfileNumber'), 1)
$roomZoneSpecialPermission.Add($accessWeeklyProfileNumber)

$validFrom = [System.Xml.Linq.XElement]::new($ns + [System.Xml.Linq.XName]::Get('ValidFrom'), '2023-07-31')
$roomZoneSpecialPermission.Add($validFrom)

$roomZoneSpecialPermissions.Add($roomZoneSpecialPermission)

$doc.Save($outputFilename)
jdweng
  • 33,250
  • 2
  • 15
  • 20
0
  • The primary problem is that the 2nd argument in your $xmlObject.SelectSingleNode("//a:RoomZoneSpecialPermissions", $xmlObject.DocumentElement.NamespaceURI) call is of the wrong type - see the next section for how to fix this; however, given that you know the position of the target element, you can use PowerShell's adaption of the XML DOM in the form of dot notation, as used in the code below, and as also explained in the next section.

  • While Jack Fleeting's answer shows a generally superior approach to inserting a new nested element into an existing document, namely using [xml].CreateDocumentFragment(), but the inserted fragment would end up in the wrong namespace (xmlns="").

    • Note that your attempt of creating the new child element iteratively, using repeated [xml].CreateElement() calls with the target namespace in each call would work - but is quite cumbersome.

The following solution fixes both your problems:

# Use PowerShell's dot notation to obtain a reference to the the parent element.
$roomZoneSpecialPermissions = $xmlObject.AccessPermissions.RoomZoneSpecialPermissions

# Create the new element - empty for now - 
# in the document's default namespace, and append it as a child.
$newChild = $roomZoneSpecialPermissions.AppendChild(
  $xmlObject.CreateElement(
    'RoomZoneSpecialPermission', 
    $xmlObject.DocumentElement.NamespaceURI
  )
)

# Now that the element has been appended and is part of the document, 
# assigning to its .InnerXml respects the element's default namespace and 
# automatically applies it to the newly created  nested elements.
# (The string is the value of your $newxml variable 
# without the outer <RoomZoneSpecialPermission> element.)
$newChild.InnerXml = '<RoomZoneNumber>7</RoomZoneNumber><AccessWeeklyProfileNumber>1</AccessWeeklyProfileNumber><ValidFrom>2023-07-31</ValidFrom>'

# Output the extended document's XML representation.
$xmlObject.OuterXml

Note that it's tempting to use the .CreateDocumentFragment() approach for simplicity and embed an xmlns=... attribute matching the document's default namespace in the fragment text, but - unfortunately - this attribute is retained on importing the fragment, which - while being technically correct - creates a potential maintenance headache due to this unnecessary duplication.

# !! Works, but the new child element has a duplicate of the document
# !! element's xmlns=... attribute.
$newChildFragment = $xmlObject.CreateDocumentFragment()
# Note the use of string interpolation $(...) in order to 
# duplicate the document element's namespace  attribute, which ensures that
# the fragment is in the same namespace.
# Unfortunately, this duplicate is *not* removed by .AppendChild()
$newChildFragment.InnerXML = "<RoomZoneSpecialPermission xmlns=`"$($xmlObject.DocumentElement.NamespaceURI)`"><RoomZoneNumber>7</RoomZoneNumber><AccessWeeklyProfileNumber>1</AccessWeeklyProfileNumber><ValidFrom>2023-07-31</ValidFrom></RoomZoneSpecialPermission>" 
$roomZoneSpecialPermissions.AppendChild($newChildFragment)

As for the error messages you saw:

Error: Cannot find an overload for "SelectSingleNode" and the argument count 2

This error stems from the 2nd argument in your $xmlObject.SelectSingleNode("//a:RoomZoneSpecialPermissions", $xmlObject.DocumentElement.NamespaceURI) call being of the wrong type:

You can fix this as follows:

# Create a namespace manager for the target document's name table...
$nsMgr = [System.Xml.XmlNamespaceManager]::new($xmlObject.NameTable)
# ... and add the document element's namespace URI as an entry
#     with a self-chosen prefix, which can then be used in XPath queries.
$nsMgr.AddNamespace('a', $xmlObject.DocumentElement.NamespaceURI)
# Perform the XPath query, using the self-chosen namespace prefix.
$roomZoneSpecialPermissions =
  $xmlObject.SelectSingleNode('//a:RoomZoneSpecialPermissions', $nsMgr)

That said, you can use PowerShell's adaption of the XML DOM, which allows drilling down into XML documents with dot notation, i.e. allows treating XML element names (and attributes) like property names; since this notation ignores XML namespaces and only operates on the elements' local names, getting the desired element is as simple as:

# Use PowerShell's namespace-agnostic dot notation to target the element of interest.
$roomZoneSpecialPermissions =
  $xmlObject.AccessPermissions.RoomZoneSpecialPermissions

Exception calling "AppendChild" with "1" arguments "The node to be inserted is from a different document context"

You're not showing the actual attempt, but this error is likely caused by trying to insert the separate document ([xml]) stored in $newPermissionObject directly into your $xmlObject document, which isn't supported.

# !! Fails, because $newPermissionObject is a separate document.
$roomZoneSpecialPermissions.AppendChild(
  $newPermissionObject.DocumentElement
)

In order to insert nodes from a different document into a given document, those nodes must be imported first, using [xml].ImportNode():

$roomZoneSpecialPermissions.AppendChild(
  $xmlObject.ImportNode(
    $newPermissionObject.DocumentElement, 
    $true  # also copy all nested elements
  )
)

However, then too you'll run into the namespace problem discussed in the top section.

mklement0
  • 382,024
  • 64
  • 607
  • 775