Replacing of XML Namespaces via VBA doesn't work after adding new data - excel

I'm generating a XML file from an Excel table with a VBA code. The code also replaces namespaces that Excel names incorrectly. It works, as long as I only have a single set of data for an element (the element can occur more than once). As soon as I want to add new data, the VBA code only creates and saves the file without changing the namespaces. Can someone tell me how to change the code so it still changes the namespaces after adding new elements?
The VBA code:
Option Explicit
Sub ExportXml()
Dim exportResult As XlXmlExportResult
Dim exportPath As String
Dim xmlMap As String
Dim fileContents As String
exportPath = RequestExportPath()
If exportPath = "" Or exportPath = "False" Then Exit Sub
xmlMap = range("XmlMap")
exportResult = ActiveWorkbook.XmlMaps(xmlMap).Export(exportPath, True)
If exportResult = xlXmlExportValidationFailed Then
Beep
Exit Sub
End If
fileContents = ReadInTextFile(exportPath)
fileContents = ApplyReplaceRules(fileContents)
WriteTextToFile exportPath, fileContents
End Sub
Function ApplyReplaceRules(fileContents As String) As String
Dim replaceWorksheet As Worksheet
Dim findWhatRange As range
Dim replaceWithRange As range
Dim findWhat As String
Dim replaceWith As String
Dim cell As Integer
Set findWhatRange = range("FindWhat")
Set replaceWithRange = range("ReplaceWith")
For cell = 1 To findWhatRange.Cells.Count
findWhat = findWhatRange.Cells(cell)
If findWhat > "" Then
replaceWith = replaceWithRange.Cells(cell)
fileContents = Replace(fileContents, findWhat, replaceWith)
End If
Next cell
ApplyReplaceRules = fileContents
End Function
Function RequestExportPath() As String
Dim messageBoxResult As VbMsgBoxResult
Dim exportPath As String
Dim message As String
message = "The file already exists. Do you want to replace it?"
Do While True
exportPath = Application.GetSaveAsFilename("", "XML Files (*.xml),*.xml")
If exportPath = "False" Then Exit Do
If Not FileExists(exportPath) Then Exit Do
messageBoxResult = MsgBox(message, vbYesNo, "File Exists")
If messageBoxResult = vbYes Then Exit Do
Loop
RequestExportPath = exportPath
End Function
Function FileExists(path As String) As Boolean
Dim fileSystemObject
Set fileSystemObject = CreateObject("Scripting.FileSystemObject")
FileExists = fileSystemObject.FileExists(path)
End Function
Function ReadInTextFile(path As String) As String
Dim fileSystemObject
Dim textStream
Dim fileContents As String
Dim line As String
Set fileSystemObject = CreateObject("Scripting.FileSystemObject")
Set textStream = fileSystemObject.OpenTextFile(path)
fileContents = textStream.ReadAll
textStream.Close
ReadInTextFile = fileContents
End Function
Sub WriteTextToFile(path As String, fileContents As String)
Dim fileSystemObject
Dim textStream
Set fileSystemObject = CreateObject("Scripting.FileSystemObject")
Set textStream = fileSystemObject.CreateTextFile(path, True)
textStream.Write fileContents
textStream.Close
End Sub
I named everything I wanted to change FindWhat and everything that should replace it ReplaceWith.
I expect the output to be e.g. Melder, instead it still shows me ns1:Melder. This only happens, when I have more than one listing of an element. Otherwise it works.
A sampel of the XML I get right now is:
<?xml version="1.0" encoding="UTF-8"?>
<ns1:LIEFERUNG-DIREK xmlns:ns1="http://www.bundesbank.de/xmw/direk/2015-01-01" xmlns:ns2="http://www.bundesbank.de/xmw/2003-01-01" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1" erstellzeit="2001-12-17T09:30:47Z" stufe="Test" dateireferenz="1" bereich="Statistik">
<ns1:MELDER>
<ns1:FIRMENNR>Muster</ns1:FIRMENNR>
<ns1:NAME>Muster</ns1:NAME>
</ns1:MELDER>
<ns1:FORMULAR-K3>
<ns1:K3 lfdnr="1" meldeart="endgueltig">
<ns1:BILANZ>
<ns1:BILANZSTICHTAG>2015-12-31</ns1:BILANZSTICHTAG>
</ns1:BILANZ>
</ns1:K3>
<ns1:K3 lfdnr="2" meldeart="endgueltig">
<ns1:BILANZ>
<ns1:BILANZSTICHTAG>2015-12-31</ns1:BILANZSTICHTAG>
</ns1:BILANZ>
</ns1:K3>
</ns1:FORMULAR-K3>
</ns1:LIEFERUNG-DIREK>
What I need:
<?xml version="1.0" encoding="UTF-8"?>
<LIEFERUNG-DIREK xmlns:bbk="http://www.bundesbank.de/xmw/2003-01-01" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.bundesbank.de/xmw/direk/2015-01-01" version="1.0" erstellzeit="2019-06-07T08:30:54Z" stufe="Test" dateireferenz="1" bereich="Statistik" xsi:schemaLocation="http://www.bundesbank.de/xmw/direk/2015-01-01 BbkXmwDirek_2015.xsd">
<bbk:MELDER>
<FIRMENNR>Muster</FIRMENNR>
<bbk:NAME>Muster</bbk:NAME>
</bbk:MELDER>
<FORMULAR-K3>
<K3 lfdnr="1" meldeart="endgueltig">
<BILANZ>
<BILANZSTICHTAG>2015-12-31</BILANZSTICHTAG>
</BILANZ>
</K3>
<K3 lfdnr="2" meldeart="endgueltig">
<BILANZ>
<BILANZSTICHTAG>2015-12-31</BILANZSTICHTAG>
</BILANZ>
</K3>
</FORMULAR-K3>
</LIEFERUNG-DIREK>
As you can see K3 appears more than once. If it only appears once, the code works.

