Web DevCenter    
 Published on Web DevCenter (http://www.oreillynet.com/javascript/)
 See this if you're having trouble printing code examples


Web Services with AppleScript and Perl

by Randal L. Schwartz and Apple Developer Connection
09/24/2002

Prior to Mac OS X, Perl scripters were forced to telnet to a shell prompt on a remote Unix box to get full access to their favorite language. But now, with its BSD underpinnings and a terminal window, Mac OS X has the same version of Perl you'd find on any Unix system.

Of course, Apple's own scripting language, AppleScript, has been around for years. And a recent release of the language added the ability to use SOAP services, which makes it a nice complement to Perl.

You can't create a SOAP web service with AppleScript, but you can have it act as a SOAP web client and use Perl to create the service. So it becomes a simple matter of passing structured data between Perl and AppleScript, with the Perl service running either on the same machine or remotely. This is much nicer than firing off a new Perl process with the do shell script AppleScript extension, because the startup time is much faster and thus the latency is nearly eliminated. Also, state can be preserved without having to scribble it down to disk between invocations, such as a hard-to-marshal database connection or cached results.

A Perl-built SOAP web service also has full access to the Mac OS X platform from the Unix perspective and can make use of the thousands of pre-written routines from the Comprehensive Perl Archive Network (CPAN). In fact, a SOAP web service in Perl can even use other SOAP web services to repackage, filter, or summarize data.

(Beware that a SOAP web service runs with the full permissions of the user who started the process. Think twice before letting people connect to your web service remotely. Think ten times before starting it as root or an admin user.)

Creating a Simple Application

To show off the connection between an AppleScript and a Perl server, I created a simple task to keep the mechanics to a minimum: fetching headlines from a news site.

Sites that provide news items frequently include a Rich Site Summary (RSS) file with headlines and links to the detailed stories. These RSS feeds can then be used by other sites to provide a sidebar of relevant stories.

The XML::RSS module (found in the CPAN) parses an RSS structure and returns an object that can be used to examine particular stories. I wanted an applet that, when clicked, would fetch RSS headlines from a favorite site of mine and then pop up a browser window with a listing of those headlines as clickable links. It turned out that it didn't take take much code to accomplish this task.

AppleScript can drive the process, but it can't fetch a URL directly or parse the resulting XML, so I delegated that to the Perl program. The AppleScript applet will give an RSS URL to the Perl program (running as a daemon) via the SOAP protocol, and the Perl script will take care of the fetching, RSS parsing, and repackaging of the essential information. Then the AppleScript will reformat the data as HTML and hand it to a browser to be viewed.

AppleScript in a Nutshell

Related Reading

AppleScript in a Nutshell
By Bruce W. Perry

There are probably better ways of doing this. For example, it would be trivial to have Perl simply return the repackaged HTML, or put the HTML into a file and return the name of that temp file or even launch the browser directly. But this approach allows me to return the structured data in a nice way. And by keeping the Perl side to a minimum, AppleScripters can use what they know to provide alternate views of the data, letting the Perl hackers concentrate on fetching the data.

Creating the Perl SOAP Server

Before you try to run the Perl SOAP server you will need a few modules: XML::Parser, which requires the Expat parser, and SOAP::Lite, XML::RSS, and LWP::Simple. If you don't have Expat, start by installing that with the instructions found here. To install the other modules, just open a Terminal window and type sudo perl -MCPAN -eshell (authenticate if necessary), then install XML::Parser, install SOAP::Lite, install XML::RSS, and install LWP::Simple. Answer any necessary questions and be sure to say "yes" when asked if you want to install SOAP::Transport::HTTP. The CPAN installer may ask you for some configuration information if this is your first time running it. You can get details on the whole process by entering perldoc CPAN at a prompt.

Take a look at the Perl SOAP server. You can see the entire script here. I've included line numbers to make it easier to read. Here's how I begin every Perl program I write:

=1= #!/usr/bin/perl -w
=2= use strict;
=3= $|++;

Line 1 turns on Perl warnings and identifies this as a Perl script by providing the path to the Perl compiler/interpreter on this system. Line 2 enables three very useful compiler restrictions, demanding that I declare my variables (so a typo doesn't ruin a good day), use only hard references (as opposed to accidentally de-referencing a text string), and forgo bare-words-as-text-strings. Line 3 causes every print to be flushed, effectively giving me output as soon as I ask for it, not buffered for efficiency until later.

Line 5 pulls in the SOAP::Transport::HTTP module, part of the SOAP::Lite distribution, found in the CPAN:

=5=	use SOAP::Transport::HTTP;

Lines 7 through 9 create a new SOAP::Transport::HTTP::Daemon object, with enough information to become a SOAP endpoint:

=7= my $daemon = SOAP::Transport::HTTP::Daemon
=8=   -> new (LocalAddr => 'localhost', LocalPort => 8001, Reuse => 1)
=9=   -> dispatch_to ('Server');

I defined the host for binding the server socket as localhost which keeps non-local processes from connecting to the socket. The port number is arbitrarily chosen as 8001. Anything between 7000 and 65535 is usually OK as long as no other process is using it.

The port is declared with a Reuse of true (1), so I can stop the server and immediately restart it. Normally, a server port is quarantined for a period of time after exit to ensure that stray TCP packets don't end up in the inbox of a new server, but we're in control here, so that protection isn't necessary.

The dispatch_to method in line 9 directs incoming methods to be found within the Server class, defined later in this same file.

Line 11 tells us that the server is up, and reminds us of the settings for the server:

=11=	print "Contact SOAP server at ", $daemon->url, "\n";

If you leave out the LocalPort parameter (see Line 8), the server can fire up on a system-selected available port, and the message would tell us what to edit in to the client software to contact this server.

Line 12 puts this server into an event-based loop, handling each incoming request until the program is aborted. The program never returns from this method invocation:

=12=	$daemon->handle;

As each SOAP request comes in on the HTTP server port, it is parsed into the method name and parameters. The methods are expected to be found in the Server class, and this is defined in lines 14 to 32:

=14= BEGIN {
=15=   package Server;
=16=   use base qw(SOAP::Server::Parameters);
=17=
=18=   use XML::RSS;
=19=   use LWP::Simple;
=20=
=21=   sub fetch_headlines {
=22=     my $p = pop->method;
=23=     my $uri = $p->{uri} or die "missing uri parameter";
=24=     my $rdf = get $uri or die "Can't fetch rdf";
=25=     (my $rss = XML::RSS->new)->parse($rdf); # might die, we don't care
=26=
=27=     return [map {
=28=       my $item = $_;
=29=       +{ title => $item->Web Services with AppleScript and Perl, 
           link => $item->{link} };
=30=     } @{$rss->{items}}];
=31=   }
=32= }

Note that we've wrapped this in a BEGIN block to emulate a use Server compilation directive.

Line 15 starts the Server package, effective until the end of the block in which it was declared (which happens to be the end of the BEGIN block).

Line 16 pulls in the SOAP::Server::Parameter class, and also declares the Server class to be inherited from SOAP::Server::Parameter. The effect is that each method is handed an additional parameter at the end of the argument list of type SOAP::SOM, which contains methods to access named parameters instead of just positional parameters.

Lines 18 and 19 pull in the XML::RSS and LWP::Simple modules.

Related Reading

Perl & LWP
By Sean M. Burke

Lines 21 to 31 define the fetch_methods SOAP method. In Line 22, the parameters' hashref is obtained by popping the argument list and calling method on the resulting object, lifted directly from the SOAP::Lite manpage. (Kudos to the many SOAP::Lite examples in the documentation and distribution.)

Line 23 extracts the uri parameter from the hashref. If we die here, the SOAP server will capture this abnormal exit and automatically return a SOAP fault back to the requester. AppleScript's default response was to pop up a dialog box with the error text, so this seemed perfectly acceptable for a simple example, but I imagine I'd want to wrap that into a AppleScript try block for more robust handling. Or perhaps trap this on the Perl server side and return a default response for an invalid input.

Line 24 fetches the RSS file at the given URI. The get subroutine is imported from LWP::Simple, taking a single URI and returning the contents of the page, or undef if it fails. Again, a death here will simply generate a SOAP fault, triggering an error on the AppleScript side.

Line 25 takes the resulting RSS content and parses it into an XML::RSS object, and then Lines 27 to 31 return the response. Basically, we're constructing an arrayref to a list of elements, and each element will be a hashref to a hash containing a headline and its URL. We'll get that by iterating over the list of items (Line 30), extracting each title and link and creating a hashref from that (Line 29), and then wrapping the whole thing into an anonymous array constructor (the brackets in Lines 27 and 30). The beauty of this is that the SOAP::Lite methods figure out how to bundle this appropriately into a list and records for AppleScript to process properly.

And that's it on the server side. Launch this script, and you're greeted with:

Contact SOAP server at http://localhost:8001/

...and your server is up and running at the indicated address.

Creating the SOAP Client in AppleScript

Now, for the other side: the AppleScript client that will contact this server. You can see the entire script, along with line numbers, here. I must caution the reader here: while this code is workable, it is not as robust as it might be. It is only a simple example of using SOAP services with AppleScript. For more complete information, see the techpubs documentation.

Lines 1 to 5 define the subroutine that lets you go from AppleScript to a Perl SOAP server.

=1= on fetch_SOAP_lite(endpoint, method, p)
=2=   using terms from application "http://www.apple.com/placebo"
=3=     tell application endpoint to return call soap 
        {method name:method, method namespace uri:endpoint, 
        SOAPAction:(endpoint & "#" & method), parameters:p}
=4=   end using terms from
=5= end fetch_SOAP_lite

The using terms from line 2 I got from Apple's AppleScript guide for SOAP. Apparently, when the application in a tell application ... is variable, AppleScript has no clue what vocabulary to use for the interactions. AppleScript does know, however, that if it's an HTTP URL, then we're using the SOAP or XML-RPC vocabulary. So you put in a dummy URL to keep AppleScript happy (the placebo), and all is well.

The endpoint parameter consists of both the URL as reported by the startup of the script, plus the name of the class I've selected for dispatching the methods. So, to get to the aforementioned server, we'll be using http://localhost:8001/Server here.

The method parameter is the particular SOAP method within the class. This will be fetch_headlines.

The p parameter is an AppleScript record corresponding to the named parameters received by the SOAP method.

Line 3 performs the SOAP call, connects to the proper endpoint, selects the right method, and passes the right parameters. The response is returned, unless a SOAP fault or exception is thrown, in which case the appropriate exception propagates outward.

Lines 7 through 9 define a fetch_headlines routine which uses the fetch_SOAP_lite subroutine to talk to our particular server:

=7= on fetch_headlines(uri)
=8=   return fetch_SOAP_lite("http://localhost:8001/Server", 
      "fetch_headlines", {uri:uri})
=9= end fetch_headlines

Given a URI parameter, I pass that as the named uri parameter to our Perl SOAP server, and then return the response. This effectively binds fetch_headlines in this AppleScript as if it was the fetch_headlines method in the remote server. And there's the magic of SOAP, if you ignore all the plumbing below.

Line 11 invokes the fetch_headlines subroutine (which calls the remote server to get a response), and stores the response in the response AppleScript variable:

=11= set response to fetch_headlines("http://www.perl.com/pace/perlnews.rdf")

I'm using the URI for the RSS headlines of O'Reilly's Perl.com web site, getting the latest information in the Perl world.

Lines 12 to 16 format the output of the response as a very minimal HTML page:

=12= set output to "<ul>" & return
=13= repeat with i in response
=14=  set output to output & "<li><a href=\"" 
      & (i's link) & "\">" & (i's title) & "</a></li>" & return
=15=   end repeat
=16=   set output to output & "</ul>" & return

A bullet list is created in output, where each item comes from grabbing the link and title items from the item records of the response. By using a record, the server can eventually return more than just the title and link (e.g., the age of the item) without breaking existing code. This is important to consider when writing client/server applications: designing the data so it can survive upgrades in an upward-compatible way.

Now that I've got the HTML, I need to write it to a file so a browser can read it. I'll select the name using the built-in temporary items folder special selector, appending fetch_headlines.html to the name, computed in line 17:

=17= set output_file to (path to temporary items folder as string)
     & "fetch_headlines.html"

Line 18 invokes the write_to_file subroutine (defined later) to put the content of the generated HTML into the designated file:

=18=	write_to_file(output, output_file, false)

Finally, Line 19 tells the Finder to open the file:

=19= tell application "Finder" to open output_file

As this file ends in .html, we'll be activating whatever we've designated as the default browser for HTML files. For me, that popped up an Internet Explorer window containing the appropriate links.

Lines 21 to 38 were lifted from a script found created by the AppleScript team:

=21= -- borrowed code
=22= on write_to_file(this_data, target_file, append_data)
=23=   try
=24=     set the target_file to the target_file as text
=25=     set the open_target_file to 
=26=       open for access file target_file with write permission
=27=     if append_data is false then 
=28=       set eof of the open_target_file to 0
=29=     write this_data to the open_target_file starting at eof
=30=     close access the open_target_file
=31=     return true
=32=   on error
=33=     try
=34=       close access file target_file
=35=     end try
=36=     return false
=37=   end try
=38= end write_to_file

Related Reading

Programming Web Services with SOAP
By James Snell, Doug Tidwell, Pavel Kulchenko

This takes the data in the first parameter and writes it (or appends it, depending on the third parameter) to the file named in the second parameter. I'll just treat it as a black box and give it to you in the same manner. One of the nice things about AppleScript is that it's been around long enough to have lots of snippets like this available online.

Conclusion

And there you have it. Stick that into a compiled script using something like Script Editor, and then run it. The application will contact the Perl SOAP server to get the current headlines from Perl.com. The data then gets reformatted by AppleScript into a nice bulleted list, and your browser is automatically opened to that page. If you see a headline you like, click on it and get the whole story.

In real life, I'd probably have the AppleScript pop up a dialog box asking me to choose from a list of sites instead of just hard wiring the one name into the script. In fact, you could build it as a full Cocoa application using AppleScript Studio and have the application itself show the headlines and make clickable links, which then get opened by a browser. And with an on idle loop, the AppleScript side could fetch the headlines once every five minutes or so, flagging new items in a bright color to get your attention. You could then leave it running on some corner of your screen, so you'd see the news as it happens.

Now that you know how easy it is to create a SOAP web service with Perl, you can provide services for your local AppleScripts and access much more than AppleScript can do on its own.

Randal L. Schwartz is a two-decade veteran of the software industry. He is skilled in software design, system administration, security, technical writing, and training.


Return to the Web Development DevCenter.

Copyright © 2009 O'Reilly Media, Inc.