10.2. Refactoring the ModelIt's time to refactor Hyperbola to use Smack rather than the prototype model. Doing this requires quite a number of minor but pervasive changes to accommodate new class and method names. Rather than detailing each of these changes, we take this opportunity to step back and show you how to use more Eclipse tooling, for example, code completion, organized imports, and refactoring. We also take you on a tour of the final code to highlight some of the transformation. With the information provided here, it is quite feasible for you to do the refactoring yourself. Of course, if you would rather skip to the final code, you can compare against the sample code for this chapter. 10.2.1. Introduction to SmackNow that you have a Smack plug-in, let's look at the Smack APIs. Smack and XMPP are based on a few very simple concepts. The XMPP protocol is an IETF standard for instant messaging and presence awareness. The basic idea is that a client connects to a server. The server manages a list of contacts for each user and routes chat messages from one user to another. Messages are sent to and from the server in packets that are simple blocks of XML. There are only three basic message types: <message/>, <presence/>, and <iq/>. For example, the following XMPP message is sent by a client to indicate a presence change to "away" or that the user has been idle for some time. The details of the markup are not particularly interesting except to say that the protocol is extensible and packets can have subtypes. The XMPP specification process includes a number of Jabber[1] Enhancement Proposals (JEPs) that seek to add all manner of functionality such as file transfer.
<presence>
<c
node="http://exodus.jabberstudio.org/caps"
ver="0.9.0.0"
xmlns="http://jabber.org/protocol/caps"/>
<show>away</show>
<priority>1</priority>
</presence>Of course, no one really wants to write code to parse XML and manage streams of packets. Smack does all the message parsing and stream management for you. It also provides clients with APIs that hide the XMPP implementation details. Smack provides helpful classes that take care of listening to common packets and maintain models of many of the basic messaging concepts such as chats (Chat), the contacts list (Roster), individual contacts (RosterEntry), groups of contacts (RosterGroup), and connections to the server (XMPPConnection). It's pretty easy to do things with Smack without needing intimate knowledge of XMPPthat is why we like it! The typical workflow for a Smack client is to connect to the server and then send and receive messages while listening to packets sent and received using PacketListeners. The following code snippet illustrates how to use a PacketListener to look for incoming chat requests and tell the chat editor when one arrives. org.eclipsercp.hyperbola/ApplicationWorkbenchAdvisor
XMPPConnection connection = session.getConnection();
if (connection != null) {
PacketListener listener = new PacketListener() {
public void processPacket(Packet packet) {
Message message = (Message) packet;
if (message.getType() == Message.Type.CHAT)
startChat(message);
}
};
PacketFilter filter = new PacketTypeFilter(Message.class);
connection.addPacketListener(listener, filter);
}The packet listener is added to the connection for a particular server. It is triggered when a message arrives from that server. This is a standard pattern in Smack: register a listener and do something as a result of an incoming or outgoing message. Another common example is listening for presence and contact list changes. In XMPP, the server manages your contacts list. Changes to the list or the state of those in the list are sent to the client by the server. As such, you can use a PacketListener to track the state of the contact list and the status of your contacts. 10.2.2. Design ObjectivesWhen the Hyperbola prototype was created, the hope was that by decoupling the UI from the domain logic, we could ignore the details of messaging and eventually replace the prototype model with a real messaging library without major changes. Indeed, the Smack domain model is close to that of the original prototype. For example, in the prototype there are three main classes called Contacts, ContactsGroup, and ContactsEntry; in Smack, the equivalent classes are called Roster, RosterGroup, and RosterEntry. There are two approaches to integrating the Smack infrastructure into Hyperbola:
The proxy approach is attractive as it isolates the changes. Unfortunately, it creates duplicate classes and overhead for keeping the proxies synchronized with the Smack classes. It is an interesting approach if you have the requirements to support different XMPP librariesthat is not the case here. So instead, the Hyperbola model is replaced entirely by that of Smack. The Session class is retained as it is still useful and does not have a Smack equivalent. 10.2.3. Deleting Prototype ClassesTo start the refactoring, add a dependency from the Hyperbola plug-in to the newly created org.jivesoftware.smack plug-in. This is what you did with the smack.testing plug-in a little earlier via the plug-in editor's Dependencies page. Be sure to include the org.jivesoftware.smack plug-in in the Hyperbola product by adding it to the Plug-ins list on the Configuration page of the product editor. Now that the Smack classes are available in Hyperbola, it's time to change the code. The general strategy for the refactoring is to do all the following steps on each model class in turn:
Renaming and deleting are relatively easy. In the Package Explorer, right-click on the Contacts class and select Refactor > Rename... You are prompted for a new name and given the option to Update references. Ensure that this option is selected. You can also select some of the other update options, but they are not particularly needed here. Click OK and the tooling renames the class and updates all references. Use this to rename Contacts, ContactsEntry, and ContactGroup to Roster, RosterEntry, and RosterGroup, respectively. You need to also rename IContactsListener to RosterListener. The Chat and Presence classes have equivalent names in Smack, so you can leave them alone. Note you do not need to change the package name here. This step serves to update all the references to use the new short class names. Tip When "following the little red x's," it is convenient to use "Ctrl+." to cycle between lines with compilation errors in the Java editor. Typically this is much easier than scrolling the file manually. Next, delete all the renamed model classes except for Session. The Session class is still needed and is augmented with a ConnectionDetails class to track information about the user logged in. Then, start working through the compile errors. For example, open the AddContactAction class and use Source > Organize Imports to fix the import list. Alternatively, click on the project or package in the Package Explorer and select the same operation to organize imports in all classes in the project or package. Then look at the remaining errors and fix the method calls. In general, the changes are just updates to referenced method names. For example, the snippet below shows the AddContactAction class' run() after being updated: org.eclipsercp.hyperbola/AddContactAction public void run() { Object item = selection.getFirstElement(); if (item instanceof RosterGroup) { RosterGroup group = (RosterGroup) item; AddContactDialog d = new AddContactDialog(window.getShell()); int code = d.open(); if (code == Window.OK) try { Roster list = Session.getInstance().getConnection().getRoster(); String user = d.getUserId() + "@" + d.getServer(); String[] groups = new String[] { group.getName() }; list.createEntry(user, d.getNickname(), groups); } catch (XMPPException e) { // handle } } } Once the refactoring is done, you are left with just Session in the original model packageSmack does not have an equivalent class. Smack does have an XMPPConnection class that manages the notion of server connections, but it connects to the server in its constructor. This makes it impossible to model connections before they are actually connected. By adding a connection field to Session, a session can simply have a null connection until the user has logged in. For convenience, we also added a simple ConnectionDetails class as a data structure to store login information. 10.2.4. Adding ChatsSmack provides an object that models chats. To expose this in the UI, ChatEditors need to hook into Smack such that new chats cause an editor to be opened and incoming messages are directed to the correct ChatEditor. Previously when we were introducing the PacketListeners, we showed an example of hooking a listener that calls startChat(Message) for all incoming chat messages. The following snippet shows the implementation of startChat(Message) and the logic for directing messages. org.eclipsercp.hyperbola/ApplicationWorkbenchAdvisor
private void startChat(final Message message) {
String user = StringUtils.parseBareAddress(message.getFrom());
Chat chat = session.getChat(user, false);
if (chat != null)
return;
IWorkbench workbench = getWorkbenchConfigurer().getWorkbench();
workbench.getDisplay().asyncExec(new Runnable() {
public void run() {
openChatEditor(message);
}
});
}
private void openChatEditor(Message message) {
IWorkbenchPage page = findPageForSession(session);
if (page != null) {
String user = message.getFrom();
ChatEditorInput editorInput = new ChatEditorInput(session, user);
try {
ChatEditor editor =
(ChatEditor)page.openEditor(editorInput, ChatEditor.ID);
editor.processFirstMessage(message);
} catch (PartInitException e) {
e.printStackTrace();
}
}
}The code first looks for a Chat object matching the sender of the current message. If one is found, a chat already exists and the related ChatEditor is already listening for messages. No further action is needed since the editor gets its own notification and has a chance to display the message. If, however, this is a new chat, we have to open a new ChatEditor, as shown in openChatEditor(Message). Note that since the new editor will have missed the first messageit didn't exist and so was not listeningwe have to prime it with the first message. Note There is an important pattern shown in startChat(). All Eclipse UI drawing and interaction must take place on the UI thread. So, when a method can be run on any thread, you must ensure that any UI-related code is wrapped in either Display.asyncExec(Runnable) or Display.syncExec(Runnable). Here the startChat() is called when an XMPP packet is received and there are no guarantees about the current thread. This pattern is very common in Eclipse applications using SWT. |