#!/usr/bin/perl # Example of a SOAP server. # Run like this: # ./server.pl --verbose=2 (optional, or use -v/-vv/-vvv) # # this will start a server in the background # # ./client.pl --verbose=2 (optional, or use -v/-vv/-vvv) # # inter-actively call procedures on the server # # If you process the four examples in order, you will see increasingly # complex examples. The last example demonstrates what to do without # WSDL file (or in case of too many bugs in the definition, not uncommon). # # ./servtempl.pl is an empty base, to start your own server # Thanks to Thomas Bayer, for providing this example service # See http://www.thomas-bayer.com/names-service/ # Author: Mark Overmeer, January 24, 2009 # Using: XML::Compile 1.00 # XML::Compile::SOAP 2.00 # XML::Compile::SOAP::Daemon 2.00 # Copyright by the Author, under the terms of Perl itself. # Feel invited to contribute your examples! # Of course, all Perl programs start like this! use warnings; use strict; # constants, change this if needed (also in the client script?) use constant SERVERHOST => 'localhost'; use constant SERVERPORT => '8877'; # To make Perl find the modules without the package being installed. use lib '../../lib' # This server implementation , '.'; # To access My*.pm helpers # This could come from a database... use MyExampleData qw/$namedb/; # This module defines my additional (non-WSDL) calls use MyExampleCalls; # All the other XML modules should be automatically included. use XML::Compile::SOAP::HTTPDaemon; use XML::Compile::WSDL11; use XML::Compile::SOAP11; # The client and server scripts can be translated easily, using the # 'example' translation table name-space. trace/info/error come from # the LogReport error dispatch infra-structure. use Log::Report 'example', syntax => 'SHORT'; # Other useful modules use Getopt::Long qw/:config no_ignore_case bundling/; use List::Util qw/first/; use Data::Dumper; # Data::Dumper is your friend. $Data::Dumper::Indent = 1; # Forward declarations allow prototype checking sub get_countries($$); sub get_name_info($$); sub get_names_in_country($$); sub get_name_count($$); sub create_get_name_count($); ## #### MAIN ## # # I do not like Net::Server to process my command-line options, so # process them before Net::Server can get it's hand on them. # my $mode = 0; GetOptions # 3 ways to set the verbosity for Log::Report dispatchers # select (at least one) of these ways. 'v+' => \$mode # -v -vv -vvv , 'verbose=i' => \$mode # --verbose=2 (0..3) , 'mode=s' => \$mode # --mode=DEBUG (DEBUG,ASSERT,VERBOSE,NORMAL) or die "Deamon is not started"; # # XML::Compile::* uses Log::Report. The 'default' dispatcher for error # messages is here changed from PERL (die/warn) into using syslog. # # This is an example of Log::Report translation/exception syntax error __x"No filenames expected on the command-line" if @ARGV; # # Create the daemon set-up # my $daemon = XML::Compile::SOAP::HTTPDaemon->new ( # You may wish to use other daemon implementations, for instance # when your platform does not have a fork. You may also provide # a prepared Net::Server daemon object. # , based_on => 'Net::Server::PreFork' # is default ); # # Get the WSDL and Schema definitions # # Of course, you find this information in the applicable manual pages of # the XML-Compile-SOAP distributions. my $wsdl = XML::Compile::WSDL11->new('namesservice.wsdl'); $wsdl->importDefinitions('namesservice.xsd'); # This will give you some understanding about what is defined. #$wsdl->schemas->namespaces->printIndex; # If you have a WSDL, then most of the infrastructure is auto-generated. # The only thing you have to do, is provide call-back code references # for each of the portNames in the WSDL. my %callbacks = ( getCountries => \&get_countries , getNamesInCountry => \&get_names_in_country , getNameInfo => \&get_name_info ); $daemon->operationsFromWSDL ( $wsdl , callbacks => \%callbacks ); # Add a handler which is not defined in a WSDL create_get_name_count $daemon; # # Start the daemon # All (slow) preparations done, let's start the server # # replace the 'default' output 'PERL' with output to syslog #dispatcher SYSLOG => 'default', mode => $mode; $daemon->run ( # any Net::Server option. Difference SOAP daemon extensions add extra # configuration options. It also depends on the Net::Server # implementation you base the SOAP daemon on. See new(base_on) name => 'NamesService' , host => SERVERHOST , port => SERVERPORT # Net::Server::PreFork parameters , min_servers => 1 , max_servers => 1 , min_spare_servers => 0 , max_spare_servers => 0 ); info "Daemon stopped\n"; exit 0; ## ### Server-side implementations of the operations ## # # First example, no incoming data # sub get_countries($$) { my ($server, $in) = @_; # We do not have to look at the incoming data ($in) in this case, # because this message doesn't provide any. # The output structure needs all names of header and body message # parts, as defined in the WSDL. This message only contains a # message part named 'parameters'. my %parameters; # 'getCountriesResponse' element, see *xsd my @countries = sort keys %$namedb; $parameters{country} = \@countries; # You can use XML::Compile::Schema::template(PERL) to figure-out what # the getCountryResponse element structure looks like. { parameters => \%parameters } } # # Second example, with decoding of incoming data # sub find_name($$) { my $name = lc shift; my $names = shift || []; (first {lc($_) eq $name} @$names) ? 1 : undef; } sub get_name_info($$) { my ($server, $in) = @_; # debugging daemons is not easy, but you could do things like: # (debug mode is enabled by Log::Report dispatchers with # -vvv on the [server] command-line) trace join '', 'get_name_info', Dumper $in; # In the message description, the getNameInfo message has only # one part, named `parameters'. Its structure is an optional # name string. my $name = $in->{parameters}{name} || ''; # It is probably easier for your regression testing to put more # complex data processing in seperate files; not in the server # file. my ($males, $females, @countries) = (0, 0); foreach my $country (sort keys %$namedb) { my $male = find_name $name, $namedb->{$country}{ male}; my $female = find_name $name, $namedb->{$country}{female}; $male or $female or next; $males = 1 if $male; $females = 1 if $female; push @countries, $country; } my $gender = $males && $females ? 'either' : $males ? 'male' : $females ? 'female' : undef; # The output message is constructed, which has one body element, named # 'parameters'. It's structure is one optional 'nameinfo' element my %country_list = (country => \@countries); my %nameinfo = ( name => $name, countries => \%country_list , gender => $gender, male => $males, female => $females ); my %parameters = (nameinfo => \%nameinfo); { parameters => \%parameters }; # if you are not afraid for references, you simply write # { parameters => # { nameinfo => # { name => $name, countries => {country => \@countries} # , gender => $gender, male => $males, female => $females }}} # Perl looks like Lisp, sometimes ;-) } ## ### The third example ## sub get_names_in_country($$) { my ($server, $in) = @_; # this should look quite familiar now... a bit more compact! my $country = $in->{parameters}{country} || ''; my $data = $namedb->{$country} || {}; my @names = sort @{$data->{male} || []}, @{$data->{female} || []}; { parameters => { name => \@names } }; } # # The last example shows how to add your own non-WSDL calls # You have to visit each of the levels of the procedure yourself: # 1 collect the schemas you need # 2 specify the protocol details # 3 defined the incoming and outgoing message explicitly. # (see the client.pl, which requires exactly the same info) # 4 define how to recognize the message # 5 add the procedure to the knowledge of the server # Steps 1 and 2 can be shared of all procedures you add manually. sub create_get_name_count($) { my $daemon = shift; ##### BEGIN only once per script # I want to base my own methods on the WSDL definitions $wsdl->importDefinitions(\@my_additional_schemas); my $soap11 = XML::Compile::SOAP11::Server->new(schemas => $wsdl); # You could also do # my $soap11 = XML::Compile::SOAP11::Server->new; # $soap11->importDefinitions($_) for @my_additional_schemas; ##### END only once per script ##### BEGIN usually in initiation phase of the daemon # For each of the messages you want to be able to handle, you need to # implement this block, run before the daemon starts. # The 'input' and 'output' roles are the reversed in the client. my $decode = $soap11->compileMessage(RECEIVER => @get_name_count_input); my $encode = $soap11->compileMessage(SENDER => @get_name_count_output); ##### END in initiation phase of daemon # How do we know that this message is the one arriving? The selector # CODE ref is called with the XML::LibXML::Document which has arrived # and must return true when it feels addressed. # The ::compileFilter() implementation is quite thorough, because it # needs to understand messages from the WSDL which look much alike. # You may implement something else. # So, either # my $selector = $soap11->compileFilter(@get_name_count_input); # or my $selector = sub { my ($xml, $info) = @_; @{$info->{body}} && $info->{body}[0] =~ m/\}getNameCount$/; }; # The handler is the client-side plug, default produces an error reply my $handler = $soap11->compileHandler ( name => 'getNameCount' , selector => $selector # important! , decode => $decode , encode => $encode , callback => \&get_name_count ); $daemon->addHandler('getNameCount', $soap11, $handler); } sub get_name_count($$) { my ($server, $in) = @_; # Althought the message is not specified in a WSDL, the handler is # still the same. my $count = 0; if(my $country = $in->{request}{country}) { my $data = $namedb->{$country} || {}; $count = @{$data->{male} || []} + @{$data->{female} || []}; } {answer => {count => $count}}; }