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
andcamel
via User/Groups tab. First user will be used to communicate via instant messenger with thecamel
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 asbob@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”, usebob@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 XmppEndpoint
s, but we need SharedConnectionXmppEndpoint
s).
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.