m a j o r d o j o | About majordojo | Mailing Lists | Bugs | Contact Us |
The key in overriding properties of the root element of the response is to write a customer serializer. What is a serializer? A serializer converts abstract data into a format that can more easily be transmitted between endpoints. In this case, a serializer converts scalar and complex data into XML. By writing our own serializer we can tell SOAP::Lite how to construct the XML representing the response.
To create a custom serializer, you need to know a little bit about Perl OOP (Object Oriented Programing) and inheritence. The following code block shows a custom serializer which inherits all properties and behavior from the default SOAP::Lite Serializer, but overrides one of its methods/subroutines with its own:
package MySerializer; @MySerializer::ISA = 'SOAP::Serializer'; sub envelope { $_[2] = SOAP::Data->name($_[2])->prefix('temp')->uri('urn:TemperatureService') if $_[1] =~ /^(?:method|response)$/; shift->SUPER::envelope(@_); }
This is just a simple serializer, one that changes the root element only, but a serializer could be used to serialize any part of a message (in this case a response).
You can save your serializer to its own file (in the form of a perl module), and then 'use' it. Or alternatively, you can encapsulate the package in a BEGIN { } block anywhere in your code. Either way, to tell SOAP::Lite to use your serializer as opposed to its own, you will need to do the following within your Web service instance:
SOAP::Transport::HTTP::CGI ->serializer(MySerializer->new) ->dispatch_to('MyService') ->handle;
You can find an example of a trivial temperature conversion service which utilizes this custom serializer: here.
The following code fragment shows how you can easily turn on and off SOAP::Lite debugging information from the command line. Obviating your need to go into the code everytime you want to toggle debugging:
use Getopt::Long; my $result = GetOptions ("debug" => \$DEBUG); if ($DEBUG) { eval "use SOAP::Lite +trace => 'debug';"; } else { eval "use SOAP::Lite;"; }
Note: The Getopt::Long package typically comes standard with Perl, but if for some reason your Perl doesn't have it, just download it from CPAN.
The following code snippet shows how to specify an additional namespace. It is easy - in fact anytime you need to specify additional attributes for an XML element in SOAP::Lite, just use the SOAP::Data->attr() subroutine like so:
my $method = SOAP::Data->name('methodName') ->attr({'xmlns:ns2' => 'urn:SecondaryNamespace'}) ->prefix('ns1') ->uri('urn:PrimaryNamespace'); my $params = SOAP::Data->name('param' => '123'); my $results = $search->call($method => $params);
The secret in changing the root element is to make use of the call subroutine. This allows you to specify a SOAP::Data element (or string) as the method name, and then a set of SOAP::Data elements as parameters. For example:
my $method = SOAP::Data ->name("myMethodName") ->prefix("ns") ->uri("urn:MerchantAccountService") ->type("ns:Merchant"); my $service->call($method => SOAP::Data->name("param" => "value");
Will produce a SOAP Body that looks like this:
<soap:Body> <ns:myMethodName xmlns:ns="urn:MerchantAccountService" xsi:type="ns:Merchant"> <param>value</param> </ns:myMethodName> </soap:Body>
Nesting elements is actually a bit tricky. This article I wrote a while ago for builder.com.com (why on earth is their domain dotcom-dotcom? how innane) discusses this topic in much more detail. But here is the short answer:
SOAP::Data->name('foo' => \SOAP::Data->value(SOAP::Data->name('bar' => '123'))
In a nutshell, the above code will produce the following XML:
<foo><bar>123</bar></foo>
The trick is dereferencing the nested SOAP::Data element (shown in bold above). Doing so is very unintuitive, but it works.
The original problem reported that the Apache Axis Web service was claiming that it could not find the operation called 'getAirFareQuote.' However, upon inspecting the WSDL, one can see that it is indeed defined.
So I spent the next couple of minutes trying to figure out why it wouldn't recognize the operation, especially when I could call other operations documented in the same WSDL file. Weird.
In the end, it had to do with the way Axis was expecting its input parameters... I was misinterpreting an XML multiref that looked like this:
# <ns1:getAirFareQuote # soapenv:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" # xmlns:ns1="urn:SBGAirFareQuotes.sbg.travel.ws.dsdata.co.uk"> # <in0 href="#id0"/> # </ns1:getAirFareQuote> # <multiRef id="id0" soapenc:root="0" # soapenv:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" # xsi:type="ns2:AirFareQuoteRequest" # xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/" # xmlns:ns2="urn:SBGAirFareQuotes.sbg.travel.ws.dsdata.co.uk"> # <outwardDate # xsi:type="xsd:dateTime">2003-04-28T08:00:00.000Z</outwardDate> # <returnDate # xsi:type="xsd:dateTime">2003-05-05T08:00:00.000Z</returnDate> # <originAirport xsi:type="xsd:string">LAX</originAirport> # <destinationAirport xsi:type="xsd:string">JFK</destinationAirport> # </multiRef>
Into something that looked like this:
# <ns1:getAirFareQuote # soapenv:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" # xmlns:ns1="urn:SBGAirFareQuotes.sbg.travel.ws.dsdata.co.uk"> # <outwardDate # xsi:type="xsd:dateTime">2003-04-28T08:00:00.000Z</outwardDate> # <returnDate # xsi:type="xsd:dateTime">2003-05-05T08:00:00.000Z</returnDate> # <originAirport xsi:type="xsd:string">LAX</originAirport> # <destinationAirport xsi:type="xsd:string">JFK</destinationAirport> # </ns1:getAirFareQuote>
When it should have looked like this:
# <ns1:getAirFareQuote # soapenv:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" # xmlns:ns1="urn:SBGAirFareQuotes.sbg.travel.ws.dsdata.co.uk"> # <in0> # <outwardDate # xsi:type="xsd:dateTime">2003-04-28T08:00:00.000Z</outwardDate> # <returnDate # xsi:type="xsd:dateTime">2003-05-05T08:00:00.000Z</returnDate> # <originAirport xsi:type="xsd:string">LAX</originAirport> # <destinationAirport xsi:type="xsd:string">JFK</destinationAirport> # </in0> # </ns1:getAirFareQuote>
Ok, ok, ok, that is a lot of XML... in the end, the following client was hacked together for SOAP::Lite to fetch airfares for a given set of times, destinations and origins:
#!/usr/bin/perl # # airfare.pl # Author: breese at grandcentral.com # Location of the WSDL file my $WSDL ="http://wavendon.dsdata.co.uk:8080/axis/services/SBGGetAirFareQuote?wsdl"; my $NS = "urn:SBGAirFareQuotes.sbg.travel.ws.dsdata.co.uk"; my $HOST = "http://wavendon.dsdata.co.uk:8080/axis/services/SBGGetAirFareQuote"; use strict; use SOAP::Lite +trace => qw (debug); my $search = SOAP::Lite ->readable(1) ->xmlschema('http://www.w3.org/2001/XMLSchema') ->on_action( sub { return '""';} ) ->proxy($HOST) ->uri($NS); # CCYY-MM-DDThh:mm:ss my $outwardDate = "2003-04-28T08:00:00.000Z"; my $returnDate = "2003-05-05T08:00:00.000Z"; my $origin = "LAX"; my $destination = "JFK"; my $method = SOAP::Data->name('getAirFareQuote') ->prefix('air') ->uri($NS); my $params = SOAP::Data ->type('air:AirFareQuoteRequest') ->name('in0' => \SOAP::Data->value( SOAP::Data->name('outwardDate' => $outwardDate)->type('xsd:dateTime'), SOAP::Data->name('returnDate' => $returnDate)->type('xsd:dateTime'), SOAP::Data->name('originAirport' => $origin), SOAP::Data->name('destinationAirport' => $destination) )); my $results = $search->call($method => $params); # Loop through the results foreach my $result (@{$results->{'result'}}) { print $result->{airlineName} . ": " . $result->{fare} . "\n"; } 1;
SOAP-MIME-0.55-7 (Released 16-April-2003)
Relaxed parsing of the start parameter located inside the Content-type header
There could certainly be some debate about this change, especially for those of you who are standards or RFC junkies, but I chose to mirror the functionality of Apache Axis in this regard. Here is the background, the specification IMHO is clear that the start parameter requires angle brackets, but others believe the RFC detailing how content ids should be used (2111) is unclear in this regard which has led some SOAP implementations especially more forgiving low-level SOAP APIs to break more strict toolkits like SOAP::Lite. The set of cases where it would break are rare, but sufficient enough to warrant the change.
Fixed bug around an overlooked unqualified package name
Embarrasingly, this is something I should have caught initially considering I had to make a very similar change to the code on the line just prior to the one this bug was on. But alas, I didn't. But it's fixed now.
Added CHANGE_LOG and INSTALL files to distro
This is really just to be more conventional in regards to Perl modules.