Rather than handle XML changes using text files read/writes, consider XSLT, the special-purpose language designed to transform XML. VBA can run XSLT 1.0 scripts with the MSXML library. One of XSLT's strengths is handling namespaces including default and multiple prefixes which is a challenge with your needs.
Specifically, below XSLT walks down the tree re-writing the needed elements for their local names (i.e., without prefixes) mapping to the new default: xmlns="http://www.bundesbank.de/xmw/direk/2015-01-01".
XSLT (save below as .xsl file, a special .xml file)
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:doc="http://www.bundesbank.de/xmw/direk/2015-01-01"
xmlns:bbk="http://www.bundesbank.de/xmw/2003-01-01"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.bundesbank.de/xmw/direk/2015-01-01 BbkXmwDirek_2015.xsd"
xmlns:ext="urn:schemas-microsoft-com:xslt"
exclude-result-prefixes="ext" >
<xsl:output indent="yes"/>
<xsl:strip-space elements="*"/>
<xsl:param name="bbk_nmsp" select="'http://www.bundesbank.de/xmw/2003-01-01'"/>
<xsl:variable name="vbbk">
<xsl:element name="bbk:x" namespace="{$bbk_nmsp}"/>
</xsl:variable>
<!-- IDENTITY TRANSFORM -->
<xsl:template match="#*|node()">
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="doc:LIEFERUNG-DIREK">
<xsl:element name="LIEFERUNG-DIRE" namespace="http://www.bundesbank.de/xmw/direk/2015-01-01">
<xsl:copy-of select="namespace::*[.='xsi']"/>
<xsl:copy-of select="ext:node-set($vbbk)/*/namespace::*[.=$bbk_nmsp]"/>
<xsl:attribute name="xsi:schemaLocation">http://www.bundesbank.de/xmw/direk/2015-01-01 BbkXmwDirek_2015.xsd</xsl:attribute>
<xsl:apply-templates select="node()|#*"/>
</xsl:element>
</xsl:template>
<xsl:template match="doc:MELDER">
<xsl:element name="bbk:MELDER">
<xsl:apply-templates select="node()|#*"/>
</xsl:element>
</xsl:template>
<xsl:template match="doc:FIRMENNR|doc:NAME|doc:FORMULAR-K3|doc:K3|doc:BILANZ|doc:BILANZSTICHTAG">
<xsl:element name="{local-name()}" namespace="http://www.bundesbank.de/xmw/direk/2015-01-01">
<xsl:apply-templates select="node()|#*"/>
</xsl:element>
</xsl:template>
</xsl:stylesheet>
Online Demo
VBA
Sub XSLTransform()
On Error GoTo ErrHandle
Dim xmldoc As New MSXML2.DOMDocument, xslDoc As New MSXML2.DOMDocument
Dim newDoc As New MSXML2.DOMDocument
' LOAD XML AND XSL FILES
xmlDoc.async = False
xmlDoc.Load "C:\Path\To\InputXML.xml"
xslDoc.async = False
xslDoc.Load "C:\Path\To\XSLT_Script.xml"
' TRANSFORM XML
xmldoc.transformNodeToObject xslDoc, newDoc
newDoc.Save "C:\Path\To\OutputXML.xml"
Set xmlDoc = Nothing: Set xslDoc = Nothing: Set newDoc = Nothing
End Sub

