Smack is a FLOSS XMPP client library for Java and Android app development. It takes away much of the burden a developer of a chat application would normally have to carry, so the developer can spend more time working on nice stuff like features instead of having to deal with the protocol stack.
Many (80+ and counting) XMPP Extension Protocols (XEPs) are already implemented in Smack. Today I want to bring you along with me and add support for one more.
What Smack does very well is to follow the Open-Closed-Principle of software architecture. That means while Smacks classes are closed for modification by the developer, it is pretty easy to extend Smack to add support for custom features. If Smack doesn’t fit your needs, don’t change it, extend it!
The most important class in Smack is probably the XMPPConnection
, as this is where messages coming from and going to. However, even more important for the developer is what is being sent.
XMPP’s strength comes from the fact that arbitrary XML elements can be exchanged by clients and servers. Heck, the server doesn’t even have to understand what two clients are sending each other. That means that if you need to send some form of data from one device to another, you can simply use XMPP as the transport protocol, serialize your data as XML elements with a namespace that you control and send if off! It doesn’t matter, which XMPP server software you choose, as the server more or less just forwards the data from the sender to the receiver. Awesome!
So lets see how we can extend Smack to add support for a new feature without changing (and therefore potentially breaking) any existing code!
For this article, I chose XEP-0428: Fallback Indication as an example protocol extension. The goal of Fallback Indication is to explicitly mark <body/> elements in messages as fallback. For example some end-to-end encryption mechanisms might still add a body with an explanation that the message is encrypted, so that older clients that cannot decrypt the message due to lack of support still display the explanation text instead. This enables the user to switch to a better client 😛 Another example would be an emoji in the body as fallback for a reaction.
XEP-0428 does this by adding a fallback element to the message:
<message from="alice@example.org" to="bob@example.net" type="chat"> <fallback xmlns="urn:xmpp:fallback:0"/> <-- THIS HERE <encrypted xmlns="urn:example:crypto">Rgreavgl vf abg n irel ybat gvzr nccneragyl.</encrypted> <body>This message is encrypted.</body> </message>
If a client or server encounter such an element, they can be certain that the body of the message is intended to be a fallback for legacy clients and act accordingly. So how to get this feature into Smack?
After the XMPPConnection
, the most important types of classes in Smack are the ExtensionElement
interface and the ExtensionElementProvider
class. The later defines a class responsible for deserializing or parsing incoming XML into the an object of the former class.
The ExtensionElement
is itself an empty interface in that it does not provide anything new, but it is composed from a hierarchy of other interfaces from which it inherits some methods. One notable super class is NamedElement
, more on that in just a second. If we start our XEP-0428 implementation by creating a class that implements ExtensionElement
, our IDE would create this class body for us:
package tk.jabberhead.blog.wow.nice; import org.jivesoftware.smack.packet.ExtensionElement; import org.jivesoftware.smack.packet.XmlEnvironment; public class FallbackIndicationElement implements ExtensionElement { @Override public String getNamespace() { return null; } @Override public String getElementName() { return null; } @Override public CharSequence toXML(XmlEnvironment xmlEnvironment) { return null; } }
The first thing we should do is to change the return type of the toXML()
method to XmlStringBuilder
, as that is more performant and gains us a nice API to work with. We could also leave it as is, but it is generally recommended to return an XmlStringBuilder
instead of a boring old CharSequence
.
Secondly we should take a look at the XEP to identify what to return in getNamespace()
and getElementName()
.
<fallback xmlns="urn:xmpp:fallback:0"/> [ ^ ] [ ^ ] element name namespace
In XML, the part right after the opening bracket is the element name. The namespace follows as the value of the xmlns
attribute. An element that has both an element name and a namespace is called fully qualified. That’s why ExtensionElement
is inheriting from FullyQualifiedElement
. In contrast, a NamedElement
does only have an element name, but no explicit namespace. In good object oriented manner, Smacks ExtensionElement
inherits from FullyQualifiedElement
which in term is inheriting from NamedElement
but also introduces the getNamespace()
method.
So lets turn our new knowledge into code!
package tk.jabberhead.blog.wow.nice; import org.jivesoftware.smack.packet.ExtensionElement; import org.jivesoftware.smack.packet.XmlEnvironment; public class FallbackIndicationElement implements ExtensionElement { @Override public String getNamespace() { return "urn:xmpp:fallback:0"; } @Override public String getElementName() { return "fallback"; } @Override public XmlStringBuilder toXML(XmlEnvironment xmlEnvironment) { return null; } }
Hm, now what about this toXML()
method? At this point it makes sense to follow good old test driven development practices and create a JUnit test case that verifies the correct serialization of our element.
package tk.jabberhead.blog.wow.nice; import static org.jivesoftware.smack.test.util.XmlUnitUtils.assertXmlSimilar; import org.jivesoftware.smackx.pubsub.FallbackIndicationElement; import org.junit.jupiter.api.Test; public class FallbackIndicationElementTest { @Test public void serializationTest() { FallbackIndicationElement element = new FallbackIndicationElement(); assertXmlSimilar("<fallback xmlns=\"urn:xmpp:fallback:0\"/>", element.toXML()); } }
Now we can tweak our code until the output of toXml()
is just right and we can be sure that if at some point someone starts messing with the code the test will inform us of any breakage. So what now?
Well, we said it is better to use XmlStringBuilder
instead of CharSequence
, so lets create an instance. Oh! XmlStringBuilder
can take an ExtensionElement
as constructor argument! Lets do it! What happens if we return new XmlStringBuilder(this);
and run the test case?
<fallback xmlns="urn:xmpp:fallback:0"
Almost! The test fails, but the builder already constructed most of the element for us. It prints an opening bracket, followed by the element name and adds an xmlns
attribute with our namespace as value. This is typically the “head” of any XML element. What it forgot is to close the element. Lets see… Oh, there’s a closeElement()
method that again takes our element as its argument. Lets try it out!
<fallback xmlns="urn:xmpp:fallback:0"</fallback>
Hm, this doesn’t look right either. Its not even valid XML! (ノಠ益ಠ)ノ彡┻━┻ Normally you’d use such a sequence to close an element which contained some child elements, but this one is an empty element. Oh, there it is! closeEmptyElement()
. Perfect!
<fallback xmlns="urn:xmpp:fallback:0"/>
package tk.jabberhead.blog.wow.nice; import org.jivesoftware.smack.packet.ExtensionElement; import org.jivesoftware.smack.packet.XmlEnvironment; public class FallbackIndicationElement implements ExtensionElement { @Override public String getNamespace() { return "urn:xmpp:fallback:0"; } @Override public String getElementName() { return "fallback"; } @Override public XmlStringBuilder toXML(XmlEnvironment xmlEnvironment) { return new XmlStringBuilder(this).closeEmptyElement(); } }
We can now serialize our ExtensionElement
into valid XML! At this point we could start sending around FallbackIndications to all our friends and family by adding it to a message object and sending that off using the XMPPConnection
. But what is sending without receiving? For this we need to create an implementation of the ExtensionElementProvider
custom to our FallbackIndicationElement
. So lets start.
package tk.jabberhead.blog.wow.nice; import org.jivesoftware.smack.packet.XmlEnvironment; import org.jivesoftware.smack.provider.ExtensionElementProvider; import org.jivesoftware.smack.xml.XmlPullParser; public class FallbackIndicationElementProvider extends ExtensionElementProvider<FallbackIndicationElement> { @Override public FallbackIndicationElement parse(XmlPullParser parser, int initialDepth, XmlEnvironment xmlEnvironment) { return null; } }
Normally implementing the deserialization part in form of a ExtensionElementProvider
is tiring enough for me to always do that last, but luckily this is not the case with Fallback Indications. Every FallbackIndicationElement
always looks the same. There are no special attributes or – shudder – nested named child elements that need special treating.
Our implementation of the FallbackIndicationElementProvider
looks simply like this:
package tk.jabberhead.blog.wow.nice; import org.jivesoftware.smack.packet.XmlEnvironment; import org.jivesoftware.smack.provider.ExtensionElementProvider; import org.jivesoftware.smack.xml.XmlPullParser; public class FallbackIndicationElementProvider extends ExtensionElementProvider<FallbackIndicationElement> { @Override public FallbackIndicationElement parse(XmlPullParser parser, int initialDepth, XmlEnvironment xmlEnvironment) { return new FallbackIndicationElement(); } }
Very nice! Lets finish the element part by creating a test that makes sure that our provider does as it should by creating another JUnit test. Obviously we have done that before writing any code, right? We can simply put this test method into the same test class as the serialization test.
@Test public void deserializationTest() throws XmlPullParserException, IOException, SmackParsingException { String xml = "<fallback xmlns=\"urn:xmpp:fallback:0\"/>"; FallbackIndicationElementProvider provider = new FallbackIndicationElementProvider(); XmlPullParser parser = TestUtils.getParser(xml); FallbackIndicationElement element = provider.parse(parser); assertEquals(new FallbackIndicationElement(), element); }
Boom! Working, tested code!
But how does Smack learn about our shiny new FallbackIndicationElementProvider
? Internally Smack uses a Manager class to keep track of registered ExtensionElementProviders to choose from when processing incoming XML. Spoiler alert: Smack uses Manager classes for everything!
If we have no way of modifying Smacks code base, we have to manually register our provider by calling
ProviderManager.addExtensionProvider("fallback", "urn:xmpp:fallback:0", new FallbackIndicationElementProvider());
Element providers that are part of Smacks codebase however are registered using an providers.xml file instead, but the concept stays the same.
Now when receiving a stanza containing a fallback indication, Smack will parse said element into an object that we can acquire from the message object by calling
FallbackIndicationElement element = message.getExtension("fallback", "urn:xmpp:fallback:0");
You should have noticed by now, that the element name and namespace are used and referred to in a number some places, so it makes sense to replace all the occurrences with references to a constant. We will put these into the FallbackIndicationElement
where it is easy to find. Additionally we should provide a handy method to extract fallback indication elements from messages.
... public class FallbackIndicationElement implements ExtensionElement { public static final String NAMESPACE = "urn:xmpp:fallback:0"; public static final String ELEMENT = "fallback"; @Override public String getNamespace() { return NAMESPACE; } @Override public String getElementName() { return ELEMENT; } ... public static FallbackIndicationElement fromMessage(Message message) { return message.getExtension(ELEMENT, NAMESPACE); } }
Did I say Smack uses Managers for everything? Where is the FallbackIndicationManager then? Well, lets create it!
package tk.jabberhead.blog.wow.nice; import java.util.Map; import java.util.WeakHashMap; import org.jivesoftware.smack.Manager; import org.jivesoftware.smack.XMPPConnection; public class FallbackIndicationManager extends Manager { private static final Map<XMPPConnection, FallbackIndicationManager> INSTANCES = new WeakHashMap<>(); public static synchronized FallbackIndicationManager getInstanceFor(XMPPConnection connection) { FallbackIndicationManager manager = INSTANCES.get(connection); if (manager == null) { manager = new FallbackIndicationManager(connection); INSTANCES.put(connection, manager); } return manager; } private FallbackIndicationManager(XMPPConnection connection) { super(connection); } }
Woah, what happened here? Let me explain.
Smack uses Managers to provide the user (the developer of an application) with an easy access to functionality that the user expects. In order to use some feature, the first thing the user does it to acquire an instance of the respective Manager
class for their XMPPConnection
. The returned instance is unique for the provided connection, meaning a different connection would get a different instance of the manager class, but the same connection will get the same instance anytime getInstanceFor(connection)
is called.
Now what does the user expect from the API we are designing? Probably being able to send fallback indications and being notified whenever we receive one. Lets do sending first!
... private FallbackIndicationManager(XMPPConnection connection) { super(connection); } public MessageBuilder addFallbackIndicationToMessage( MessageBuilder message, String fallbackBody) { return message.setBody(fallbackBody) .addExtension(new FallbackIndicationElement()); }
Easy!
Now, in order to listen for incoming fallback indications, we have to somehow tell Smack to notify us whenever a FallbackIndicationElement
comes in. Luckily there is a rather nice way of doing this.
... private FallbackIndicationManager(XMPPConnection connection) { super(connection); registerStanzaListener(); } private void registerStanzaListener() { StanzaFilter filter = new AndFilter(StanzaTypeFilter.MESSAGE, new StanzaExtensionFilter(FallbackIndicationElement.ELEMENT, FallbackIndicationElement.NAMESPACE)); connection().addAsyncStanzaListener(stanzaListener, filter); } private final StanzaListener stanzaListener = new StanzaListener() { @Override public void processStanza(Stanza packet) throws SmackException.NotConnectedException, InterruptedException, SmackException.NotLoggedInException { Message message = (Message) packet; FallbackIndicationElement fallbackIndicator = FallbackIndicationElement.fromMessage(message); String fallbackBody = message.getBody(); onFallbackIndicationReceived(message, fallbackIndicator, fallbackBody); } }; private void onFallbackIndicationReceived(Message message, FallbackIndicationElement fallbackIndicator, String fallbackBody) { // do something, eg. notify registered listeners etc. }
Now that’s nearly it. One last, very important thing is left to do. XMPP is known for its extensibility (for the better or the worst). If your client supports some feature, it is a good idea to announce this somehow, so that the other end knows about it. That way features can be negotiated so that the sender doesn’t try to use some feature that the other client doesn’t support.
Features are announced by using XEP-0115: Entity Capabilities, which is based on XEP-0030: Service Discovery. Smack supports this using the ServiceDiscoveryManager
. We can announce support for Fallback Indications by letting our manager call
ServiceDiscoveryManager.getInstanceFor(connection) .addFeature(FallbackIndicationElement.NAMESPACE);
somewhere, for example in its constructor. Now the world knows that we know what Fallback Indications are. We should however also provide our users with the possibility to check if their contacts support that feature as well! So lets add a method for that to our manager!
public boolean userSupportsFallbackIndications(EntityBareJid jid) throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException { return ServiceDiscoveryManager.getInstanceFor(connection()) .supportsFeature(jid, FallbackIndicationElement.NAMESPACE); }
Done!
I hope this little article brought you some insights into the XMPP protocol and especially into the development process of protocol libraries such as Smack, even though the demonstrated feature was not very spectacular.
Quick reminder that the next Google Summer of Code is coming soon and the XMPP Standards Foundation got accepted 😉
Check out the project ideas page!
Happy Hacking!
One response to “How to Implement a XEP for Smack.”
[…] How to Implement a XEP for Smack. […]