user2226161 user2226161 - 1 month ago 16
Perl Question

XML::LibXML and namespaces

I'm familiar with XML, having put it to some simple uses. However, I've been requested to create a custom solution (that must use Perl) for submitting payments to AuthorizeNet's gateway. AuthNet offers no support for Perl, so I'm on my own.

I am using AuthNet's latest method, which relies on sending and receiving transaction data in XML format.

I have managed to get test (sandbox) transactions successfully submitted and approved, using the Perl module XML::LibXML.

I hit a snag when trying to parse the return data. I finally figured out I was dealing with namespaces for the first time. The sandbox return data I get is as follows.

<createTransactionResponse
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns="AnetApi/xml/v1/schema/AnetApiSchema.xsd">

<refId />

<messages>
<resultCode>Ok</resultCode>
<message>
<code>I00001</code>
<text>Successful.</text>
</message>
</messages>

<transactionResponse>
<responseCode>1</responseCode>
<authCode>GTR92Q</authCode>
<avsResultCode>Y</avsResultCode>
<cvvResultCode>P</cvvResultCode>
<cavvResultCode>2</cavvResultCode>
<transId>40003699670</transId>
<refTransID />
<transHash>7E1148E18D5D0E0BE202EB1BCE4B0F09</transHash>
<testRequest>0</testRequest>
<accountNumber>XXXX1111</accountNumber>
<accountType>Visa</accountType>
<messages>
<message>
<code>1</code>
<description>This transaction has been approved.</description>
</message>
</messages>
<userFields>
<userField>
<name>MerchantDefinedFieldName1</name>
<value>MerchantDefinedFieldValue1</value>
</userField>
</userFields>
<transHashSha2 />
</transactionResponse>

</createTransactionResponse>


I need to extract certain values from that. I take the above data, returned from the AuthNet gateway, store it in the var
$cont
, and prepare it:

my $dom = XML::LibXML->load_xml(string => $cont);


Then I attempt to parse through the data nodes, searching for certain values, for example:

foreach my $transres ($dom->findnodes("/createTransactionResponse/messages/message")) {
print "Message: " . $transres->to_literal() . "<br />\n";
}


This never returns any values. Then I realized I was dealing with namespace issues. I tried many variations of registering the namespaces, to no avail, for example:

my $xpc = XML::LibXML::XPathContext->new($dom);
$xpc->registerNs("xsi", "http://www.w3.org/2001/XMLSchema-instance");
$xpc->registerNs("xsd", "http://www.w3.org/2001/XMLSchema");

foreach my $transres ($xpc->findnodes("//xsi:resultCode")) {
print "Result: " . $transres->to_literal() . "<br />\n";
}


This was all just casting about, hoping for some kind of result. I noticed that, even though the return data refers to multiple namespaces, none of them actually get used anywhere in the rest of the data. I found a number of StackOverflow responses on the XML namespace issue, but I could never find an example of how to reference nodes when none of the specified namespaces get used elsewhere in the XML data. Like, what is the default (if you will) namespace reference to use, when no namespace get referenced... If that makes sense.

I came across an entry elsewhere in StackOverflow, wherein it mentions using a "local-name" function that allows the ignoring of namespaces. I thought that might bypass the problem, and when I tried it, I did finally see some data display.

foreach my $transres ($dom->findnodes("//*[local-name()='resultCode']")) {
print "Result: " . $transres->to_literal() . "<br />\n";
}


This resulted in the return of the desired/expected value "Ok". It also works on the other nodes I have tried thus far. However, I am usually leery of using "just ignore it" type solutions, unless they are genuinely appropriate and called for.

My question comes down to this: Is the bypass-namespace method I used to finally access data from the returned XML data advisable?

I was wondering why AuthNet specifies multiple namespaces in the return data, and then uses none of them in the data - at least the data I am currently getting back. What if I just bypass the namespaces in my code, but then later, especially when we go live, the different namespaces become relevant? I actually don't know, and the AuthNet so-called "support" I have been able to reach has ranged from unhelpful to completely unresponsive.

Any advice/guidance from those more experienced with Perl/XML/namespace issues would be greatly appreciated.

Answer

You're on broadly the right lines. The problem is that your XML data uses a default namespace which applies to all identifiers that have no explicit namespace prefix

xmlns="AnetApi/xml/v1/schema/AnetApiSchema.xsd"

As you observed, the xsi and xsd prefixes are unused in your data, so there is no need to specify them in your Perl code. Meanwhile, XPath has no concept of implicit namespaces, so you must specify an explicit abbreviation for the AuthNet namespace and use it in all of your XPath expressions.

The abbreviation used doesn't have to be the same as the one defined in the XML data, and clearly it cannot match in the case of a default namespace.

Here's how I would code your access to the resultCode element. I always prefer to use a full XPath expression from the document root, rather than search all elements in the document with, say, //anet:resultCode

my $dom = XML::LibXML->load_xml( string => $cont );

my $xpc = XML::LibXML::XPathContext->new( $dom );
$xpc->registerNs( anet  => 'AnetApi/xml/v1/schema/AnetApiSchema.xsd' );

for my $transres ( $xpc->findnodes('/anet:createTransactionResponse/anet:messages/anet:resultCode' ) ) {
    printf "Result: %s<br />\n", $transres->to_literal;
}