by James Turner
August 13, 2007
Well, it's been a busy season for Larry and his llama store (for more on Larry, see part one and part two of this series). True, most of his employees quit because of his intrusive and obnoxious human resource practices, but sales are booming! Not only did Oprah make the llama her animal of the month, but Jennifer Lopez had a photo spread with llamas in Sports Illustrated. But Larry knows that fads don't last forever, and he's decided to diversify into llama-themed instant messaging clients.
In other words, in the final installment of this series, we're going to watch Larry set up a very simple IM client using Swing. This is not intended to be a tutorial on Swing, but rather on how to hook up the AIM Java API in client applications. At the end of the exercise, you'll understand the threading implications of the client API, as well as the basics of how to maintain a buddy list and how to send and receive IMs. We'll also discuss briefly how to get a code signature, and all the things left to learn after we're done here.
Before Larry dives in, let's look at what the basic requirements are for an AIM client, according to the terms and conditions of the client license. At a minimum, a client must implement:
- Text instant messaging functionality.
- Online status indicators if a contact list of screen names is displayed.
- A sign-on screen allowing users to enter their screen name and password.
- The ability for users to report IM spam to AOL.
- The ability for users to block a user (e.g., an IM spammer).
The client Larry is going to build implements the first three of those functions, so he can expect a call from the AOL legal department any day. For practical purposes, the last item is fairly trivial to implement by maintaining a list of banned users and ignoring incoming requests from them. The IM spam reporting tool would involve integration with an email client, and is beyond the scope of this article.
Creating the Wireframe
Larry is going to start out in Eclipse, setting up a Swing project using the Matisse toolkit. Matisse is a drop-and-drag form designer for Swing, and comes as part of the MyEclipse package that Larry already owns. It can also be installed manually into base Eclipse. Larry first sketches out the basic UI elements in a non-functional "wireframe" version using Swing.
The basic elements of the UI (apart from decorations such as the title) are:
- Input fields for the handle and password, as well as a button to log in and out:

- A label that will be used for a status display:

- A scrolling window to hold the buddy list:

- A scrolling window used to hold the chat history:

- And a non-scrolling window used to hold the text that the user types before they send it.

