Skip to main content

Creating a soap client in Perl

Perl include the soap::lite modules that allow a soap client to be created. The documentation specifies how simple soap clients can be created, and there are many other pages that provide equivalent documentation, however there are only a few pages (e.g. 'Pauls' page )that provide information on how clients for more complex soap interfaces can be created

This is my attempt to document my simple implrmentation of an interface to a complex soap interface. Using Padwalker, and running the Perl within Eclipse is probably the best way to examine returned data to work out who the soap data shoudl be extracted if there are any problems.

A basic soap interface

The soap object declaration

As my background is with object oriented code my instinct was to create an object/package to handle the interface, which has two member variables; sIf for the underlying SOAP::Lite interface and errorStr to persist returned error strings

package soapObj;

import SOAP;
use SOAP::Lite;
use Moose;
use MooseX;

has 'sIf' => (is => 'rw', isa => 'SOAP::Lite');
has 'errorStr' => (is => 'rw');

Initialising the soap interface

In this case I wanted to use simple HTTP authorisation on the soap interface, so the username and password are passed into the init routine, and the URI is hardwired. The $uri could be passed in as a parameter and/or the authorisation code removed (the ->proxy can then be removed) as necessary.


method init($username, $password)
{

my $Uri = 'server.poppleton.ac.uk:4338';

$self -> sIf(SOAP::Lite
->uri('http://'.$Uri)
->proxy('http://'.$username.':'.$password.'@'.$Uri));

return 1;
}

The basic template for using a soap interface: returning a string (WDSL)

The simplest situation is an interface passing no parameters and returning a string, integer etc, which looks like:

method ServerVersion()
{
my $result = $self -> sIf ->serverVersion( SOAP::Data->type('none'));
if ($self -> IsFault($result)) {return 0;}
return $result -> result();
}

where 'serverVersion' is the underlying soap method name.

Fault detection

This and all subsequent examples uses 'IsFault' which checks to see if the method fails, and if it does retrieves the fault string which can be accessed subsequently.

method IsFault($result)
{
if ($result -> fault)
{
$self ->errorStr (join ',',$result->faultcode,$result->faultdetail);
return 1;
}
return 0;
}

Sample Client code

The following code uses the class that has just been defined to initialise the soap interface and return the serverVersion string

package main;
my $soapIf = new soapObj;

# Initialise the soap interface to the server

if ($soapIf -> init("username","password"))
{
# Get the server version
my $sv = $soapIf ->ServerVersion();
print 'ServerVersion:'.$sv."\n";
}

 

More complex requests

Multiple parameters (WDSL)

The next, slightly more complex situation, is when there are one or more simple parameters for the soap call. In the following sample firstName, lastName and age are the names of the parameters as declared in the wdsl.


method initUserData ($firstName,$surName,$age)
{

my $var1 = SOAP::Data->type('string') ->name('firstName')->value($firstName);
my $var2 = SOAP::Data->type('string') ->name('surName')->value($surName);
my $var3 = SOAP::Data->type('integer') ->name('age')->value($age);
my $var = SOAP::Data->type('complex') ->value($var1,$var2,$var3) ;

my $result = $self -> sIf ->initUserData ($var);
if ($self -> IsFault($result)) {return 0;}
return $result -> result();
}

Passing an array, eg of Strings (WDSL)

This is easy as SOAP::Lite automatically appears to do what is needed if the data is passed in as an array. The data type ('tns:stringArray' in the example below) is the name as stated in the wdsl.

