Discoing with Vorpal

One of the really nice XMPP feature is service discovery viz. the ability for the client to discover the capabilities and services (eg. multiuser chat, pubsub, file transfer, etc.) offered by the server. The services discovery mechanism is described in great detail in XEP-0030. As far as specification goes, XEP-0300 is surprisingly a well written and quite easy to read.

In a nutshell, XEP-0030 works using 2 XML stanza

  • disco#info – tell me about yourself. This includes the server’s identity and the features that it supports
  • disco#items – give me a list of all your services or Jabber entities

I’ll show how service discovery works by using my customer query example (cross selling!). In the example, a client can perform customer information query by sending a <message> with the customer’s id in the <body> of the message to query@customer.batcomputer. What service discovery does is allows it allows query@customer.batcomputer to be discovered via a series of interactions.

But first the setup

  • We will use the the following URI uri:customer_query to denote the customer query service. So if a Jabber entity has this URI associated with it, then that entity supports our customer query service
  • The entity query@customer.batcomputer must be discoverable; the entity must also have uri:customer_query associated with it
  • The customer query service, an external component (XEP-0114), is deployed in the following subdomain: customer.batcomputer. So batcomputer is our XMPP server, customer.batcomputer is a subdomain of the server and query@customer.batcomputer is a JID entity within the customer.batcomputer subdomain.

Looking for Customer Service

The process is very simple; the client will look for a Jabber entity with uri:customer_query associated with it. The client starts by first sending a disco#info to batcomputer

<iq type=’get’ to=’batcomputer’ from=’client@batcomputer/pidgin’…>
   <query xmlns=’http://jabber.org/protocol/disco#info’/&gt;
</iq>

batcomputer then replies with a little info about itself and a list of services that it supports

<iq type=’result’ to=’client@batcomputer/pidgin’ from=’batcomputer’ …>
   <query xmlns=’http://jabber.org/protocol/disco#info’&gt;
      <identity category=’server’ …/>
      <feature var=’vcard-temp’/>
      <feature var=’http://jabber.org/protocol/disco#items’/&gt;
         …
   </query>
</iq>

The client then looks through the <feature> list to see if the server supports uri:customer_query. It doesn’t; it will now look to see if disco#items is supported. A disco#items indicates that  batcomputer has more jabber entities, so the client now sends a disco#items over to retrieve the list of entities

<iq type=’get’ to=’batcomputer’ from=‘client@batcomputer/pidgin’ …>
   <query xmlns=’http://jabber.org/protocol/disco#items’/&gt;
</iq>

batcomputer now returns a list of entities including customer.batcompter. For customer.batcomputer entity to be listed in the response, the customer service external component must be ‘up and running’.

<iq type=’result’ to=’client@batcomputer/pidgin’ from=’batcomputer’ …>
   <query xmlns=’http://jabber.org/protocol/disco#items’&gt;
      <item jid=’pubsub.batcomputer’ …/>
      <item jid=’conference.batcomputer’ …/>
      <item jid=’customer.batcomputer’ …/>
         …
   </query>
</iq>

The client repeats the above process for each <item>. If an entity does not support disco#items, it means that that entity is a ‘leaf’ node; no further introspection is possible. So when the client sends a disco#info to customer.batcomputer the following stanza is returned

<iq type=’result’ to=’client@batcomputer/pidgin’ from=’customer.batcomputer‘ …>
   <query xmlns=’http://jabber.org/protocol/disco#info’&gt;
      <identity category=’component’ …/>
      <feature var=’http://jabber.org/protocol/disco#items’/&gt;
         …
   </query>
</iq>

On not finding uri:customer_query, the client now sends a disco#items to customer.batcomputer; our customer service component now returns the following list of entities

<iq type=’result’ to=’client@batcomputer/pidgin’ from=’customer.batcomputer‘…>
   <query xmlns=’http://jabber.org/protocol/disco#items’&gt;
      <item jid=’query@customer.batcomputer’ …/>
   </query>
</iq>

A disco#info now goes out to query@customer.batcomputer. Here is the reply

<iq type=’result’ to=’client@batcomputer/pidgin’ from=’query@customer.batcomputer‘ …>
   <query xmlns=’http://jabber.org/protocol/disco#info’&gt;
      <identity category=’client’ type=’bot’/>
      <feature var=’uri:customer_query’/>
   </query>
</iq>

We have now found the entity that supports customer query!

Handling disco#info and disco#items with Vorpal

