JabChapter 10
From WikiContent
(→What Demo::JBook Will Do) |
(content navi added) |
||
| Line 1: | Line 1: | ||
| + | {{Content Programming Jabber}} | ||
=Pointers for Further Development= | =Pointers for Further Development= | ||
<br>The previous two chapters have demonstrated | <br>The previous two chapters have demonstrated | ||
Revision as of 14:23, 18 September 2009
Contents
|
Pointers for Further Development
The previous two chapters have demonstrated
how Jabber can be used to build applications and solutions in many
functional areas. They expanded upon and indeed went beyond the theme of
instant messaging (IM) to employ the fundamental features of contextual
messaging, presence, and request/response sequences, in a wide range of
scenarios.
While these scenarios have in some ways been inward looking, they are natural progressions that originated inside the IM world and matured into applications and solutions that retain much of the messaging flavor. Let's consider what else Jabber has to offer as a messaging and routing mechanism.
This chapter explores some "outward looking" scenarios, to give you pointers and ideas for the future. With Demo::JBook, we consider the possibility of "Jabber without Jabber"—in other words, using Jabber as an infrastructure, in this case as a data store, without focus on any particular Jabber client or IM functionality. We also explore how Jabber is the perfect transport partner for procedure calls formalized in XML: JabberRPCRequester and JabberRPCResponder are scripts that exchange method calls and responses encoded in XML-RPC.
Using Jabber as a conduit to foreign systems is also a theme of this chapter. With ldapr, we build a script that reflects the hierarchy and contents of an LDAP data store, allowing that store to be navigated from a Jabber client. Finally, we look to the business world, employing Jabber in a tiny but crucial role as a conduit between SAP systems and their users.
A Simple Jabber-Based Address Book
With the availability of many
different off-the-shelf Jabber clients and the use of these clients as
generic tools to interact with diverse Jabber-based services, it's easy
to lose sight of the fact that Jabber can also be used to contribute to
infrastructure solutions. That is, applications and utilities can be
built using the Jabber protocols, in conjunction with Jabber
server-based services, without the need for a Jabber client.
By way of illustration, let's build a simple two-level address book using Jabber services. We'll call it Demo::JBook. We'll use this address book to look up details of our friends and colleagues while we're on the move. The ideal platform for this is going to be a web browser, in that it's accessible from personal workstations, airport web consoles, cybercafés, and personal digital assistants (PDAs) that offer access to the Internet.
The point of this illustration is not to show that there's a single solution to the problem of disconnected, incompatible, and unsynchronized directory information (because, despite any answers that you may get to the contrary—no such solution exists). Instead, the goal is to show that it's possible to make use of Jabber services and get to information stored and managed by those services without having to use a Jabber client.
Using the JUD and vCards
The two levels in our address book are
going to reflect two distinct (but related) mechanisms in Jabber. We're
going to base our address book on the Jabber User Directory (JUD)
component and supply further information, in a "drill-down" action,
using vCards.
- The JUD
- Our address book will act as a query frontend for a user directory in
- the form of a JUD. It doesn't matter which JUD we use; obviously,
- that depends on how the application is to be deployed. On one hand, it
- might be appropriate to point it at your company's internal JUD, if
- you have one. On the other hand, it might also be just as appropriate
- to point it at one of the larger public JUDs, such as the one
- connected to the Jabber server running on jabber.org (which is
- users.jabber.org).
- vCards
- Every Jabber entity—users, components, and servers—has the potential
- to have a vCard. We saw in Chapter 4 that the Jabber server itself,
- and many of the components connected to it, had a vCard definition.
- While the vCard standard is still fluid, the implementation within
- Jabber, as described in Section 6.5.1 in Chapter 6, is enough to be
- useful. The key to the application is that both the Jabber mechanisms
- that it relies upon—the JUD and vCards—can be accessed independently
- of the availability of the users that the information stored in those
- mechanisms represents. The JUD runs as an independent component and
- manages the directory information using its own data store. With the
- default JUD and XML Database (XDB) component configurations, this data
- store will be in the Jabber server's spool directory in a file called
- jud/global.xdb.
Users interact with the JUD to manage their information, using IQ elements qualified by the jabber:iq:register namespace. User vCard information is also stored at the server side, and users manage the vCard contents using IQ elements qualified by the vcard-temp namespace. Again, with the default configuration, the vCard information, stored along with the rest of the user data relevant to the Jabber server in user-specific spool files, will be held in the Jabber server's spool directory in files called [hostname]/[user].xml.
What Demo::JBook Will Do
The JUD can be queried using a normal
Jabber client. In Figure 10-1, we can see the search form of Jabber
Instant Messenger (JIM).
</code> The search fields are not fixed;
instead, they're dynamically generated, according to the result of an
initial IQ-get in the jabber:iq:search namespace. Just as an
IQ-get in the jabber:iq:register namespace (as illustrated in
Section 8.3) is used for registering users, the
jabber:iq:search namespace is is used for search requests. This
is illustrated in Example 10-1, where an IQ-get is sent to the JUD at
jud.gnu.mine.nu.
An IQ-get to a JUD in the jabber:iq:search namespace
SEND: <iq type="get" id="9138" to="jud.gnu.mine.nu">
<query xmlns="jabber:iq:search"/> </iq>
RECV: <iq from='jud.gnu.mine.nu' id='9138' to='qmacro@jabber.org/study' type='result'> <query xmlns='jabber:iq:search'> <first/> <last/> <instructions> Fill in a field to search for any matching Jabber users. </instructions> </query> </iq> In response to the IQ-get request, the JUD sends back a list of fields with which the directory can be searched, along with some simple instructions.
The actual search follows the same registration pattern (using the jabber:iq:register namespace) that we saw in Section 8.3. After receiving a list of possible search fields, a search request is made with an IQ-set, as shown in Example 10-2. The results, returned in an IQ-result from the JUD, are listed with each entry contained in an <item/> tag. The search results are shown in Figure 10-2.
[[image:title=Searching the JUD: the search
results|image=0596002025-jab_1002.png</code>
The JUD search request and response
SEND: <iq type="set" id="2627" to="users.jabber.org">
<query xmlns="jabber:iq:search"> <last>adams</last>
</query> </iq>
RECV: <iq from='users.jabber.org' id='2627' to='qmacro@jabber.org/study' type='result'> <query xmlns='jabber:iq:search'> <item jid='qmacro@jabber.org'> <nick>qmacro</nick> <first>DJ</first> <email>dj.adams@gmx.net</email> <last>Adams</last> </item> <item jid='qmacro@jabber.com'> <nick>dj</nick> <first>DJ</first> <email>dj.adams@gmx.net</email> <last>Adams</last> </item> <item jid='joseph@gnu.mine.nu'> <nick>joseph</nick> <first>Joseph</first> <email>joseph@pipetree.com</email> <last>Adams</last> </item>
... (more items) ...
</query> </iq>
Searching a JUD
The first level that we'll build into
Demo::JBook is the ability to query a JUD. The address book is to be
browser-based, so we'll generate HTML on the fly according to the search
fields we receive in response to our IQ-get. It should look something
like that shown in Figure 10-3.
</code>
The items returned in the search results don't really provide much
information:
<item jid='joseph@gnu.mine.nu'>
<nick>joseph</nick> <first>Joseph</first>
<email>joseph@pipetree.com</email>
<last>Adams</last> </item>
Here, we have the user's first and last names, his nickname, and his
email address. One of the functions of the JUD is to determine which
fields are storable.
{{Sidebar|JUD FieldsThe fields in a JUD that are used to store the
directory data are determined either by a hardcoded list in the JUD
source itself or a configurable list that's stored and maintained
separate from the JUD code. The list fields allowed for searching a JUD
may or may not be used for storing the information; it may be a subset
(it makes no sense, of course, for it to be a superset). For example, if
the fields used to store information in a JUD are:
<name/> <email/> <first/> <last/>
<nick/> <text/> it may be that the fields available for
searching may be just:
<first/> <last/>
as shown in Example 10-1.
In case you're wondering how to discover the fields that can be used to populate the information in the JUD, an IQ-get can be used in the jabber:iq:register namespace to qualify a registration conversation with the JUD, which is where the information is "registered," or stored. This is shown in Example 10-3.
</code>
An IQ-get to a JUD in the jabber:iq:register namespace
SEND: <iq type='get' to='jud.gnu.mine.nu'> <query
xmlns='jabber:iq:register'/> </iq>
RECV: <iq from='jud.gnu.mine.nu' to='qmacro@jabber.com/study' type='result'> <query xmlns='jabber:iq:register'> <nick/> <first/> <email/> <last/> <text/> <name/> <instructions> Fill in all of the fields to add yourself to the JUD. </instructions> </query> </iq> We can see in Figure 10-4 how the results will typically be rendered.
[[image:jab_1004.png|JUD search results as rendered in Demo::JBook|center|350 px
</code>
Retrieving vCard information
As well as registering with a
JUD, it's possible that a user has maintained more information about
himself—in his vCard. Depending on the Jabber client used, various user
information can be stored in a personal vCard, which is stored on the
server side. In Figure 10-5, we see the JIM client's vCard maintenance
window, titled User Profile.
The result of entering information and clicking the OK button in Figure 10-5 can be seen in Example 10-4, where an IQ-set in the vcard-temp namespace is made to store the information (some of the vCard tags have been omitted to keep the example short). Notice how no to attribute is specified in the IQ-set and how the result appears to come from the sender (qmacro@jabber.com/study). The storage of personal—user-specific—vCard information is a function of the Jabber Session Manager (JSM), which is where the <iq/> element will be routed automatically, as it is coming in over a client connection (defined by the jabber:client stream-level namespace). This is further discussed in Section 5.4.3.1.
[[image:jab_1005.png|Updating personal vCard information with JIM|center|350 px
</code>
Setting vCard information in the vcard-temp namespace
SEND: <iq type="set"> <VCARD version="3.0"
xmlns="vcard-temp"> <N> <GIVEN>DJ</GIVEN>
<FAMILY>Adams</FAMILY> <MIDDLE/> </N>
<NICKNAME>qmacro</NICKNAME> <EMAIL> <INTERNET/>
<PREF/> dj.adams@gmx.net </EMAIL>
...
</VCARD> </iq>
RECV: <iq type='result' from='qmacro@jabber.com/study' to='qmacro@jabber.com/study'/> As well as being storable, the information in a personal vCard is also retrievable by anyone, anytime. The idea of a personal vCard is that the information it contains is permanently available. Because the vCard data is stored server side, it can be retrieved anytime the user is online.
The key (literally and metaphorically) of each of the search result items returned is a JID. We can see this in Example 10-2, where each <item/> tag has a jid attribute. You won't be surprised to know that the key to accessing someone's vCard is his JID too. So we have a great way for Demo::JBook to jump from level 1, which displays JUD search results, to level 2 by retrieving and displaying a vCard via the JID. The results of jumping from a JUD result entry to a vCard display for the selected user, via the JUD, can be seen in Figure 10-6.
[[image:jab_1006.png|A vCard as displayed in Demo::JBook|center|350 px-</code>
Using Demo::JBook as an Apache Handler
The Demo::JBook
application is going to exist as an Apache mod_perl handler, that
is, we'll use the power of Perl's integration into the Apache web server
to write an Apache module in Perl. You can find out more about
mod_perl at http://perl.apache.org.
Being a mod_perl module, Demo::JBook exists in the form of a Perl module and is configured in Apache to service calls to http://[hostname]/jbook. The configuration can be comfortably placed in the main Apache configuration file, httpd.conf, or, as is common in mod_perl installations, in an extra file usually called perl.conf, which is linked to httpd.conf as follows:
<IfModule mod_perl.c> Include conf/perl.conf
</IfModule>
The configuration for Demo::JBook is placed in the perl.conf file,
as shown in Example 10-5.
Configuring the module as a handler in Apache
require conf/startup.pl
...
<Location /jbook> SetHandler perl-script PerlHandler Demo::JBook </Location> The PerlHandler directive refers to the Demo::JBook module, which is the JBook.pm file that exists in the Demo/ directory. The module will be invoked to handle calls to the relative URL /jbook. You can add the location of the Demo::JBook module to mod_perl's list of directories in the BEGIN section of the startup.pl script, in the conf/ directory, like this:
BEGIN { use Apache (); use lib '[the directory location of
Demo::JBook]';
}
The Demo::JBook Script
Before taking the Demo::JBook script
apart, let's have a look at the script in its entirety, shown in
Example 10-6. Written in Perl, Demo::JBook uses the
Jabber::Connection library.
The Demo::JBook script, written in Perl
package Demo::JBook;
use strict;
use Jabber::Connection; use Jabber::NodeFactory; use Jabber::NS qw(:iq
- misc);
use constant SERVER => 'gnu.mine.nu'; use constant USER => 'jbook'; use constant PASS => 'pass'; use constant RESOURCE => 'jbook'; use constant JUD => 'jud.gnu.mine.nu';
sub handler {
my $r = shift; my @a = $r->args;
my $nf = Jabber::NodeFactory->new;
$r->content_type('text/html'); $r->send_http_header;
$r->print("<html><head><title>JBook</title>
</head><body>"); $r->print("<h1><a
href="/jbook">JBook</a></h1>");
# Connect to the Jabber server
my $c = Jabber::Connection->new(server => SERVER); unless
($c->connect) { $r->print("Sorry, no connection to Jabber
available at ".SERVER); $r->print("</body></html>");
return;
}
$c->auth(USER, PASS, RESOURCE);
# No arguments: Instructions
if (scalar @a == 0) {
# Construct IQ-get in iq:search namespace
my $iq = $nf->newNode('iq'); $iq->attr('to', JUD);
$iq->attr('type', IQ_GET); $iq->insertTag('query', NS_SEARCH);
# Send the IQ-get my $result = $c->ask($iq);
if ($result->attr('type') eq IQ_ERROR) { $r->print("sorry, no
connection to JUD available at ".JUD);
$r->print("</body></html>"); $c->disconnect;
return;
}
my $info = $result->getTag(, NS_SEARCH);
# Display the results in HTML
$r->print("<p><strong>".JUD."</strong></p>
;\n");
$r->print("<p>".$info->getTag('instructions')->data."
</p>\n"); $r->print("<form><table>\n"); foreach
my $field ($info->getChildren) { next if $field->name eq
'instructions'; $r->print("<tr>");
$r->print("<td>".ucfirst($field->name)."</td>");
$r->print("<td><input type="text'
name="".$field->name.""></td>");
$r->print("</tr>\n");
}
$r->print("<tr><td></td><td><input
type="submit"></td></tr>\n");
$r->print("</table></form>\n");
}
# Multiple arguments: JUD lookup
elsif (scalar @a > 1) {
# Treat the arguments as a hash my %a = @a;
# Construct an IQ-set
my $iq = $nf->newNode('iq'); $iq->attr('to', JUD);
$iq->attr('type', IQ_SET); my $query = $iq->insertTag('query',
NS_SEARCH);
while (my($name, $val) = each(%a)) {
$query->insertTag($name)->data($val) if $val;
}
# Make call my $result = $c->ask($iq);
if ($result->attr('type') eq IQ_ERROR) { $r->print("sorry,
cannot query JUD"); $r->print("</body></html>");
$c->disconnect; return;
}
my $info = $c->ask($iq)->getTag(, NS_SEARCH);
my $items = 0;
$r->print("<p><strong>".JUD."</strong></p>
;\n"); $r->print("<table border="1">\n");
foreach my $item ($info->getChildren) {
# Heading
unless ($items) { $r->print("<tr>"); foreach my $tag
($item->getChildren) {
$r->print("<th>".ucfirst($tag->name)."</th>");
}
$r->print("</tr>\n");
}
$r->print("<tr>"); my $flag = 0; foreach my $tag
($item->getChildren) { unless (length($tag->data) == 0 or
$flag++) { $r->print("<td><a
href="/jbook?".$item->attr('jid')."">");
$r->print($tag->data."</a></td>");
}
else { $r->print("<td>".$tag->data."</td>");
}
}
$r->print("</tr>\n");
$items++;
}
$r->print("</table>\n");
$r->print("<p>$items results found</p>");
}
# Single argument: vCard lookup
else {
# Construct query
my $iq = $nf->newNode('iq'); $iq->attr('to', $a[0]);
$iq->attr('type', IQ_GET); $iq->insertTag('vcard', NS_VCARD);
# Make call and retrieve results my $result = $c->ask($iq);
if ($result->attr('type') eq IQ_ERROR) { $r->print("sorry,
cannot retrieve vCard for $a[0]");
$r->print("</body></html>"); $c->disconnect;
return;
}
my $vcard = $result->getTag(, NS_VCARD);
print ("<strong>$a[0]</strong>\n");
# Display each of the top-level tags if they contain data
foreach my $tag ($vcard->getChildren) { print
"<br/>".$tag->name." : ".$tag->data."\n" if
$tag->data;
}
}
$r->print("</body></html>"); $c->disconnect;
return;
}
1;
Taking Demo::JBook Step by Step
Now that we've got a hold on the
scope and scale of Demo::JBook, let's take the script apart—step by
step—to see how it works.
Declarations
Because Demo::JBook is an Apache handler, it
exists as a Perl module—hence the package declaration at the
top of the file:
package Demo::JBook;
use strict;
use Jabber::Connection; use Jabber::NodeFactory; use Jabber::NS qw(:iq
- misc);
use constant SERVER => 'gnu.mine.nu'; use constant USER => 'jbook'; use constant PASS => 'pass'; use constant RESOURCE => 'jbook'; use constant JUD => 'users.jabber.org'; This code exists within the Demo::JBook package that was declared as the handler for the http://[hostname]/jbook location, shown in Example 10-5. We're going to make full use of the Jabber::Connection library and bring in all three of its modules for managing the Jabber server connection, for dispatching elements that arrive (Jabber::Connection), for building and manipulating elements (Jabber::NodeFactory), and using common Jabber programming constants (Jabber::NS). In the case of Jabber::NS, we need only a few namespaces to manage our JUD and vCard queries, so the :iq and :misc tags will be used to refer a collection of constants in Jabber::NS.
The constants SERVER, USER, PASS, and RESOURCE define the connection to the Jabber server. This connection doesn't have to be made to the JUD that will be queried by Demo::JBook. By way of illustration, we have jabber.org's JUD (users.jabber.org) specified as the value for the JUD constant. For the purpose of this example, this will be the JUD that Demo::JBook will query.
You can use the reguser script, described in Section 7.4, to create the Demo::JBook user. See Section 8.2.1.1 for an example of how this can be done.
General handler preparation
Following the script's
declarations, it's time to define the handler function that Apache will
call to handle incoming requests to the
http://[hostname]/jbook location. The name of the
handler must be handler():
sub handler {
my $r = shift; my @a = $r->args;
my $nf = Jabber::NodeFactory->new;
$r->content_type('text/html'); $r->send_http_header;
$r->print("<html><head><title>JBook</title>
</head><body>"); $r->print("<h1><a
href="/jbook">JBook</a></h1>");
# Connect to home Jabber server
my $c = Jabber::Connection->new(server => SERVER); unless
($c->connect) { $r->print("Sorry, no connection to Jabber
available at ".SERVER); $r->print("</body></html>");
return;
}
$c->auth(USER, PASS, RESOURCE);
The mod_perl mechanism hands the handler() function an argument that is stored in a variable called $r. This is the HyperText Transfer Protocol (HTTP) request that has been made, which is handled by the function.
Calling the args() method on our request object gives us a list of arguments. These arguments follow the question mark in a typical HTTP GET request. In Figure 10-3, the URL in the Location bar is http://www.pipetree.com/jbook. In this URL, there is neither a question mark nor any arguments. The assignment to @a here:
my @a = $r->args;
would leave @a empty.
{{Note|The first part of the URL containing the hostname is truncated by
the size of the browser window.
</code> However, if the URL in the Location bar (shown in Figure 10-4) contained question marks and/or arguments, such as:
http://www.pipetree.com/jbook?name=&first=&last=adams&
- nick=&email=
the arguments that @a would receive follow the question mark and are separated in pairs with ampersands and further separated with equals signs. So here, @a would receive the arguments:
name, (blank), first, (blank), last, adams, nick, (blank),
email, (blank) In the final URL example, shown in Figure 10-6:
http://www.pipetree.com/jbook?dj@gnu.mine.nu the array
received by @a is just a single element:
dj@gnu.mine.nu The content of @a defines which stage
of the script takes the appropriate action. Queries will be generated
in the form of IQ-gets and IQ-sets to retrieve information from the JUD
as well as to retrieve personal vCards. To do this, we need to create an
instance of a node factory, so we can build elements:
my $nf = Jabber::NodeFactory->new;
After retrieving the request arguments and creating a node factory
instance, it's time to generate some HTML that will be common to all of
Demo::JBook's features. The print() method will be used on
the request object $r to send a response back to the requesting
web browser:
$r->print("<html><head><title>JBook</title>&
lt;/head><body>"); $r->print("<h1><a
href="/jbook">JBook</a></h1>");
Next, we need to create a connection to the Jabber server for each
request:
# Connect to home Jabber server my $c =
Jabber::Connection->new(server => SERVER); unless ($c->connect)
{ $r->print("Sorry, no connection to Jabber available at ".SERVER);
$r->print("</body></html>"); return;
}
$c->auth(USER, PASS, RESOURCE);
It will be much more efficient to create a persistent connection that could be used to serve further requests. One way to do this is to fork a daemon that connects to the Jabber server, acting as a proxy for this script. Rather than make direct requests to the Jabber server, Demo::JBook is used to send the IQ elements to the daemon, which in turn uses its persistent Jabber connection to make queries on Demo::JBook's behalf. A setup like this is shown in Figure 10-7.
</code>
State 1: Build the JUD query form
Based upon how many
arguments we have in the @a array, as described earlier, we can
build the response, which will represent one of three states:
- JUD query form
- JUD query results
- vCard display If Demo::JBook is called without arguments (i.e.,
http://[hostname]/jbook), which we test for like this:
if (scalar @a == 0) {
then we want to build and present the JUD search form. This form—the
fields that the form consists of—will be specific to the particular JUD
that will be searched.
We construct an IQ-get to send to the JUD to ask for a list of search fields and instructions. What we're looking for is something like the IQ-get shown in Example 10-1, like this:
SEND: <iq type="get" to="users.jabber.org"> <query
xmlns="jabber:iq:search"/> </iq>
To construct this, we start with a new node (element) created by
using the node factory:
my $iq = $nf->newNode('iq'); $iq->attr('to', JUD);
$iq->attr('type', IQ_GET); $iq->insertTag('query', NS_SEARCH);
In previous recipes, we've called methods in the Jabber libraries
(Jabberpy, JabberBeans, Net::Jabber, and
Jabber::Connection) to send an element to the Jabber server.
Typically, such a method is called a send() method. Here, we
don't use a send() method. Instead, we use
Jabber::Connection's ask(). Like
Net::Jabber's SendAndReceiveWithID() method, and
Jabberpy's method of the same name, ask() not only
sends the element to the Jabber server, it waits for a reply.
A reply—in Jabber terms—is a response in the form of an element that comes back along the stream, with a matchingidattribute. In this case, we're making an IQ-get and are expecting a response back from the recipient of that IQ-get. One way of expecting and handling the response would be to use a predefined callback specified for <iq/> elements and send our IQ-get with the send() method. However, this means that the script receives the response in an element callback that's somewhat independent of our call to send(). As well as catching the response in the callback, we also need some way of matching it up with the original request. Not to mention getting back on track with the flow of execution that represented the logical sequence of events that we were in the middle of following when we called send().
There's an easier way, if we want to make a request and wait for the response to that request before continuing on in the script. This way makes use of the ask() method.
What the ask() method does is avoid the need to catch responses in callbacks and match them up with their originating requests. It send()s the element to the Jabber server and blocks until an element with a matching id attribute value is received. Other elements that might be received while waiting for the response are duly dispatched as normal. It's not as if elements get queued up if the response takes a moment or two to arrive. When the matching element arrives on the stream, it is passed directly back to the caller of the ask() method, in the same form as if it had been handed to a callback—in object form, as an instance of a Jabber::NodeFactory::Node object, in this case.
my $result = $c->ask($iq);
So here, $result receives the response to the
<iq/> element just sent. This response will look
something like this:
RECV: <iq from='users.jabber.org' id='43'
to='jbook@gnu.mine.nu/jbook' type='result'> <query
xmlns='jabber:iq:search'> <nick/> <first/> <email/>
<last/> <instructions> Fill in a field to search for any
matching Jabber users. </instructions> </query> </iq>
It's addressed to the JID that the script is using,
jbook@gnu.mine.nu/jbook. But hold on—there's something that
doesn't look quite right here, that is, when compared to the example of
the IQ-get we just sent. Indeed—there's an id attribute in the
response. We didn't specify one in the Perl code that built the
<iq/> element. The ask() method did it for us.
Knowing that it's going to have to check the id attribute on
every incoming element to find a match for the element it's just sent
off for us, the ask() method makes sure that the outgoing
element actually has an id attribute. If it doesn't, it
adds one, giving it a unique value. That way, it stands a fighting
chance of returning to the caller something this side of the end of
time.
{{Note|Talking of time, if you're uneasy about calling blocking
functions in general, you can always set an alarm() to
interrupt the call after a certain length of time.
</code> For more information on matching requests and responses, see Section 2.5 in Chapter 2.
Now let's move on to the response to IQ-get, which we now have in $result. While we're expecting an IQ-result in response to IQ-get, the request might not have been succesful, and we simply bail out gracefully if it isn't, ending our connection with the Jabber server:
if ($result->attr('type') eq IQ_ERROR) {
$r->print("Sorry, no connection to the JUD available at ".JUD);
$r->print("</body></html>"); $c->disconnect; return;
}
Otherwise, we're expecting a result, containing the search fields and some instructions. These will be contained within the query tag qualified by the jabber:iq:search namespace (the tag is usually called query, but here, as elsewhere in the script, we're not taking any chances and are looking for "the first (hopefully the only!) occurrence of a child tag qualified by the jabber:iq:search namespace."
my $info = $result->getTag(, NS_SEARCH);
An HTML form is built from the instructions and the search fields; the
instructions are retrieved with:
$info->getTag('instructions')->data
which retrieves the <instructions/> tag and extracts its
contents.
The getChildren() method is called upon our <query/> tag to discover what fields are available. For all those fields, barring the "instructions" one, we create an input text field:
# Display the results in HTML
$r->print("<p><strong>".JUD."</strong></p>\n"
);
$r->print("<p>".$info->getTag('instructions')->data."<
/p>\n"); $r->print("<form><table>\n"); foreach my
$field ($info->getChildren) { next if $field->name eq
'instructions'; $r->print("<tr>");
$r->print("<td>".ucfirst($field->name)."</td>");
$r->print("<td><input type="text'
name="".$field->name.""></td>");
$r->print("</tr>\n");
}
$r->print("<tr><td></td><td><input
type="submit"></td></tr>\n");
$r->print("</table></form>\n");
}
Once the form has been built, the work for this stage is complete—the form is relayed to the user, who will submit a completed form, thereby invoking the next state.
State 2: Query the JUD
The submission of the HTML form will
cause a number of name/value pairs to be passed as part of the HTTP GET
request. These names and values are captured into the @a array
as described earlier. If we have more than one entry in @a, we
know it's a form submission and must respond to that by querying the JUD
and returning the results. In this case, as we know that the contents of
@a are name/value pairs, we can view those contents as a hash,
using a new variable %a:
elsif (scalar @a > 1) { my %a = @a;
Now, %a will contain entries where the keys are the names of
the search fields and the values are the values entered in the form.
In the same way that we constructed an IQ-get to query the JUD for the search fields and instructions, we construct an IQ-set to perform the actual query:
my $iq = $nf->newNode('iq'); $iq->attr('to', JUD);
$iq->attr('type', IQ_SET); my $query = $iq->insertTag('query',
NS_SEARCH);
Using the information in %a, we insert tags for each of the search fields for which a value was specified in the form. For example, if only the value adams was specified, in the field representing the <last/> search field, as shown in Figure 10-3, we would only want to insert:
<last>adams</last>
as a child of the IQ-set's <query/> tag:
while (my($name, $val) = each(%a)) {
$query->insertTag($name)->data($val) if $val;
}
Also in a similar way to handling state 1, we make the call by using the ask() method; the response will be received into the $result variable as an object representation of the IQ-result (or IQ-error) element:
my $result = $c->ask($iq);
We deal simply with any error situation:
if ($result->attr('type') eq IQ_ERROR) {
$r->print("Sorry, cannot query the JUD");
$r->print("</body></html>"); $c->disconnect; return;
}
otherwise proceeding to extract the <query/> tag from the search result. This tag will contain the <item/>s representing the JUD entries found to match the search criteria submitted (see the response in Example 10-2):
my $info = $c->ask($iq)->getTag(, NS_SEARCH);
We want to display a simple table of results, with each table row
representing an <item/>:
my $items = 0;
$r->print("<p><strong>".JUD."</strong></p>
;\n"); $r->print("<table border="1">\n");
foreach my $item ($info->getChildren) {
During the first iteration of the foreach loop, that is, on our first child tag of <query/>, we take the opportunity to write out a heading row, using the HTML <th/> (table heading) tags:
unless ($items) { $r->print("<tr>"); foreach my
$tag ($item->getChildren) {
$r->print("<th>".ucfirst($tag->name)."</th>");
}
$r->print("</tr>\n");
}
The main part of our loop translates the information found in each <item/> tag:
<item jid='joseph@gnu.mine.nu'>
<nick>joseph</nick> <first>Joseph</first>
<email>joseph@pipetree.com</email>
<last>Adams</last> </item>
into HTML table rows, with one table cell (<td/>) for
each item field:
$r->print("<tr>"); my $flag = 0; foreach my $tag
($item->getChildren) { unless (length($tag->data) == 0 or $flag++)
{ $r->print("<td><a
href="/jbook?".$item->attr('jid')."">");
$r->print($tag->data."</a></td>");
}
else { $r->print("<td>".$tag->data."</td>");
}
}
$r->print("</tr>\n");
In this section of the code, we also make the link between the first and second level of the address book. The JID, specified in each <item/> tag's jid attribute, is the key to the JUD entry that the item represents and also the key to the vCard of the user that has that JID. For each of the item lines in the table, we need to build a selectable link to lead the user to the second level, which allows him to view the vCard. This is what we want each link to look like:
http://www.pipetree.com/jbook?dj@gnu.mine.nu
That is, a single argument, representing the JID, specified after the
question mark. The condition:
unless (length($tag->data) == 0 or $flag++)
serves to ensure that the link is made on a single, nonempty field in
the JUD item. When registering with a standard JUD, none of the fields
are compulsory, so it's quite possible for there to be missing values
returned in the search results. So we want to make sure that the
<a href="...">...</a> link that we build actually
surrounds some value; otherwise, it wouldn't be clickable. The
$flag variable just ensures we build only one link and not a
link for every nonempty field.
Finally, the number of items found is displayed with:
$items++;
}
$r->print("</table>\n");
$r->print("<p>$items results found</p>");
}
State 3: Retrieve a vCard
If we receive a single argument in
the request, we'll take it to be a JID, passed from the link in the
table build in the previous state, and immediately build an IQ-get to
retrieve the vCard. The JID is to be found in the first element in the
@a array—$a[0].
else {
my $iq = $nf->newNode('iq'); $iq->attr('to', $a[0]);
$iq->attr('type', IQ_GET); $iq->insertTag('vcard', NS_VCARD);
Notice how the name of the query tag in this query is not query, but vcard. See Section 6.5.1 in Chapter 6 for details.
The retrieval query is in the form of an IQ-get, rather than an IQ-set. As we needed to send information in our JUD query (the search criteria), an IQ-set was appropriate. Here, an IQ-get is appropriate as we're not including any information to qualify our request; all we need to send is this:
<iq type='get' to='dj@gnu.mine.nu'> <vcard
xmlns='vcard-temp'/> </iq>
After sending the <iq/> element, waiting for the
response, and checking for any errors, we extract the vCard detail and
display it:
my $result = $c->ask($iq);
if ($result->attr('type') eq IQ_ERROR) { $r->print("Sorry,
cannot retrieve the vCard for $a[0]");
$r->print("</body></html>"); $c->disconnect;
return;
}
The structure of the vCard namespace is rather complicated and long-winded, and it's common for many of the fields to remain unfilled. So to keep the script simple, we're going to display all the top-level fields that aren't empty:
my $vcard = $result->getTag(, NS_VCARD);
print ("<strong>$a[0]</strong>\n");
foreach my $tag ($vcard->getChildren) { print
"<br/>".$tag->name." : ".$tag->data."\n" if
$tag->data;
}
}
General handler close
Once we've dealt with the possible
states, we send the closing HTML statements common to all three of them,
and disconnect from the Jabber server:
$r->print("</body></html>"); $c->disconnect;
return;
At this stage, there's nothing more for the module to do. Having discerned the state from the arguments in the URL (and thereby the appropriate action) and having carried out that action, the module ends, handing control back to its mod_perl host. Remembering that Demo::JBook is an Apache handler in the form of a Perl module, we need to ensure that the module itself returns a true value (as with any Perl module):
}
1;
Notes for Improvement
The Demo::JBook script is merely an
example. On top of tightening up the error and exception handling, there
are a few other things that you might want to consider doing to improve
upon it:
- Jabber connectivity
- As mentioned already, you'll probably want to improve the connection
- efficiency to the Jabber server by holding a socket open and sharing
- this connection across multiple calls to the handler.
- Choice of JUD
- The JUD to be queried is fixed; you may prefer to allow the user to
- select which JUD will be searched. What's more, selection of more than
- one JUD would allow a powerful search across public Jabber user
- directories.
- Key handling
- We've seen how a JUD is queried in Example 10-2. Some JUDs use the
- simple key-based security and pass an additional <key/>
- tag containing random data—a sort of session key, as described in
- Section 6.2.11 and Section 6.2.13. Any <key/> tag
- received from the JUD in response to an IQ-get must be sent back
- verbatim to the JUD in the subsequent IQ-set. Otherwise the search
- will fail, and you'll get a response similar to that shown in Example
- 10-7.
- Visual impact
- Last but not least, the visual impact of the end result as shown here
- (in Figure 10-3, Figure 10-4, and Figure 10-6) lacks a certain
- something. You might want to do something about that—give it a grander
- design, make it more pleasing, or at least interesting, to the eye.
- The HTML has been kept deliberately basic in this recipe, so as not to
- cloud the real theme of "Jabber without Jabber."
Failure to return a key could cause a search to fail
RECV: <iq from='users.jabber.com' id='jud33'
to='jbook@gnu.mine.nu/jbook' type='error'> <query
xmlns='jabber:iq:search'> <error code='405'>Keys do not
match.</error> </query> </iq>
XML-RPC over Jabber
XML-RPC is an easy way to get software that's
running on different operating systems to be able to make and respond to
procedure calls (the "RPC" part of the name stands for "Remote Procedure
Call") over the Internet.
The basis of XML-RPC is straightforward and is described at XML-RPC's home page (http://www.xml-rpc.com). The procedure calls, each consisting of the name of the procedure (or method) to call and a set of arguments to go with that call and the corresponding responses, each consisting of a set of results, are encoded in an XML-based format. The requests and responses, so encoded, are exchanged over HTTP, carried as the payloads of POST requests.
Example 10-8 shows a typical request in XML-RPC encoding. It's calling a procedure called examples.getStateName, and passing a single integer parameter with the value 41.
An XML-RPC request
<?xml version="1.0"?> <methodCall>
<methodName>examples.getStateName</methodName>
<params> <param>
<value><i4>41</i4></value> </param>
</params> </methodCall>
Example 10-9 shows a typical response to that request. The response
consists of a single string value "South Dakota."
An XML-RPC response
<?xml version="1.0"?> <methodResponse> <params>
<param> <value><string>South
Dakota</string></value> </param> </params>
</methodResponse>
The choice of the word "payload" to describe the encoded requests and
responses is significant: each request, headed with an XML declaration
(<?xml version="1.0"?>) and encapsulated as a single tag
(<methodCall/>), and each response, also headed with an
XML declaration and encapsulated as a single tag
(<methodResponse/>), are succinct, fully formed, and
complete parcels that have meaning independent of their HTTP carrier.
While the XML-RPC specification stipulates that these parcels be carried over HTTP, we could take advantage of the power and simplicity of the encoding and carry procedure calls and responses over Jabber.
Jabber-RPC
Jabber-RPC is the name given to the marriage of
encoding from the XML-RPC specification and Jabber as the transport
mechanism. As well as building upon a stable specification, Jabber-RPC
brings advantages of its own to the world of procedure calls over the
Internet. Many of the potential XML-RPC responders are HTTP servers
behind corporate firewalls or one-way Network Address Translation (NAT)
mechanisms and are therefore unreachable from the Internet. Substituting
Jabber as a transport gives calls a better chance of reaching their
destination. If a Jabber-RPC responder—a program that connects to a
Jabber server, is addressable by a JID, and can receive (and respond to)
XML-RPC-encoded calls—is connected to a Jabber server visible to the
Internet, then request calls have to make it only to that Jabber server,
and internal packet routing within the server will allow the parcels to
reach their destinations behind the firewall, whether those destinations
are client-based or component-based responders.
It should be clear by now that the idea of Jabber-RPC is to transport the XML-RPC-encoded parcels in an extension, an attachment, to an IQ element. Just as the details for a search attempt (for example of a Jabber User Directory) are carried in an IQ-set extension qualified by the jabber:iq:search namespace (as shown in Example 10-2), so the Jabber-RPC method calls are carried in an IQ-set extension qualified by a namespace of its own. This namespace is a new one and is jabber:iq:rpc.[1]
Similarly, as the results of a JUD search are returned in an IQ-result, so Jabber-RPC method responses are returned in an IQ-result. Example 10-10 shows a simple Jabber-RPC-based method call and response, using the same XML-RPC-encoded parcels as shown in Example 10-8 and Example 10-9.
A Jabber-RPC request/response conversation
SEND: <iq type='set' to='responder@company-a.com/jrpc-server'
id='1'> <query xmlns='jabber:iq:rpc'> <methodCall>
<methodName>examples.getStateName</methodName>
<params> <param>
<value><i4>41</i4></value> </param>
</params> </methodCall> </query> </iq>
RECV: <iq type='result' to='requester@company-b.com/jrpc-client' from='responder@company-a.com/jrpc-server' id='1'> <query xmlns='jabber:iq:rpc'> <methodResponse> <params> <param> <value><string>South Dakota</string></value> </param> </params> </methodResponse> </query> </iq> It's clear that the parcels of XML-RPC encoding lend themselves very well to being transported in a meaningful way over Jabber and that Jabber's ultimate flexibility makes this possible.
The only major difference between the payloads as carried in an HTTP POST and the payloads as carried in an <iq/> element is that there's no XML declaration. It's not possible of course, when you consider the context of the IQ elements. They're document fragments in the XML stream between the requester (or responder) and the Jabber server. As explained in Section 5.3, these documents are fanfared with their own XML declaration, and any further declaration within the document is illegal from an XML point of view.
{{Sidebar|Jabber-RPC Requesters and RespondersA quick word about
Jabber-RPC requesters and reponders. Connection to a Jabber server is
possible in two main ways, as we've seen in the recipes so far: as a
client via the JSM or as a component connected directly to the jabberd
backbone. At the simplest level, all a Jabber-RPC requester or responder
is, is something that makes a Jabber connection, sends and receives IQ
elements, and uses a standard XML-RPC library to encode, decode, and
service the requests and responses.
It doesn't matter whether the requesters and responders are built as clients or components. One could argue that the Jabber client model fits more naturally into the role of a requester, and the Jabber component model fits more naturally into the role of a responder, but this isn't a requirement. Indeed, if you're not the Jabber server administrator and don't have access to the server configuration (to be able to insert a <service/> stanza for a new component—see Section 9.3.1.1), building a Jabber-RPC responder as a Jabber client may be the path of least resistance.
</code>
Building a Requester and a Responder
Let's have a look at the
power and flexibility of Jabber-RPC and how to build requesters and
responders. We're going to build a client-based requester, in Python,
and a client-based responder, in Java. There are two great XML-RPC
library implementations for Python and Java that we'll use to do the
legwork for us. Figure 10-8 shows what we want to build.
To keep things fairly simple, we'll just implement and call something similar to the getStateName() function already shown in the examples: getCountyName().
[[image:jab_1008.png|The Jabber-RPC requester and responder|center|350 px
</code>
The responder: JabberRPCResponder
We'll start up the Java
client-based Jabber-RPC responder (imaginatively called
JabberRPCResponder) specifying a handler class that is to service
the XML-RPC-encoded method calls, get it to connect to a Jabber server,
and listen for incoming Jabber-RPC requests. It will use the Helma
XML-RPC library (at http://xmlrpc.helma.org) to service incoming
requests using the handler class.
Being a Java script, we'll use the JabberBeans library. We'll need to extend the library's capabilities to handle extensions in the new jabber:iq:rpc namespace.
The requester: JabberRPCRequester
The Python client-based
Jabber-RPC requester will also start up and connect to a Jabber server.
We'll use a different server than the one the responder is connected to;
after all, the whole point of XML-RPC and Jabber-RPC is to make the RPC
world a smaller and bigger place at the same time, through the power of
the Internet. We're going to use the Pythonware XML-RPC library
(http://www.pythonware.com/products/xmlrpc/index.htm) to encode a
getCountyName() request and decode the response.
JabberRPCResponder
Let's look at the Jabber-RPC responder first.
There is a single script, called JabberRPCResponder, but there are
also a number of supporting classes that we need. Let's take things one
at a time.
The RPCHandler class
The Helma XML-RPC library implementation
allows you to build XML-RPC responders independent of any particular
transport by using an instance of the XmlRpcServer object,
which represents an abstraction of an XML-RPC server. You can then
construct a class—the handler class—containing your methods to be
callable via XML-RPC. The calling of these methods is coordinated by the
XmlRpcServer object, which you tell about your handler class
using the addHandler() method.
This is what the handler class, called RPCHandler, looks like:
// RPCHandler: A class of XML-RPC-callable methods
public class RPCHandler {
// Note: This is a "traditional" list of counties!
private String county[] = {"Bedfordshire", "Berkshire",
"Buckinghamshire", "Cambridgeshire", "Cheshire", "Cornwall",
"Cumberland", "Derbyshire", "Devon", "Dorset", "Durham", "Essex",
"Gloucestershire", "Hampshire", "Herefordshire", "Hertfordshire",
"Humberside", "Huntingdonshire", "Kent", "Lancashire",
"Leicestershire", "Lincolnshire", "London", "Middlesex", "Norfolk",
"Northamptonshire", "Northumberland", "Nottinghamshire",
"Oxfordshire", "Rutland", "Shropshire", "Somerset", "Staffordshire",
"Suffolk", "Surrey", "Sussex", "Warwickshire", "Westmorland",
"Wiltshire", "Worcestershire", "Yorkshire"};
public RPCHandler() {}
public String getCountyName(int c) { return county[c - 1];
}
} The class has three elements:
- The list of counties
- The names of counties are stored in a simple array, county[].
- The constructor
- We don't need to do anything in the constructor,
- RPCHandler(), as there's no requirement to manipulate objects
- directly, so the constructor remains empty.
- The single available method
- All public methods in the class are available through the
- XmlRpcServer object. Here we have a single method for the
- purposes of this recipe: getCountyName() returns the name of
- the county for the index given. When we examine the
- JabberRPCResponder script (in Section 10.2.3.3) we'll see how the
- XmlRpcServer object is instantiated and how this
- RPCHandler class is used.
The IQRPC classes
JabberBeans deals with Jabber
element extensions—<query/> and <x/>
tags—using individual helper classes. We've seen this in the
HostAlive recipe in Section 8.2, where the
IQAuthBuilder class is used to construct an authorization
extension:
<query xmlns='jabber:iq:auth'>
<username>alive</username> ... </query>
The helper classes are oriented around namespaces. Because we have a new
namespace to deal with (jabber:iq:rpc), JabberBeans
needs to have helper classes to handle extensions in that namespace.
We need a minimum of two helper classes. We need a class that represents an extension—a <query/> tag—in the jabber:iq:rpc namespace. If we are going to construct IQ elements containing such Jabber-RPC extensions, we also need a class to build those extensions. The class that represents the jabber:iq:rpc extension is called IQRPC, and the class that is the builder for the extensions is called IQRPCBuilder.
For an example we're already familiar with, look at the steps leading up to the authorization phase in the HostAlive script (and indeed our JabberRPCResponder script, which is shown in the next section); the authorization IQ element is constructed as follows:
InfoQueryBuilder iqb = new InfoQueryBuilder(); IQAuthBuilder
iqAuthb = new IQAuthBuilder();
iqb.setType("set");
iqAuthb.setUsername(user); iqAuthb.setPassword(pass); iqAuthb.setResource(resource);
iqb.addExtension(iqAuthb.build()); If we bear in mind that this is what we want to end up with:
<iq type='set'> <query xmlns='jabber:iq:auth'>
<username>...</username>
<password>...</password>
<resource>...</resource> </query> </iq>
then we can understand what's going on:
- The <query/> tag in the jabber:iq:auth
namespace is built with an authorization builder, iqAuthb, which is an instance of IQAuthBuilder.
- Values for the tags within <query/>, such as <username/> and
<password/>, are set using methods belonging to that builder class.
- The actual extension is generated with a call to the
build() method. Using addExtension(), the newly generated extension is added to the container representing the <iq/> element being constructed by the IQ builderiqb, an instance of InfoQueryBuilder.
Figure 8-3 shows the relationship between these builders and the things they create. (For a review of what's required to authenticate with a Jabber server, see Section 7.3.)
Although necessary, the IQRPC classes aren't the focus of this recipe and can be found in Appendix B. They are essentially modified copies of the classes for the jabber:iq:time namespace: IQTime and IQTimeBuilder. They just need to be compiled and made available in the $CLASSPATH. Putting them in the same directory as the JabberRPCResponder script will work fine.
The JabberRPCResponder script
Example 10-11 shows the
JabberRPCResponder script in its entirety. In the next section we'll
take it piece by piece.
The JabberRPCResponder Script, written in Java
import org.jabber.jabberbeans.*; import
org.jabber.jabberbeans.Extension.*; import
org.jabber.jabberbeans.util.JID; import java.net.InetAddress; import
java.util.Enumeration; import java.io.*; import helma.xmlrpc.*;
public class JabberRPCResponder implements PacketListener { private String server = "gnu.mine.nu"; private String user = "server"; private String pass = "pass"; private String resource = "jrpc-server";
private XmlRpcServer responder;
private ConnectionBean cb;
// Constructor
public JabberRPCResponder() { responder = new XmlRpcServer();
responder.addHandler("examples", new RPCHandler());
}
// Main program
public static void main(String args[]) { JabberRPCResponder server =
new JabberRPCResponder(); try { server.start();
}
catch (Exception e) { System.out.println("Cannot start server: " +
e.toString());
}
}
public void start() throws Exception { cb = new ConnectionBean();
InetAddress addr;
cb.addPacketListener(this);
// Connect cb.connect(addr=InetAddress.getByName(server));
// Authenticate InfoQueryBuilder iqb = new InfoQueryBuilder(); IQAuthBuilder iqAuthb = new IQAuthBuilder();
iqb.setType("set");
iqAuthb.setUsername(user); iqAuthb.setPassword(pass); iqAuthb.setResource(resource);
iqb.addExtension(iqAuthb.build());
InfoQuery iq = (InfoQuery)iqb.build();
cb.send(iq);
// Send presence PresenceBuilder pb = new PresenceBuilder(); cb.send(pb.build());
}
// Packet listener interface:
public void receivedPacket(PacketEvent pe) { Packet packet =
pe.getPacket(); System.out.println("RECV:" + packet.toString());
if (packet instanceof InfoQuery) { Enumeration e =
((InfoQuery)packet).Extensions(); if (e.hasMoreElements()) {
Extension ext = (Extension)e.nextElement();
String request = ext.toString(); String id =
((InfoQuery)packet).getIdentifier(); JID from =
((InfoQuery)packet).getFromAddress();
ByteArrayInputStream bis = new
ByteArrayInputStream(request.getBytes()); String result = new
String(responder.execute(bis));
String response = result; int pos = result.lastIndexOf("?>");
if (pos >= 0) { response = result.substring(pos + 2);
}
IQRPCBuilder iqrpcb = new IQRPCBuilder();
iqrpcb.setPayload(response);
InfoQueryBuilder iqb = new InfoQueryBuilder();
iqb.setType("result"); iqb.setIdentifier(id);
iqb.setToAddress(from); iqb.addExtension(iqrpcb.build());
InfoQuery iq; try { iq = (InfoQuery)iqb.build();
}
catch (InstantiationException ie) { System.out.println("Build
failed: " + ie.toString()); return;
}
cb.send(iq);
} } }
public void sentPacket(PacketEvent pe) { Packet packet =
pe.getPacket(); System.out.println("SEND:" + packet.toString());
}
public void sendFailed(PacketEvent pe) { Packet packet =
pe.getPacket(); System.out.println("failed to send:" +
packet.toString());
}
}
Looking at JabberRPCResponder Step by Step
Now let's examine the
JabberRPCResponder script step by step so we can see how it works:
import org.jabber.jabberbeans.*; import
org.jabber.jabberbeans.Extension.*; import
org.jabber.jabberbeans.util.JID; import java.net.InetAddress; import
java.util.Enumeration; import java.io.*; import helma.xmlrpc.*;
We need to bring in the jabberbeans classes as shown, as well
as some core Java features that we'll see used in the script a bit
later: an InetAddress to represent the Jabber server's
hostname, an Enumeration interface to access the extensions in
the incoming IQ elements, and java.io features for feeding the
XML-RPC-encoded requests to the XmlRpcServer object. We also
bring in the classes from the Helma XML-RPC library.
public class JabberRPCResponder implements PacketListener {
private String server = "gnu.mine.nu"; private String user =
"server"; private String pass = "pass"; private String resource =
"jrpc-server";
private XmlRpcServer responder;
private ConnectionBean cb;
The definition of our JabberRPCResponder class looks similar to that of the HostAlive class in Section 8.2. However, rather than merely connecting to a Jabber server and sending packets off down the stream, we want to listen for incoming packets—in this case, IQ elements carrying jabber:iq:rpc-qualified payloads—and handle them. Accordingly, we specify that our main class implements PacketListener, a JabberBeans interface that Jabber clients use to receive notification of incoming packets. The interface describes three methods: receivedPacket(), sentPacket(), and sendFailed(). We'll define our receivedPacket() method, described later in this section, to catch and process the incoming Jabber-RPC requests.
We're going to use an XmlRpcServer object, in responder, to provide the translation services between our RPCHandler class that contains the methods we want to "expose," and the XML-RPC-encoded requests and responses.
Naturally, we also need a JabberBeans ConnectionBean, which we'll hold in cb.
public JabberRPCResponder() { responder = new XmlRpcServer();
responder.addHandler("examples", new RPCHandler());
}
The JabberRPCResponder class constructor, JabberRPCResponder(), will be called in the main() method later in the script. It is used to create an instance of the Helma XmlRpcServer object, and associate our RPCHandler class with it, as a handler for method calls. The addHandler() method takes two arguments: the first is the method prefix name, such as examples in the methodName specification:
<methodName>examples.getCountyName</methodName>
and the second is the object—an instantiation of our handler class—which
contains the methods that the XmlRpcServer will use to
"service" requests. In other words, the XmlRpcServer object
will determine that a call to examples.getCountyName should be
handled, as getCountyName, by the RPCHandler object.
The main() method is quite short:
public static void main(String args[]) { JabberRPCResponder
server = new JabberRPCResponder(); try { server.start();
}
catch (Exception e) { System.out.println("Cannot start server: " +
e.toString());
}
}
We instantiate our JabberRPCResponder object into server and call the start() method. Various functions in start() can raise exceptions; we take care of them all here with a simple catch clause and abort if necessary, rather than include all of start()'s functions here and have multiple try and catch statements:
public void start() throws Exception { cb = new
ConnectionBean(); InetAddress addr;
cb.addPacketListener(this);
// Connect cb.connect(addr=InetAddress.getByName(server));
// Authenticate InfoQueryBuilder iqb = new InfoQueryBuilder(); IQAuthBuilder iqAuthb = new IQAuthBuilder();
iqb.setType("set");
iqAuthb.setUsername(user); iqAuthb.setPassword(pass); iqAuthb.setResource(resource);
iqb.addExtension(iqAuthb.build());
InfoQuery iq = (InfoQuery)iqb.build();
cb.send(iq);
// Send presence PresenceBuilder pb = new PresenceBuilder(); cb.send(pb.build());
}
Most of the content of this start() function should be fairly familiar. We instantiate a ConnectionBean and will use that, in cb, throughout the script to send elements to the Jabber server. Our PacketListener is added as a listener to the ConnectionBean, causing a method of that interface (packetReceived()) to be invoked when an incoming element appears on the stream managed by that ConnectionBean.
In the same way as described in Section 8.2.2 in Chapter 8, we build our IQ element to authenticate, and send it to the server with the ConnectionBean's send() method. We also send an initial availability, in the form of a simple <presence/> element to the server.
Having connected to and authenticated with the server and sent initial availability, we can sit back and relax. All the subsequent activity will be initiated by the arrival of IQ elements on the stream. As described already, these will be made known (and available) in the form of calls to the receivedPacket() method of the PacketListener interface:
// Packet Listener interface:
public void receivedPacket(PacketEvent pe) { Packet packet =
pe.getPacket(); System.out.println("RECV:" + packet.toString());
The method receives a PacketEvent object, which represents both the event of a packet (an element) arriving and the packet itself, which we retrieve into packet with the getPacket() method. For debugging purposes, we print out what we get.
Now to determine what has actually arrived:
if (packet instanceof InfoQuery) { Enumeration e =
((InfoQuery)packet).Extensions(); if (e.hasMoreElements()) { Extension
ext = (Extension)e.nextElement();
We check to see if the element is an IQ element, and, if so, we retrieve
the extensions—the <query/> tags—that the element
contains. Calling the Extensions() method on the
packet object returns an Enumeration of those tags.
We're expecting only one tag, and we pull that into ext using the nextElement() method on our Enumeration.
{{Note|For the sake of simplicity, we're not checking here whether the
<query/> tag received is qualified by the
jabber:iq:rpc namespace. You might wish to do that in your
version.
</code>
String request = ext.toString(); String id =
((InfoQuery)packet).getIdentifier(); JID from =
((InfoQuery)packet).getFromAddress();
We can pull the extension into string form, with the toString()
method, ready for passing to our XmlRpcServer object. At this
stage, ext contains the complete XML-RPC-encoded parcel,
starting with the <methodCall> opening tag:
<methodCall>
<methodName>examples.getCountyName</methodName>
<params> <param>
<value><i4>14</i4></value> </param>
</params> </methodCall>
So that we can send a response back to the requester, we need two other
things from the incoming element besides the actual request payload. In
general, when an <iq/> element is sent, representing a
request, either as an IQ-get or an IQ-set, the sender is expecting the
response, either an IQ-result or an IQ-error, with the same value in
the id attribute. This is so the responses, once received, can
be matched up with the original requests. So we need to store the
id value, available to us through the getIdentifier()
method of the packet object. We also need the JID of the
sender, retrieved with the getFromAddress() method, so we can
specify it in the to attribute of the IQ-result we'll be
sending back.
Now it's time to service the request:
ByteArrayInputStream bis = new
ByteArrayInputStream(request.getBytes()); String result = new
String(responder.execute(bis));
With the execute() method of our XmlRpcServer object
in responder, we convert the <methodCall/> into
a <methodResponse/>, in effect. The decoding of the
XML-RPC-encoded request, the determination of which method in which
class to call (in our case it will be the getCountyName()
method in our RPCHandler class), the calling of that method,
and the encoding of the result into an XML-RPC-encoded response are all
done magically and transparently for us by XmlRpcServer (thank
goodness, or this recipe would be unbearably long!).
There's a bit of jiggling about required before we can make the execute() call, though. The method is expecting an InputStream object, and we've got a String. Not to worry, we just convert it with a ByteArrayInputStream, which can take a byte array (from request.getBytes()) and produce an InputStream object bis. Similarly, execute() produces a byte array, so that is converted to a String by wrapping the call with String().
The incoming XML-RPC-encoded request, being carried in the IQ element, will not include an XML declaration. Luckily, the XmlRpcServer does not mind that one is not present before decoding and dispatching the request. It will, however, include one on the encoded response it emits. So we must check for that and strip it off if there is one:
String response = result; int pos =
result.lastIndexOf("?>"); if (pos >= 0) { response =
result.substring(pos + 2);
}
Now we have everything we need to return the XML-RPC-encoded response to the requester. It's time to call on the services of our IQRPC classes:
IQRPCBuilder iqrpcb = new IQRPCBuilder();
iqrpcb.setPayload(response);
We create an instance of the IQRPCBuilder and call the single
method setPayload() to load the XML-RPC-encoded response into
the <query/> tag, which is qualified by the
jabber:iq:rpc namespace. This is effectively the "extension" in
JabberBeans parlance:
InfoQueryBuilder iqb = new InfoQueryBuilder();
iqb.setType("result"); iqb.setIdentifier(id);
iqb.setToAddress(from); iqb.addExtension(iqrpcb.build());
InfoQuery iq; try { iq = (InfoQuery)iqb.build();
}
catch (InstantiationException ie) { System.out.println("Build
failed: " + ie.toString()); return;
}
cb.send(iq);
} } }
After creating an IQ element container using an InfoQueryBuilder and setting the appropriate attributes, we generate the <query/> tag (iqrpcb.build()) and add it to the IQ element container with addExtension().
The <iq/> is then generated—it now contains our payload—and sent back to the server, where it will be routed to the original requester.
That's pretty much all there is to it. We have a couple of other methods belonging to the PacketListener interface:
public void sentPacket(PacketEvent pe) { Packet packet =
pe.getPacket(); System.out.println("SEND:" + packet.toString());
}
public void sendFailed(PacketEvent pe) { Packet packet =
pe.getPacket(); System.out.println("Failed to send:" +
packet.toString());
}
} We fill these methods with debugging-style output statements for our convenience.
JabberRPCRequester
Now that we've got our Jabber-RPC responder
all set up, it's time to turn our attention to our requester,
JabberRPCRequester, shown in Example 10-12. This is a Python script
that uses the Jabberpy library and Pythonware's
xmlrpclib library. It's a pretty simple affair.
The JabberRPCRequester script, written in Python
import jabber import xmlrpclib import string import sys
Server = 'qmacro.dyndns.org' Username = 'client' Password = 'pass' Resource = 'jrpc-client' Endpoint = 'server@gnu.mine.nu/jrpc-server'; Method = 'examples.getCountyName';
county = string.atoi(sys.argv[1])
con = jabber.Client(host=Server) try: con.connect() except IOError, e: print "Couldn't connect: %s" % e sys.exit(0)
con.auth(Username,Password,Resource)
request = xmlrpclib.dumps(((county),), methodname=Method);
iq = jabber.Iq(to=Endpoint, type="set') iq.setQuery('jabber:iq:rpc') iq.setQueryPayload(request)
result = con.SendAndWaitForResponse(iq)
if result.getType() == 'result': response = str(result.getQueryPayload()) parms, func = xmlrpclib.loads(response) print parms[0] else: print "Could not complete call"
con.disconnect()
Looking at JabberRPCRequester Step by Step
After importing the
libraries that we will need:
import jabber import xmlrpclib import string import sys
we specify a number of parameters:
Server = 'qmacro.dyndns.org' Username = 'client' Password =
'pass' Resource = 'jrpc-client'
Endpoint = 'server@gnu.mine.nu/jrpc-server'; Method = 'examples.getCountyName'; The script connects to the Jabber server defined in Server, with the username defined in Username. The resource that will be passed in the authentication request is jrpc-client. There is as much significance in this name as there is in the name of the resource used by JabberRPCResponder (jrpc-server): none. It's just a useful naming convention to adopt when writing requesters and responders.
A single parameter, which will be interpreted as the index of the county to retrieve via the call to examples.getCountyName, is expected.
county = string.atoi(sys.argv[1])
The method expects an integer, so we convert it directly. This has a
favorable secondary effect when we come to XML-RPC encode the request;
if we hadn't called the string.atoi() function and left
county as a string, this is what the XML-RPC-encoded parcel
would have looked like:
<methodCall>
<methodName>examples.getCountyName</methodName>
<params> <param>
<value><string>1</string></value> </param>
</params> </methodCall>
However, this is what we really want:
<methodCall>
<methodName>examples.getCountyName</methodName>
<params> <param>
<value><int>1</int></value> </param>
</params> </methodCall>
In the same way as in the previous Python recipes, we connect to the
Jabber server and authenticate:
con = jabber.Client(host=Server) try: con.connect() except
IOError, e: print "Couldn't connect: %s" % e sys.exit(0)
con.auth(Username,Password,Resource) Now all we have to do is compose our XML-RPC-encoded request and send it on its way to the Jabber-RPC responder, which in our case, identified here by the JID in the Endpoint variable, is our JabberRPCResponder script.
It's time to call on the services of the XML-RPC library. We use the dumps() function to build an XML-RPC encoding, passing the single parameter (in a tuple, which is required by dumps()) representing the county index and the name of the method to call:
request = xmlrpclib.dumps(((county),), methodname=Method); We
build the IQ-set containing a <query/> element in the
jabber:iq:rpc namespace, by creating an instance of an
Iq object, and calling methods to specify that namespace
(setQuery()) and insert the XML-RPC encoding as the payload
(setQueryPayload()):
iq = jabber.Iq(to=Endpoint, type='set')
iq.setQuery('jabber:iq:rpc') iq.setQueryPayload(request) At this
stage, we need to send it off and wait for a response. You have probably
noticed that, unlike in previous Python recipes, this script hasn't
defined or registered a callback to handle incoming elements. This is
because we're approaching the task in a slightly different way in this
script. The method used to send the element to the Jabber
server—SendAndWaitForResponse():
result = con.SendAndWaitForResponse(iq)
is the rather verbose cousin of the Jabber::Connection
library's ask() method, as described in Section 10.1.5.3. It
does "exactly what it says on the tin," namely, send the element off to
the server and block until an element is received that is deemed, by a
matchup of the id attribute values on both elements, to be the
response. If no id attribute exists on the outgoing element, it
is stamped with one (with a value unique within the current usage of the
jabber class). Both are also relations of
Net::Jabber's SendAndReceiveWithID().
So result receives an element object. It's an IQ element, as that is what a response to an IQ element will be. If the call transport (as opposed to the call itself) is successful, the response will be an IQ-result. If not, for example, if the Endpoint that we specified doesn't exist, then the response will be an IQ-error.
if result.getType() == 'result': response =
str(result.getQueryPayload()) parms, func = xmlrpclib.loads(response)
print parms[0] else: print "Could not complete call"
Conversely, while the call transport might have succeeded, the call
itself might have failed, for example, if we had specified a nonexistent
method name in Method. However, for our purposes, we're going
to assume that the call was successful. So we grab the payload (i.e.,
the contents of the <query/> tag) and string it with
str().
The loads() function of xmlrpclib is used to extract the details from an XML-RPC-encoded parcel; it will produce two values, which we capture in resp and func. The first is the set of parameters, common to either a request or a response, and the second is the method name, if it exists in the encoding. We want to decode a <methodResponse/> here, so there's no method name present, but there should be a set of parameters.
We take the first (and only) element from the set of parameters in resp and print it out.
Jabber-RPC in Perl
Exciting as this recipe might be, it's not
very visual. There are no screenshots of note to show, just a couple of
STDOUTs from two scripts started at the command line. To fix this
"problem," we're going to round this recipe off with a quick look at
Jabber-RPC in Perl. Based on the Jabber::Connection library is
a fairly new Perl library called Jabber::RPC, which sports two
modules: Jabber::RPC::Client.pm and
Jabber::RPC::Server.pm.
If you're slightly perplexed about what it takes to extend Jabber support in Java, take a look at Example 10-13 and Example 10-14. The first is an implementation, in Perl, of the JabberRPCResponder and all its class periphery. The second is an implementation of our JabberRPCRequester.
A Jabber-RPC Responder in Perl
use strict; use Jabber::RPC::Server;
my @county = ("Bedfordshire", "Berkshire", "Buckinghamshire", "Cambridgeshire", "Cheshire", "Cornwall", "Cumberland", "Derbyshire", "Devon", "Dorset", "Durham", "Essex", "Gloucestershire", "Hampshire", "Herefordshire", "Hertfordshire", "Humberside", "Huntingdonshire", "Kent", "Lancashire", "Leicestershire", "Lincolnshire", "London", "Middlesex", "Norfolk", "Northamptonshire", "Northumberland", "Nottinghamshire", "Oxfordshire", "Rutland", "Shropshire", "Somerset", "Staffordshire", "Suffolk", "Surrey", "Sussex", "Warwickshire", "Westmorland", "Wiltshire", "Worcestershire", "Yorkshire");
sub getCountyName { my $county = shift; return $county[$county - 1]; }
my $server = new Jabber::RPC::Server( server => 'gnu.mine.nu', identauth => 'server:pass', resource => 'jrpc-server', methods => {'examples.getCountyName' => \&getCountyName}, );
$server->start;
A Jabber-RPC Requester in Perl
use strict; use Jabber::RPC::Client;
my $client = new Jabber::RPC::Client( server => 'qmacro.dyndns.org', identauth => 'client:pass', resource => 'jrpc-client', endpoint => 'dj@localhost/jrpc-server', );
print $client->call('examples.getCountyName', $ARGV[0])
|| $client->lastFault;
That's it. There's no more code than that. So don't lose heart. Just use Perl; you know it makes sense.
Browsing LDAP
Browsing, a relatively new Jabber feature
(introduced in Section 2.9 and described through its associated
namespace in Section 6.2.5), is an extremely powerful and flexible
beast. Whereas many of the standard Jabber namespaces such as
jabber:iq:oob, jabber:iq:auth, and
jabber:iq:register busy themselves with providing context for
relatively narrow areas of information (out-of-band information
exchange, authentication, and registration, respectively),
jabber:iq:browse is a namespace that qualifies, and therefore
defines, a flexible container that can be used to carry all sorts of
information—information that is wrapped in meaning and context and that
can have virtually unlimited levels of hierarchy. What's more, it can
deliver information in a standard way that can be understood by Jabber
clients.
{{Note|As of this writing, the only Jabber client to implement browsing
is WinJab.
</code> Put another way, it means that we can extend the scope of Jabber deployment to areas outside what we traditionally see as the "Jabber world." The power and simplicity of the Jabber browsing namespace design means that we can adapt its use to whatever purpose we have in mind. As we push out the Jabber protocol and give entities within our networks the gift of speech and presence (in other words, give them a JID and graft on a Jabber connection stub), we will want to identify those entities as something more than a collection of hazy objects that sit behind Jabber addresses.
- Want to find out about dictionaries that are reachable by Jabber?
Browse to a directory and pull out those JIDs—the dictionaries' addresses—that are identified as keyword/dictionary JID-types.
- Want to allow your users to navigate an information hierarchy outside
the Jabber space but from within the comfort of their Jabber client? Build a "reflector" that navigates the hierarchy on behalf of your Jabber users and transforms it into a Jabber browsing context—by formulating the information in jabber:iq:browse terms. Jabber browsing is one of those oddities that is so simple and so ultimately flexible that it's sometimes better to demonstrate it than to talk about it. Let's have a look at what browsing can do by building ldapr, a "reflector" for information in a database accessed by the Lightweight Directory Access Protocol (LDAP).
Building the Reflector
We're going to build the reflector as a
component that connects directly to the Jabber backbone. This makes
sense, as it's a service that we'll probably want to run continuously
(and perhaps start up and shut down in conjunction with the Jabber
server itself), rather than something more transient like a client-based
'bot, for example.
Before we go any further, have a look at Figure 10-9.
</code>
This figure shows WinJab's browser window, which, when first opened, requests the top-level browse information from the Jabber server that WinJab is connected to (cicero, in this case). This is the information in the <browse/> section of the JSM component custom configuration, as described in Section 4.4.3.8, which looks like:
<browse> <conference type='public' jid='conf.cicero'
name='Public Chat'/> </browse> We can see just one icon,
representing the Public Chat conference service.
WinJab sensibly uses this location—the Jabber server (specifically the JSM) itself— as a starting position for browsing navigation. From the description of jabber:iq:browse in Section 6.2.5, we know that each element within a <browse/> section is identified with a JID, in the jid attribute. Here, the Public Chat element has a JID of conf.cicero. If we click on the element's icon in WinJab's browser window, it would make a further browse request—an IQ-get with an empty extension qualified by the jabber:iq:browse namespace—to that JID. Thus is a browse hierarchy descended. Example 10-15 shows what that browse request might look like.
Browsing to the Public Chat service
<iq type='get' to='conf.cicero' id='82A'> <query
xmlns='jabber:iq:browse'/> </iq>
Identifying ldapr in the browsing hierarchy
This is exactly
where our reflector service, in the form of a script called ldapr,
will fit in. In the same way as we described our RSS news agent
(newsagent) in the JSM configuration's <browse/>
section, we'll now describe ldapr. We add it, like this:
<browse> <conference type='public' jid='conf.cicero'
name='Public Chat'/> <service type='x-ldap' jid='ldap.cicero'
name='LDAP Reflector'/> </browse>
There are a couple of things to note in the definition:
- service/x-ldap
- The LDAP reflector is a service, so we use the service
- category for the tag name used to describe it. While there are already
- many subtypes defined within the jabber:iq:browse namespace
- (such as irc, aim, and jud), none of them
- matches what this service is going to offer, so in the same way as we
- invent Multipurpose Internet Mail Extensions (MIME) subtypes in the
- x- space, we specify x-ldap for the type
- attribute here.
- ldap.cicero
- Each component has a JID. In browsing, the JID is the key to
- navigation. When the icon representing this service is clicked, it's
- to this JID that the browse request is sent. To provide a smooth
- navigational path through the hierarchy, it's up to the component to
- return browse data items that are identified by JIDs appropriate for
- further navigation. We'll see what this means in the next section.
Navigating into the LDAP hierarchy
Having our reflector
component defined in the JSM's <browse/> list makes for a
smooth transition into the reflection. We're going to navigate the LDAP
hierarchy in a similar way to what was described in Section 6.2.5.1. On
receipt of an initial browse request, ldapr must return the top
level of the LDAP hierarchy it has been set up to reflect:
<iq type='get' to='ldap.cicero' id='7'> <query
xmlns='jabber:iq:browse'/> </iq>
Figure 10-10 shows the LDAP hierarchy we've discussed in Section
6.2.5.1. It's part of an imaginary structure devised to represent people
and departments in an organization. The base distinguished name, or
base DN (an LDAP term meaning the common suffix used in the
identifiers of all elements in a particular LDAP structure), is
dc=demo,dc=org. A DN, or distinguished name, can be thought
of as a key for a particular element. Levels within the LDAP structure
are identified with DNs of ever-increasing lengths, as they get more
specific the deeper the hierarchy is descended.
</code> If
ldapr were to reflect this hierarchy, we want the response to an initial browse request to return information about the People and Groups nodes. In Example 10-16, we see what this response looks like.
An initial browse request elicits a reflection of the People and
Groups nodes
SEND: <iq type="get" id="B88" to="ldap.cicero"> <query
xmlns="jabber:iq:browse"/> </iq>
RECV: <iq id='B88' type='result' to='dj@cicero/basement' from='ldap.cicero'> <query xmlns='jabber:iq:browse'> <item jid='ldap.cicero/ou=People' name='ou=People'/> <item jid='ldap.cicero/ou=Groups' name='ou=Groups'/> </query> </iq> In the response, we're returning a single level of the LDAP hierarchy. The People and Groups nodes are represented by <item/> tags, within the generic container tag <query/>.
This is in slight contrast to the browse <iq/> elements shown in Section 6.2.5.1, in which, effectively, two levels of hierarchy are returned:
RECV: <iq type='result' id='B89' to='dj@cicero/basement'
from='ldap.cicero'> <item name='root entry'
xmlns='jabber:iq:browse' jid='ldap.cicero'> <item name='ou=People'
jid='ou=People@ldap.cicero'/> <item name='ou=Groups'
jid='ou=Groups@ldap.cicero'/> </item> </iq>
It's really up to you to decide how many levels of information you want
each browse response to emit. It depends on circumstances (will the
requester be able to interpret multiple levels of hierarchy?) and the
type of application scenario in which you're wanting to employ Jabber
browsing. In this case, despite the difference in appearance, WinJab
will interpret the information correctly whichever way you play it.
{{Sidebar|No Query Tag?This last example looks slightly odd, because the
familiar <query/> tag, usually the container for
information within various IQ namespaces, is conspicuously absent. It's
actually still there in spirit, in the form of the first
<item/> tag, which takes the <query/>
tag's role in carrying the xmlns='jabber:iq:browse' namespace
declaration.
</code> So, we've got our first two LDAP levels back. Figure 10-11 shows how they're displayed in WinJab's browser window. The JID we've just browsed to—ldap.cicero—is shown in the Jabber Address field. The People and Groups nodes are represented by <item/> tags within the jabber:iq:browse-qualified result; these are translated into folder icons in the browser window.
</code> The browser displays folder
icons for these nodes because we've described them using jabber:iq:browse's generic "holder" tag <item/>. While the namespace describes many categories to represent many different things (service, conference, user, and so on), there's not really a category that fits the "LDAP_ hierarchy node" description. So we plump for the generic <item/>, reserved specially for such occasions.
To navigate to the next level in the hierarchy, all we do is click on one of the icons now displayed to us. WinJab will send this next browse request to the JID that's associated with the icon we click. In the case of the People icon, we know from the browse result shown in Example 10-16 that the JID is ldap.cicero/ou=People. This is what that browse request and response look like:
SEND: <iq type="get" id="B105" to="ldap.cicero/ou=People">
<query xmlns="jabber:iq:browse"/> </iq>
RECV: <iq id='B105' type='result' to='dj@cicero/basement' from='ldap.cicero/ou=People'> <query xmlns='jabber:iq:browse'> <item jid='ldap.cicero/ou=UK, ou=People' name='ou=UK'/> <item jid='ldap.cicero/ou=France, ou=People' name='ou=France'/> <item jid='ldap.cicero/ou=Germany, ou=People' name='ou=Germany'/> </query> </iq> The results of this browse request are shown in Figure 10-12. Again, notice the JID displayed in the Jabber Address window—it's the JID that we've just browsed to. The JIDs that are assigned to the items displayed here reflect the next level in the hierarchy. And so it goes on.
</code>
What the reflector is actually doing
Each of the JIDs that are
browsed to look like this:
[component name]/[relative LDAP DN] such as:
ldap.cicero/ou=UK, ou=People The hostname part of the
JID—[component_name]—is the name of the LDAP
reflector component, and the resource part of the
JID—[relative_LDAP_DN]—reflects the DN of the node
(minus the base DN suffix) within the LDAP structure that the
<item/> with that JID represents.
The crucial bit is that the [component_name] part remains the same (ldap.cicero) across every call. This means that all of the jabber:iq:browse requests made in the navigation sequence go to the component, in this case, the ldapr script.
Once the component receives these requests, it disassembles the JID, extracting the resource part, and uses it to query the LDAP server on the Jabber client's behalf. It then builds a response, in the form of an IQ-result containing the all-important jabber:iq:browse-qualified extension, containing the results of the LDAP query.
JIDs are used to convey information to the component, as well as to have the request delivered to the right place.
{{Sidebar|Using JID Parts to Convey InformationThe example from Section
6.2.5.1 showed the information being conveyed as the user part of
the JID:
<item name='ou=People' jid='ou=People@ldap.yak'/>
This is perfectly fine, as long as the information we want to convey
doesn't contain characters deigned illegal for that part of the JID.
There are far fewer restrictions on how the resource part of a JID may
be constructed. So it should usually be your first choice of location
for piggybacking information:
<item jid='ldap.cicero/ou=People' name='ou=People'/>
</code>
The ldapr script
While the explanation might be long, the actual
script, shown in Example 10-17, is relatively short. Written in Perl,
the ldapr script uses the Jabber::Connection library.
The ldapr script, written in Perl
use strict; use Jabber::Connection; use Jabber::NodeFactory; use
Jabber::NS qw(:all); use Net::LDAP;
my $ldapsrv = 'cicero'; my $basedn = 'dc=demo,dc=org';
my $ldap = Net::LDAP->new($ldapsrv) or die $@;
debug("connecting to Jabber"); my $c = new Jabber::Connection( server => 'localhost:9389', localname => 'ldap.cicero', ns => 'jabber:component:accept', );
unless ($c->connect()) { die "Oops: ".$c->lastError; }
debug("registering IQ handlers"); $c->register_handler('iq',\&iq_browse); $c->register_handler('iq',\&iq_notimpl);
debug("authenticating"); $c->auth('pass');
debug("waiting for requests"); $c->start;
sub iq_browse {
my $node = shift; return unless $node->attr('type') eq IQ_GET and
my $query = $node->getTag(, NS_BROWSE);
my ($obj) = $node->attr('to') =~ /\/(.*)$/; debug("request: $obj");
my $result = $ldap->search( base => $obj ? join(',', $obj,
$basedn) : $basedn, filter => "(objectclass=*)", scope =>
'one', );
if ($result->code) { debug("search error: ".$result->error);
}
else { foreach my $entry ($result->all_entries) { my $e =
strip($entry->dn, $basedn); debug("found: $e"); my $item =
$query->insertTag(isUser($e) ? "user" : "item");
$item->attr('jid', join('/', $ID, $e)); $item->attr('name',
[split(/,/, $e)]->[0]);
}
}
$node = toFrom($node); $node->attr('type', IQ_RESULT);
$c->send($node);
return r_HANDLED;
}
sub iq_notimpl {
my $node = shift; $node = toFrom($node); $node->attr('type',
IQ_ERROR); my $error = $node->insertTag('error');
$error->attr('code', '501'); $error->data('Not Implemented');
$c->send($node); return r_HANDLED;
}
sub strip {
my ($fqdn, $basedn) = @_; my @fqdn = split(/,/, $fqdn); my
@basedn_elements = split(/,/, $basedn); return join(',', @fqdn[0 ..
($#fqdn - scalar @basedn_elements)]);
}
sub toFrom { my $node = shift; my $to = $node->attr('to');
$node->attr('to', $node->attr('from')); $node->attr('from',
$to); return $node;
}
sub isUser {
my $rdn = shift; return $rdn =~ /^cn/ ? 1 : 0
}
sub debug {
print STDERR "debug: ", @_, "\n";
}
Looking at ldapr Step by Step
Taking the script step by step, we
start on familiar ground:
use strict; use Jabber::Connection; use Jabber::NodeFactory; use
Jabber::NS qw(:all); use Net::LDAP;
my $ldapsrv = 'cicero'; my $basedn = 'dc=demo,dc=org'; After declaring the modules we want to use (the only one we haven't seen so far is the Net::LDAP module that we'll need to connect to and query an LDAP server), we define a couple of variables. $ldapsrv is the name of the LDAP server that ldapr is going to be reflecting, and $basedn is the base distinguished name that will be used as the suffix in all of the LDAP queries.
If you don't have an LDAP server of your own, a number of public ones are available that you could point this script at. The two variables $ldapsrv and $basedn go together—make sure you specify the correct base DN for the LDAP server you want to reflect.
{{Note|Depending on the configuration, some LDAP servers will require
you to bind to them with a username and password before you can
perform searches. To do this, you'll need to include an extra step in
this script, using the bind() method in Net::LDAP.
</code> Having opened our connection to the LDAP server:
my $ldap = Net::LDAP->new($ldapsrv) or die $@;
we then proceed to connect to the Jabber server as a component. We're
connecting to localhost:9389, which means this component script
is going to run on the same host as the Jabber server, and connect to it
on port 9389:
debug("connecting to Jabber"); my $c = new Jabber::Connection(
server => 'localhost:9389', localname => 'ldap.cicero', ns
=> 'jabber:component:accept', );
unless ($c->connect()) { die "cannot connect: ".$c->lastError; } We'll also need a <service/> stanza in the Jabber server's configuration file to describe this component. The stanza in Example 10-18 would be appropriate. Refer to Section 9.3.1.1 for more details on connecting to Jabber as a component.
A component instance definition for ldapr
<service id='ldap.cicero'> <accept>
<ip>localhost</ip> <port>9389</port>
<secret>secret</secret> </accept> </service>
The script isn't going to do much apart from reflect
jabber:iq:browse queries as LDAP searches. Consequently, it
doesn't need to be able to handle anything apart from incoming IQ
elements:
debug("registering IQ handlers");
$c->register_handler('iq',\&iq_browse);
$c->register_handler('iq',\&iq_notimpl);
The iq_browse() function is where all the work will be done. As
in the newsagent recipe (Section 8.3)," we also have a
"catchall" function (iq_notimpl()) that cleans up any "stray"
IQ elements that it doesn't know about. Furthermore, because we aren't
registering any handlers for <message/> or
<presence/> elements, the dispatcher in
Jabber::Connection will just throw them away, leaving ldapr
blissfully ignorant of them—which is what we want.
After authenticating with the server by calling the auth() method to send the <handshake/> element:
debug("authenticating"); $c->auth('secret');
it's time to set the main event loop going. With a call to
start(), we hand over control to Jabber::Connection,
safe in the knowledge that we have a function (iq_browse()) to
deal with the incoming requests that we're supposed to deal with, and
that we don't care a hoot about anything else:
debug("waiting for requests"); $c->start;
Performing the actual reflection
Now, let's move on to the
meat of the script. The main handler, iq_browse(), starts by
making sure it has an IQ element:
sub iq_browse {
my $node = shift; return unless $node->attr('type') eq IQ_GET and
my $query = $node->getTag(, NS_BROWSE);
What we're looking for is an IQ-get with a jabber:iq:browse-qualified query extension. If there isn't one, we exit out of the function, and dispatching continues to the iq_notimpl() function, because we didn't return the special value represented by m_HANDLED.
If we do get a valid request, we first extract the relative DN from the resource part of the JID specified in the IQ-get's to attribute—the JID. If the request was sent to ldap.cicero/ou=UK, ou=People, then we extract the relative DN ou=UK, ou=People into $obj like this:
my ($obj) = $node->attr('to') =~ /\/(.*)$/; debug("request:
$obj");
Armed with a specification of what part of the LDAP hierarchy needs to
be searched, the next step is to call the search() method on
the LDAP object in $ldap:
my $result = $ldap->search( base => $obj ? join(',',
$obj, $basedn) : $basedn, filter => "(objectclass=*)", scope =>
'one', );
As you can see, we're specifying three parameters in the
search() method:
- base
- This is the point within the LDAP hierarchy from which to start
- looking. We must specify this as a full DN, so we append the base DN
- (dc=demo,dc=org) to the relative DN received in the request
- to make an absolute DN:
ou=UK, ou=People, dc=demo, dc=org
- filter
- Specifying (objectclass=*) effectively means "look for
- anything."
- scope
- There may be many levels below the point in the hierarchy that we're
- going to start searching from. Specifying 1 for the scope
- parameter tells the search to descend only one level. After all, we
- want to return only one level back to the requester in the reflection.
- If the search failed for some reason (e.g., if a relative DN specified
- in the request didn't exist in the hierarchy),[2] then don't bother checking the
- results. Instead, a warning debug message with the error details is
- issued:
if ($result->code) { debug("search error:
".$result->error);
}
However, if the search succeeded, the results should be translated into the jabber:iq:browse extension:
else { foreach my $entry ($result->all_entries) { my $e =
strip($entry->dn, $basedn); debug("found: $e"); my $item =
$query->insertTag(isUser($e) ? "user" : "item");
$item->attr('jid', join('/', $ID, $e)); $item->attr('name',
[split(/,/, $e)]->[0]);
} }
Calling the all_entries() method on the search results returns a list of LDAP entries, in the form of objects. Calling the dn() method on one of these objects (in $entry) returns us its full DN. Searching with a base of:
ou=UK, ou=People, dc=demo, dc=org
in the demonstration database would return two entries:
cn=Janet Abrams, ou=UK, ou=People, dc=demo, dc=org cn=Paul
Anthill, ou=UK, ou=People, dc=demo, dc=org
The task of this section of the iq_browse() function is to turn
the information, in the form of these two entries, into something like
this:
<iq id='B25' type='result' to='dj@cicero/basement'
from='ldap.cicero/ou=UK, ou=People'> <query
xmlns='jabber:iq:browse'> <user name='cn=Janet Abrams'
jid='ldap.cicero/cn=Janet Abrams, ou=UK, ou=People'/> <user
name='cn=Paul Anthill' jid='ldap.cicero/cn=Paul Anthill, ou=UK,
ou=People'/> </query> </iq>
which in turn should be rendered into something like the contents of
WinJab's browser window as shown in Figure 10-13.
</code> The function strip()
takes two arguments and strips away a base DN from a fully qualified DN to leave the significant, relative part. Calling strip() on this:
cn=Janet Abrams, ou=UK, ou=People, dc=demo, dc=org
when specifying the base DN in $basedn, would return this:
cn=Janet Abrams, ou=UK, ou=People
For each of the entries found, we insert a tag into the
<query/> extension. The name of the tag is either
item, if the entry is simply an LDAP hierarchy node, or
user (a valid jabber:iq:browse category), if the entry
points to a person. We make the decision on the basis of the first part
of the DN—if it's cn=, then it's a reference to a person. The
isUser() function makes this decision for us.
Once inserted, we embellish the <item/> or <user/> tag with name and jid attributes. The jid attribute is crucial, as it represents the path to further descend within the LDAP hierarchy on the next request. It is given the whole of the relative DN as a value. The name attribute is simply given the most significant portion of the DN as a value.
Whether we found something or not, we still want to return a result to the requester:
$node = toFrom($node); $node->attr('type', IQ_RESULT);
$c->send($node);
So we swap around the to and from attributes (remembering we're a component, and not a client) change the IQ element's type from get to result, and send it back.
We end the function by telling the dispatcher that the element has been handled:
return r_HANDLED;
}
Supporting functions
The rest of the script consists of minor
functions that play roles in assisting the core iq_browse().
The iq_notimpl() function is exactly the same as in the newsagent script in Section 8.3; it serves to catch stray IQ elements that in this case aren't IQ-gets containing a jabber:iq:browse query, and send them back with a "Not Implemented" error. With IQs, this is preferable to not responding at all, as responses are usually expected, even if those responses are IQ-errors. It's different with <message/> and <presence/> elements, as they can be seen as "one-way" and valid fodder for an element-sink.
sub iq_notimpl {
my $node = shift; $node = toFrom($node); $node->attr('type',
IQ_ERROR); my $error = $node->insertTag('error');
$error->attr('code', '501'); $error->data('Not Implemented');
$c->send($node); return r_HANDLED;
} The strip() function, as described already, removes a base DN from a fully qualified DN:
sub strip {
my ($fqdn, $basedn) = @_; my @fqdn = split(/,/, $fqdn); my
@basedn_elements = split(/,/, $basedn); return join(',', @fqdn[0 ..
($#fqdn - scalar @basedn_elements)]);
} As well as sharing the iq_notimpl() function with the newsagent script, ldapr also shares the toFrom() function, which flips around the values of the to and from attributes of an element:
sub toFrom { my $node = shift; my $to = $node->attr('to');
$node->attr('to', $node->attr('from')); $node->attr('from',
$to); return $node;
}
The isUser() function, also described earlier, makes an
arbitrary distinction between LDAP nodes that it thinks are
People and those that it thinks aren't:
sub isUser {
my $rdn = shift; return $rdn =~ /^cn/ ? 1 : 0
} Last but not least, we have a simple debug() function, which in this case is simply an abstraction of the classic "print to STDERR" method:
sub debug {
print STDERR "debug: ", @_, "\n";
}
Building an ERP Connection
To some extent, SAP's R/3, an
Enterprise Resource Planning (ERP) system, has until recently been a
monolithic piece of software, both from an actual and psychological
perspective.
The drive to replace disparate and incompatible business system "islands" with an integrated software solution that covered business processes across the board was strong in the 1980s and 1990s. With good reason. For example, SAP delivered the ability for companies to manage their different end-to-end processes coherently, without requiring custom-built interfaces between logistics and financial accounting packages. The flow of information was automatic between the application subsystems that were standard within SAP's R/2 and R/3 products.
The upside to this was, of course, the seamless integration of data and functions within the SAP universe, a universe vast enough to offer application coverage for pretty much every conceivable business function.
The downside was, ironically, the universe itself. All-encompassing as SAP's products were, and indeed they were forever changing and expanding to meet business demands, it was never enough, if you wanted to break out of the mould formed by the omnipresent standard proprietary client called SAPGUI. Sure, these days, with a huge installed base of R/2 and R/3 systems in production around the world, there are a multitude of ways to get data in and out of SAP systems, but despite the varied successes of alternative client initiatives, user interaction with the business processes remains largely orientated around the SAPGUI.
This recipe is extremely simple compared to the others in this chapter. It breaks out of the SAPGUI mold and the monolithic software culture to use open source tools and technologies to add value to our SAP business processes. The point of this recipe is not particularly what the script looks like or how it's written, but what it does and how it does it.
Building an Order Approval Notification Mechanism
We're going to
use a standard Jabber client as an SAP R/3 client—obviously not to
replace SAPGUI, rather to allow someone who perhaps has a single
R/3-related task to perform and connects only to SAP occasionally. In
stark contrast to the SAPGUI client, an off-the-shelf Jabber client is
much smaller. It takes up less screen space, memory, and CPU and
generally for focused access is a great way for someone to play his part
in business processes from the comfort of familiar communication
surroundings—his IM client.
Of course, the available Jabber clients don't have any built-in R/3 functionality per se, but as clients that can receive messages and recognize URLs,[3] they provide enough horsepower for us to achieve our goal.
That goal is to notify a supervisor whenever a sales order is placed that requires his approval. The notification will arrive in the form of a <message/> element, carrying some descriptive text and, crucially, a URL, which points to an Apache-based handler. When invoked, the handler pulls the relevant information for the order out of R/3, requests verification of the viewer's identity, and offers a chance to approve the order with the click of a button.
Effectively we're building a miniworkflow scenario: in one direction a notification is transmitted out of the bounds of the SAP universe to the approver, and in the other direction the notification process is turned around in a one-step approval cycle via Apache. Figure 10-14 shows this scenario and where our Jabber client and script, called approv, fits in.
</code> The goal here is to get the
notification message out of R/3 and send it to the supervisor's JID along with a URL that he can follow back into the R/3 system to carry out the approval process. The return process via Apache is outside this recipe's scope and has been left as an exercise for you.
Getting the notification out of R/3
As we've already
mentioned, there are many ways of getting data in and out of SAP. We're
going to use a generic, lowest common denominator feature of the R/3
Basis system to invoke a script and pass it parameters.
{{Note|At this stage, if you're squeamish about R/3 Basis or SAP's ABAP
language or are of another ERP persuasion, it's time to look away. What
we're going to do here is not rocket science, nor is the general process
specific to R/3. We're just going to call a script, at the operating
system level, from within an application inside R/3.
</code> The function group SXPT encompasses a number of function modules related to the definition, management, and execution of operating system commands. Each of these commands is described within sets of configuration parameters that define how and where they can be invoked. Using the program RSLOGCOM, you can create definitions for these operating system commands manually.
We need to define such a command that refers to the approv script. Figure 10-15 shows the RSLOGCOM definition of approv as an external command that is called ZNOTIFY.
</code> Once approv has been defined
this way, it can be invoked by passing parameters with a call to a function module in the SXPT function group (SXPG_EXECUTE_COMMAND). Example 10-19 shows how the script might be invoked using this function module in ABAP. Code like this could typically be installed in a customer exit—a place in a standard R/3 application where custom processing can be added without having to go through the involved process of creating and carrying out a modification request.
Calling approv, via ZNOTIFY, from within R/3
data: sxpg_exec_protocol like btcxpm occurs 0 with header line,
sxpg_add_parms like SXPGCOLIST-PARAMETERS,
sxpg_target_system like RFCDISPLAY-RFCHOST value 'gnu.mine.nu'.
concatenate ordernumber approver into sxpg_add_parms separated by space.
call function 'SXPG_COMMAND_EXECUTE' exporting commandname = 'ZNOTIFY' additional_parameters = sxpg_add_parms operatingsystem = sy-opsys targetsystem = sxpg_target_system stdout
= 'X' stderr = 'X' terminationwait = 'X' table
exec_protocol = sxpg_exec_protocol exception others
= 1.
if sy-subrc <> 0. raise notification_failed. endif. We pass two parameters to approv via the SXPG_COMMAND_EXECUTE: the number of the order that needs approving (from ordernumber) and the JID of the approver (from approver). The SXPG mechanism invokes the approv script with the two parameters, and the notification starts its journey.
The approv Script
As mentioned already, the approv script
(shown in Example 10-20) is rather small and insignificant. Its purpose
is to take the order number it receives, wrap it in a descriptive
message that includes a URL, and send it to the approver's JID. Written
in Perl, the approv script uses a minimum set of features from the
Jabber::Connection library.
The approv script, written in Perl
use strict; use Jabber::Connection;
my ($order, $approver) = @ARGV; my $c = new Jabber::Connection(server => 'qmacro.dyndns.org'); die "Cannot connect: ".$c->lastError unless $c->connect(); $c->auth('approv','secret','approv');
$c->send(<<EO_MSG); <message to="$jid"> <subject>Order Approval Required</subject> <body> An order ($order) requiring your approval has been placed. Please visit http://qmacro.dyndns.org/approv?$order to approve.
Thank you. </body> </message> EO_MSG
$c->disconnect; The script receives the two parameters passed from R/3 into the $order and $approver variables. Having connected to the Jabber server at qmacro.dyndns.org and authenticated as the user approv, it sends off a formatted message to the approver, before disconnecting.
The script itself is extremely simple and is not of primary importance. What is crucial here is the role that Jabber is playing. The <message/> element of Jabber's protocol is used to span the R/3 world with the IM world, enabling a business process cycle to take place outside the normal boundaries of R/3 interaction. On a simple level, that's all it takes to merge the ERP business process world with the world of IM.
The return path to R/3, via the Apache-based CGI application, is initiated when the user clicks on the URL in the message, as shown in Figure 10-16.
</code> Here we see that Jarl has
recognized the http:// address and has rendered it as an active link. That's all it has to do. There's no attempt on Jarl's part to make an HTTP request to the Apache server and render the HTML received. Focusing on "the right tools for the right job," we should leave that role to a web browser, as indeed Jarl does, starting up the browser of our choice.[4]
Taking This Further
The journey through the recipes in the last
three chapters of the book has taken us from the simplest CVS
notification mechanism with the cvsmsg script (Section 8.1) through
a fairly involved RSS component (Section 9.3) to the transporting of
XML-RPC-encoded requests and responses with our JabberRPCRequester
and JabberRPCResponder scripts (Section 10.3).
This chapter, and indeed the book, ends on a simple note, in the form of the approv script. Not without reason, we've completed the circle of script and application complexity. While we can build very useful and successful applications that are naturally complex, a Jabber-powered solution doesn't necessarily have to be. To employ the Jabber philosophy, the technology, and the protocol elements to build bridges between previously separate systems, and to span different areas of technology, open and proprietary alike, using Jabber's open, extensible, and flexible protocol, is what it's all about.
Finally, it's hopefully clear from the diversity of recipes shown in this part of the book that deploying solutions with Jabber, as noted in the preface, really is fun!