sub addUserNames (@userNames)
{
my $var1 = SOAP::Data->type('tns:stringArray') ->name('userNames') ->value(@userNames);
my $var = SOAP::Data->type('complex') ->value($var1) ;

my $result = $self -> sIf ->addUserNames($var);
...

More complex responses

Returning multiple items of data (WDSL)

It is possible to return multiple items of data in a response, which will be defined in the wdsl using a complex data type with named components (in this case 'major' and 'minor'). SOAP::Lite automatically converts this to a hash, so can be returned as a single Perl object using the code as in the previous examples. The code for extracting these values will then be somthing like:


my $v = $soapIf ->ServerVersion();
print 'SoftwareVersion:'.$v -> {'major'}.":".$v -> {'minor'}."\n";

 

Returning an array (WDSL)

This is where it gets a little more tricky as SOAP::Lite puts the array behind a hash in the response. The method interface is simplified if only the underlying array is returned, in which case the code should look like:

sub GetUserNames()
{

my $result = $self -> sIf ->PssmSetNames(SOAP::Data->type('none'));
if ($self -> IsFault($result)) {return 0;}
my $s = $result -> result();

return @{$result -> result() -> {'string'}};
}

where the @{ ->{'string'}} extracts the array from the result. The hash value ('string' in this case) can be determined from the wdsl. The datatype in the wdsl is 'stringArray', but the SOAP::Lite interface removes the 'Array' componet of the name as this is implicit in the fact that the underlying data is in an array.

The code to exctract the values from the returned array then looks something like:

my @names = $soapIf ->GetUserNames();
print "Users: ";
for my $i (0 .. @names-1)
{
print $names[$i].',';
}

 

@names holds the array. When used in '(0 ..@names-1)', the context results in @names being interpreted as a scalar, which Perl interprets as the length of an array. A 'length()' operator would have been far more transparent, but Perl seems to have an aversion to constructs that create readable code.

Note that in '$names[$i]', we no longer appear to be referring to the @names variable as @names but $names. The Perl 'explanation' for this is that in this case the dollar is an indication that we are returning a scalar value from the array element.

Returning an arraY WHEN THE DATA MAY ONLY CONTAIN ONE ENTRY

Unfortunately, when the Perl code is expecting an array to be returned over the soap interface, but only one value is returns then SOAP::Lite helpfully returns this as a scalar, making it very difficult to have generic code that handles the returned data. To avoid this problem, the returned data needs to be examined, and if a scalar is returned then it should be converted into an array. The following code also checks for when no data at all is returned, and converts this to an array with no elements.

sub GetUserNames()
{

my $result = $self -> sIf ->PssmSetNames(SOAP::Data->type('none'));
if ($self -> IsFault($result)) {return 0;}
my $s = $result -> result() -> {'string'};

if (defined($s)) {
if (ref($s) ne "ARRAY"){
$result -> result() ->{'string'} = [$s];
}
}
else {
$result -> result() ->{'string'} = [];

return @{$result -> result() -> {'string'}};
}

Returning a two dimensional array (WDSL)

In this situation, SOAP::Lite converts the incoming data to a hash pointing to an array where each entry is a hash pointing to an array. As in the previous example it is possible to loose the first hash before returning the data, but the caller will need to cater for the second hash when extracting the data.

The datatype returned is a 'stringArrayArray', so as in the previous example, SOAP::Lite decodes this and removes the redundant 'Array', such that the underlying array can be returned with the following code


method GetUserNameSets()
{
my $var = SOAP::Data->type('complex') ->value(SOAP::Data->type('none')) ;
my $result = $self -> sIf ->userNameSets($var);

if ($self -> IsFault($result)) {return 0;}

return @{$result -> result() -> {'stringArray'}};
}

The code for extracting the data from the two dimensional array is however a classic example of how Perl forces you to write unreadable and difficult to decipher code (A recent conversation at Cold Spring Harbor confirmed that I am far from alone in thinking this)

The outer loop is much the same as as the previous example, where @names is used to extract the number of items in the array. $s is used as a pointer to each sub vector within the array. Although @s can be used at this point, $s works better, partly because what is returned is a pointer to the vector rather than the vector. The wdsl refers to the datatype as 'stringArray', so as before the Perl code should remove the redundant 'Array'.

In '@$s -1' the '$' is being used to dereference the pointer, rather than because we are referring to a variable called '$s' (apparently). In the following line $s yet again means that we are deferencing a pointer, and $$s[$j] indicates that we are returning a scalar value. Interestingly, Perl allows this to be written @$s[$j] as well.


my @names = $soapIf -> GetUserNameSets();

for my $i (0 .. @names-1) {
print "Set".$i.": ";
my $s = @names[$i] -> {'string'};

for my $j (0 .. @$s-1) {
print $$s[$j].',';}

print "\n";
}