7.4 Tying the Interface Code to SOAP


With the basic interface code designed and laid out, and the HTTP transport method chosen , it's now time to start merging the two into a functioning SOAP server. The key things to watch are how the class methods are exposed as services and how the server manages user authentication.

7.4.1 Starting Out

The SOAP transport class based on the HTTP::Daemon class of LWP is very easy to use. It looks a lot like the server snippet at the top of the chapter, in fact. What's more, you can even launch this server as a Perl one-liner!

Setting aside the one-liner approach for the moment, Example 7-3 shows the initial server in its entirety.

Example 7-3. The HTTP::Daemon-based server
 #!/usr/bin/perl     use strict;     use SOAP::Transport::HTTP; use WishListCustomer;     my $port = pop(@ARGV)  9000; my $host = shift(@ARGV)  'localhost';     SOAP::Transport::HTTP::Daemon     ->new(LocalAddr => $host, LocalPort => $port)     ->dispatch_with({ 'urn:/WishListCustomer' =>                       'WishListCustomer' })     ->objects_by_reference('WishListCustomer')     ->handle;     exit; 

Here it is, for the sake of fun, as a one-liner:

 perl -MSOAP::Lite -MWishListClient -e '$port = pop(@ARGV) \    9000;$host = shift(@ARGV)  "localhost"; \   SOAP::Transport::HTTP::Daemon \     ->new(LocalAddr => $host, LocalPort => $port) \     ->dispatch_with({ "urn:/WishListCustomer" => \                       'WishListCustomer' }) \     ->objects_by_reference("WishListCustomer")->handle' 

Well, it's a one-liner except for the line breaks added to keep it readable, but this code can (and has) run as a fully functioning SOAP server.

There is a problem, however, with the server as it is shown. There is no access to the actual HTTP request, and thus the ability of the client to authenticate isn't there, even though the authentication code is in the underlying class. The encapsulated class ( WishListCustomer ) can get access to the full SOAP envelope by including the class, SOAP::Server::Parameters , in the inheritance tree. Unfortunately, that won't help in this case because the data the class needs is in the HTTP headers, not the SOAP Header block. Instead, it will be necessary to subclass both the transport and data classes. But why both classes?

7.4.2 Subclassing the Components

The reason for subclassing both classes here is simple: the subclass of SOAP::Transport::HTTP::Daemon is going to provide the request information by overloading the request method. The overloaded version of request pulls the desired information and sticks it into a global variable. Rather than reengineering the main WishListCustomer class just to fit into a SOAP server, a subclass of it intercepts the calls that are sensitive to the authentication headers and inserts the information into the list of arguments. Handling user-authentication is a tricky enough prospect by itself. It is better to not complicate the basic classes.

There are also other approaches: methods within SOAP::Server can be overloaded to insert the authentication information directly into the SOAP message's headers. If SOAP header-based authentication is already part of the server design, this might be a better solution. The Apache- related transport class has the advantage of being able to access the request object more readily from within the code. It also can take advantage of the phases of the request lifecycle under Apache that are specific to authentication.

For now, the global variable-based approach is used. Another benefit of subclassing the WishListCustomer class is that the new version can also manage the error messages directly, turning them into SOAP::Fault objects and freeing the client from the responsibility of checking the type of return value as a way of looking for success.

7.4.2.1 WishListCustomer::Daemon

The subclass for the daemon is very simple and short. Example 7-4 shows the new class in its entirety. Note that each call to this version of request clears the "global cookie jar," and that the loop over the data from the cookie headers still checks the return value from the regular expression match, rather than assuming the user-agent will never send invalid cookie data.

Example 7-4. The WishListCustomer::Daemon class
 package WishListCustomer::Daemon;     use strict; use vars qw(@ISA);     use SOAP::Transport::HTTP; @ISA = qw(SOAP::Transport::HTTP::Daemon);     1;     sub request {     my $self = shift;         if (my $request = $_[0]) {         my @cookies = $request->headers->header('cookie');         %WishListCustomer::SOAP::COOKIES = ( );         for my $line (@cookies) {             for (split(/; /, $line)) {                 next unless /(.*?)=(.*)/;                 $WishListCustomer::SOAP::COOKIES{} = ;             }         }     }         $self->SUPER::request(@_); } 

Because the SOAP::Transport::HTTP classes handle everything else in a way that meets the project needs, there is nothing more for this class to do.

7.4.2.2 WishListCustomer::SOAP

In contrast, this class has more responsibility. Besides managing any authentication data sent in the request headers, there is also the basic matter of presenting the underlying functionality of WishListCustomer in a way that is more useful to a SOAP context. This should be done with as little impact on the original class as possible. Having no impact at all on the parent class is the ideal goal.

