Configuring request-reply pattern with Apache Camel and XMPP

Setting up XMPP Server and Client

Setup OpenFire XMPP Server

  • Download OpenFire (version 3.7.1 as of writing)
  • unzip, e. g. C:\java\openfire
  • run c:\java\openfire\bin\openfired
    ...
    Openfire 3.7.1 [Feb 14, 2012 12:04:15 PM]
    Admin console listening at http://127.0.0.1:9090
  • Visit admin console and walk through the install steps.
  • Setup ports, make note of the server name, use the embedded database, and use default mode for user and server storage
  • Make note of the server ports for incoming client connection (default: 5222)
  • When setup finishes, add two new users: bob and camel via User/Groups tab. First user will be used to communicate via instant messenger with the camel user listening in the Camel context.

Jabber/XMPP client

  • Use your Miranda or download Pandion client for Windows.
  • Login as bob@[server name]. Note that you cannot login as bob@localhost from Pandion, you’ll have to use the proper server name. If your server name (see Server manager tab, Server name field in OpenFire Admin Console) is “bigdog”, use bob@bigdog as a login name.
  • Add the camel user as to your contact list.

Setting up Camel context

Let’s assume that you’ve already got your Camel project set-up and running.

Add camel-xmpp module to your dependencies, since it is necessary to allow the XMPP support.

<dependency>
    <groupId>org.apache.camel</groupId>
    <artifactId>camel-xmpp</artifactId>
    <version>2.9.0</version>
</dependency>       

I will assume that you’ve already put the camel-stream plug-in into your dependencies — it is a necessity when you want to route messages to System.out.

A trivial example: receiving messages and routing to stdout

Routing XMPP messages to stdout is simple. Just create a simple runner class

public static void main(String[] args) throws Exception {
    Main camelMain = new Main();
    camelMain.enableHangupSupport();
    camelMain.addRouteBuilder(new RouteBuilder() {
        @Override
        public void configure() throws Exception {
            from("xmpp://camel@bigdog?password=c4m3l")
            .to("stream:out");
        }
    });
    camelMain.run();
}

We route messages from camel user account in the XMPP server with specified password. All messages will be routed to System.out.

Run your Camel context, open up Pandion conversation window and just send messages to camel user. Camel will react with flurry of DEBUG logging messages, but do not despair: your message text will appear on the console as well.