Related

Import XML data using Excel VBA

I'm trying to import specific data from and XML file to an Excel sheet.
The code I'm using is this.
Dim oXMLFile As New DOMDocument60
Dim books As IXMLDOMNodeList
Dim results() As String
Dim i As Integer, booksUBound As Integer
Dim book As IXMLDOMNode, title As IXMLDOMNode, author As IXMLDOMNode
oXMLFile.Load "C:\example.xml"
Set books = oXMLFile.SelectNodes("/OUT_MESSAGE/LINES/OUT_MESSAGE_LINE")
booksUBound = books.Length - 1
ReDim results(booksUBound, 1)
For i = 0 To booksUBound
Set book = books(i)
Set title = book.SelectSingleNode("C00")
If Not title Is Nothing Then results(i, 0) = title.Text
Next
Dim wks As Worksheet
Set wks = ActiveSheet
wks.Range(wks.Cells(1, 1), wks.Cells(books.Length, 2)) = results
Which works with this XML
<?xml version="1.0" encoding="UTF-8"?>
<OUT_MESSAGE>
<LINES>
<OUT_MESSAGE_LINE>
<C00>1231231</C00>
<C01>3213213</C01>
</OUT_MESSAGE_LINE>
<OUT_MESSAGE_LINE>
<C00>1231234</C00>
<C01>3213214</C01>
</OUT_MESSAGE_LINE>
</LINES>
</OUT_MESSAGE>
My problem is that my XML file looks like this.
<?xml version="1.0" encoding="UTF-8"?>
<OUT_MESSAGE xmlns="urn:randomaddress-com:schema:test_out_message" xmlns:xsi="http://www.randomurl.com/123">
<LINES>
<OUT_MESSAGE_LINE>
<C00>1231231</C00>
<C01>3213213</C01>
</OUT_MESSAGE_LINE>
<OUT_MESSAGE_LINE>
<C00>1231234</C00>
<C01>3213214</C01>
</OUT_MESSAGE_LINE>
</LINES>
</OUT_MESSAGE>
Which I originally thought I could simply get to work by replacing
Set books = oXMLFile.SelectNodes("/OUT_MESSAGE/LINES/OUT_MESSAGE_LINE")
With
Set books = oXMLFile.SelectNodes("/OUT_MESSAGE xmlns='urn:randomaddress-com:schema:test_out_message' xmlns:xsi='http://www.randomurl.com/123'/LINES/OUT_MESSAGE_LINE")
But that gives me a runtime error.
If anyone know what changes I have to do to the original code that would be much appreciated.
This worked for me:
Dim xDoc, nodes, oNode
Set xDoc = CreateObject("MSXML2.DOMDocument.6.0")
'Note: added an `x=` to the default namespace so we can reference it later
xDoc.setProperty "SelectionNamespaces", _
"xmlns:x='urn.randomaddress.com.schema.test_out_message'"
xDoc.LoadXML Sheet2.Range("A4").Value 'load XML from sheet
'use the "x" prefix we added above
Set nodes = xDoc.SelectNodes("/x:OUT_MESSAGE/x:LINES/x:OUT_MESSAGE_LINE")
Debug.Print nodes.Length ' = 1
For Each oNode In nodes
Debug.Print oNode.SelectSingleNode("x:C00").nodeTypedValue
Debug.Print oNode.SelectSingleNode("x:OBJSTATE").nodeTypedValue
'etc
Next oNode
using this XML:
<?xml version="1.0"?>
<OUT_MESSAGE xmlns="urn.randomaddress.com.schema.test_out_message"
xmlns:xsi="http://www.randomurl.com/123">
<LINES>
<OUT_MESSAGE_LINE>
<C00>321312</C00>
<C01>12312312</C01>
<OBJSTATE>Posted</OBJSTATE>
<OBJEVENTS>Accept^Reject^</OBJEVENTS>
<STATE>Posted</STATE>
</OUT_MESSAGE_LINE>
</LINES>
</OUT_MESSAGE>

