Extract Key from XML - powershell

I am invoking an API using "Invoke-WebRequest" and getting the below as return XML.
<?xml version="1.0" encoding="utf-8"?>
<string xmlns="http://www.sft.com/">a879-956c3452f55e3c</string>
How can I extract the "a879-956c3452f55e3c" part from this retun XML.
I tried this code
$tmpstring1 = $temp.Content -split 'com/">'
$finalString1 = $tmpstring1[1] -split "</"
$Key = $finalString1[0]

String parsing of XML text is best avoided, because it is brittle; it's always preferable to us a dedicated XML parser; fortunately, PowerShell provides easy access to .NET's System.Xml.XmlDocument type ([xml]):
$xmlText = #'
<?xml version="1.0" encoding="utf-8"?>
<string xmlns="http://www.sft.com/">a879-956c3452f55e3c</string>
'#
([xml] $xmlText).string.InnerText # .'#text' works too
Note: PowerShell conveniently adapts the XML DOM for dot notation, so that elements and attributes can be accessed as if they were regular object properties - see this answer for more information.
Normally, an XML element that only has text content (a text child node, such as the <string> element's a879-956c3452f55e3c value here) directly returns that text content when accessed with dot notation (.string).
However, because the <string> element has a namespace declaration (xmlns=...), .string actually returns a [System.Xml.XmlElement] instance whose text child node must explicitly be accessed, either via its .InnerText property or via the adapted property named for the (generic) node name of the text child element, .'#text'.

Related

Convert Flexform XML string from database to array, convert array to Flexform XML string to write to database in TYPO3

Question: How to convert Flexform XML to a data format where settings can be migrated and then write it back to DB. Are there some pitfalls or things I have to consider? Is there any reason not to just use GeneralUtility::xml2array() and GeneralUtility::array2Xml?
TYPO3 v11 / v12.
More details:
This is a common use case for me in my extensions: Often the schema of the Flexform is changed to improve usability. For the sake of the editors I want to migrate existing values to the new fields.
This would be pretty straightforward (in an Upgrade Wizard or Command):
Get the XML string representation from $row['pi_flexform']
Convert it to an array
Make the changes in the array
Convert it again to an XML string representation
If this is different from previous, write to $row['pi_flexform'].
The problem is figuring out which TYPO3 functions to use. There is FlexFormService, FlexFormTools and there is GeneralUtility::xml2array() and GeneralUtility::array2Xml.
The simplest solution seems to be to use array2xml and xml2array.
The second problem is figuring out what is valid XML for Flexform (unless the functions already take care of that).
Let's say I had something like this in pi_flexform which should be converted:
<?xml version="1.0" encoding="utf-8" standalone="yes" ?>
<T3FlexForms>
<data>
<sheet index="sDEF">
<language index="lDEF">
<field index="settings.foo">
<value index="vDEF">content of foo</value>
</field>
...
I don't really need the language index but that is how it looks now.
The foo field is gone in the new Flexform and the content should be converted to:
....
<field index="settings.bar">
<value index="vDEF">content of foo</value>
</field>
...
The real use case is more complicated and actually makes sense ;-)
Unfortunately, the API is not so clear.
use TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools;
use TYPO3\CMS\Core\Service\FlexFormService;
Convert XML to array: Solution 1
$xml = $this->flexformTools->cleanFlexFormXML('tt_content', 'pi_flexform', $row);
$xmlArray = $this->flexformTools->cleanFlexFormXML;
Result: If the Flexform schema only contains the new fields, the old fields are removed - so we can't use this if the schema changes
Convert XML to array: Solution 2
$xml = $row['pi_flexform'];
$this->flexformService->convertFlexFormContentToArray($xml);
Result: This works, but the resulting $xmlArray looks like this ['settings' => ['foo' => ...
can be used for flexforms with settings but probably not best choice if is to be written again
Convert XML to array:
solution 3
$xml = $row['pi_flexform'];
$xmlArray = \TYPO3\CMS\Core\Utility\GeneralUtility::xml2array($xml)
Unclear: what should be in second and third parameter?
the result actually looks good: ['data' => ['sDEF' => ['lDef' => ['settings.foo]
Write array to XML: Solution A
$xml = $this->flexformTools->flexArray2Xml($xmlArrray, true);
Write array to XML: Solution B
$xml = GeneralUtility::array2xml($xmlArrray);
Solution A would write it like this (if combined with convertFlexFormContentToArray). Apart from that they should both work.
<?xml version="1.0" encoding="utf-8" standalone="yes" ?>
<T3FlexForms>
<settings>
<link>http://csd.informatik.uni-oldenburg.de/adam/adamMC.bib</link>
<sort>none</sort>
<sortfixed>0</sortfixed>
<filterType>none</filterType>
<filterEntries></filterEntries>
</settings>
</T3FlexForms>

How to query XAML file with Powershell's `select-xml` commandlet?

I'm trying to use the select-xml cmdlet in Powershell to query some XAML files in my .NET project. Here's what I've tried:
select-xml -content (cat $xaml_file_path) -xpath "//GroupBox"
Where $xaml_file_path is simply a string containing the file path to the XAML file of interest.
This throws an error:
Select-Xml: Cannot validate argument on parameter 'Content'. The argument is null, empty, or an element of the argument collection contains a null value. Supply a collection that does not contain any null values and then try the command again.
I know the XAML is valid since it compiles fine on Visual Studio. So I'm thinking there might be something else going on here.
Is there a way to query XAML files using Powershell? If so how?
To query an XML (XAML) document that uses namespaces, Select-Xml requires you to:[1]
declare all the namespaces that any nodes involved in your XPath query are in, via a hashtable that maps self-chosen prefixes to namespace URIs (parameter -Namespace).
Those self-chosen prefixes may, but needn't, be the same as in the input document, with the exception of the default namespace (xmlns), for which a name must be chosen too, but which can not be xmlns (the example below uses _).
use those self-chosen prefixes for all the nodes referenced in the XPath query (parameter -XPath), including those in the default (implied) namespace.
A simple example:
# Create a sample XAML file.
#"
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Test"
Title="MainWindow" Height="450" Width="500">
<Grid>
<TextBox x:Name="UserInnput" Height="140" TextWrapping="Wrap" VerticalAlignment="Top" AcceptsReturn="True" AcceptsTab="True" Padding="4" VerticalScrollBarVisibility="Auto" />
<Button x:Name="Save" Content="Save" HorizontalAlignment="Left" VerticalAlignment="Top" Width="100" IsDefault="True" Height="22" Margin="170,150,0,0" />
</Grid>
</Window>
"# | Set-Content sample.xml
# Create the hashtable that uses self-chosen names to the URIs
# of those namespaces involved in the XPath query below.
$namespaces = #{
# This is the default namespace, which the elements in the input
# document that have *no* namespace prefix are *implicitly* in.
# You must NOT use 'xmlns' as the prefix.
_ = 'http://schemas.microsoft.com/winfx/2006/xaml/presentation'
# For non-default namespaces, you may choose the same prefixes
# as in the original document.
# Note:
# * This namespace isn't actually used in the query below.
# * However, if the nodes involved in the query do fall into
# this or other namespaces, they must be declared here too,
# and their prefixes must then be used in the query.
x = 'http://schemas.microsoft.com/winfx/2006/xaml'
}
# Query the XAML file, passing the hashtable to -Namespace, and
# using its keys as namespace prefixes; here, '_:' must be used
# to refer to nodes in the default namespace.
Select-Xml -LiteralPath sample.xml -Namespace $namespaces -XPath '//_:TextBox'
If you want to avoid having to deal with namespaces:
In the context of Select-Xml (as well as the underlying System.Xml.XmlDocument.SelectNodes() .NET method), the only way to avoid having to deal with namespaces is to use the *[local-name()='...'] workaround shown in your own answer.
In the context of PowerShell's adaption of the [xml] DOM, which adds "virtual" properties to instances of [xml] (System.Xml.XmlDocument):
These properties are always named for the non-prefixed node names; that is, namespaces are effectively ignored.
This is convenient and often sufficient, but it limits you to "drilling down" with dot notation into the document, as opposed to having being able to run XPath queries against the document; for the latter, you can use the .SelectNodes() and .SelectSingleNode() methods, which, however, again necessitate namespace management.[1]
The equivalent example with dot notation, building on the same sample file:
# Parse the XML file into an [xml] instance.
($doc = [xml]::new()).Load((Convert-Path sample.xml))
# Drill down to the <TextBox> element.
# Note: If the <Grid> element had *multiple* <TextBox> children,
# they would *all* be returned, as a System.Xml.XmlElement array.
$doc.Window.Grid.TextBox
[1] The need for namespace management applies analogously to direct use of the underlying System.Xml.XmlDocument.SelectNodes() and System.Xml.XmlDocument.SelectSingleNodes() .NET API, although constructing the prefix-to-URI mapping table is a little more cumbersome there - see this answer.
I think that, similarly to xmllint there might be an issue with handling namespaces.
I've found that the following does work:
(select-xml -path $xaml_file_path -xpath "//*[local-name()='GroupBox']").Node
If anyone knows of a cleaner/better way to do this (i.e. using the -namespace flag), I would be curious to hear about it.

Setting a default list of items in project_attribute in a GNAT GPS Plugin

I'm working on a custom GNAT GPS plugin (for GPS 6.1.2).
My plugin XML creates a project attribute "example_list_of_files".
This is a list of strings, that correspond the names of Ada files in the project.
I want to default the entries in that list to "a.adb","b.adb","c.adb". However I've been unable to find the correct syntax for this. Instead i end up with a single string of all the values.
What i want to see is what happens when you manually add three elements, as shown below:
Here is the code for this example:
GPS.parse_xml('<?xml version="1.0" ?>' + """
<my_plugin>
<project_attribute
name="example_list_of_files"
label="example_list_of_files"
description="A description...."
package="MyPackage"
editor_page="MyPage"
editor_section="Build"
hide_in="wizard library_wizard"
omit_if_default="false"
list="true"
base_name_only="true">
<string type="file" filter="project" default="'a.adb','b.adb','c.adb' " />
</project_attribute>
</my_plugin>""");
Notice the string element with the project attribute default. Instead of a list of entries in the project manager it gives me a single entry, containing the string "'a.adb', 'b.adb', 'c.adb'".
Anyone got any ideas? I've also tried multiple string elements, adding brackets, braces, square-brackets, space separators, prefixing with 'array(' with no luck.
thanks
Matt
It seems indeed this is not supported. The standard plug-in projects.py has several list attributes, but all of them have a single value as the default. I'll check what can be done to improve here.
However, your approach might be wrong in the first place. The default you are setting only concerns the project editor. That means that if a user uses a default project (like project default is end default) and never goes through the project editor, your attribute example_list_of_files will in fact not exist (and have a default empty value). So it seems that this should in fact be handled in your plug-in, when you query the value of the attribute (like via GPS.Project.get_attribute_as_list). If that function returns an empty list, then use ("a.adb", "b.adb", "c.adb") instead. That way, things work fine even with a default, unedited project.
From the GPS User's Guide:
The tag accepts the following attributes:
[...]
list (boolean, default false)
If true, the project attribute contains a list of values, as opposed
to a single value. An example is the list of source directories in
standard projects.
In your example:
<string type="file" filter="project" default="'a.adb','b.adb','c.adb' " />
This is a single string value. Instead, you should specify a list of string values, like this:
<string type="file" filter="project" default="a.adb" />
<string type="file" filter="project" default="b.adb" />
<string type="file" filter="project" default="c.adb" />

Possibility of more then one xml input to jet?

Can we give more then one xml input to jet(java emitter template)?
If we can, then please give me an example.
You can't do it directly, but there are several techniques you can use to specify multiple XML's as input.
You can name one XML file inside the other or create a third XML file that simply names the two inputs (or however many you have). Say you have an XML file named in the input XML to JET:
<inputs>
<input name="...full file name here..." />
<inputs>
You can load this file in for use in JET like this:
<c:load url="{/inputs/input/#name}" var="root"/>
The load tag reads a file (defaults to XML, but other types can be input.) and makes its contents available via the variable name you specify ("root" in this case). Normally the url attribute expects a String value that is the file name, but since we're using an attribute out of the model we use the curly brackets and xpath notation to indicate the attribute value to use as the file name.
Once you've loaded the file in you can access its contents. For example, if the root element in that XML file was and it had an attribute named company, then you attribute could access that attribute value with the c:get
<c:get select="$root/policies/#company" />
You can load as many files as you want. Just use a different variable name to refer to the root of each parsed file.
You could get a bit more complex and store your multiple files in the same directory using a naming convention. Then you could just specify the one directory path and derive the file names for all of your inputs.

XPath Powershell query for a string

I'd like to know how to search for a string within an xml document. The object type is System.Xml.XmlNode.XmlDocument. The string can be anything with the document. I.e. attribute or element.
I tried
Select-Xml -Xml $xml -XPath "./Test"
but got no results
The pattern you are trying to use selects root nodes named Test.
You could use the pattern (//text()|//#*)[contains(string(), "test")], that selects the attributes that contain the string test or the text nodes that contain it (i.e. not the elements).
But you want to select the elements, right? Using (//*|//#*)[contains(., "test")] does that, but it selects elements that contain the string test, even if it is through some child element, which is not what is wanted either.
So I guess you'll have to use something like (//*[contains(text(), "test")]|//#*[contains(., "test")]), which gives you what you want, but is not very pretty.