Caligola
Caligola

Reputation: 99

Find and replace an XML node

I have an XML file defining the processes and some other parameters, which will be loaded by a CAN controller at start up. I will use this as a template for differrent controllers, so I need to manipulate the items in some sections of the XML. In particular I will be defining different Subcomponents and modifying some of the existing ones. And I have to use Excel VBA to handle all this stuff: eventually there will be tenths if not hundreds of controller with different definitions.

I've managed to get around the process of cloning and appending new nodes, but it looks like my brain is refusing to grasp the concepts behind the XML parsing, editing etc.

My problem at hand now is how to find and replace (or not if one or more attributes already satisfy some conditions: path on disk mostly) a child node of the Subcomponents tree. Here the template:

<?xml version="1.0" ?>
<Application Name="CONTROLLER_A" Type="Application">
  <InstanceHelp> This is a template for creating a controller applications.</InstanceHelp>
  <Handle>12</Handle>
  <!-- NetworkInterfaces -->
   <NetworkInterface Name="ETH0" MAC="" IPAddress="192.168.0.21" SubnetMask="255.255.255.0"></NetworkInterface>
  <CNTMessages>
  </CNTMessages>
  <!-- Console configuration
  Mode: VGA, VESA_8, VESA_16, VESA_24, VESA_32, Text, None. Use VESA_32 for graphical displays, Text or None for alphanumeric displays.
  Background: Black, White, LightGray, DarkGray, Brown
  Resolution: 320x240, 640x480, 800x600, 1024x768, 1280x1024
  HFreqMin: Minimum horizontal frequency [KHz], limited by monitor.
  HFreqMax: Maximum horizontal frequency [KHz], limited by monitor.
  VFreqMin: Minimum vertical frequency [Hz], limited by monitor.
  HFreqMax: Maximum vertical frequency [Hz], limited by monitor.
  VerticalRefresh: Desired vertical refresh rate [Hz], actual refresh may deviate some from specified value.
  MouseSpeed: Mouse speed multiplier.
  -->
  <Console Mode="VESA_32" Background="#f0f0f0" Resolution="1024x768" HFreqMin="24.0" HFreqMax="62.0" VFreqMin="50.0" VFreqMax="77.0" VerticalRefresh="75" Mouse="PS2" MouseSpeed="1"></Console>

  <Components>
  </Components>

  <!-- Custom application values -->

  <Subcomponents>
  <!-- I/o servers, -->
    <Subcomponent Name="Main" Type="Entry" src="Components\CNT\Main.xml"></Subcomponent>
    <Subcomponent Name="Messenger" Type="Messenger" src="Components\CNT\Messenger.xml"></Subcomponent>
    <Subcomponent Name="WebServer" Type="WebServer" src="Components\CNT\WebServer.xml"></Subcomponent>
    <Subcomponent Name="SerialStringCom1" Type="SerialStringDispatcher" src="Components\SerialStringCom1.xml"></Subcomponent>
    <Subcomponent Name="SerialStringCom2" Type="SerialStringDispatcher" src="Components\SerialStringCom2.xml"></Subcomponent>
    <Subcomponent Name="SerialStringCom3" Type="SerialStringDispatcher" src="Components\SerialStringCom3.xml"></Subcomponent>
    <Subcomponent Name="NmeaLog"   Type="SimpleNMEA" src="Components\NmeaLog.xml"></Subcomponent>
    <Subcomponent Name="NmeaAlarm" Type="SimpleNMEA" src="Components\NmeaAlarm.xml"></Subcomponent>
    <Subcomponent Name="NmeaCon"   Type="SimpleNMEA" src="Components\NmeaCon.xml"></Subcomponent>

    <Subcomponent Name="CANIO"        Type="CANIO"         src="Components/CANIO.xml"></Subcomponent>
    <Subcomponent Name="MessengerIOServer" Type="MessengerIOServer" src="Components\CNT\MessengerIOServer.xml"></Subcomponent>
    <Subcomponent Name="CodeSys" Type="CoDeSys" src="Components\CodeSys.xml"></Subcomponent>

  </Subcomponents>

  <Signals>
    <!-- Sysinfo signals -->
    <Signal Name="CPULoad"          Value="1.0" Type="double" RouteType="Variable" Routing="No routing" Description="CPU load [busy/idle, 0..1]."></Signal>
    <Signal Name="MemTotal"         Value="0"   Type="double" RouteType="Variable" Routing="No routing" Description="Memory total [kb]."></Signal>
    <Signal Name="MemUsed"          Value="0"   Type="double" RouteType="Variable" Routing="No routing" Description="Memory used [kb]."></Signal>
    <Signal Name="MemUsedRelative"  Value="0"   Type="double" RouteType="Variable" Routing="No routing" Description="Memory used relative [used/total, 0..1]."></Signal>
  </Signals>
  <Alarms>
    <Alarm Name="Broken signal routing" Level="Error" Enabled="1" Text="Something is wrong with the routing for one or more of the signals." Description="Something is wrong with the routing for one or more of the signals."></Alarm>
    <Alarm Name="Signal not updated"      Level="Error" Enabled="1" Text="The signal is not updated (time stamp too old). Probably lost connection with i/o."   Description="The signal is not updated (time stamp too old). Probably lost connection with i/o.."></Alarm>
  </Alarms>