Create new XML Element as a Parent Node of Existing Element

I would like to create a new XML element to make my existing XML node as a child node of this new element. The structure of my current XML file is:
<?xml version="1.0" encoding="utf-8"?>
<component>
<type name="A"></type>
<type name="B"></type>
</component>
My idea is to create new element "masterType" and make it as a parent node of existing "type" element.
<?xml version="1.0" encoding="utf-8"?>
<component>
<masterType>
<type name="A"></type>
<type name="B"></type>
</masterType>
</component>
My question is, how can I make this new element as a parent node of my existing xml node? What happens if I used insertBefore(), the "masterType" already ends before the element "type".
<?xml version="1.0" encoding="utf-8"?>
<component>
<masterType>
</masterType>
<type name="A"></type>
<type name="B"></type>
</component>
Here's my code
Dim fileName As String
fileName = ActiveSheet.OLEObjects("TextBox1").Object.Text
XMLFileName = fileName
Dim Found As Boolean
Dim docXMLDOM As DOMDocument
Dim nodeType As IXMLDOMNodeList
Dim nodElement As IXMLDOMElement
Dim nodNewElement As IXMLDOMElement
Dim nodReference As IXMLDOMElement
Set docXMLDOM = New DOMDocument
docXMLDOM.Load XMLFileName
Set nodeType = docXMLDOM.getElementsByTagName("type")
For Each nodElement In nodeType
If nodElement.Attributes.getNamedItem("name").Text = "A" Then
Set nodReference = nodElement
Set nodNewElement = docXMLDOM.createElement("masterType")
nodElement.ParentNode.InsertBefore nodNewElement, nodElement
Exit For
End If
Next
docXMLDOM.Save XMLFileName
Simplified example:
Sub AddParentNode()
Dim docXMLDOM As MSXML2.DOMDocument60
Dim els As IXMLDOMNodeList
Dim masterEl As IXMLDOMElement
Dim el As IXMLDOMElement
Set docXMLDOM = New MSXML2.DOMDocument60
docXMLDOM.LoadXML Range("A1").Value 'for testing
Debug.Print "*** Before ***"
Debug.Print docXMLDOM.XML
Set els = docXMLDOM.getElementsByTagName("type")
If els.Length > 0 Then
'create the new parent element
Set masterEl = docXMLDOM.createElement("masterType")
els(1).ParentNode.appendChild masterEl
End If
'append each "type" element into the new parent node
For Each el In els
masterEl.appendChild el.CloneNode(True)
el.ParentNode.RemoveChild el
Next
Debug.Print "*** After ***"
Debug.Print docXMLDOM.XML
End Sub

xpath query with backslashes return empty