14:56:38.082 [Smack Listener Processor (0)] DEBUG o.a.camel.component.xmpp.XmppLogger - INBOUND : <message xml:lang="en" id="sd16" to="camel@bigdog/Camel" from="bob@bigdog/Pandion" type="chat"><body>hello</body><html xmlns="http://jabber.org/protocol/xhtml-im"><body xmlns="http://www.w3.org/1999/xhtml" style="font-style: normal; font-family: arial; color: #004200; font-size: 9pt; font-weight: normal; text-decoration: ">hello</body></html><x xmlns="jisp:x:jep-0038"><name>shinyicons</name></x><active xmlns="http://jabber.org/protocol/chatstates" /></message>
14:56:38.082 [Smack Listener Processor (0)] DEBUG o.a.c.component.xmpp.XmppConsumer - Received XMPP message for camel from camel : hello
14:56:38.083 [Smack Listener Processor (0)] DEBUG o.a.camel.processor.SendProcessor - >>>> Endpoint[stream://out] Exchange[XmppMessage: org.jivesoftware.smack.packet.Message@25cdf58f]
14:56:38.083 [Smack Listener Processor (0)] DEBUG o.a.c.c.stream.StreamProducer - Writing as text: hello to java.io.PrintStream@26c1186f using encoding: UTF-8
hello

Advanced example: fortune cookie chat

Let’s try a more advanced example: a fortune cookie generator powered by Camel via XMPP. This example shows how to configure request-reply pattern via XMPP. This means that you can message Camel via XMPP client and Camel will send back replies, practically establishing a simple chat.

But beware, it is not easy.

Let us assume that we have a simple fortune cookie generator POJO bean.

public class FortuneCookieGenerator {
    private List<String> cookies = Arrays.asList(
            "Camel is as camel does.", 
            "Again I tell you, it is easier for a camel to go through the eye of a needle than for a rich man to enter the kingdom of heaven.",
            "Do not free the camel of the burden of his hump; you may be freeing him from being a camel."
            );

    private static Random random = new Random();

    public String generateFortuneCookie() {
        int i = random.nextInt(cookies.size());
        return cookies.get(i);
    }
}

Each message to Camel will trigger a random fortune cookie which will be used as a response for client XMPP request.

You may be tempted to write a simple version. We need to create two endpoints: first will pose as a receiver, where we provide an user authenticated in the XMPP server with password. The second endpoint will specify a participant, which represents the XMPP user that receives Camel responses. As of now, this is hardwired to novotnyr@rn-pc.

Unfortunately, this won’t work.

    Main camelMain = new Main();
    camelMain.enableHangupSupport();
    camelMain.enableTrace();

    camelMain.addRouteBuilder(new RouteBuilder() {
        @Override
        public void configure() throws Exception {
            from("xmpp://camel@rn-pc?password=c4m3l")               
            .bean(FortuneCookieGenerator.class, "generateFortuneCookie")                
            .to("xmpp://camel@rn-pc/novotnyr@rn-pc?password=c4m3l");
        }
    });
    camelMain.run();

Running this example will fail with stream:error (conflict) exception from Smack, the underlying XMPP client implementation

stream:error (conflict)
    at org.jivesoftware.smack.PacketReader.parsePackets(PacketReader.java:260)
    at org.jivesoftware.smack.PacketReader.access$000(PacketReader.java:43)
    at org.jivesoftware.smack.PacketReader$1.run(PacketReader.java:70)

This is due to the strange (or specified?) behaviour of Camel: it creates two separate XMPP endpoints with two separate XMPP connections. Essentially, this means that it will try to connect to XMPP server twice with the same credentials (once for each endpoint), but this is not allowed.

On the other hand, you are allowed to connect with the same login and password, but you need to specify separate XMPP resources.

But I do not recommend it: some XMPP clients (e. g. Pandion) will get confused and display your Camel endpoints as two separate users, where one of them will be restricted to message sending and the other will be limited to message reception.

Actually, the trouble lies with the nonshared XMPP connection object. If we could share the XMPPConnection instance between two endpoints, Camel would be able to get logged on the XMPP server just once, but with two endpoints.

This idea requires some class deriving. We will take the XmppEndpoint, subclass it, override the createConnection() method where we will return the same XmppConnection that was specified in the other endpoint.

public class SharedConnectionXmppEndpoint extends XmppEndpoint {
    private XMPPConnection xmppConnection;

    public SharedConnectionXmppEndpoint(String uri, XmppComponent component, XMPPConnection xmppConnection) {
        super(uri, component);
        this.xmppConnection = xmppConnection;       
    }

    @Override
    public XMPPConnection createConnection() throws XMPPException {
        if(xmppConnection != null) {
            return xmppConnection;
        }

        return super.createConnection();
    }
}

Now let’s proceed to the building of the route. Things won’t be as simple as in the trivial nonworking example, but there is nothing more that could be done about that.

At first, we will retrieve the first XMPP endpoint from the Camel context. This endpoint will be properly configured from URL to receive messages.

Then we will make note of the owning XmppComponent that created this endpoint, since we will share it with the second endpoint.

Second endpoint will be instantiated manually. We need to specify both URL and the actual XMPP endpoint properties, since they won’t be automatically parsed from the URL (this is done by the XmppComponent, which is of no use now — it creates XmppEndpoints, but we need SharedConnectionXmppEndpoints).

The rest is easy: messages will be routed from the first endpoint to the fortune cookie generator bean, thus calling the generateFortuneCookie method. The result of this method will be wrapped into a XMPP message and routed to the second endpoint, which will pass it to the XMPP instant messenger of the client.

The whole configuration looks as follows

camelMain.addRouteBuilder(new RouteBuilder() {
    @Override
    public void configure() throws Exception {
        XmppEndpoint endpoint = getContext().getEndpoint("xmpp://camel@rn-pc?password=c4m3l", XmppEndpoint.class);
        XmppComponent xmppComponent = (XmppComponent) endpoint.getComponent();

        SharedConnectionXmppEndpoint endpoint2 = new SharedConnectionXmppEndpoint("xmpp://camel@rn-pc/novotnyr@rn-pc?password=c4m3l", xmppComponent, endpoint.createConnection());
        endpoint2.setUser("camel");
        endpoint2.setPassword("c4m3l");
        endpoint2.setParticipant("novotnyr@rn-pc");
        endpoint2.setHost("rn-pc");

        from(endpoint)              
        .bean(FortuneCookieGenerator.class, "generateFortuneCookie")                
        .to(endpoint2);
    }
});

Now try to send some messages, for example via separate Java test class:

ConnectionConfiguration config = new ConnectionConfiguration("rn-pc", 5222);

XMPPConnection connection = new XMPPConnection(config);

connection.connect();
SASLAuthentication.supportSASLMechanism("PLAIN", 0);
connection.login("novotnyr", "a");

Chat chat = connection.getChatManager().createChat("camel@rn-pc", new MessageListener() {

    @Override
    public void processMessage(Chat chat, Message message) {
        System.out.println(message);
    }
});

chat.sendMessage(new Message("Camel, bring me your fortune cookie."));
connection.disconnect();

Note that neither actual message content nor message sender matter. A single fortune cookie will be generated and sent to the novotnyr@rn-pc user. However, it is not that difficult to modify the example to route the messages back to the actual sender.