</Application>

In particular: say that I want to delete:

<Subcomponent Name="NmeaCon" Type="SimpleNMEA" src="Components\NmeaCon.xml"></Subcomponent>

And also to change:

<Subcomponent Name="CANIO" Type="CANIO" src="Components/CANIO.xml"></Subcomponent>

to:

<Subcomponent Name="CANOPEN" Type="CANOPEN" src="Components/Canopen/CANOPEN.xml"></Subcomponent>

For removing I tried:

Set appXML = New MSXML2.DOMDocument
With appXML
    If Not .LoadXML("ControllerApp.xml") Then Exit Sub
    Set xmlSubTree = .SelectSingleNode("/Application/Subcomponents/Subcomponent").ParentNode
    dumString =  "//Subcomponent[@Name='SerialStringCom1']"
    Set oldNode = xmlSubTree.SelectSingleNode(dumString)
    ' checking oldNode here gives all the expected attributes values (text)
    ' but it fails on the next statement: Object does not support this property or method:
    oldNode.ParentNode.RemoveChild(oldNode)
End With

Upvotes: 0

Views: 1107

Answers (1)

Yitzhak Khabinsky
Yitzhak Khabinsky

Reputation: 22167

It is very easy to achieve by using XSLT. You just need to call XSLT from VBA.

XSLT will process your input XML and generate a new XML file:

XML + XSLT => new XML

XSLT

<?xml version="1.0"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    <xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
    <xsl:strip-space elements="*"/>

    <!-- identity template -->
    <xsl:template match="@* | node()">
        <xsl:copy>
            <xsl:apply-templates select="@* | node()"/>
        </xsl:copy>
    </xsl:template>

    <!-- remove Subcomponent template -->
    <xsl:template match='Subcomponent[@Name="NmeaCon" and @Type="SimpleNMEA" and @src="Components\NmeaCon.xml"]'>
    </xsl:template>

    <!-- modify Subcomponent template -->
    <xsl:template match='Subcomponent[@Name="CANIO" and @Type="CANIO" and src="Components/CANIO.xml"]'>
        <Subcomponent Name="CANOPEN" Type="CANOPEN" src="Components/Canopen/CANOPEN.xml"></Subcomponent>
    </xsl:template>
</xsl:stylesheet>

VBA

Private Sub Transform(sourceFile, stylesheetFile, resultFile)

    Dim source As New MSXML2.DOMDocument60
    Dim stylesheet As New MSXML2.DOMDocument60
    Dim result As New MSXML2.DOMDocument60

    ' Load data.
    source.async = False
    source.Load sourceFile

    ' Load style sheet.
    stylesheet.async = False
    stylesheet.Load stylesheetFile

    If (source.parseError.ErrorCode <> 0) Then
       MsgBox ("Error loading source document: " & source.parseError.reason)
    Else
        If (stylesheet.parseError.ErrorCode <> 0) Then
            MsgBox ("Error loading stylesheet document: " & stylesheet.parseError.reason)
        Else
            ' Do the transform.
            source.transformNodeToObject stylesheet, result
            result.Save resultFile
        End If
    End If

End Sub

Upvotes: 1

Related Questions