非常奇怪的问题:我使用 DOM 来编辑一个 xml 文件(一个需要与我们交互的应用程序的 .exe.config 文件),但是看到我必须批量添加几个类似的部分,我做了一个函数来插入整个需要的块。
调用此函数一次可以完美运行。之后使用不同的参数再次调用它会产生异常(参见代码下方的说明)。
编码:
// 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;
最初,UpdateConfig
函数是这样完成的:
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');
此代码第一次运行良好,但第二次在setAttribute
. AddAssemblyBinding
它变得更加奇怪......当我删除将属性设置为 assemblyIdentityNode 的三行时,其余代码对于其他节点运行良好。
我能想象的唯一相关的是,这些是我在HasNode
函数中查询的节点,以查看块是否已经存在。DOM 不能通过未保存的更改处理查询吗?所以我编辑了代码以提前进行存在性检查并将结果存储在布尔值中,因为我认为问题可能是在修改后的树上寻找节点。但是现在它给出了一个关于尝试将节点嵌套在自身或它自己的子节点下的错误(“msxml3.dll:不允许在自身下插入节点或其祖先”),就dependentAssemblyNode.appendChild(bindingRedirectNode);
行了。这些错误都没有任何意义。
我似乎越来越喜欢它了。EnsureXPath
,在必须添加节点的情况下第二次使用时,也会出现非法嵌套错误。我感觉不知何故,该对象在某处神秘地变为空,并且该空被视为处理节点对象的函数中的根节点。
有谁知道可能导致这种行为的原因?
我正在编辑的 XML 通常如下所示:
<?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>
(还有更多这些dependentAssembly 部分......但这并不重要)