I am trying to select a single MSXML2 node in excel using XPath predicates. I am able to select it just fine when I supply a string without backslashes. But as soon as I try with a file path string, the expression returns nothing.
Here is my XML:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Directory>
<Document>
<Path/>
<Status/>
<Notes/>
</Document>
<Document>
<Path>C:\Users\Ivelin\Desktop\Workspace\Requests\File.xlsm</Path>
<Status>Started</Status>
<Notes/></Document>
<Document>
<Path>TEST</Path>
<Status>Started</Status>
<Notes/>
</Document>
</Directory>
This works:
Dim Stat As IXMLDOMNode
Dim strPath
strPath = "/Directory/Document[Path='TEST']/Status/text()"
MsgBox (strPath)
Set Stat = XDoc.SelectSingleNode(strPath)
MsgBox (Stat.NodeValue)
This returns null:
Dim Stat As IXMLDOMNode
Dim strPath
strPath = "/Directory/Document[Path='C:\Users\Ivelin\Desktop\Workspace\Requests\File.xlsm']/Status/text()"
MsgBox (strPath)
Set Stat = XDoc.SelectSingleNode(strPath)
MsgBox (Stat.NodeValue)
I tried different suggestions, double backslashes etc. but no luck. Since I am interested in file names/paths, I don't really have other option, but to use backslashes.
Any pointers on how to solve this are welcome.
I see nothing wrong with your xpath. Perhaps the error lies elsewhere. I used the following loading your xml from file; no problem.
Option Explicit
Public Sub test()
Dim xmlDoc As Object, item As Object
Set xmlDoc = CreateObject("MSXML2.DOMDocument") 'New MSXML2.DOMDocument60
With xmlDoc
.validateOnParse = True
.setProperty "SelectionLanguage", "XPath"
.async = False
If Not .Load("C:\Users\User\Desktop\Test.xml") Then
Err.Raise .parseError.ErrorCode, , .parseError.reason
End If
End With
Dim path As String
path = "/Directory/Document[Path='C:\Users\Ivelin\Desktop\Workspace\Requests\File.xlsm']/Status/text()"
Set item = xmlDoc.SelectSingleNode(path)
Debug.Print item.Text
End Sub

Get the values of id2

<?xml version="1.0" encoding="UTF-8"?>
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
<s:Body xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<id xmlns="id.services/">
<ids1>
<response xmlns="">
<ids xmlns="ids">
<Id-info xmlns="" id0="123" id1="0" id2="2345" />
<Id-info xmlns="" id0="456" id1="1" id2="6789" />
</ids>
</response>
</ids1>
</id>
</s:Body>
</s:Envelope>
How can I get the values of id2 using vba excel?. This is the code that I have tried
Dim xmlDoc As DOMDocument30
Set xmlDoc = New DOMDocument30
xmlDoc.Load ("C:test.xml")
Dim id As String
id = xmlDoc.SelectSingleNode("//ids/Id-info").Attributes.getNamedItem("id2").Text
You will only access one value with that.
Try
Option Explicit
Public Sub test()
Dim xmlDoc As Object, items As Object, node As IXMLDOMElement
Set xmlDoc = CreateObject("MSXML2.DOMDocument") 'New MSXML2.DOMDocument60
With xmlDoc
.validateOnParse = True
.setProperty "SelectionLanguage", "XPath"
.async = False
If Not .Load("C:\Users\User\Desktop\Test.xml") Then
Err.Raise .parseError.ErrorCode, , .parseError.reason
End If
End With
Set items = xmlDoc.SelectNodes("//Id-info")
If Not items Is Nothing Then
For Each node In items
Debug.Print node.getAttribute("id2")
Next
End If
End Sub

Excel VBA - XML DomDocument return attribute values

