Reputation: 149
I have a hashtable with incoming parameters, for example (the number of parameters and the parameters themselves may vary):
$inParams = @{
mode = "getevents"
username = "vbgtut"
password = "password"
selecttype = "one"
itemid = 148
ver = 1
}
I need to convert the hashtable to the XML-RPC format described in the specification.
That is, for a hashtable, I have to use the struct
data type described in the specification. The struct
structure contains members corresponding to the key-value pairs of the hashtable.
The values in the members can be one of six scalar types (int
, boolean
, string
, double
, dateTime.iso8601
and base64
) or one of two composite ones (struct
and array
). But for now, two scalar types are enough for me: string
and int
.
I wrote the following function for this:
function toXML($hashT) {
$xml = '<?xml version="1.0"?>'
$xml += "<methodCall>"
$xml += "<methodName>LJ.XMLRPC." + $hashT["mode"] + "</methodName>"
$xml += "<params><param><value><struct>"
foreach ($key in $hashT.Keys) {
if ($key -ne "mode") {
$xml += "<member>"
$xml += "<name>" + $key + "</name>"
$type = ($hashT[$key]).GetType().FullName
if ($type -eq "System.Int32") {$type = "int"} else {$type = "string"}
$xml += "<value><$type>" + $hashT[$key] + "</$type></value>"
$xml += "</member>"
}
}
$xml += "</struct></value></param></params>"
$xml += "</methodCall>"
return $xml
}
This function is doing its job successfully (I'm using PowerShell 7 on Windows 10):
PS C:\> $body = toXML($inParams)
PS C:\> $body
<?xml version="1.0"?><methodCall><methodName>LJ.XMLRPC.getevents</methodName><params><param><value><struct><member><name>ver</name><value><int>1</int></value></member><member><name>username</name><value><string>vbgtut</string></value></member><member><name>itemid</name><value><int>148</int></value></member><member><name>selecttype</name><value><string>one</string></value></member><member><name>password</name><value><string>password</string></value></member></struct></value></param></params></methodCall>
I do not need to receive XML in a readable form, but for this question I will bring XML in a readable form for an example:
PS C:\> [System.Xml.Linq.XDocument]::Parse($body).ToString()
<methodCall>
<methodName>LJ.XMLRPC.getevents</methodName>
<params>
<param>
<value>
<struct>
<member>
<name>ver</name>
<value>
<int>1</int>
</value>
</member>
<member>
<name>username</name>
<value>
<string>vbgtut</string>
</value>
</member>
<member>
<name>itemid</name>
<value>
<int>148</int>
</value>
</member>
<member>
<name>selecttype</name>
<value>
<string>one</string>
</value>
</member>
<member>
<name>password</name>
<value>
<string>password</string>
</value>
</member>
</struct>
</value>
</param>
</params>
</methodCall>
My question is as follows: Is it possible in PowerShell to convert a hashtable to XML-RPC format in a more optimal way than in my ToXML
function?
I tried using the cmdlet ConvertTo-Xml
. It works well, but converts the hashtable to regular XML, not to XML-RPC format. Maybe this cmdlet can be configured somehow so that it works in XML-RPC format?
I also heard about the library xml-rpc.net
, but her website is unavailable. It looks like this library is no longer being developed. Because of this, I am afraid to use it. Is it worth trying to use it?
Upvotes: 1
Views: 534
Reputation: 149
I was inspired by @Theo's answer and changed my approach to writing this function. I used the 'Unix philosophy' (writing small functions that work together and use text exchange) and used the fact that the XML tree (that is, an XML-RPC tree) has the property of recursiveness.
I took into account @Theo's remark about adding strings (performance issue) and replaced all the concatenation operators +
and +=
with one format operator -f
for each function (and I also used the -join
operator).
I took into account @mclayton's remark about the need to escape some characters in strings. Indeed, the XML-RPC format specification requires escaping only two characters: &
and <
(replaced by &
and <
). After that, escaping the >
character is not required.
To the previously required processing of hashtables, strings and integers, I added array processing. Now it is possible to process hash tables and arrays nested in each other.
Here's what I got (six functions):
function toXMLScalar($val) {
$type = $val.GetType().Name
if ($type -eq "Int32") {$type = "int"} else {
$type = "string"
$val = $val -replace '&', '&'
$val = $val -replace '<', '<'
}
"<{0}>{1}</{0}>" -f $type, $val
}
function toXMLValue($val) {
$val = if ($val -is [System.Array]) {
toXMLArray $val
} elseif ($val.GetType().Name -eq "Hashtable") {
toXMLStruct $val
} else {
toXMLScalar $val
}
"<value>{0}</value>" -f $val
}
function toXMLArray($arr) {
$values = foreach ($elem in $arr) { toXMLValue $elem }
$values = -join $values
"<array><data>{0}</data></array>" -f $values
}
function toXMLMember($key, $val) {
$val = toXMLValue $val
"<member><name>{0}</name>{1}</member>" -f $key, $val
}
function toXMLStruct($hashT) {
$members = foreach ($key in $hashT.Keys) {
if ($key -ne "mode") {
toXMLMember $key $hashT[$key]
}
}
$members = -join $members
"<struct>{0}</struct>" -f $members
}
function toXML($hashT) {
$xml = -join @(
'<?xml version="1.0"?>'
"<methodCall><methodName>LJ.XMLRPC.{0}</methodName>"
"<params><param>{1}</param></params></methodCall>"
)
$value = toXMLValue $hashT
$xml -f $hashT["mode"], $value
}
This approach has a disadvantage: with a large nesting of hash tables and/or arrays, it is possible to reach the call stack limit. People write that since the second version of PowerShell, this limit is 1000 calls. But I don't need a lot of nesting depth, so a recursive approach suits me.
To use these functions, as before, you need to make such a call:
$body = toXML($inParams)
Next, the toXML
function will call the toXMLValue
function, it will call the toXMLStruct
function, and so on.
Upvotes: 1
Reputation: 437953
As discussed, the XML-RPC protocol seems to be dying.
On the plus side, given that the protocol isn't evolving anymore, you may be able to use even older libraries, as long as they're still technically compatible with your runtime environment.
For instance, the third-party XmlRpc
module, last updated in 2017, may still work for you (its data-type support doesn't include base64
):
# Install the module, if necessary
if (-not (Get-Module -ListAvailable XmlRpc)) {
Write-Verbose -Verbose "Installing module 'XmlRpc' in the current user's scope..."
Install-Module -ErrorAction Stop -Scope CurrentUser XmlRpc
}
# Sample call
$result =
ConvertTo-XmlRpcMethodCall -Name LJ.XMLRPC.getevents @{
username = "vbgtut"
password = "password"
selecttype = "one"
itemid = 148
ver = 1
# ... sample values to demonstrate full data-type support and XML escaping
array = 'one & two', 3
double = 3.14
boolean = $true
date = Get-Date
}
# Post-process the results to make the data-type element
# names conform to the spec.
# ('Double' -> 'double', ..., and 'Int32' -> 'int')
[regex]::Replace(
$result,
'(</?)([A-Z]\w+)',
{
param($m)
$m.Groups[1].Value + ($m.Groups[2].Value.ToLower() -replace '32$')
}
)
This produces the following output, but note:
As you've discovered, the data-type XML element names used by ConvertTo-XmlRpcMethodCall
(the underlying ConvertTo-XmlRpcType
) aren't spec-compliant, in that they start with a capital letter when they should all be all-lowercase (e.g. <String>
instead of <string>
), and <Int32>
should be <int>
. This problem is corrected in a post-processing step via [regex]::Replace()
, which isn't ideal, but should work well enough in practice.
For simplicity, I've removed the mode = "getevents"
entry from the hashtable and have included its value as a literal part of the method name passed to -Name
.
The real output isn't pretty-printed; it was done here for readability.
Because the command operates on - inherently unordered - hashtables, the entry-definition order isn't guaranteed - though that shouldn't matter. (Unfortunately, the module isn't designed to accept ordered hashtables ([ordered] @{ ... }
)).
Note how the &
in 'one & two'
was properly escaped as &
<?xml version="1.0"?>
<methodCall>
<methodName>LJ.XMLRPC.getevents</methodName>
<params>
<param>
<value>
<struct>
<member>
<name>date</name>
<value>
<dateTime.iso8601>20230212T15:33:52</dateTime.iso8601>
</value>
</member>
<member>
<name>username</name>
<value>
<string>vbgtut</string>
</value>
</member>
<member>
<name>selecttype</name>
<value>
<string>one</string>
</value>
</member>
<member>
<name>double</name>
<value>
<double>3.14</double>
</value>
</member>
<member>
<name>boolean</name>
<value>
<boolean>True</boolean>
</value>
</member>
<member>
<name>array</name>
<value>
<array>
<data>
<value>
<string>one & two</string>
</value>
<value>
<int>3</int>
</value>
</data>
</array>
</value>
</member>
<member>
<name>itemid</name>
<value>
<int>148</int>
</value>
</member>
<member>
<name>password</name>
<value>
<string>password</string>
</value>
</member>
<member>
<name>ver</name>
<value>
<int>1</int>
</value>
</member>
</struct>
</value>
</param>
</params>
</methodCall>
Upvotes: 1
Reputation: 61068
Currently your function uses a lot of +=
lines to construct a string, but that is very time and memory consuming.
You could consider using a StringBuilder object for that or a List object that has an .Add()
method.
Another approach is to use two template Here-Strings you can use to fill in the data from the Hashtable. Something like this:
function ConvertTo-XmlRpc {
param (
[Hashtable]$hashT
)
# template for the xml
$xmlTemplate = @'
<?xml version="1.0"?>
<methodCall>
<methodName>LJ.XMLRPC.{0}</methodName>
<params>
<param>
<value>
<struct>
{1}
</struct>
</value>
<param>
</params>
</methodCall>
'@
# template for a member node
$memberTemplate = @'
<member>
<name>{0}</name>
<value>
<{1}>{2}</{1}>
</value>
</member>
'@
# first construct the member nodes
$members = foreach ($key in $hashT.Keys) {
if ($key -ne "mode") {
# just checking for 'int' or 'string' here, but you can extend to different types
$type = if ($hashT[$key] -is [int] -or $hashT[$key] -match '^\d+$') {'int'} else {'string'}
$memberTemplate -f $key, $type, $hashT[$key]
}
}
# return the completed xml
$xmlTemplate -f $hashT["mode"], ($members -join [environment]::NewLine)
}
$inParams = @{
mode = "getevents"
username = "vbgtut"
password = "password"
selecttype = "one"
itemid = 148
ver = 1
}
ConvertTo-XmlRpc $inParams
Note: I have renamed the function to comply to the Verb-Noun naming recommendations in PowerShell
Upvotes: 2