Now that we seen how service discovery works, lets implement this with Vorpal. The following table shows the request and response to and from our customer service

Request – client to customer.batcomputer Response – customer.batcomputer to client
1) Sends disco#info
Indicate support for disco#items in response
2) Sends disco#items
Returns query@customer.batcomputer entity
3) Sends disco#info to query@customer.batcomputer
Response with identity and support for uri:customer_query

We will show how to capture the above set of interactions with Vorpal.

@RequestScoped
@IQ @Query
public class CustomerServiceDiscovery {
   @Query(“http://jabber.org/protocol/disco#info&#8221;)
   public Object discoInfo() {
      return (http://jabber.org/protocol/disco#items&#8221;);
   }
}

The above class CustomerServiceDiscovery, shows the first pair of request/response.

  • Note that @IQ is a top level annotation that marks this class as an IQ packet handler (the only other top level annotation for message handler is @Message). This is immediately followed by @Query annotation. This annotation assures us that this handler will only process IQ packets with <query>. Other type of IQ packets without <query> as a child will be ignored.
  • The @Query annotation on discoInfo() method has the disco#info namespace. What this means is that for the discoInfo() method to fire, the <query> element must have the namespace that matches http://jabber.org/protocol/disco#info. <query> elements with http://jabber.org/protocol/disco#items will not match this annotation.
  • When discoInfo() method is invoked in response to a disco#info query, it will return the disco#items namespace to indicate that it has Jabber entities. Besides returning a String, you can also return a URI, URL or a com.kenai.jabberwocky.iq.FeatureSpecification instance.

Now lets look at how we implement the second set of interaction were the client sends a disco#items.

@RequestScoped
@IQ @Query
public class CustomerServiceDiscovery {
   @Inject @Named(“__domain__”) private String domain;

   @Query(“http://jabber.org/protocol/disco#info&#8221;)
   public Object discoInfo() {
      return (“http://jabber.org/protocol/disco#items&#8221;);
   }

   @Query(“http://jabber.org/protocol/disco#items&#8221;)
   public Object discoItems() {
      return (new ItemSpecification(“query@” + domain
            , “Customer Query Service”)
);
   }
}

  • The discoItems() method is used to handle disco#items request. Again we use a qualified @Query annotation to filter out other types of query packets
  • To return the correct subdomain, we inject the subdomain that this external component is deployed to to domain member. The __domain__ is a predefined name in Vorpal.
  • Finally in discoItem() we return an instance of com.kenai.jabberwocky.iq.ItemSpecification. The <item> element has an option for a user friendly string and also entry for node (see schema for disco#item here). In this example, we are setting the Jabber entity (the JID) and also a user friendly string. You can also just return a JID or a Stringify JID if you wish to be brief. If you have more than one entity, you can return a collection eg Collection<String>, List<JID>, Set<ItemSpecification>

The third set of interaction is a little different; since we have returned query@customer.batcomputer as an entity in the previous interaction, a disco#info will now be directed to is JID. The following code shows how to handle this.

@RequestScoped
@IQ @Query
public class CustomerServiceDiscovery {
   @Inject @Named(“__domain__”) private String domain;
   @Query(“http://jabber.org/protocol/disco#info&#8221;)
   public Object discoInfo() {
      return (“http://jabber.org/protocol/disco#items&#8221;);
   }
   @Query(“http://jabber.org/protocol/disco#items&#8221;)
   public Object discoItems() {
      return (new ItemSpecification(“query@” + domain
            , “Customer Query Service”)); 
    }

   @Query(“http://jabber.org/protocol/disco#info&#8221;)
   @To(“query@{__domain__}”)
   public Collection<Object> queryDiscoInfo() {
      List<Object> result = new LinkedList<Object>();
      result.add(new IdentitySpecification(“client”, “bot”));
      result.add(“uri:customer_query”);
      return (result);
   }
}

Salient point for this final set of interactions

  • We now add an additional annotation @To to indicate that queryDiscoInfo() will only fire if it is a disco#info for query@customer.batcomputer. The previous disco#info was to customer.batcomputer. See this for method resolution.
  • Note that the return type is a collection because we will be returning the identity of the Jabber entity (required by the spec) and also a list of features that the entity will support. In our case it’ll only be uri:customer_query.
  • The service chose to identify itself as a bot. See Service Discovery Identity for a list of predefined type and categories.

The latest build of Vorpal, Jabberwocky container and Vorpal NetBeans plugin can be found here.

Feedback appreciated.