Very strange problem: I use the DOM to edit an xml file (a .exe.config file for an app that needs to interact with ours), but seeing as I have to bulk-add several similar sections, I made a function to insert the whole needed block.
Calling this function once works perfectly. Calling it again with different parameters just afterwards gives an exception (see explanation below the code).
The code:
// Split a string into an array using passed delimeter
procedure Explode(var Dest: TArrayOfString; Text: String; Separator: String);
var
i: Integer;
begin
i := 0;
repeat
SetArrayLength(Dest, i+1);
if Pos(Separator,Text) > 0 then
begin
Dest[i] := Copy(Text, 1, Pos(Separator, Text)-1);
Text := Copy(Text, Pos(Separator,Text) + Length(Separator), Length(Text));
i := i + 1;
end
else
begin
Dest[i] := Text;
Text := '';
end;
until Length(Text)=0;
end;
// Ensures an XPath exists, creating nodes if needed
function EnsureXPath(const XmlDoc: Variant; XPath: string): Variant;
var
PathParts: TArrayOfString;
TestNode, CurrentNode, NewNode: Variant;
NodeList: Variant;
i, j: Integer;
found: Boolean;
begin
CurrentNode:=XMLDoc.documentElement;
Explode(PathParts, XPath, '/');
MsgBox('Array length: ' + IntToStr(GetArrayLength(PathParts)), mbInformation, MB_OK);
for i := 0 to GetArrayLength(PathParts) - 1 do
begin
MsgBox('Current path part:'#13#10 + '''' + pathparts[i] + '''', mbInformation, MB_OK);
if pathparts[i] <> '' then
begin
//MsgBox('Current node:'#13#10 + '''' + CurrentNode.NodeName + '''' + #13#10'Current path part:'#13#10 + '''' + PathParts[i] + '''' + #13#10'List length: ' + IntToStr(NodeList.Length), mbInformation, MB_OK);
MsgBox('Current node:'#13#10 + '''' + CurrentNode.NodeName + '''', mbInformation, MB_OK);
MsgBox('Current path part:'#13#10 + '''' + PathParts[i] + '''', mbInformation, MB_OK);
NodeList:= CurrentNode.childNodes;
MsgBox('List length: ' + IntToStr(NodeList.Length), mbInformation, MB_OK);
found:=false;
for j := 0 to NodeList.Length - 1 do
begin
TestNode:=NodeList.Item[j]
if (TestNode.NodeName = pathparts[i]) then
begin
currentNode:= TestNode;
found:=true;
end;
end;
if (not found) then
begin
newNode := XMLDoc.createElement(PathParts[i]);
currentNode.appendChild(newNode);
currentNode:=currentNode.lastChild;
end;
end;
end;
Result:=currentNode;
MsgBox('Last node:'#13#10 + '''' + CurrentNode.NodeName + '''', mbInformation, MB_OK);
end;
// Seeks out a node, returning the node in "resultnode", and whether it was found as Result.
function SeekNode(const ParentNode: Variant; var resultnode: Variant; subNodePath, attrName, attrValue :String; IsFirstCall: Boolean): Boolean;
var
NodesList: Variant;
AttrNode: Variant;
AttrList: Variant;
Attr: Variant;
PathParts, NewPathParts: TArrayOfString;
i, j, truelength: Integer;
currentPath, remainderPath: String;
CallAgain,callResult: Boolean;
begin
Explode(PathParts, subNodePath, '/');
truelength:=GetArrayLength(PathParts);
for i:=0 to GetArrayLength(PathParts) -1 do
begin
if PathParts[i] = '' then
truelength:=truelength-1;
end;
if (truelength <> GetArrayLength(PathParts)) then
begin
SetArrayLength(NewPathParts, truelength);
truelength:=0;
for i:=0 to GetArrayLength(PathParts) -1 do
begin
if PathParts[i] <> '' then
begin
NewPathParts[truelength] := PathParts[i];
truelength:=truelength+1;
end;
end;
end
else
NewPathParts:=PathParts;
CallAgain:=GetArrayLength(NewPathParts)>1;
currentPath:=NewPathParts[0];
remainderPath:='';
for i:=1 to GetArrayLength(NewPathParts) -1 do
begin
if (remainderPath <> '') then
remainderPath:=remainderPath + '/';
remainderPath:=remainderPath + NewPathParts[i];
end;
NodesList:=ParentNode.childNodes;
//MsgBox('Node count for ' + currentPath + ':'#13#10 + '''' + IntToStr(NodesList.length) + '''', mbInformation, MB_OK);
Result:=false;
for i := 0 to NodesList.length - 1 do
begin
attrNode := NodesList.Item[i];
//MsgBox('Current node:'#13#10 + '''' + attrNode.NodeName + ''''#13#10'Current path:'#13#10+ '''' + currentPath + '''', mbInformation, MB_OK);
if (attrNode.NodeName = currentPath) then
begin
if CallAgain then
begin
//MsgBox('Remainder of path:'#13#10 + '''' + remainderPath + '''', mbInformation, MB_OK);
callResult:=SeekNode(attrNode, resultnode, remainderPath, attrName, attrValue, false);
if callResult then
begin
Result:=true;
if IsfirstCall then
resultnode:=attrNode;
exit;
end;
end
else
begin
AttrList:=attrNode.Attributes;
//MsgBox('Node:'#13#10 + '''' + attrNode.NodeName + '''' + #13#10'Attributes count:'#13#10 + '''' + IntToStr(AttrList.Length) + '''', mbInformation, MB_OK);
for j := 0 to AttrList.length - 1 do
begin
Attr:= AttrList.Item[j];
//MsgBox('Node:'#13#10'''' + attrNode.NodeName + ''''#13#10'Attribute:'#13#10'''' + Attr.NodeName + ''''#13#10'Value:'#13#10'''' + Attr.NodeValue + ''''#13#10'To find:'#13#10'''' + AttrValue + '''', mbInformation, MB_OK);
if (Attr.NodeName = attrName) then
begin
if (Attr.NodeValue = attrValue) then
begin
//MsgBox('Attribute found.', mbInformation, MB_OK);
resultnode:=attrNode;
Result:=true;
Exit;
end
else
begin
Result:=false;
Exit;
end;
end;
end;
end;
end;
end;
end;
// Use of SeekNode: Remove node
function removeNode(const ParentNode: Variant; subNodePath, attrName, attrValue :String): Boolean;
var
resultNode: Variant;
begin
Result:=SeekNode(ParentNode, resultNode, subNodePath, attrName, attrValue, true);
if (Result) then
ParentNode.removeChild(resultNode);
end;
// Use of SeekNode: test node existence
function hasNode(const ParentNode: Variant; subNodePath, attrName, attrValue :String): Boolean;
var
resultNode: Variant;
begin
Result:=SeekNode(ParentNode, resultNode, subNodePath, attrName, attrValue, true);
end;
// Adds a single assembly binding block into the xml
procedure AddAssemblyBinding(const XmlDoc: Variant; const ParentNode: Variant; aiName, aiCulture, aiKey, brOld, brNew, cbVer, cbHref: String);
var
dependentAssemblyNode: Variant;
assemblyIdentityNode: Variant;
bindingRedirectNode: Variant;
codeBaseNode: Variant;
publisherPolicyNode: Variant;
begin
// <assemblyIdentity name="ECompas.Runtime" culture="" publicKeyToken="f27ad8cb97726f87" />
// <bindingRedirect oldVersion="3.0.1.0 - 3.0.1.133" newVersion="3.0.1.133" />
// <codeBase version="3.0.1.133" href="[TARGETDIR]Ecompas.Runtime.dll" />
// <publisherPolicy apply="no"/>
dependentAssemblyNode:= XMLDoc.createElement('dependentAssembly');
assemblyIdentityNode:= XMLDoc.createElement('assemblyIdentity');
assemblyIdentityNode.setAttribute('name', aiName);
assemblyIdentityNode.setAttribute('culture', aiCulture);
assemblyIdentityNode.setAttribute('publicKeyToken', aiKey);
dependentAssemblyNode.appendChild(assemblyIdentityNode);
if ((brOld <> '') and (brNew <> '')) then
begin
bindingRedirectNode:= XMLDoc.createElement('bindingRedirect');
bindingRedirectNode.setAttribute('oldVersion', brOld);
bindingRedirectNode.setAttribute('newVersion', brNew);
dependentAssemblyNode.appendChild(bindingRedirectNode);
end;
codeBaseNode:= XMLDoc.createElement('codeBase');
codeBaseNode.setAttribute('version', cbVer);
codeBaseNode.setAttribute('href', cbHref);
dependentAssemblyNode.appendChild(codeBaseNode);
publisherPolicyNode:= XMLDoc.createElement('publisherPolicy');
publisherPolicyNode.setAttribute('apply', 'no');
dependentAssemblyNode.appendChild(publisherPolicyNode);
// Doesn't work? No idea why it gives it an xmlns while its parent already has one.
//dependentAssemblyNode.RemoveAttribute('xmlns');
// It seems the actual variables of the nodes are somehow lost after adding
// them to a parent - so add everything to them in advance!
ParentNode.appendChild(dependentAssemblyNode);
end;
function UpdateConfig(const AFileName, Appdir: string; delete:Boolean): boolean;
var
XMLDoc: Variant;
RootNode, MainNode, AddNode: Variant;
bECompasRuntime, bECompasMetamodel, bECompasDatabaseMS: Boolean;
begin
try
XMLDoc := CreateOleObject('MSXML2.DOMDocument');
except
RaiseException('MSXML is required to complete the post-installation process.'#13#10#13#10'(Error ''' + GetExceptionMessage + ''' occurred)');
end;
XMLDoc.async := False;
XMLDoc.resolveExternals := False;
XMLDoc.load(AFileName);
if XMLDoc.parseError.errorCode <> 0 then
begin
MsgBox('XML processing error:'#13#10 + XMLDoc.parseError.reason, mbInformation, MB_OK);
Result:=False;
exit;
end;
XMLDoc.setProperty('SelectionLanguage', 'XPath');
RootNode:=XMLDoc.documentElement;
if (RootNode.nodeName <> 'configuration') then
begin
MsgBox('XML processing error:'#13#10'Root element ''configuration'' not found.', mbInformation, MB_OK);
Result:=False;
exit;
end;
MainNode:=EnsureXPath(XMLDoc, 'runtime/assemblyBinding');
bECompasRuntime := HasNode(MainNode,'dependentAssembly/assemblyIdentity','name','ECompas.Runtime');
bECompasMetamodel := HasNode(MainNode,'dependentAssembly/assemblyIdentity','name','ECompas.Metamodel');
bECompasDatabaseMS := HasNode(MainNode,'dependentAssembly/assemblyIdentity','name','ECompas.Database.MS');
if (not delete) then
begin
if not bECompasRuntime then
AddAssemblyBinding(XMLDoc, MainNode, 'ECompas.Runtime', '', 'f27ad8cb97726f87', '3.0.1.0 - 3.0.1.133', '3.0.1.133', '3.0.1.133', Appdir + '\Ecompas.Runtime.dll');
if not bECompasMetamodel then
AddAssemblyBinding(XMLDoc, MainNode, 'ECompas.Metamodel', '', 'f27ad8cb97726f87', '3.0.1.0 - 3.0.1.133', '3.0.1.133', '3.0.1.133', Appdir + '\Ecompas.Metamodel.dll');
if not bECompasDatabaseMS then
AddAssemblyBinding(XMLDoc, MainNode, 'ECompas.Database.MS', '', 'f27ad8cb97726f87', '3.0.1.0 - 3.0.1.133', '3.0.1.133', '3.0.1.133', Appdir + '\Ecompas.Database.MS.dll');
end
else
begin
removeNode(MainNode,'dependentAssembly/assemblyIdentity','name','ECompas.Runtime');
removeNode(MainNode,'dependentAssembly/assemblyIdentity','name','ECompas.Metamodel');
removeNode(MainNode,'dependentAssembly/assemblyIdentity','name','ECompas.Database.MS');
end;
MainNode:=EnsureXPath(XMLDoc, 'appSettings');
if (not delete) then
begin
//<add key="logdir" value=".\log" />
if (not HasNode(MainNode,'add','key','logdir')) then
begin
AddNode:= XMLDoc.createElement('add');
AddNode.setAttribute('key', 'logdir');
AddNode.setAttribute('value', '.\log');
MainNode.appendChild(AddNode);
end;
end
else
begin
removeNode(MainNode,'add','key','logdir');
end;
XMLDoc.Save(AFileName);
Result:=true;
end;
Originally, the UpdateConfig
function was done like this:
if (not HasNode(MainNode,'dependentAssembly/assemblyIdentity','name','ECompas.Runtime')) then
AddAssemblyBinding(XMLDoc, MainNode, 'ECompas.Runtime', '', 'f27ad8cb97726f87', '3.0.1.0 - 3.0.1.133', '3.0.1.133', '3.0.1.133', Appdir + '\Ecompas.Runtime.dll');
if (not HasNode(MainNode,'dependentAssembly/assemblyIdentity','name','ECompas.Metamodel')) then
AddAssemblyBinding(XMLDoc, MainNode, 'ECompas.Metamodel', '', 'f27ad8cb97726f87', '3.0.1.0 - 3.0.1.133', '3.0.1.133', '3.0.1.133', Appdir + '\Ecompas.Metamodel.dll');
if (not HasNode(MainNode,'dependentAssembly/assemblyIdentity','name','ECompas.Database.MS')) then
AddAssemblyBinding(XMLDoc, MainNode, 'ECompas.Database.MS', '', 'f27ad8cb97726f87', '3.0.1.0 - 3.0.1.133', '3.0.1.133', '3.0.1.133', Appdir + '\Ecompas.Database.MS.dll');
This code ran fine the first time, but the second time, it gave the aforementioned "Unknown Method" error on setAttribute
in AddAssemblyBinding
. It got more bizarre... when I removed the three lines setting the attributes to assemblyIdentityNode, the rest of the code DID run fine for the other nodes.
The only thing I could imagine is related is that these are the nodes I query in the HasNode
function to see if the block already exists. Can DOM not handle querying through unsaved changes? So I edited the code to do the existence checks in advance and store the result in Booleans, because I thought maybe the problem was seeking the nodes on a modified tree. But now it gives an error about trying to nest a node under itself or its own child ("msxml3.dll: Inserting a Node or its ancestor under itself is not allowed"), on the dependentAssemblyNode.appendChild(bindingRedirectNode);
line. Neither of these errors makes any sense whatsoever.
I seem to get loads more like it. EnsureXPath
, when used a second time in a situation where it had to add nodes, also gave the illegal nesting error. I get the feeling that somehow, the object mysteriously becomes null somewhere, and that that null is seen as the root node in functions handling node objects.
Does anyone have any clue what may be causing this behaviour?
The XML I'm editing typically looks like this:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="AppString1" publicKeyToken="43265234666" culture="neutral"/>
<bindingRedirect oldVersion="1.0.0.0-1.1.99.99" newVersion="1.2.0.0"/>
</dependentAssembly>
<dependentAssembly>
<assemblyIdentity name="AppString2" publicKeyToken="43265234666" culture="neutral"/>
<bindingRedirect oldVersion="1.0.0.0-1.1.99.99" newVersion="1.2.0.0"/>
</dependentAssembly>
</assemblyBinding>
</runtime>
</configuration>
(with some more of these dependentAssembly sections... but that hardly matters)