One of the things that makes this tricky is that clients using this interface with cookie-based authentication don't expect to have to call methods such as new explicitly. These clients expect to call methods such as Wishlist and AddBook immediately, without knowing what extra steps are involved.

To do this, the subclass borrows a trick from SOAP::Lite itself. The methods that aren't constructor-oriented (all but new and SetUser ) get their $self value in an unusual way:

 my $self = shift->new; 

In turn , the version of new for this class has as the first two lines:

 my $class = shift; return $class if ref($class); 

This extra bit of indirection means that the routines that require a validated user structure can still get it, if the needed information is carried in the cookie headers. Several of the data-oriented methods need this, so they are built up using another trick from the SOAP::Lite source:

 BEGIN {     no strict 'refs';         for my $method qw(GetBook BooksByAuthor BooksByTitle                       Wishlist AddBook RemoveBook                       PurchaseBooks) {         *$method = sub {             my $self = shift->new;             die SOAP::Fault                     ->faultcode('Server.RequestError')                     ->faultstring('Could not get object')                 unless $self;                 my $smethod = "SUPER::$method";             my $res = $self->$smethod(@_);             die SOAP::Fault                     ->faultcode('Server.ExecError')                     ->faultstring("Execution error: $res")                 unless ref($res);                 $res;        };     } } 

The three methods that can be static get this treatment as well; if the client calls them without establishing authentication, they still behave as documented. This also handles the issue of turning error reports (signaled by nonreference return values) into SOAP::Fault objects.

The CanPurchase method also needs to be defined in a similar way as the previous block, but it has an added responsibility. Perl's relaxed approach to scalar values in boolean evaluation means that there are no definitive, distinct true or false values. Perl routines simply use 1 and , or even undef versus anything. But for a SOAP environment, the values that CanPurchase returns must be typed as boolean; the SOAP serializer interprets the return value of WishListCustomer::CanPurchase as an integer and encodes it as such.

The overloaded version of the method handles sending a proper boolean response by explicitly typing the return value of the superclass call:

 SOAP::Data->name('return', $self->SUPER::CanPurchase)           ->type('xsd:boolean'); 

All that remains are the new and SetUser methods, which are shown in Example 7-5. The full version of WishListCustomer::SOAP is listed in Appendix D with the rest of the source files.

