Question
· Jan 3, 2023

How to canonicalize XML enabled classes?

I have a registered object class that extends %XML.Adaptor and I want to convert an object of it into canonical XML. 

From reading the documentation and some trial and error, it seems like I would need to use %XML.Writer to write the object to XML first using the RootObject() method, then read it with %XML.Reader, and then use the Canonicalize() method of %XML.Writer to write it out again.

Is there a better approach?

Product version: Caché 2017.1
Discussion (2)1
Log in or sign up to continue

I had to do something like this a few years ago to add a digital signature to a message in XML format. If I remember correctly you have to get your object into a %XML.Document which works in conjunction with %XML.Node. %XML.Node is used to traverse the %XML.Document to get to the section you want in canonical form. Then you pass the Node to ##class(%XML.Writer).Canonicalize(Node) to get the XML as a string which is then passed to the encryption function you use to get your digest/signature. You can pass the whole document or just a subsection to the canonicalize function.

I can't say if it's the only or best way to do it but it was sufficiently quick enough to handle thousands of messages per minute.

Here's the code I use (by @Dmitry Zasypkin):

/// Canonicalize XML.
/// in: XML string or stream to canonicalize.
/// out: Canonicalized XML is returned in this argument. If it's a string, out must be passed by refrence.
/// elementId: attrubute Id to canonicalize. If elementId="", the entire document would be canonicalized.
/// prefixList: a local of namespace=prefix pairs to add to a root tag, only in a case of exclusive canonicalization.
ClassMethod canonicalize(in As %Stream.Object, ByRef out As %Stream.Object, isInclusive As %Boolean = {$$$NO}, keepWhitespace = {$$$YES}, elementId As %String = "", ByRef prefixList As %String = "", writer As %XML.Writer = {##class(%XML.Writer).%New()}) As %Status
{
	#dim sc As %Status = $$$OK
	
	#dim importHandler As %XML.Document = ##class(%XML.Document).%New()
	set importHandler.KeepWhitespace = keepWhitespace
	
	if $isObject(in)
	{
		set sc = ##class(%XML.SAX.Parser).ParseStream(in, importHandler,, $$$SAXFULLDEFAULT-$$$SAXVALIDATIONSCHEMA)
	}
	else
	{
		set sc = ##class(%XML.SAX.Parser).ParseString(in, importHandler,, $$$SAXFULLDEFAULT-$$$SAXVALIDATIONSCHEMA)
	}
	if $$$ISERR(sc) quit sc
	
	if $isObject(in) && $isObject($get(out)) && (in = out) do in.Clear()
	
	if $isObject($get(out))
	{
		set sc = writer.OutputToStream(out)
	}
	else
	{
		set sc = writer.OutputToString()
	}
	if $$$ISERR(sc) quit sc
	
	#dim node As %XML.Node = importHandler.GetDocumentElement()
	if (elementId '= "") set node = importHandler.GetNode(importHandler.GetNodeById(elementId))

	// Main part
	if isInclusive
	{
		set sc = writer.Canonicalize(node, "c14n")
	}
	else
	{
		if (+$data(prefixList) >= 10)
		{
			#dim prefix As %String = ""
			for 
			{
				set prefix = $order(prefixList(prefix))
				if (prefix = "") quit
				do writer.AddNamespace(prefixList(prefix), prefix)
			}	
		}

		set sc = writer.Canonicalize(node)
	}
	if $$$ISERR(sc) quit sc
	
	if '$isObject($get(out))
	{
		set out = writer.GetXMLString(.sc)
		if $$$ISERR(sc) quit sc
	}
	
	do writer.Reset()
	
	quit $$$OK
}