Monday, April 2, 2018

Exporting and Importing an XML Fragment in Delphi

I had a requirement to save part of an XML message, specifically part of a SOAP response containing a SAML token.

The objective here is to pull out a section of an XML document into another standalone document, which can then be saved, loaded or shared with another piece of code. This standalone fragment can then be inserted into another XML document.

This may sound like a simple exercise, but to get it to work correctly and on multiple platforms took a bit of research. I needed my code to work on Windows and MacOS. As originally written, it worked on Windows but failed with access violations in MacOS.

The key thing that I did not understand is that an XML node in Delphi needs to be created by the XML document that owns it. In Windows, I could assign a node from one document as a child of a node in the target document and it worked. Under MacOS, it did not.

I wrote a small library to copy the fragments from one document to another. The code for this was found in XML.XMLDoc in Delphi, but annoyingly it's internal code and cannot be accessed externally.

Here's my interface section.

unit SAML.Token.Helpers;



  TSAMLTokenHelper = class
    class procedure CopyChildNodes(const ADestDoc: IXMLDocument; const ASrcNode,
      ADestNode: IXMLNode);
    class function CloneNodeToDoc(const ASourceNode: IXMLNode;
      const ATargetDoc: IXMLDocument): IXMLNode;
    class procedure RemoveDeclNode(const ASourceDoc: IXMLDocument);
    class function ExportXMLFragment(const ASourceDoc: IXMLDocument;
      const ASourceNode: IXMLNode): IXMLDocument;
    class procedure ImportXMLFragment(const ASourceDoc, ADestDoc: IXMLDocument;
      const ADestNode: IXMLNode);

There are two public methods:

ExportXMLFragment - takes a source document and node and provides a new XML document containing the fragment.

ImportXMLFragment - takes a source and destination document and the parent node where the fragment is to be inserted. The source here is the fragment document.

There are three private methods used to copy the nodes and also to remove any XML declaration elements.

The implementation of these methods is as follows:


class function TSAMLTokenHelper.CloneNodeToDoc(const ASourceNode: IXMLNode; const ATargetDoc: IXMLDocument): IXMLNode; var i: Integer; begin case ASourceNode.nodeType of ntElement: begin Result := ATargetDoc.CreateElement(ASourceNode.NodeName, ASourceNode.NamespaceURI); for i := 0 to ASourceNode.AttributeNodes.Count - 1 do Result.AttributeNodes.Add(CloneNodeToDoc(ASourceNode.AttributeNodes[i], ATargetDoc)); end; ntAttribute: begin Result := ATargetDoc.CreateNode(ASourceNode.NodeName, ntAttribute, ASourceNode.NamespaceURI); Result.NodeValue := ASourceNode.NodeValue; end; ntText, ntCData, ntComment: Result := ATargetDoc.CreateNode(ASourceNode.NodeValue, ASourceNode.NodeType); ntEntityRef: Result := ATargetDoc.CreateNode(ASourceNode.nodeName, ASourceNode.NodeType); ntProcessingInstr: Result := ATargetDoc.CreateNode(ASourceNode.NodeName, ntProcessingInstr, ASourceNode.NodeValue); ntDocFragment: Result := ATargetDoc.CreateNode('', ntDocFragment); end; end; class procedure TSAMLTokenHelper.CopyChildNodes(const ADestDoc: IXMLDocument; const ASrcNode, ADestNode: IXMLNode); var i: Integer; SrcChild, DestChild: IXMLNode; begin for i := 0 to ASrcNode.ChildNodes.Count - 1 do begin SrcChild := ASrcNode.ChildNodes[I]; DestChild := CloneNodeToDoc(SrcChild, ADestDoc); ADestNode.ChildNodes.Add(DestChild); if SrcChild.HasChildNodes then CopyChildNodes(ADestDoc, SrcChild, DestChild); end; end; class function TSAMLTokenHelper.ExportXMLFragment(const ASourceDoc: IXMLDocument; const ASourceNode: IXMLNode): IXMLDocument; var DestChild: IXMLNode; begin Result := NewXMLDocument; if ASourceDoc.Version <> '' then Result.Version := ASourceDoc.Version; if ASourceDoc.StandAlone <> '' then Result.StandAlone := ASourceDoc.StandAlone; if ASourceDoc.Encoding <> '' then Result.Encoding := ASourceDoc.Encoding; DestChild := CloneNodeToDoc(ASourceNode, Result); Result.Node.ChildNodes.Add(DestChild); CopyChildNodes(Result, ASourceNode, DestChild); end; class procedure TSAMLTokenHelper.ImportXMLFragment(const ASourceDoc, ADestDoc: IXMLDocument; const ADestNode: IXMLNode); begin if ASourceDoc.ChildNodes.Count > 1 then RemoveDeclNode(ASourceDoc); CopyChildNodes(ADestDoc, ASourceDoc.Node, ADestNode); end; class procedure TSAMLTokenHelper.RemoveDeclNode(const ASourceDoc: IXMLDocument); var FirstNode: IXMLNode; begin FirstNode := ASourceDoc.Node.ChildNodes[0]; if (FirstNode.NodeName = 'xml') or (FirstNode.NodeName = '#xmldecl') then ASourceDoc.ChildNodes.Delete(0); end;

The key thing here is that the nodes are cloned from the source document to the fragment document. If the source document gets deleted by reference counting, the fragment will still be intact.

The fragment is a valid XML document which can be saved, loaded or passed to another bit of code.

I've shared this code in that hope that it proves useful to other Delphi developers.