Example 7-5. The new and SetUser methods
 sub new {     my $class = shift;     return $class if ref($class);         my $self;     # If there are no arguments, but available cookies, then     # that is the signal to work the cookies into play     if ((! @_) and (keys %COOKIES)) {         # Start by getting the basic, bare object         $self = $class->SUPER::new( );         # Then call SetUser. It will die with a SOAP::Fault         # on any error         $self->SetUser;     } else {         $self = $class->SUPER::new(@_);     }         $self; }     sub SetUser {     my $self = shift->new;     my %args = @_;         return $self->SUPER::SetUser(%args) if (%args);         my $user;     my $cookie = $COOKIES{user};     return $self unless $cookie;     ($user = $cookie) =~ s/%([0-9a-f]{2})/chr(hex())/ge;     $user =~ s/%([0-9a-f]{2})/chr(hex())/ge;     $user =~ s/::.*//;         my $res = $self->SUPER::SetUser(user   => $user,                                     cookie => $cookie);     die SOAP::Fault             ->faultcode('Server.AuthError')             ->faultstring("Authorization failed: $res")         unless ref($res);         $self;} 

Note that the routines here use die to signal an error. Just returning a SOAP::Fault object isn't enough; the serializer will catch it before the server has the chance to create a proper SOAP fault response.

7.4.3 Revising the Daemon-Based Server

With the two new classes, it is time to revisit the server code, to see what will be needed to integrate these interfaces into the original framework. The new version of the server is shown in Example 7-6.

Example 7-6. The next-generation HTTP::Daemon-based server
 #!/usr/bin/perl     use strict;     use WishListCustomer::SOAP; use WishListCustomer::Daemon;     my $port = pop(@ARGV)  9000; my $host = shift(@ARGV)  'localhost';     WishListCustomer::Daemon     ->new(LocalAddr => $host, LocalPort => $port)     ->dispatch_with({ 'urn:/WishListCustomer' =>                       'WishListCustomer::SOAP' })     ->objects_by_reference('WishListCustomer::SOAP')     ->handle;     exit; 

That is all there is to integrating the new code. Does that seem too easy to be true? Perl's object-oriented programming model may often be criticized by object- purists , but this is an excellent case where it helps the programmer tremendously.

7.4.4 Simple Access with a SOAP::Lite Client

Before moving to a further revision of the server using Apache, check out the sample client in Example 7-7. In this client, the code loads in some extra libraries to manually create a cookie with the username and password because in a more traditional environment the user would have already been in contact with the server and thus have the appropriate cookies in the browser's cookie file. Because the server is returning objects by reference, the client could use the object handle for subsequent calls. The later example with Apache shows why the client is using cookies here instead.

Example 7-7 shows the retrieval of a user's wish list, with the fuller description of books, formatted in a simple, clean display.

Example 7-7. A simple client for formatting the lists of books
 #!/usr/bin/perl     use strict;     use URI; use HTTP::Cookies; use SOAP::Lite; use WishListCustomer; # for make_cookie     my ($user, $passwd) = (shift, shift); die "USAGE: 
 #!/usr/bin/perl     use strict;     use URI; use HTTP::Cookies; use SOAP::Lite; use WishListCustomer; # for make_cookie     my ($user, $passwd) = (shift, shift); die "USAGE: $0 username passwd [ endpoint ]\n"     unless ($user and $passwd);     my $endpoint = shift  'http://localhost.localdomain:9000'; my $uri = URI->new($endpoint); my $cookie = WishListCustomer::make_cookie($user, $passwd); my $cookie_jar = HTTP::Cookies->new( ); $cookie_jar->set_cookie(0, user => $cookie, '/', $uri->host,                         $uri->port);     my $soap = SOAP::Lite->uri('urn:/WishListCustomer')                ->proxy($endpoint,                        cookie_jar => $cookie_jar);     my $result = $soap->Wishlist; if ($result->fault) {     die "$0: Operation failed: " . $result->faultstring; } my $books = $result->result;     format = @<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<            @>>>>>> $result->{title},                        $result->{us_price} @<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< @>>>>>>>>>>>>>>>>> $result->{authors},                       $result->{isbn}     .     for ( sort { $a->{title} cmp $b->{title} } @$books) {     $result = $soap->GetBook($_->{isbn});     # Quietly skip books that cause faults     next if ($result->fault);     $result = $result->result;     write; } exit; 
username passwd [ endpoint ]\n" unless ($user and $passwd); my $endpoint = shift 'http://localhost.localdomain:9000'; my $uri = URI->new($endpoint); my $cookie = WishListCustomer::make_cookie($user, $passwd); my $cookie_jar = HTTP::Cookies->new( ); $cookie_jar->set_cookie(0, user => $cookie, '/', $uri->host, $uri->port); my $soap = SOAP::Lite->uri('urn:/WishListCustomer') ->proxy($endpoint, cookie_jar => $cookie_jar); my $result = $soap->Wishlist; if ($result->fault) { die "
 #!/usr/bin/perl     use strict;     use URI; use HTTP::Cookies; use SOAP::Lite; use WishListCustomer; # for make_cookie     my ($user, $passwd) = (shift, shift); die "USAGE: $0 username passwd [ endpoint ]\n"     unless ($user and $passwd);     my $endpoint = shift  'http://localhost.localdomain:9000'; my $uri = URI->new($endpoint); my $cookie = WishListCustomer::make_cookie($user, $passwd); my $cookie_jar = HTTP::Cookies->new( ); $cookie_jar->set_cookie(0, user => $cookie, '/', $uri->host,                         $uri->port);     my $soap = SOAP::Lite->uri('urn:/WishListCustomer')                ->proxy($endpoint,                        cookie_jar => $cookie_jar);     my $result = $soap->Wishlist; if ($result->fault) {     die "$0: Operation failed: " . $result->faultstring; } my $books = $result->result;     format = @<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<            @>>>>>> $result->{title},                        $result->{us_price} @<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< @>>>>>>>>>>>>>>>>> $result->{authors},                       $result->{isbn}     .     for ( sort { $a->{title} cmp $b->{title} } @$books) {     $result = $soap->GetBook($_->{isbn});     # Quietly skip books that cause faults     next if ($result->fault);     $result = $result->result;     write; } exit; 
: Operation failed: " . $result->faultstring; } my $books = $result->result; format = @<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< @>>>>>> $result->{title}, $result->{us_price} @<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< @>>>>>>>>>>>>>>>>> $result->{authors}, $result->{isbn} . for (sort { $a->{title} cmp $b->{title} } @$books) { $result = $soap->GetBook($_->{isbn}); # Quietly skip books that cause faults next if ($result->fault); $result = $result->result; write; } exit;

This example is shown in its entirety to illustrate the flexibility of the system. The same client will be used later with an Apache-based server, without any changes being made.



Programming Web Services with Perl
Programming Web Services with Perl
ISBN: 0596002068
EAN: 2147483647
Year: 2000
Pages: 123

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net