I am working with the following XML response in Excel VBA.
<XXXXX docType="GetSegmentSpeed" copyright="Copyright XXXXX Inc." versionNumber="12.9" createdDate="2018-11-26T15:08:37Z" statusId="0" statusText="" responseId="06d3aad3-c3aa-40a5-9d2c-f1ac8f713729">
<SegmentSpeedResultSet coverage="255">
<SegmentSpeedResults timestamp="2018-11-26T15:08:36Z">
<Segment code="213423027" type="XDS" speed="53" average="34" reference="40" score="30" c-value="63" travelTimeMinutes="0.649" speedBucket="3"/>
<Segment code="213423023" type="XDS" speed="53" average="38" reference="41" score="30" c-value="58" travelTimeMinutes="0.603" speedBucket="3"/>
<Segment code="213423026" type="XDS" speed="52" average="34" reference="39" score="30" c-value="71" travelTimeMinutes="0.486" speedBucket="3"/>
<Segment code="213423050" type="XDS" speed="52" average="34" reference="39" score="30" c-value="71" travelTimeMinutes="0.48" speedBucket="3"/>
<Segment code="213423051" type="XDS" speed="52" average="35" reference="39" score="30" c-value="78" travelTimeMinutes="0.486" speedBucket="3"/>
</SegmentSpeedResults>
</SegmentSpeedResultSet>
</XXXXX>
I want to find the total of the travelTimeMinutes attributes of Segments.
To begin with, I thought I would try and get the value for the first segment. This is my code:
Sub SegSetTimes()
' Declare Private Variables
Dim SegString As String 'Segment set to be used for calculation
Dim hReq As New WinHttpRequest 'HttpRequest path
Dim strResp As String 'Response String
Dim xmlDoc As MSXML2.DOMDocument60 'DomDocument for parsing XML
' Import Segment Set
SegString = Join(WorksheetFunction.Transpose(Range("A2", Range("A2").End(xlDown)).Value), "|XDS,")
' Call for real-time segment information
hReq.Open "Get", "http://eu.api.XXXXX.com/Traffic/XXXXX.ashx?Action=GetSegmentSpeed" & "&token=" & AuthToken & "&Segments=" & SegString
hReq.Send
' Create string from response text
strResp = hReq.ResponseText
' Import response text into DomDocument for parsing within VBA
Set xmlDoc = New MSXML2.DOMDocument60
If Not xmlDoc.LoadXML(strResp) Then
MsgBox "Load Error"
End If
Dim n As IXMLDOMNodeList
Set n = xmlDoc.SelectNodes("//XXXXX/SegmentSpeedResultSet/SegmentSpeedResults")
Dim TT As Single
TT = n.Item(0).Attributes.getNamedItem("travelTimeMinutes")
End Sub
It fails with the following error:
Run-time error '91': Object variable or With block variable not set'
When stepping through in Locals, my IXMLDOMNodeList n looks correct. I just cannot see how to get at the values I want to.
Does anybody have any suggestions?
Reading in from a file I use an XPath to get the relevant nodes and then extract the value using getAttribute
Public Sub testing()
Dim xmlDoc As New MSXML2.DOMDocument60, items As Object, item As IXMLDOMElement, total As Double
Set xmlDoc = New MSXML2.DOMDocument60
xmlDoc.Load "C:\Users\User\Desktop\Test.xml"
Set items = xmlDoc.SelectNodes("//Segment[#travelTimeMinutes]")
For Each item In items
total = total + item.getAttribute("travelTimeMinutes")
Next
Debug.Print total
End Sub
Alternatively, consider running XSLT to retrieve the sum() across all nodes without looping:
XSLT (save as .xsl file, a special .xml file to be referenced in VBA)
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output indent="yes"/>
<xsl:strip-space elements="*"/>
<xsl:template match="/XXXXX">
<result>
<xsl:value-of select="format-number(sum(descendant::Segment/#travelTimeMinutes), '#.###')"/>
</result>
</xsl:template>
</xsl:stylesheet>
XSLT Demo
VBA
Dim xmlDoc As New MSXML2.DOMDocument60, items As Object, item As IXMLDOMElement, total As Double
' NEW REFERENCES
Dim xslDoc As New MSXML2.DOMDocument60, newDoc As New MSXML2.DOMDocument60
' RETRIEVE WEB REQUEST
...same code as above...
' LOAD XML AND XSL FILES
xmlDoc.async = False
xml.LoadXML strResp
xslDoc.async = False
xslDoc.Load "C:\Path\to\XSLT\File.xsl"
' TRANSFORM XML
xmlDoc.transformNodeToObject xslDoc, newDoc
' OUTPUT RESULT (NO LOOPING)
Debug.Print newDoc.SelectSingleNode("/result").Text
' 2.704

Resources