With all this in place, Larry is ready to start rigging up the actual functionality. Before he starts, he needs to get a Custom Client key from the AOL developer site. This key is requested in the same way that he got his bot key in the previous examples. There are two kinds of Client keys: development and deployment. We'll talk at the end of the article about how you go about getting a deployment key. As with the bot example, the development key has restrictions (500 messages a day), but this will be more than enough for testing.
Thread Safety? That's for Wimps!
The first piece of code Larry writes is a new class called AIMSession, which is going to manage the state of the various AIM activities. The AIMSession will know about the form object, so that it can update it when events occur. The form, in turn, will know about the AIMSession object, so that it can send messages and sign in and out of AIM. However, we immediately run into a problem. Remember that the way you process AIM events is with a piece of code like this:
try {
AccSession.pump(50);
} catch (Exception e) {
e.printStackTrace();
}
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
In our previous examples, it was fine to run this code in the base thread, because we weren't doing anything but processing incoming events and acting on them. However, in the case of an IM client, the main control loop is going to be the Swing one, handling UI events. We really want a separate thread to handle incoming AIM events and take the appropriate UI actions, running asynchronously from the UI thread. Simple enough; we'll just spawn a thread with the "pump and sleep" loop inside the run method.
Well, perhaps not so simple. The problem is that the AIM API is not particularly thread-safe. If you create the session inside the new thread, the parent thread can't access it successfully (it's there, but if you try to call on method on it, nothing happens). Similarly, if you create the session outside the thread, the code in the child thread breaks trying to sign in. So we need to keep all the AIM code inside the child thread. But if we do this, how will the UI request actions? With a queue, of course!
Larry creates a new class just to hold operations in the queue, which he cleverly calls AIMOperation. Let's see what Larry came up with:
package biz.blackbear.aimclient;
public class AIMOperation {
public final static int SIGNIN = 1;
public final static int SIGNOUT = 2;
public final static int SEND_MESSAGE = 3;
Integer operation;
Object[] arguments;
public Integer getOperation() {
return operation;
}
public void setOperation(Integer operation) {
this.operation = operation;
}
public Object[] getArguments() {
return arguments;
}
public void setArguments(Object[] arguments) {
this.arguments = arguments;
}
private AIMOperation() {
super();
}
public static AIMOperation createSignIn(String handle, String password) {
AIMOperation op = new AIMOperation();
op.operation = SIGNIN;
Object[] args = { handle, password };
op.arguments = args;
return op;
}
public static AIMOperation createSignOut() {
AIMOperation op = new AIMOperation();
op.operation = SIGNOUT;
op.arguments = new Object[0];
return op;
}
public static AIMOperation createSendMessage(String handle, String message) {
AIMOperation op = new AIMOperation();
op.operation = SEND_MESSAGE;
Object[] args = { handle, message };
op.arguments = args;
return op;
}
}
For the moment, Larry is only concerned about three operations: SIGNIN, SIGNOUT, and SEND_MESSAGE. The AIMOperation class has an integer operation member that holds the "verb" value, and an array of objects to hold any arguments that the verb needs. Larry also creates three constructors to make sure that only valid operations are ever created. For the pathologically curious who wonder why this new code is in the biz.blackbear namespace rather than the com.blackbear namespace I've used in previous examples, it's because in between the last article and this one, someone bought blackbear.com from me.
Over at the AIMSession class, he creates a pair of helper methods to maintain the queue:
LinkedList<AIMOperation> operations = new LinkedList<AIMOperation>();
public void addOperation(AIMOperation op) {
synchronized (operations) {
operations.add(op);
}
}
public AIMOperation getOperation() {
synchronized (operations) {
if (operations.size() > 0) {
return operations.remove();
}
}
return null;
}
All these two methods do is to add items to the end of the queue and take them off the start (we're using a FIFO queue). They are synchronized to make them thread-safe.
Getting Down to Business
Now Larry has everything in place he needs to rig up the application. He decides to write the AIM side first, and then write the UI code that will call it. The constructor for the class takes the Swing form as the only argument. It stores it, and then starts up a thread. The class implements both AccEvents (so it can be an AIM application) and Runnable (so it can run in a separate thread).
public class AIMSession implements AccEvents, Runnable {
AccSession session;
String key = "Key:la19ehUjdkM3OJg4";
Thread listenThread;
public AIMSession(MainClientForm formArg) {
super();
form = formArg;
listenThread = new Thread(this);
listenThread.start();
}
Next he tackles the run method, which is immediately called by the newly created thread.
public void run() {
try {
session = new AccSession();
// Add event listener
session.setEventListener(this);
// set key
AccClientInfo info = session.getClientInfo();
info.setDescription(key);
// setup prefs so anyone can IM us, but not chats or DIMs
session.setPrefsHook(new Prefs());
AccPreferences prefs = session.getPrefs();
prefs.setValue("aimcc.im.chat.permissions.buddies",
AccPermissions.AcceptAll);
prefs.setValue("aimcc.im.chat.permissions.nonBuddies",
AccPermissions.AcceptAll);
prefs.setValue("aimcc.im.direct.permissions.buddies",
AccPermissions.AcceptAll);
prefs.setValue("aimcc.im.direct.permissions.nonBuddies",
AccPermissions.AcceptAll);
prefs.setValue("aimcc.im.standard.permissions.buddies",
AccPermissions.AcceptAll);
prefs.setValue("aimcc.im.standard.permissions.nonBuddies",
AccPermissions.AcceptAll);
} catch (Exception e) {
e.printStackTrace();
return;
}
while( running )
{
try {
AccSession.pump(50);
AIMOperation operation = this.getOperation();
if (operation != null) {
this.processOperation(operation);
}
Thread.sleep(50);
} catch (Exception e) {
e.printStackTrace();
}
}
}
The first part of this method is pretty much identical to the code that Larry used in his bot. It creates a session, and then sets up the handle and password, as well as some prefs. Unlike the bot, this client accepts all kinds of connections from just about anyone. In a real client, these would be configurable so that, for example, a user could choose not to receive IMs from strangers. Two things are different, however. First, the code doesn't sign in the user, because when it first starts, there may not be a handle or password available yet. And secondly, in between pumping and sleeping, the main loop calls a method called processOperation if it finds an operation waiting on the queue.
private void processOperation(AIMOperation operation) throws AccException {
switch (operation.getOperation()) {
case AIMOperation.SIGNIN:
{
Class[] types = {String.class, String.class};
this.checkArgs(operation, types);
String handle = (String) operation.arguments[0];
String password = (String) operation.arguments[1];
// set screen name
session.setIdentity(handle);
session.signOn(password);
break;
}
case AIMOperation.SIGNOUT:
{
Class[] types = new Class[0];
this.checkArgs(operation, types);
session.signOff();
break;
}
case AIMOperation.SEND_MESSAGE:
{
Class[] types = {String.class, String.class};
this.checkArgs(operation, types);
String userName = (String) operation.arguments[0];
String message = (String) operation.arguments[1];
AccIm im = session.createIm(message, null);
AccImSession imSession = session.createImSession(userName, AccImSessionType.Im);
imSession.sendIm(im);
break;
}
}
}
This method handles all of the outgoing actions that the client can take. If the request is to sign in, the handle and password are taken from the arguments of the operation and the signOn method of the AccSession is called. A sign out is handled in a similar fashion, although it needs no arguments. Finally, a request to send a message causes the code to create an IM session with that user (as opposed to a Direct or Chat session) and send the message text to that user. checkArgs is a simple helper function that makes sure that the appropriate number and type of arguments were passed, and throws an IllegalArgumentException if not.
Making the Buttons Work
At this point, Larry jumps over to the Swing code to start wiring some of the events up there. First off, he implements the logic for the Sign In/Sign Off button.
//GEN-FIRST:event_loginButtonMouseClicked
private void loginButtonMouseClicked(java.awt.event.MouseEvent evt) {
if (session == null) {
session = new AIMSession(this);
}
if (!loggedIn) {
this.loginButton.setEnabled(false);
session.addOperation(AIMOperation.createSignIn(this.handleName
.getText(), this.password.getText()));
} else {
this.loginButton.setEnabled(false);
session.addOperation(AIMOperation.createSignOut());
}
}//GEN-LAST:event_loginButtonMouseClicked
public void setSignedIn() {
this.loginButton.setText("Sign Off");
this.loginButton.setEnabled(true);
this.messageEntry.setEnabled(true);
loggedIn = true;
}
public void setSignedOut() {
this.loginButton.setText("Sign On");
this.loginButton.setEnabled(true);
this.messageEntry.setEnabled(false);
loggedIn = false;
}
The first method (loginButtonMouseClicked) is the Swing event handler for the button. It checks to makes sure we've got an AIMSession (and creates one if not), and then checks to see if we're currently logged in. If we aren't, then we must be logging in, so it disables the button for the time being, creates an operation by fetching the handle and password from the appropriate fields, and adds it to the queue. Otherwise, we must be logging out, so it disables the button and adds a signout operation to the queue. The other two methods are merely helpers to enable the button again and set the text appropriately when sign in or sign out is complete. The sign-in helper also enables the message entry window, while the sign-out helper disables it. This way, the user can't start typing messages until they're signed in.
How does the button ever get reenabled? Nothing here is calling those helpers. The answer lies back in the AIMSession code. Take a look at how Larry implemented the event handler for the OnStateChange event.
public void OnStateChange(AccSession arg0, AccSessionState arg1, AccResult arg2) {
form.setConnectionStatus(arg1.toString());
if (arg1 == AccSessionState.Offline) {
running = false;
if (arg2 == AccResult.ACC_E_INVALID_KEY) {
form.addChatMessage("*** Bad Client Key ***");
}
if (arg2 == AccResult.ACC_E_RATE_LIMITED) {
form.addChatMessage("*** Rate Limited ***");
}
if (arg2 == AccResult.ACC_E_RATE_LIMITED_KEY) {
form.addChatMessage("*** Key Rate Limited ***");
}
form.addChatMessage("*** Signed Out ***");
form.setSignedOut();
return;
}
if (arg1 == AccSessionState.Online) {
form.addChatMessage("*** Signed In ***");
form.setSignedIn();
}
}
The first thing the method does is to set the label that displays the current connection state to whatever the new state is. Thankfully, the toString method on an AccSessionState object returns nicely human-readable strings like "Connecting," "Online," and "Offline."
If the connection has gone offline, we check to see if it's one of the reasons we know about (such as a bad client key or trying to log in too often), and if so print an appropriate message in the chat window. The addChatMessage method is simply a helper method on the form that adds a line of output to the chat history window. Larry should probably add code to handle ACC_E_INVALID_PASSWORD, since that's far and away the most likely reason a connection is going to fail. After sending the message, the handler calls the setSignedOut method of the form, which will change the label on the button, enable and disable fields, etc. If the state change is to Online, we let the user know that we've signed in, and change the UI elements to be appropriate for that case.
Final Steps
Believe it or not, the application is almost complete. There're only three things left to handle: the buddy list, incoming IMs, and outgoing IMs. Larry starts with the incoming IMs, since the code is simple.
public void OnImReceived(AccSession arg0, AccImSession arg1,
AccParticipant arg2, AccIm arg3) { try { form.addChatMessage(arg2.getName() + ":" +
arg3.getConvertedText("text/plain")); } catch (AccException e) { e.printStackTrace(); } }
When an incoming IM arrives, all the handler has to do is add a line to the chat history with the name of the sender, a colon, and the message. Outgoing IMs aren't much harder; they're implemented as a handler on the key event of the message input field.
//GEN-FIRST:event_messageEntryKeyPressed
private void messageEntryKeyPressed(java.awt.event.KeyEvent evt) {
if (evt.getKeyChar() == '\n') {
if (session != null) {
String selectedUser = (String) this.friendList.getSelectedValue();
if (selectedUser != null) {
session.addOperation(AIMOperation.createSendMessage(selectedUser,
this.messageEntry.getText()));
this.addChatMessage("YOU: " + this.messageEntry.getText());
this.messageEntry.setText("");
evt.consume();
} else {
this.addChatMessage("*** Please select a user to send to ***");
}
}
}
}//GEN-LAST:event_messageEntryKeyPressed
If the key is a newline and there's a current session, the code makes sure that a user has been selected from the friends list. If it has, a send message operation is built with the currently selected user and the text of the field as arguments. The message is also echoed to the char history, the field is cleared, and the event is consumed to avoid ending up with a newline in the otherwise empty input field. If no user is selected, the code complains. If it wasn't a newline, the keystroke is processed normally.
Now all that's left is to display the buddy list. The code is a little tricky, but not much more so than what's already come.
public void OnBuddyListChange(AccSession arg0,
AccBuddyList arg1, AccBuddyListProp arg2) { form.clearFriendList(); try { int groups = arg1.getGroupCount(); for (int i = 0; i < groups; i++){ AccGroup group = arg1.getGroupByIndex(i); int buddies = group.getBuddyCount(); for (int c = 0; c < buddies; c++) { AccUser buddy = group.getBuddyByIndex(c); if (buddy.getState() != AccUserState.Offline) { form.addFriendList(buddy.getName()); } } } } catch (AccException e) { // TODO Auto-generated catch block e.printStackTrace(); } }
When a change in the buddy list occurs (and one happens automatically when you first sign in), the current buddy list is cleared. Then the code walks through all the buddy groups, and then all the buddies in each group. Unless the buddy is offline, they are added to the buddy list in the UI. The helper methods in the form are quite simple.
public void clearFriendList() {
this.friendList.removeAll();
}
public void addFriendList(String friendName) {
DefaultListModel model = (DefaultListModel) this.friendList.getModel();
model.addElement(friendName);
}
And that's that. When fired up, the application works just as advertised.
Unfinished Business
How about all the other stuff? First off, there's obviously a ton of functionality left to add. Persisting preferences like handle and password. Implementing all the other events that can occur in AIM, as well as code to initiate things like Direct IM connects and multi-user chats. Many more articles could be written explaining all the details, but now that you have a basic feel, you ought to be able to figure out most of it yourself.
We do need to talk about code fingerprints. The way the API works, if you want to use a deployment key with a client (which allows many many more messages a day per key), you need to register the fingerprint of the application. The fingerprint is simply a hash of the executable. For a normal Windows or Linux application, it would be the hash (determined using the acchash program included in the SDK) of the final linked program. With Java code, it's a bit weirder. You register the fingerprint of the accjwrap.dll file included in the SDK (or the equivalent file under Linux). Yes, this means that every Java client application has the same fingerprint; live with it.
We've come to the end of Larry's adventures through the AIM Java API. You may not be a llama mogul, but you can still use the API to create all sorts of useful applications, as well as embedded IM functionality in your existing applications. Hopefully, these articles have provided some illumination to start you down the path. If not, I hear Larry is now providing outsourced llama tech support.
Resources
The source code for this article is available for download in the file AimJavaCode.zip.
References
- "Creating AIM-Enabled Applications in Java, Part 1": The first article in this series
- "Creating AIM-Enabled Applications in Java, Part 2": The second article in this series

Great post in all!
Bst Rgds,
Michael B.