The newer of the two SOAP modules for Perl is SOAP::Lite , the work of coauthor Pavel Kulchenko. Unlike the DevelopMentor SOAP module, SOAP::Lite provides functionality in more abstract terms. Classes are provided for client-side functionality, server implementation, data support, and a variety of other tasks . Much of the XML is hidden from the application, except where necessary (or directly relevant). The SOAP::Lite module is also more actively maintained (keep up to date via the soaplite.com web site). It provides full support for SOAP 1.1, and is starting to provide support for SOAP 1.2 as well, with much of the draft specification already implemented. The module also implements XML-RPC, using the components already present in the package (parser, transport code, and the like). 6.3.1 Installing SOAP::LiteThe SOAP::Lite module has a thorough interactive installation process. This allows users to select which subcomponents are available, depending on whether the supporting modules are going to be installed and available. Example 6-3 shows the choices as presented when running perl Makefile.PL . Example 6-3. Starting the SOAP::Lite installation processXMLRPC::Lite, UDDI::Lite, and XML::Parser::Lite are included by default.Installed transports can be used for both SOAP::Lite and XMLRPC::Lite. Client (SOAP::Transport::HTTP::Client) [yes] Client HTTPS/SSL support (SOAP::Transport::HTTP::Client, require OpenSSL) [no] Client SMTP/sendmail support (SOAP::Transport::MAILTO::Client) [yes] Client FTP support (SOAP::Transport::FTP::Client) [yes] Standalone HTTP server (SOAP::Transport::HTTP::Daemon) [yes] Apache/mod_perl server (SOAP::Transport::HTTP::Apache, require Apache) [no] FastCGI server (SOAP::Transport::HTTP::FCGI, require FastCGI) [no] POP3 server (SOAP::Transport::POP3::Server) [yes] IO server (SOAP::Transport::IO::Server) [yes] MQ transport support (SOAP::Transport::MQ) [no] JABBER transport support (SOAP::Transport::JABBER) [no] MIME messages [required for POP3, optional for HTTP] (SOAP::MIMEParser) [no] SSL support for TCP transport (SOAP::Transport::TCP) [no] Compression support for HTTP transport (SOAP::Transport::HTTP) [no] Do you want to proceed with this configuration? [yes] These installation options are the defaults. The machine on which this output sample was generated had many of the optional Perl modules already available, but the installation process doesn't probe for them unless the user performing the installation enables a particular component. If the user chooses to not accept the default, she is prompted for each option, whether to enable it with yes or disable it with no . The current values are presented as defaults, as you'd expect. After the full range of choices have been presented, the current configuration is again shown, and the user asked whether to accept and continue, or go back and make further adjustments to the configuration. Which of the optional components to enable is a matter of taste and of what is to be expected of the applications being developed. In general, any of the components for which the needed modules are already present on the system (such as the Apache support, MIME support, or compression) might as well be enabled. Enabling secure transport support, for example, will require that the system have the Perl modules for SSL, as well as any system libraries such as OpenSSL that they require. The SSL modules, IO::Socket::SSL and Crypt::SSLeay , can be installed by CPAN as part of the installation process for SOAP::Lite itself. The configuration reports any missing modules. Like many CPAN modules, SOAP::Lite has a fairly large suite of test scripts that will run during the make test phase of building the module. During the configuration phase, the user is given the option of skipping some of the tests, specifically those that will connect to live SOAP servers. This can be useful for systems that don't have continuous Internet access for systems behind firewalls. 6.3.2 Using SOAP::Lite for ClientsBecause they share a common architecture and subsystem, the SOAP::Lite client developed for the number-to-string translation server looks a lot like the XMLRPC::Lite client developed in Chapter 4. 6.3.2.1 Number-to-text conversion with SOAP::LiteExample 6-4 shows the interesting part of the client (the command-line processing code, for example, has been omitted). Take note of how much simpler this client is than the client from Example 6-1, which used the DevelopMentor SOAP toolkit. Example 6-4. The num2name.pl script using SOAP::Lite#!/usr/bin/perl use strict; use SOAP::Lite; my $num = shift; $num =~ /^\d+$/ or die "USAGE:#!/usr/bin/perl use strict; use SOAP::Lite; my $num = shift; $num =~ /^\d+$/ or die "USAGE: $0 num\n"; my ($server, $endpoint, $soapaction, $method, $method_urn); $server = 'http://www.tankebolaget.se'; $endpoint = "$server/scripts/NumToWords.dll/soap/INumToWords"; $soapaction = "urn:NumToWordsIntf-INumToWords#NumToWords_English"; $method = 'NumToWords_English'; $method_urn = 'urn:NumToWordsIntf-INumToWords'; my $num2words = SOAP::Lite->new(uri => $soapaction, proxy => $endpoint); my $response = $num2words ->call(SOAP::Data-> name ($method) ->attr( { xmlns => $method_urn } ) => # Argument(s) listed next SOAP::Data->name(aNumber => $num)); if ($response->fault) { printf "A fault (%s) occurred: %s\n", $response->faultcode, $response->faultstring; } else { print "$num may be expressed as " . $response->result . "\n"; } exit;num\n"; my ($server, $endpoint, $soapaction, $method, $method_urn); $server = 'http://www.tankebolaget.se'; $endpoint = "$server/scripts/NumToWords.dll/soap/INumToWords"; $soapaction = "urn:NumToWordsIntf-INumToWords#NumToWords_English"; $method = 'NumToWords_English'; $method_urn = 'urn:NumToWordsIntf-INumToWords'; my $num2words = SOAP::Lite->new(uri => $soapaction, proxy => $endpoint); my $response = $num2words ->call(SOAP::Data->name($method) ->attr( { xmlns => $method_urn } ) => # Argument(s) listed next SOAP::Data->name(aNumber => $num)); if ($response->fault) { printf "A fault (%s) occurred: %s\n", $response->faultcode, $response->faultstring; } else { print "$num may be expressed as " . $response->result . "\n"; } exit; The first area of difference comes in the inclusion of code to provide the SOAP functionality. The four use statements in the original are replaced with: use SOAP::Lite; The SOAP::Lite package loads other modules as needed. We interact with the server via a SOAP::Lite object. Its construction is simply: my $num2words = SOAP::Lite->new(uri => $soapaction, proxy => $endpoint); The meaning of uri and proxy , and of the values in $soapaction and $endpoint , are covered later. The remote method call is done in an extremely verbose way, for example: my $response = $num2words->call(SOAP::Data ->name($method) ->attr( { xmlns => $method_urn } ) => # Argument(s) listed next SOAP::Data->name(aNumber => $num)); Depending on the strictness of the server, that call might be as short as: my $response = $num2words->$method($num); This syntax is covered later. [2]
Finally, the examination of the return value uses methods available on the returned object to test whether the response received was a fault or not. If it was, further methods ( faultcode and faultstring ) are used to construct an error message. Otherwise, the result is easily extracted from the response object. The assignments to $server and $endpoint differ slightly from those in Example 6-1. In that example, $soapaction was not actually used because, by chance, its form matched what the SOAP module created internally from the values of $method and $method_urn . Here it is used when creating the SOAP::Lite handle object. The value of $endpoint ends up being the full URL, because the logic within SOAP::Transport examines the string to choose the proper transport binding. In the first example, the server's hostname and port had to be passed separately due to the way in which SOAP created the final URL. As was mentioned earlier in the commentary , there are several places where this code could have been written more concisely. As it stands, it is roughly 10% shorter than the version which used SOAP . 6.3.2.2 Translating a use.perl.org journal stream to RSSThis next example uses the SOAP interface at the use Perl; discussion site (http://use.perl.org), taking the last 15 journal entries for a specified user [3] and creating a RSS 1.0 syndication feed from them.
The use Perl; site is built on the Slash web form code (http://www.slashcode.org). In addition to news and conversational forums, it provides web-log facilities to its registered users. Many well-known characters in the Perl community maintain ongoing commentary on their projects and tasks. The collection of story headlines is retrievable as a RSS feed, as is the list of most-recently updated journals. But the journal of an individual user isn't (currently) available as a RSS feed. Into this gap steps the upj2rss.pl utility in Example 6-5. Example 6-5. upj2rss.pl, turning a journal log into RSS#!/usr/bin/perl -w use strict; use SOAP::Lite; use XML::RSS; my $user = shift die "Usage:#!/usr/bin/perl -w use strict; use SOAP::Lite; use XML::RSS; my $user = shift die "Usage: $0 userID [ usernick ]\n\nStopped"; my $nick = shift "#$user"; my $host = 'http://use.perl.org'; my $uri = "$host/Slash/Journal/SOAP"; my $proxy = "$host/journal.pl"; # SOAP material starts here: my $journal = SOAP::Lite->uri($uri)->proxy($proxy); my $results = $journal->get_entries($user, 15)->result; my $rss = XML::RSS->new(version => '1.0'); $rss->channel(title => "use.perl.org journal of $nick", 'link' => $proxy, description => "The use.perl.org journal of $nick"); $rss->add_item(title => $_->{subject}, 'link' => $_->{url}) for (@$results); print STDOUT $rss->as_string; exit;userID [ usernick ]\n\nStopped"; my $nick = shift "#$user"; my $host = 'http://use.perl.org'; my $uri = "$host/Slash/Journal/SOAP"; my $proxy = "$host/journal.pl"; # SOAP material starts here: my $journal = SOAP::Lite->uri($uri)->proxy($proxy); my $results = $journal->get_entries($user, 15)->result; my $rss = XML::RSS->new(version => '1.0'); $rss->channel(title => "use.perl.org journal of $nick", 'link' => $proxy, description => "The use.perl.org journal of $nick"); $rss->add_item(title => $_->{subject}, 'link' => $_->{url}) for (@$results); print STDOUT $rss->as_string; exit; Example 6-5 uses another CPAN module, XML::RSS , which is a useful tool for creating and parsing syndication feeds. Here it creates the syndication-feed version of the journal data. For this example, assume that it is a working black box that doesn't need further explanation. Starting right after the comment that says, "SOAP material starts here," a handle object is created in much the same way as in Example 6-4. Here, rather than calling new( ) explicitly, the application lets the SOAP::Lite module do that at the time of the first method invocation ( uri , in this case). Calling one of the class methods as a static method automatically triggers this behavior. The uri( ) and proxy( ) methods set the SOAPAction header and communication URL, respectively. In the previous example, these were passed as parameters to the constructor. The next line uses this client object to invoke a method called get_entries on the server. Two arguments are passed (both integers), and the return value invokes a method called result . The return value from a method call is a reference to an object in the SOAP::SOM class. This is a class used to encapsulate the returned message body from the server. In more detailed examples later, this object will be more fully utilized. For now, it is enough to know that the result method takes the data content of the response and transforms it to a native Perl datatype. Example 6-4 returned a single string, but in this case, the data is an array reference with up to 15 journal entries ( assuming there were at least that many available) for the user whose account ID on use.perl.org was the value in $user . The rest of the script details aren't really that important. Absent here is any real checking of return values or tests for data validity. The uri and proxy methods can be safely assumed to return a valid object reference. No connections are made by calling those methods; they set only values internal to the object. The method invocation itself on the next line could have had problems, though. 6.3.2.3 Basic classes and componentsThese initial examples showed how few basic components are needed to accomplish tasks with the SOAP::Lite toolkit. Several classes other than SOAP::Lite came in to play, but their presence was quietly abstracted by the interface. In fact, the primary role that the SOAP::Lite class plays is to provide shortcuts to methods that are part of the classes used behind the scenes. There are methods that are actual parts of SOAP::Lite , however, as will be seen later. What are these classes, then? Which of them should an application developer be concerned with? Table 6-1 lists the classes developers should begin with. Table 6-1. The basic elements of the SOAP::Lite toolkit
The elements in Table 6-1 were mentioned in the discussion of the two previous examples. Two have already been explained, the other two are:
In general, the methods from SOAP::Lite are considered accessor methods and as such return their current value when no arguments are passed in to the call. If arguments are passed, they return the calling object to enable the chaining of method calls. The examples thus far have used only a few of the SOAP::Lite methods:
This is a confusing distinction to many people. When using SOAP over HTTP to do remote procedure calls, the messages have to have a URI that identifies the "service." This may be the same as the HTTP address, similar to it, or completely different. It is just another identifier, though, one that happens to look like a web address in most cases.
The latter two methods here actually act as shortcuts to the SOAP::Transport class, one of the several classes that SOAP::Lite abstracts the user from in most cases. The SOAP::Lite class also has several settings that are activated when the library is imported into the application, for example: use SOAP::Lite +autodispatch, +trace; Many of the standard settings may also be specified here, such as uri , proxy , etc. When these are set at the import-level like this, they become the values on the global object instance that SOAP::Lite quietly maintains behind the scenes. It is from this object that other instances take their default values, and from this object that the on-the-fly construction of object instances happens. (Recall from the explanation following Example 6-5, that most of the methods will automatically instantiate an object of the class when they are called as static methods.) Here are the settings that may be given to SOAP::Lite when it's imported to an application:
Returning to the SOAP::Trace class mentioned earlier, this is the class which encapsulates the debugging/tracing facility. This is another "behind the scenes" class, whose only visible interface is the +debug / +trace option defined earlier. As was mentioned then, the tracing class is implemented in terms of a set of events, which are listed in Table 6-2. Not all the events are relevant to both clients and servers; the table explains their roles. Table 6-2. SOAP::Trace events and their meanings
There is a last, pseudo-event specifier called all . Normally it isn't needed because passing +trace with no event list has the effect of enabling all events. However, you can disable an event by prefixing its name with a hyphen. Thus, the following line enables all tracing events except the debug events: use SOAP::Lite +trace => [qw(all -debug)]; The default action for these events is to log the data that they get passed to STDERR . Alternately, the event given on the importing line can specify a subroutine reference or a closure, and have that called on the event instead. These trace-event callbacks receive the same information that the native ones would have received, as given in Table 6-2. An example of tracing faults might look like this: use SOAP::Lite +trace => [fault => \&follow_fault]; # ... sub follow_fault { warn "Fault occured:\n\t" . join("\n\t", @_) . "\n"; } 6.3.2.4 Dispatching methods and the object styleRecall the syntax alluded to in Example 6-4, which was used in Example 6-5. The client object called a method on the use.perl.org server in this fashion: $journal->get_entries($user, 15) It's no surprise that the SOAP::Lite class, which $journal holds an instance reference of, doesn't actually implement a method called get_entries . Nor would you expect a class to try to predict all the potential method names that may ever be conjured. This is a syntactical shortcut SOAP::Lite provides to client objects, one that makes the syntax of the application around that object look more comfortable and familiar. Perl has an extremely flexible feature called auto-loading , [4] which is used in a variety of creative ways by other modules from both within the core of Perl and from CPAN. This is another highly creative application of that functionality.
When a client object tries to call a method that doesn't exist in the package, the class catches the call and tries to execute it remotely. The beauty of it all is that to the application using the object, it is just another method call. With that syntax, it becomes much more familiar and comfortable to use the client object as if it were any other type of garden-variety object, containing and accessing data in local memory. The sense of abstraction has extended not just to the classes and components that implement the SOAP conversation, that abstraction has resulted in almost completely hiding the fact that there even is a SOAP conversation taking place. Using the $journal object more freely , the sample program can be made to retrieve more information for each journal entry, resulting in a richer syndication: my $results = $journal->get_entries($user, 15)->result; my @results = map { my $entry = $journal->get_entry($_->{id}) ->result; # Imagine this uses HTML::Parser to get # roughly the first paragraph my $desc = extract_desc($entry->{body}); # Imagine $nick wasn't set earlier $nick = $entry->{nickname}; # Each map-value is a hash reference: { 'link' => $entry->{url}, description => $desc, title => $entry->{subject} }; } (@$results); The initialization of the $rss object is the same, except that now $nick has the user's actual nickname on use.perl.org , instead of the script defaulting to the user ID. But then, change the lines that add the contents to the RSS channel to this: $rss->add_item(%$_) for (@results); Now, the resulting RSS channel has more than just the subject lines of the journal entries. The leading paragraph or so, depending on the implementation of the hypothetical extract_desc routine, has been added as the description for the RSS entry. The possibilities are limited only by the information actually provided from the call. Also, nothing in the earlier map block looks significantly different from any other object-oriented Perl usage. Only the result method being called after the get_entry method offers any clue that this might be different from a generic database or other data-store layer. This form of method-dispatching is commonly used. However, SOAP::Lite also allows for even more- liberal dispatching, with the +autodispatch and dispatch_from options that may be set at import-time. When these are used, the functions that get automatically routed to the remote server may be any ordinary (as in non -method) function calls within the code. Note that in the following code fragment: use SOAP::Lite +autodispatch => uri => 'http://...' => proxy => 'http://...'; not_local($arg1, $arg2); both the uri and the proxy are specified at the same time as the +autodispatch setting. This provides the basis for SOAP::Lite to try and execute any "rogue" subroutines it gets handed. Assuming that the routine not_local isn't already defined somewhere else, it is handed to SOAP::Lite to try and execute remotely. The difference made by dispatch_from is one of scoping these open -ended executions to a given class namespace: use SOAP::Lite dispatch_from => Remote => uri => 'http://...' => proxy => 'http://...'; # This will fail at run-time not_local($arg1, $arg2); # This will not-- unless "not_local" isn't found remotely Remote->not_local($arg1, $arg2); The primary difference is that the main application namespace isn't going to trigger an attempt to run a procedure remotely. The dispatch_from functionality only hooks the auto-loading in at the level of the named classes. The +autodispatch mechanism is very convenient but also opens the door to a lot of potential pitfalls. The most obvious of these is the danger inherent in running remote code accidentally , perhaps by way of a typo. Using the automatic dispatching can also be noisy when warnings are enabled with Perl's -w flag because of the AUTOLOAD routine SOAP::Lite installs to enable auto-dispatching. The interpreter perceives the routine as being inherited from a superclass, which is one of the warning conditions -w checks for. Beyond the noise and potential for user error, however, is the real factor of clarity in the code, and readability to those others who may have to maintain the code long after the original author has moved on to other projects. Auto-dispatched methods that have no class qualifier at all aren't easily distinguished from actual local functions that are part of the application itself or of a local library. Consider using dispatch_from in those cases where +autodispatch seems to be the tool to use. [5] Leave +autodispatch for the smaller cases, one-liners, and utilities that are shorter and easier to maintain. It may seem less convenient at the time, but such compromises almost always pay dividends over the longer lifecycle of software.
6.3.2.5 Managing data with SOAP::Data and SOAP::SOMRecall the way data was passed to the remote method in Example 6-4. In order to maintain compliance with the server, data was constructed so that it serialized in a very strict and clean fashion. This was accomplished using the methods from the SOAP::Data class. SOAP::Data is intended to help out the application when the default serialization of a value might be either wrong or at least incomplete. A common example is when a string appears to be a number. Because of the way Perl treats scalars, a value of 123 may be treated as an integer when it needs to be (for math operations) or a string if so needed (for concatenation as an example). Because of this flexibility, it can be hard to know for certain when such a value is meant to be a specific type. There may also be other issues beyond just the type of the data: a string may require a different encoding, or the accessor name being associated with the value may need to be given a specific namespace qualification. Objects of the SOAP::Data class aren't generally instantiated for the long term . That is, most usage of the class is just to create an object long enough for it to be passed in a subroutine call, after which it is discarded. While there is a new method, its use is rarely seen. More often seen is this type of usage: SOAP::Data->name(zipCode => 95008)->type('string') This example is based on the fact that a U.S. postal Zip Code can have a four-digit extension at the end, with a hyphen separating it from the main five-digit code. Because of this, an application that accepts Zip Codes as input must take them as string data. However, the natural tendency of Perl is to treat 95008 as an integer. Because of that, such a value must be "coerced" into the correct type. Like SOAP::Lite itself, the methods within SOAP::Data each create an object when called as static methods and return the object when called with any parameters. Any of the methods, such as value , that are called with no parameters return the current setting of the given aspect of the data. The more commonly used methods provided by this class are summarized in Table 6-3, with a complete list available in Appendix B. Table 6-3. SOAP::Data class methods
Another class that is important in a "behind-the-scenes" fashion is SOAP::SOM , which was referred to in the earlier examples. This class encapsulates the returned messages from remote calls. In many cases, the class is completely transparent to the application using it. Like HTTP::Response in the LWP package, an application doesn't instantiate it. Rather, it is handed back at the completion of a remote call. Objects of this class are useful for more than simply chaining an invocation of the result method onto the end of a call. If the object itself is kept as the return value, the application can check the fault status of the call more closely. More specifically, SOAP::SOM objects give the application full access to the deserialized envelope itself. Many of the data-retrieval methods use a syntax very much like XPath notation. Depending on the depth of data in the envelopes exchanged between a client and server, the application may only need the first few of the methods explained. There are actually a large number of methods in this class, though many don't see frequent use. The more common methods are summarized in Table 6-4. Table 6-4. SOAP::SOM class methods
The previous examples built on SOAP::Lite assumed that their remote calls were successful. The next example incorporates the SOAP::SOM methods to check more carefully . 6.3.2.6 Example: Automatically announcing CPAN uploadsThe SOAP interface at use.perl.org also permits posting journal entries. At first glance, it might not be clear why a script would be used to perform a task like this, but upon closer look it becomes obvious. There are plenty of situations in which you want to automatically post some item of news to a forum, such as a developer's personal journal. This example focuses on the announcement of new CPAN uploads. When an author uploads a new release of a package to CPAN, some automatic status messages are sent back to his registered contact address by email, signaling certain points in the submittal process. Some authors choose to then announce on one or more forums that the new code is now being mirrored across the range of servers. Because the email is an automated, reliable event that signals a successful CPAN submission, it makes an excellent herald for other types of announcements. This sample script takes one of the two messages sent out by the PAUSE daemon (Perl Authors Upload Server) and uses some of the information within it to create the HTML body of a journal entry. The body is then posted to the user's journal at use.perl.org , with an appropriate subject. Not all elements of the code will be examined closely, but should be clear when looking at the script as a whole. The script will follow this explanation, as Example 6-6. This first segment of code is the creation of the HTTP cookie data that will be part of the request. Unlike the read-only operations used in earlier examples, operations such as posting a journal entry require authentication. The use.perl.org SOAP service uses the same browser cookie for validation that the web server itself uses for personalization and access to individual content. This utility may not be running in an environment that has access to the user's cookie file, so it accepts credentials as a command-line argument. There are other arguments, which will all be parsed by the Getopt::Std module. The %opts hash table is a global variable that contains these options. Preparing the cookie entails the following: $host = 'use.perl.org'; # Note that this path is UNIX-centric. Consider using # File::Spec methods in most cases. $cookie_file = "$ENV{HOME}/.netscape/cookies"; if ($opts{C}) { $cookie_jar = HTTP::Cookies->new; $cookie_jar ->set_cookie(0, user => make_cookie(split /:/, $opts{C}), '/', $host); } elsif (-f $cookie_file) { $cookie_jar = HTTP::Cookies::Netscape ->new(File => $cookie_file); } else { die "$host = 'use.perl.org'; # Note that this path is UNIX-centric. Consider using # File::Spec methods in most cases. $cookie_file = "$ENV{HOME}/.netscape/cookies"; if ($opts{C}) { $cookie_jar = HTTP::Cookies->new; $cookie_jar ->set_cookie(0, user => make_cookie(split /:/, $opts{C}), '/', $host); } elsif (-f $cookie_file) { $cookie_jar = HTTP::Cookies::Netscape ->new(File => $cookie_file); } else { die "$0: No authentication data found, cannot continue"; }: No authentication data found, cannot continue"; } The make_cookie routine that creates the cookie from the command-line data will be shown in the final script. What is noteworthy is that the tool will give the command-line priority over an existing cookie file, and because the HTTP::Cookies package doesn't provide a method to check for a specific cookie, no test is made that the cookie file even provided the script with a useful cookie for this site. If it is the case that there is no valid cookie, the script will catch that at a later point. The script next takes the mail message and extracts the data it wants from it. To make the script operate in a familiar and friendly manner, it will be designed to read the email message from the STDIN filehandle, like a filter. A sample message from a release of the RPC::XML module looks a little like this: The uploaded file RPC-XML-0.37.tar.gz has entered CPAN as file: $CPAN/authors/id/R/RJ/RJRAY/RPC-XML-0.37.tar.gz size: 86502 bytes md5: d58344bf2c80f44b95a13bf838628517 No action is required on your part Request entered by: RJRAY (Randy J Ray) Request entered on: Sat, 23 Mar 2002 06:37:19 GMT Request completed: Sat, 23 Mar 2002 06:39:00 GMT Virtually Yours, Id: paused,v 1.80 2002/03/10 06:40:03 k Exp k The most relevant piece of information here is the seventh line, which starts with the sequence, file :. This path can be easily turned into an active CPAN URL (though the file itself is still in the process of being mirrored, and so the URL may not yet work universally ). For the sake of this example, the text for the journal entry is kept to a minimum. In this case, it announces the package by the filename and provides a direct link to it using the data on line 7. Moving to the SOAP-relevant content of the script, assume that the processing of the mail message has been done. The initial lines that set up the client to the SOAP service are generally the same as with the earlier example: $host = 'use.perl.org'; $uri = "http://$host/Slash/Journal/SOAP"; $proxy = "http://$host/journal.pl"; $journal = SOAP::Lite ->uri($uri) ->proxy($proxy, cookie_jar => $cookie_jar); die "$host = 'use.perl.org'; $uri = "http://$host/Slash/Journal/SOAP"; $proxy = "http://$host/journal.pl"; $journal = SOAP::Lite ->uri($uri) ->proxy($proxy, cookie_jar => $cookie_jar); die "$0: Error creating SOAP::Lite client, cannot continue" unless $journal;: Error creating SOAP::Lite client, cannot continue" unless $journal; The difference here is in the call to the proxy method, when the client object is being created. The $cookie_jar object created earlier is passed along with the URL itself. Because the URL is recognized by SOAP::Lite as a HTTP endpoint, it also recognize the cookie_jar key (and value) as relevant to the creation of a user-agent object. The jar will be incorporated into the LWP::UserAgent object that eventually handles the connections to the remote host. There are other options that can be given to the proxy method under an umbrella-option that is simply called options and which takes a hash reference as an argument. The actual options vary from one type of transport to the next. Some, such as compress_threshold, which enables compression support, require that the functionality be enabled on both ends of the conversation. These options will be dealt with in greater detail at a later point. Making the call itself isn't that different from the earlier operation. According to the description of the service, the method used to post a journal entry is called add_entry , and it takes a sequence of key/value pairs. For now, assume that $body , $title , and $discuss were computed prior to this point: $result = $journal->add_entry(subject => $title, body => $body, discuss => $discuss); if ($result->fault) { die "$result = $journal->add_entry(subject => $title, body => $body, discuss => $discuss); if ($result->fault) { die "$0: Failed: " . $result->faultstring . "\n"; } else { printf "New entry added as %s\n", $result->result; }: Failed: " . $result->faultstring . "\n"; } else { printf "New entry added as %s\n", $result->result; } The result of the operation is saved in its native state as a SOAP::SOM object instance. This allows the script to test it with the expression ($result->fault) and act accordingly . If it is a fault, the application dies with the fault string as an error message. But if it succeeds, a brief message announcing success is given. It uses the result method on the object to get the new entry's ID number. Now for the script in full. It is designed to be run from a mail-filter program such as procmail . A sample procmail "recipe" for this might look like the following: :0 Wc * Subject.*CPAN Upload cpan2upj.pl This recipe (as procmail rules are called) matches messages whose subject line starts with CPAN Upload . The message is piped in to the script, which is called cpan2upj.pl . Example 6-6 is the complete cpan2upj.pl script. Example 6-6. cpan2upj.pl#!/usr/bin/perl -w use strict; use File::Basename 'basename'; use Getopt::Std; use Digest::MD5 'md5_hex'; use HTTP::Cookies; use SOAP::Lite; my (%opts, $user, $title, $discuss, $body, $host, $uri, $proxy, $file, $name, $cookie_file, $cookie_jar, $journal, $result); getopts('C:c', \%opts) or die <<"USAGE"; Usage:#!/usr/bin/perl -w use strict; use File::Basename 'basename'; use Getopt::Std; use Digest::MD5 'md5_hex'; use HTTP::Cookies; use SOAP::Lite; my (%opts, $user, $title, $discuss, $body, $host, $uri, $proxy, $file, $name, $cookie_file, $cookie_jar, $journal, $result); getopts('C:c', \%opts) or die <<"USAGE"; Usage: $0 [ -c ] [ -C user:pass ] -c Allow comments on journal entry -C user:pass Provide authentication in place of (or in absence of) Netscape cookies. User in this case is UID, not nickname. USAGE while (defined($_ = <STDIN>)) { $file = $1, last if /^\s+file:\s+(\S+)/; } die "$0: No filename found in input, stopped " unless $file; $name = basename $file; $file =~ s^\$CPANhttp://www.cpan.org; $user = (getpwuid($<))[0]; $title = "$name uploaded to PAUSE"; $discuss = $opts{c} ? 1 : 0; $body = << "BODY"; <p>The file <tt>$name</tt> has been uploaded to CPAN, and will soon be accessible as <a href="$file">$file</a>. Mirroring may take up to 24 hours.</p> <p><i>$0, on behalf of $user</i></p> BODY $host = 'use.perl.org'; $uri = "http://$host/Slash/Journal/SOAP"; $proxy = "http://$host/journal.pl"; # Note that this path is UNIX-centric. Consider using # File::Spec methods in most cases. $cookie_file = "$ENV{HOME}/.netscape/cookies"; if ($opts{C}) { $cookie_jar = HTTP::Cookies->new ->set_cookie(0, user => make_cookie(split /:/, $opts{C}), '/', $host); } elsif (-f $cookie_file) { $cookie_jar = HTTP::Cookies::Netscape->new(File => $cookie_file); } else { die "$0: No authentication data found, cannot continue"; } $journal = SOAP::Lite->uri($uri) ->proxy($proxy, cookie_jar => $cookie_jar); die "$0: Error creating SOAP::Lite client, cannot continue" unless $journal; $result = $journal->add_entry(subject => $title, body => $body, discuss => $discuss); if ($result->fault) { die "$0: Failed: " . $result->faultstring . "\n"; } else { printf "New entry added as %s\n", $result->result; } exit; # Taken from the Slash codebase sub make_cookie { my ($uid, $passwd) = @_; my $cookie = $uid . '::' . md5_hex($passwd); $cookie =~ s/(.)/sprintf("%%%02x", ord($1))/ge; $cookie =~ s/%/%25/g; $cookie; }[ -c ] [ -C user:pass ] -c Allow comments on journal entry -C user:pass Provide authentication in place of (or in absence of) Netscape cookies. User in this case is UID, not nickname. USAGE while (defined($_ = <STDIN>)) { $file = , last if /^\s+file:\s+(\S+)/; } die "#!/usr/bin/perl -w use strict; use File::Basename 'basename'; use Getopt::Std; use Digest::MD5 'md5_hex'; use HTTP::Cookies; use SOAP::Lite; my (%opts, $user, $title, $discuss, $body, $host, $uri, $proxy, $file, $name, $cookie_file, $cookie_jar, $journal, $result); getopts('C:c', \%opts) or die <<"USAGE"; Usage: $0 [ -c ] [ -C user:pass ] -c Allow comments on journal entry -C user:pass Provide authentication in place of (or in absence of) Netscape cookies. User in this case is UID, not nickname. USAGE while (defined($_ = <STDIN>)) { $file = $1, last if /^\s+file:\s+(\S+)/; } die "$0: No filename found in input, stopped " unless $file; $name = basename $file; $file =~ s^\$CPANhttp://www.cpan.org; $user = (getpwuid($<))[0]; $title = "$name uploaded to PAUSE"; $discuss = $opts{c} ? 1 : 0; $body = << "BODY"; <p>The file <tt>$name</tt> has been uploaded to CPAN, and will soon be accessible as <a href="$file">$file</a>. Mirroring may take up to 24 hours.</p> <p><i>$0, on behalf of $user</i></p> BODY $host = 'use.perl.org'; $uri = "http://$host/Slash/Journal/SOAP"; $proxy = "http://$host/journal.pl"; # Note that this path is UNIX-centric. Consider using # File::Spec methods in most cases. $cookie_file = "$ENV{HOME}/.netscape/cookies"; if ($opts{C}) { $cookie_jar = HTTP::Cookies->new ->set_cookie(0, user => make_cookie(split /:/, $opts{C}), '/', $host); } elsif (-f $cookie_file) { $cookie_jar = HTTP::Cookies::Netscape->new(File => $cookie_file); } else { die "$0: No authentication data found, cannot continue"; } $journal = SOAP::Lite->uri($uri) ->proxy($proxy, cookie_jar => $cookie_jar); die "$0: Error creating SOAP::Lite client, cannot continue" unless $journal; $result = $journal->add_entry(subject => $title, body => $body, discuss => $discuss); if ($result->fault) { die "$0: Failed: " . $result->faultstring . "\n"; } else { printf "New entry added as %s\n", $result->result; } exit; # Taken from the Slash codebase sub make_cookie { my ($uid, $passwd) = @_; my $cookie = $uid . '::' . md5_hex($passwd); $cookie =~ s/(.)/sprintf("%%%02x", ord($1))/ge; $cookie =~ s/%/%25/g; $cookie; }: No filename found in input, stopped" unless $file; $name = basename $file; $file =~ s^$CPANhttp://www.cpan.org; $user = (getpwuid($<))[0]; $title = "$name uploaded to PAUSE"; $discuss = $opts{c} ? 1 : 0; $body = << "BODY"; <p>The file <tt>$name</tt> has been uploaded to CPAN, and will soon be accessible as <a href="$file">$file</a>. Mirroring may take up to 24 hours.</p> <p><i>#!/usr/bin/perl -w use strict; use File::Basename 'basename'; use Getopt::Std; use Digest::MD5 'md5_hex'; use HTTP::Cookies; use SOAP::Lite; my (%opts, $user, $title, $discuss, $body, $host, $uri, $proxy, $file, $name, $cookie_file, $cookie_jar, $journal, $result); getopts('C:c', \%opts) or die <<"USAGE"; Usage: $0 [ -c ] [ -C user:pass ] -c Allow comments on journal entry -C user:pass Provide authentication in place of (or in absence of) Netscape cookies. User in this case is UID, not nickname. USAGE while (defined($_ = <STDIN>)) { $file = $1, last if /^\s+file:\s+(\S+)/; } die "$0: No filename found in input, stopped " unless $file; $name = basename $file; $file =~ s^\$CPANhttp://www.cpan.org; $user = (getpwuid($<))[0]; $title = "$name uploaded to PAUSE"; $discuss = $opts{c} ? 1 : 0; $body = << "BODY"; <p>The file <tt>$name</tt> has been uploaded to CPAN, and will soon be accessible as <a href="$file">$file</a>. Mirroring may take up to 24 hours.</p> <p><i>$0, on behalf of $user</i></p> BODY $host = 'use.perl.org'; $uri = "http://$host/Slash/Journal/SOAP"; $proxy = "http://$host/journal.pl"; # Note that this path is UNIX-centric. Consider using # File::Spec methods in most cases. $cookie_file = "$ENV{HOME}/.netscape/cookies"; if ($opts{C}) { $cookie_jar = HTTP::Cookies->new ->set_cookie(0, user => make_cookie(split /:/, $opts{C}), '/', $host); } elsif (-f $cookie_file) { $cookie_jar = HTTP::Cookies::Netscape->new(File => $cookie_file); } else { die "$0: No authentication data found, cannot continue"; } $journal = SOAP::Lite->uri($uri) ->proxy($proxy, cookie_jar => $cookie_jar); die "$0: Error creating SOAP::Lite client, cannot continue" unless $journal; $result = $journal->add_entry(subject => $title, body => $body, discuss => $discuss); if ($result->fault) { die "$0: Failed: " . $result->faultstring . "\n"; } else { printf "New entry added as %s\n", $result->result; } exit; # Taken from the Slash codebase sub make_cookie { my ($uid, $passwd) = @_; my $cookie = $uid . '::' . md5_hex($passwd); $cookie =~ s/(.)/sprintf("%%%02x", ord($1))/ge; $cookie =~ s/%/%25/g; $cookie; }, on behalf of $user</i></p> BODY $host = 'use.perl.org'; $uri = "http://$host/Slash/Journal/SOAP"; $proxy = "http://$host/journal.pl"; # Note that this path is UNIX-centric. Consider using # File::Spec methods in most cases. $cookie_file = "$ENV{HOME}/.netscape/cookies"; if ($opts{C}) { $cookie_jar = HTTP::Cookies->new ->set_cookie(0, user => make_cookie(split /:/, $opts{C}), '/', $host); } elsif (-f $cookie_file) { $cookie_jar = HTTP::Cookies::Netscape->new(File => $cookie_file); } else { die "#!/usr/bin/perl -w use strict; use File::Basename 'basename'; use Getopt::Std; use Digest::MD5 'md5_hex'; use HTTP::Cookies; use SOAP::Lite; my (%opts, $user, $title, $discuss, $body, $host, $uri, $proxy, $file, $name, $cookie_file, $cookie_jar, $journal, $result); getopts('C:c', \%opts) or die <<"USAGE"; Usage: $0 [ -c ] [ -C user:pass ] -c Allow comments on journal entry -C user:pass Provide authentication in place of (or in absence of) Netscape cookies. User in this case is UID, not nickname. USAGE while (defined($_ = <STDIN>)) { $file = $1, last if /^\s+file:\s+(\S+)/; } die "$0: No filename found in input, stopped " unless $file; $name = basename $file; $file =~ s^\$CPANhttp://www.cpan.org; $user = (getpwuid($<))[0]; $title = "$name uploaded to PAUSE"; $discuss = $opts{c} ? 1 : 0; $body = << "BODY"; <p>The file <tt>$name</tt> has been uploaded to CPAN, and will soon be accessible as <a href="$file">$file</a>. Mirroring may take up to 24 hours.</p> <p><i>$0, on behalf of $user</i></p> BODY $host = 'use.perl.org'; $uri = "http://$host/Slash/Journal/SOAP"; $proxy = "http://$host/journal.pl"; # Note that this path is UNIX-centric. Consider using # File::Spec methods in most cases. $cookie_file = "$ENV{HOME}/.netscape/cookies"; if ($opts{C}) { $cookie_jar = HTTP::Cookies->new ->set_cookie(0, user => make_cookie(split /:/, $opts{C}), '/', $host); } elsif (-f $cookie_file) { $cookie_jar = HTTP::Cookies::Netscape->new(File => $cookie_file); } else { die "$0: No authentication data found, cannot continue"; } $journal = SOAP::Lite->uri($uri) ->proxy($proxy, cookie_jar => $cookie_jar); die "$0: Error creating SOAP::Lite client, cannot continue" unless $journal; $result = $journal->add_entry(subject => $title, body => $body, discuss => $discuss); if ($result->fault) { die "$0: Failed: " . $result->faultstring . "\n"; } else { printf "New entry added as %s\n", $result->result; } exit; # Taken from the Slash codebase sub make_cookie { my ($uid, $passwd) = @_; my $cookie = $uid . '::' . md5_hex($passwd); $cookie =~ s/(.)/sprintf("%%%02x", ord($1))/ge; $cookie =~ s/%/%25/g; $cookie; }: No authentication data found, cannot continue"; } $journal = SOAP::Lite->uri($uri) ->proxy($proxy, cookie_jar => $cookie_jar); die "#!/usr/bin/perl -w use strict; use File::Basename 'basename'; use Getopt::Std; use Digest::MD5 'md5_hex'; use HTTP::Cookies; use SOAP::Lite; my (%opts, $user, $title, $discuss, $body, $host, $uri, $proxy, $file, $name, $cookie_file, $cookie_jar, $journal, $result); getopts('C:c', \%opts) or die <<"USAGE"; Usage: $0 [ -c ] [ -C user:pass ] -c Allow comments on journal entry -C user:pass Provide authentication in place of (or in absence of) Netscape cookies. User in this case is UID, not nickname. USAGE while (defined($_ = <STDIN>)) { $file = $1, last if /^\s+file:\s+(\S+)/; } die "$0: No filename found in input, stopped " unless $file; $name = basename $file; $file =~ s^\$CPANhttp://www.cpan.org; $user = (getpwuid($<))[0]; $title = "$name uploaded to PAUSE"; $discuss = $opts{c} ? 1 : 0; $body = << "BODY"; <p>The file <tt>$name</tt> has been uploaded to CPAN, and will soon be accessible as <a href="$file">$file</a>. Mirroring may take up to 24 hours.</p> <p><i>$0, on behalf of $user</i></p> BODY $host = 'use.perl.org'; $uri = "http://$host/Slash/Journal/SOAP"; $proxy = "http://$host/journal.pl"; # Note that this path is UNIX-centric. Consider using # File::Spec methods in most cases. $cookie_file = "$ENV{HOME}/.netscape/cookies"; if ($opts{C}) { $cookie_jar = HTTP::Cookies->new ->set_cookie(0, user => make_cookie(split /:/, $opts{C}), '/', $host); } elsif (-f $cookie_file) { $cookie_jar = HTTP::Cookies::Netscape->new(File => $cookie_file); } else { die "$0: No authentication data found, cannot continue"; } $journal = SOAP::Lite->uri($uri) ->proxy($proxy, cookie_jar => $cookie_jar); die "$0: Error creating SOAP::Lite client, cannot continue" unless $journal; $result = $journal->add_entry(subject => $title, body => $body, discuss => $discuss); if ($result->fault) { die "$0: Failed: " . $result->faultstring . "\n"; } else { printf "New entry added as %s\n", $result->result; } exit; # Taken from the Slash codebase sub make_cookie { my ($uid, $passwd) = @_; my $cookie = $uid . '::' . md5_hex($passwd); $cookie =~ s/(.)/sprintf("%%%02x", ord($1))/ge; $cookie =~ s/%/%25/g; $cookie; }: Error creating SOAP::Lite client, cannot continue" unless $journal; $result = $journal->add_entry(subject => $title, body => $body, discuss => $discuss); if ($result->fault) { die "#!/usr/bin/perl -w use strict; use File::Basename 'basename'; use Getopt::Std; use Digest::MD5 'md5_hex'; use HTTP::Cookies; use SOAP::Lite; my (%opts, $user, $title, $discuss, $body, $host, $uri, $proxy, $file, $name, $cookie_file, $cookie_jar, $journal, $result); getopts('C:c', \%opts) or die <<"USAGE"; Usage: $0 [ -c ] [ -C user:pass ] -c Allow comments on journal entry -C user:pass Provide authentication in place of (or in absence of) Netscape cookies. User in this case is UID, not nickname. USAGE while (defined($_ = <STDIN>)) { $file = $1, last if /^\s+file:\s+(\S+)/; } die "$0: No filename found in input, stopped " unless $file; $name = basename $file; $file =~ s^\$CPANhttp://www.cpan.org; $user = (getpwuid($<))[0]; $title = "$name uploaded to PAUSE"; $discuss = $opts{c} ? 1 : 0; $body = << "BODY"; <p>The file <tt>$name</tt> has been uploaded to CPAN, and will soon be accessible as <a href="$file">$file</a>. Mirroring may take up to 24 hours.</p> <p><i>$0, on behalf of $user</i></p> BODY $host = 'use.perl.org'; $uri = "http://$host/Slash/Journal/SOAP"; $proxy = "http://$host/journal.pl"; # Note that this path is UNIX-centric. Consider using # File::Spec methods in most cases. $cookie_file = "$ENV{HOME}/.netscape/cookies"; if ($opts{C}) { $cookie_jar = HTTP::Cookies->new ->set_cookie(0, user => make_cookie(split /:/, $opts{C}), '/', $host); } elsif (-f $cookie_file) { $cookie_jar = HTTP::Cookies::Netscape->new(File => $cookie_file); } else { die "$0: No authentication data found, cannot continue"; } $journal = SOAP::Lite->uri($uri) ->proxy($proxy, cookie_jar => $cookie_jar); die "$0: Error creating SOAP::Lite client, cannot continue" unless $journal; $result = $journal->add_entry(subject => $title, body => $body, discuss => $discuss); if ($result->fault) { die "$0: Failed: " . $result->faultstring . "\n"; } else { printf "New entry added as %s\n", $result->result; } exit; # Taken from the Slash codebase sub make_cookie { my ($uid, $passwd) = @_; my $cookie = $uid . '::' . md5_hex($passwd); $cookie =~ s/(.)/sprintf("%%%02x", ord($1))/ge; $cookie =~ s/%/%25/g; $cookie; }: Failed: " . $result->faultstring . "\n"; } else { printf "New entry added as %s\n", $result->result; } exit; # Taken from the Slash codebase sub make_cookie { my ($uid, $passwd) = @_; my $cookie = $uid . '::' . md5_hex($passwd); $cookie =~ s/(.)/sprintf("%%%02x", ord())/ge; $cookie =~ s/%/%25/g; $cookie; } |