SOAP Cookbook
According to The Feynman Problem Solving Algorithm steps you need to
undertake to solve any problem are really simple:
Write down the problem.
Think real hard.
Write down the answer.
In most cases you can't avoid step two, but sometimes you need to get
the answer as soon as possible. The Cookbook won't teach you, but it
might give you an help if you are felling lost working with SOAP or
SOAP::Lite.
Citing Larry Wall (foreword for excellent Perl Cookbook written by Tom
Christiansen & Nathan Torkington from O'Reilly): 'Easy things should
be easy, and hard things should be possible'.
--------------------------------------------------------------------------------
Making SOAP
SOAP acronym
Problem
You want to know what SOAP stands for.
Solution
Sometimes SOAP stands for Simple Object Access Protocol, and sometimes
for Services Oriented Access Protocol, depending on your view on
simplicity.
Information about SOAP protocol
Problem
You want to find more information about the SOAP protocol.
Solution
Check list of resources on http://www.soaplite.com/#LINKS where you
can find references to specifications, articles, tutorials and
presentations about SOAP and related technologies.
Information about SOAP toolkits
Problem
You are looking for the SOAP toolkit for your favorite language.
Solution
Take a look at http://www.soaplite.com/#TOOLKITS
Discussion
With more than fifty toolkits available (as of May, 2001) in more than
15 languages (Ada, C#, C++, Java, JavaScript, Perl, PHP, Python, Ruby,
Smalltalk, Tcl, VB, XSLT, Delphi, Orchard, Smalltalk, K) you shouldn't
have a problem finding a toolkit for your language of choice. If you
can't find what you are looking for, join the soapbuilders list at
http://groups.yahoo.com/group/soapbuilders and start writing your own
implementation.
See Also
The SOAP directory at http://www.soapware.org/
The SOAP services directory at http://www.xmethods.net/
Install in a custom directory
Problem
You want to install SOAP::Lite, but don't have root/administrator privileges.
Solution
Install SOAP::Lite into a custom directory using CPAN module:
# perl -MCPAN -e shell
> o conf make_arg -I~/lib
> o conf make_install_arg -I~/lib
> o conf makepl_arg LIB=~/lib PREFIX=~ INSTALLMAN1DIR=~/man/man1
INSTALLMAN3DIR=~/man/man3
> install SOAP::Lite
Discussion
Setup PERL5LIB environment variable. Depending on your shell it may look like:
PERL5LIB=/you/home/directory/lib; export PERL5LIB
lib here is the name of directory where all libraries will be
installed under your home directory.
Run CPAN module with
perl -MCPAN -e shell
and run three commands from CPAN shell
> o conf make_arg -I~/lib
> o conf make_install_arg -I~/lib
> o conf makepl_arg LIB=~/lib PREFIX=~ INSTALLMAN1DIR=~/man/man1
INSTALLMAN3DIR=~/man/man3
LIB will specify directory where all libraries will reside.
PREFIX will specify prefix for all directories (like lib, bin, man,
though it doesn't work in all cases for some reason).
INSTALLMAN1DIR and INSTALLMAN3DIR specify directories for manuals (if
you don't specify them, install will fail because it'll try to setup
it in default directory and you don't have permissions for that).
Then run:
> install SOAP::Lite
Now in your scripts you need to specify:
use lib '/your/home/directory/lib';
somewhere before 'use SOAP::Lite;'
--------------------------------------------------------------------------------
Client SOAP
Writing a client
Problem
You want to write a SOAP client.
Solution
Writing a client
#!perl -w
use SOAP::Lite;
print SOAP::Lite
-> proxy('http://services.soaplite.com/hibye.cgi')
-> uri('http://www.soaplite.com/Demo')
-> hi()
-> result;
Discussion
There is some information you need to provide for every SOAP call:
address
address of endpoint that will handle you call (specified with proxy()
method, can be http:, mailto:, tcp: or something else, depending on
what kind of SOAP server will handle your request);
namespace
namespace (URI) of method element (specified with uri() method), which
will help the SOAP server in handling your request;
method and parameters
method name with parameters (hi() in our example);
SOAPAction
optional SOAPAction header (specified with on_action() method, omitted
in this example).
Accessing envelope with autodispatch
Problem
You want to access envelope returned by an autodispatched call.
Solution
Accessing envelope with autodispatch
#!perl -w
use SOAP::Lite +autodispatch =>
uri => 'http://www.soaplite.com/Temperatures',
proxy => 'http://services.soaplite.com/temper.cgi';
c2f(37.5);
print SOAP::Lite->self->call->result;
Discussion
One of the differences between autodispatched call (AD) and call with
object interface (OO) is that the result of OO call is an envelope
(SOAP::SOM object) and the result of AD call is the pure result only,
so you don't have direct access to envelope element and hence need to
have some other way to access headers, returned parameters and
attributes. SOAP::Lite->self returns the SOAP::Lite object that
handles autodispatched calls, SOAP::Lite->self->call returns the
envelope (SOAP::SOM) object for the last call and
SOAP::Lite->self->call->result method accesses the result element of
this envelope. Following this scheme, you can access any of the
elements of the returned envelope, such as ->fault, ->faultcode,
->faultstring, ->headers, and others.
Overriding serializer (client)
Problem
You need to override the serializer to alter data serialization
Solution
Overriding serializer (client)
#!perl -w
use SOAP::Lite;
use XMLRPC::Lite;
my $soap = SOAP::Lite
-> serializer(XMLRPC::Serializer->new);
Creating custom serializer
Problem
You want to customize the serializer on client side.
Solution
Creating custom serializer
#!perl -w
use SOAP::Lite;
# define serializer
BEGIN {
package My::Serializer;
@My::Serializer::ISA = 'SOAP::Serializer';
# methods of My::Serializer
# ....
}
# register serializer
my $soap = SOAP::Lite
-> serializer(My::Serializer->new);
Overriding deserializer (client)
Problem
You want to customize the deserializer on the client side.
Solution
Overriding deserializer (client)
#!perl -w
use SOAP::Lite;
use XMLRPC::Lite;
my $soap = SOAP::Lite
-> deserializer(XMLRPC::Deserializer->new);
Customizing SOAPAction header
Making class or object calls with OO interface
Internationalization and encoding
Problem
You want to send string that has an international characters inside.
What to do and what encoding to specify?
Solution
use iso-8859-1 encoding
Internationalization and encoding
#!perl -w
use SOAP::Lite;
# specify type explicitly and it won't be encoded as base64
my $string = SOAP::Data->type(string => 'tätä');
my $result = SOAP::Lite
-> proxy (...)
-> uri (...)
# specify encoding, because default is utf-8
-> encoding('iso-8859-1')
-> hello($string)
-> result;
use UTF-8 encoding
Internationalization and encoding
#!perl -w
use SOAP::Lite;
# convert to UTF (if you're using Perl 5.6)
(my $utf = 'tätä') =~ tr/\0-\x{ff}//CU;
# specify type explicitly and it won't be encoded as base64
my $string = SOAP::Data->type(string => $utf);
my $result = SOAP::Lite
-> proxy (...)
-> uri (...)
-> hello($string)
-> result;
Discussion
Hard topic. Intent is simple, represent you data on wire, so other
side will understand you. You have your data encoded using some
encoding in your application, so you should either specify this
encoding on wire, or convert your encoding to something you can use on
wire. You best bet is to stay with iso-8859-1 or UTF-8 (and maybe
UTF-16), because every implementation should understand those
encodings.
How to do it? The first example shows how to specify encoding on wire
(if you do have this encoding in your application). The second one
shows how to convert your values into UTF-8 and use this (default)
encoding. Convertion itself may not be so straightforward if you use
older versions of Perl. You may need to use Unicode::String or
Unicode::Map8 to convert from your encoding to UTF-8.
If you have strings with non-ASCII values you need to specify type
string explicitly, otherwise autotyping will encode this string as
base64 value. Hopefully next version (after v0.50) will be more
tolerant and autotyping won't encode those strings as base64, so you
won't need to specify type explicitly.
On the server side you'll always get your data encoded as UTF-8
regardless of encoding specified on wire (data will be converted by
XML::Parser).
See Also
UTF-8 and Unicode FAQ for Unix/Linux at http://www.cl.cam.ac.uk/~mgk25/unicode.html
Iteration over the list of endpoints
Problem
You have a list of endpoints that provide the same functionality. You
want to use the next endpoint from this list if call failes and do it
transparently for the application. Is it possible?
Solution
Iteration over the list of endpoints
BEGIN {
package My::SOAP::Lite;
use SOAP::Lite;
@My::SOAP::Lite::ISA = 'SOAP::Lite';
my @alternatives = qw(
http://somethingelse/
http://and.another.one/
http://services.soaplite.com/hibye.cgi
);
sub call {
my $self = shift;
return $self->SUPER::call unless @_;
my $result = $self->SUPER::call(@_);
return $result if $self->transport->is_success;
# ok, try the next endpoint (if any) till our call succeed
while (my $next = shift @alternatives) {
$result = $self->endpoint($next)->SUPER::call(@_);
last if $self->transport->is_success;
}
return $result;
}
}
my $soap = My::SOAP::Lite
-> proxy('http://wrong/')
-> uri('Demo')
-> on_fault(sub{})
;
print $soap->hi->result;
# it should use the last successful endpoint
print $soap->hi->result;
Discussion
You can modify porvided logic and, for example, initialize
@alternatives array when it becomes empty, so calls will be restarted
from the beginning. You can also use the different lists for the
different methods.
Specifying attributes for method
Accessing service with service description (WSDL)
--------------------------------------------------------------------------------
Server SOAP
Writing a server
Accessing specific modules
Returning headers
Problem
You need to add some headers in Response message
Solution
Returning headers
return 1, SOAP::Header->name(a => 1)->uri('http://my.namespace/'), 2;
Discussion
All SOAP::Header elements will be encoded as headers in serialized
message. You can use SOAP::Header object exactly as you use SOAP::Data
and provide required name, value, namespace and attributes. Position
of headers among other parameters is not significant. Headers will be
serialized in the same order as they provided, but this behavior might
change in future versions.
Returning null value
Problem
You want to return undef value
Solution
Returning headers
return undef;
# -- OR --
return {key => undef};
# -- OR --
return [1, undef, 3];
# -- OR --
return SOAP::Data->name(name => undef);
Discussion
Nothing fancy here, variable that has undefined value, or literal
undef will be serialized, and client side gets it as the undef value.
Accessing envelope
Overriding serializer (server)
Problem
You need to override the serializer to alter data serialization
Solution
Overriding serializer (server)
use SOAP::Transport::HTTP;
my $daemon = SOAP::Transport::HTTP::Daemon
-> new (LocalPort => 80)
# register serializer
-> serializer(MySerializer->new)
-> dispatch_to(...)
;
print "Contact to SOAP server at ", $daemon->url, "\n";
$daemon->handle;
BEGIN {
package MySerializer;
@MySerializer::ISA = 'SOAP::Serializer';
# methods of MySerializer
# ....
}
Discussion
Everything looks very similar to what is done on a client side
(Overriding serializer (client)). You can also share the same
serializer between client and server side.
See also
Changing method name in response and Changing attributes for method
element in response for useful examples.
Overriding deserializer (server)
Problem
You need to override the deserializer to alter data serialization
Solution
Overriding deserializer (server)
use SOAP::Transport::HTTP;
my $daemon = SOAP::Transport::HTTP::Daemon
-> new (LocalPort => 80)
# register serializer
-> deserializer(MyDeserializer->new)
-> dispatch_to(...)
;
print "Contact to SOAP server at ", $daemon->url, "\n";
$daemon->handle;
BEGIN {
package MyDeserializer;
@MyDeserializer::ISA = 'SOAP::Deserializer';
# methods of MyDeserializer
# ....
}
Changing method name in response
Problem
You want to change method name in response message.
Solution
Changing method name
use SOAP::Transport::HTTP;
my $daemon = SOAP::Transport::HTTP::Daemon
-> new (LocalPort => 80)
-> serializer(MySerializer->new)
-> dispatch_to(...)
;
print "Contact to SOAP server at ", $daemon->url, "\n";
$daemon->handle;
BEGIN {
package MySerializer; @MySerializer::ISA = 'SOAP::Serializer';
sub envelope {
$_[2] =~ s/Response$// if $_[1] =~ /^(?:method|response)$/;
shift->SUPER::envelope(@_);
}
}
Discussion
Overriding the serializer give you full control over serialization
process and envelope generation. envelope() method will be called
every time when message is generated, and this code will drop
'Response' suffix from method name. Keep in mind, however, that in
future versions method name ($_[2]) in this case might be represented
not only as a string, but also as a SOAP::Data object, so perfectly
correct code might look like:
sub envelope {
UNIVERSAL::isa($_[2] => 'SOAP::Data')
? do { (my $method = $_[2]->name) =~ s/Response$//; $_[2]->name($method) }
: $_[2] =~ s/Response$//
if $_[1] =~ /^(?:method|response)$/;
shift->SUPER::envelope(@_);
}
Changing attributes for method element in response
Problem
You want to change attributes for method element in response message.
Solution
Changing attributes for method
use SOAP::Transport::HTTP;
my $daemon = SOAP::Transport::HTTP::Daemon
-> new (LocalPort => 80)
-> serializer(MySerializer->new)
-> dispatch_to(...)
;
print "Contact to SOAP server at ", $daemon->url, "\n";
$daemon->handle;
BEGIN {
package MySerializer; @MySerializer::ISA = 'SOAP::Serializer';
sub envelope {
$_[2] = SOAP::Data->name($_[2])
->encodingStyle("http://xml.apache.org/xml-soap/literalxml";)
if $_[1] =~ /^(?:method|response)$/;
shift->SUPER::envelope(@_);
}
}
Discussion
This example will put encodingStyle attribute directly on method
element. Considering comment for Changing method name in response,
perfectly valid code might look like:
sub envelope {
(UNIVERSAL::isa($_[2] => 'SOAP::Data') ? $_[2] : SOAP::Data->name($_[2]))
->encodingStyle("http://xml.apache.org/xml-soap/literalxml";)
if $_[1] =~ /^(?:method|response)$/;
shift->SUPER::envelope(@_);
}
Can be used to tell ApacheSOAP that enclosed XML has literal encoding,
as well as in many other cases.
--------------------------------------------------------------------------------
SOAP Faults
Returning fault
Problem
You want to return fault from the method on the server side.
Solution
Returning fault
sub my_method {
# do something here
return $result if $everything_is_ok;
die "Something bad happened\n";
}
Discussion
SOAP processor will catch every die on the server side and will
package your error as a Fault message, providing you message as the
faultstring and Client as the faultcode. Use Customizing fault advice
if you need more control on returned Fault.
Customizing fault
Problem
You want to return fault from the method on the server side, but you
also need to have a control on what to specify as a faultcode,
faultdetail and probably faultactor.
Solution
Customizing fault
sub die_with_object {
die SOAP::Data
-> name(something => 'value')
-> uri('http://www.soaplite.com/');
}
sub die_with_fault {
die SOAP::Fault->faultcode('Server.Custom') # will be qualified
->faultstring('Died in server method')
->faultdetail(bless {code => 1} => 'BadError')
->faultactor('http://www.soaplite.com/custom');
}
Discussion
You can use die in three different modes: die with string (as in
Returning fault, will be reported as faultstring); die with object (as
in die_with_object() method, will be reported as detail); die with
SOAP::Fault object (as in die_with_fault() method), allows you to
specify all parameters of provided Fault. Both faultcode and
faultstring are required, so library will specify them for you even if
you omit them.
Handling faults
--------------------------------------------------------------------------------
Transporting SOAP
Using different transports
Specifying proxy
Problem
You are behind a proxy firewall and want to configure SOAP::Lite to
take it into account.
Solution
Global
Use environment variable HTTP_proxy to specify proxy like this:
HTTP_proxy=http://proxy.my.com/
Syntax can be different depending on your command processor. You may
also specify this variable in your script:
$ENV{HTTP_proxy} = "http://proxy.my.com/";;
Local
Specifying proxy
my $soap = SOAP::Lite->proxy('http://endpoint.server/',
proxy => ['http' => 'http://my.proxy.server/']);
# -- OR --
my $soap = SOAP::Lite->proxy('http://endpoint.server/',
proxy => 'http://my.proxy.server/');
# -- OR --
my $soap = SOAP::Lite->proxy('http://endpoint.server/');
$soap->transport->proxy(http => 'http://my.proxy.server/');
Discussion
The proxy() method can accept transport-specific parameters that can
be passed as name => value pairs. If value is represented by more than
one element (as in the first example) it should be wrapped into an
array.
Alternatively, the transport() method gives you access to the
underlying module (LWP::UserAgent for HTTP protocol), so any options
supported by that transport module can be specified this way also.
Specifying timeout
Problem
You want to change the timeout value, so your SOAP calls will timeout sooner.
Solution
Specifying timeout
my $soap = SOAP::Lite->proxy('http://endpoint.server/',
timeout => 5);
# -- OR --
my $soap = SOAP::Lite->proxy('http://endpoint.server/');
$soap->transport->timeout(5);
Accessing service with basic authentication
Problem
You want to access endpoint that uses basic authentication.
Solution
Accessing service with basic authentication
#!perl -w
use SOAP::Lite;
print SOAP::Lite
-> uri('http://www.soaplite.com/My/Examples')
-> proxy('http://soaplite:authtest@services.soaplite.com/auth/examples.cgi')
-> getStateName(21)
-> result;
Discussion
Since HTTP client functionality is based on LWP::UserAgent you may use
the same techniques as you use with this module. Embedding
username:password in URL is one of the options. Another option is to
specify the get_basic_credentials() method that will be called when
asked for credentials:
Accessing service with basic authentication
#!perl -w
use SOAP::Lite +autodispatch =>
uri => 'http://www.soaplite.com/My/Examples',
proxy => 'http://services.soaplite.com/auth/examples.cgi',
;
sub SOAP::Transport::HTTP::Client::get_basic_credentials {
return 'soaplite' => 'authtest';
}
print getStateName(21);
Accessing service with proxy authentication
Problem
You want to access endpoint that uses proxy authentication.
Solution
You have several options (hope you are not surprised):
You may specify HTTP_proxy_user and HTTP_proxy_pass environment
variables for user and password and SOAP::Lite should know how to
handle it properly
Combine knowledge about Specifying proxy and Accessing service with
basic authentication and specify both proxy and information for proxy
authentication:
Accessing service with proxy authentication
my $soap = SOAP::Lite->proxy('http://user:password@endpoint.server/',
proxy => 'http://my.proxy.server/');
Accessing service with SSL
Problem
You want to access SOAP server using SSL.
Solution
Specify in your client https protocol instead of http:
Accessing service with SSL
#!perl -w
use SOAP::Lite +autodispatch =>
uri => 'http://www.soaplite.com/My/Examples',
proxy => 'https://localhost/cgi-bin/soap.cgi';
print getStateName(21);
Discussion
Is there anything to discuss?
Using cookies
Problem
You want to accept cookies from SOAP response and provide it in the
next request, probably using cookie-based authentication or for any
other reason.
Solution
Using cookies
#!perl -w
use HTTP::Cookies;
use SOAP::Lite;
# you may also add 'file' if you want to keep cookie between sessions
my $soap->proxy('http://localhost/',
cookie_jar => HTTP::Cookies->new(ignore_discard => 1));
Discussion
Cookies will be taken from the response and provided for the request.
You may always add another cookie (or extract what you need after
response) using the HTTP::Cookies interface.
Enabling compression
Problem
You want to enable SOAP::Lite's support for compression on the wire.
Solution
Client
Enabling compression (client)
print SOAP::Lite
-> uri('http://localhost/My/Parameters')
-> proxy('http://localhost/',
options => {compress_threshold => 10000})
-> echo(1 x 10000)
-> result
;
Server
Enabling compression (server)
my $server = SOAP::Transport::HTTP::CGI
-> dispatch_to('My::Parameters')
-> options({compress_threshold => 10000})
-> handle;
Discussion
SOAP::Lite provides you with the option for enabling compression on
the wire (for HTTP transport only). Both server and client should
support this capability, but this should be absolutely transparent to
your application. The Server will respond with an encoded message only
if the client can accept it (indicated by client sending an
Accept-Encoding header with 'deflate' or '*' values) and client has
fallback logic, so if server doesn't understand specified encoding
(Content-Encoding: deflate) and returns proper error code (415 NOT
ACCEPTABLE) client will repeat the same request without encoding and
will store this server in a per-session cache, so all other requests
will go there without encoding.
Compression will be enabled on the client side if the threshold is
specified and the size of current message is bigger than the threshold
and the module Compress::Zlib is available.
The Client will send the header 'Accept-Encoding' with value 'deflate'
if the threshold is specified and the module Compress::Zlib is
available.
Server will accept the compressed message if the module Compress::Zlib
is available, and will respond with the compressed message only if the
threshold is specified and the size of the current message is bigger
than the threshold and the module Compress::Zlib is available and the
header 'Accept-Encoding' is presented in the request.
Reusing sockets on restart
Problem
You want to restart Daemon or TCP server implementation, but on
restart sockets go into TIME_WAIT for quite some time before expiring.
What to do?
Solution
Use Reuse => 1 as in:
Reusing sockets on restart
my $daemon = SOAP::Transport::HTTP::Daemon
-> new (LocalPort => 10000,
Reuse => 1)
Thanks to Michael Brutsch <mbrutsch@intrusion.com>, Sean Meisner
<Sean.Meisner@verizonwireless.com> and others.
See Also
http://www.perlfect.com/articles/sockets.shtml
Several Daemon interfaces
Problem
You want to start Daemon server, but access it through different
hostnames or aliases.
Solution
Do not specify LocalAddr in new() method for HTTP::Daemon:
Several Daemon interfaces
my $daemon = SOAP::Transport::HTTP::Daemon
-> new (LocalPort => 10000)
Discussion
If you do not specify LocalAddr then you can access it with any
hostname/IP alias, including localhost or 127.0.0.1. If you do specify
LocalAddr in ->new() then you can only access it from that interface.
Thanks to Michael Percy <mpercy@portera.com>.
Changing HTTP protocol
Problem
You want to talk to a server that accepts only HTTP/1.1 requests. How
do you tell the client to send HTTP/1.1 header?
Solution
Write handler that will modify request to required protocol:
Changing HTTP protocol
use SOAP::Lite +trace =>
transport => sub {
$_[0]->protocol('HTTP/1.1') if $_[0]->isa('HTTP::Request')
}
; |