In the previous lesson we used a WebSocket hub to broadcast “chat objects” from a producer to all connected consumers. Here we will implement “chat rooms”, which require finer control over the broadcast to ensure that chat objects are delivered only to consumers that are in the same chat room as the producer of the message.
We will also show how to send a message to a specific consumer in order to implement “private messaging”.
We start with the code of Lesson 3 as a basis. We need to modify ChatMain
and replace ChatConsume
. Let’s see the first:
package org.spiderwiz.tutorial.lesson4; import java.util.List; import java.util.Map; import java.util.UUID; import org.spiderwiz.core.DataObject; import org.spiderwiz.tutorial.objectLib.Chat; import org.spiderwiz.tutorial.objectLib.ConsoleMain; import org.spiderwiz.zutils.ZDictionary; /** * Provides the entry point of the application. Initializes and executes the Spiderwiz framework. */ public class ChatMain extends ConsoleMain{ private static final String ROOT_DIRECTORY = ""; private static final String APP_NAME = "Chat Room Client"; private static final String APP_VERSION = "Z1.01"; // Version Z1.01: Initial version private String myName; // Get this from the configuration file private String myChatRoom; // Get this from the configuration file private Chat chat = null; // A Chat object for committing chat messages private UUID lastSender = null; // Stores the application UUID of the last received message /** * Class constructor with constant parameters. * @param confFileName configuration file name, provided as a command argument */ public ChatMain(String confFileName) { super(ROOT_DIRECTORY, confFileName, APP_NAME, APP_VERSION); } /** * @return the class instance as ChatMain type. */ public static ChatMain getInstance() { return (ChatMain)ConsoleMain.getInstance(); } public String getMyChatRoom() { return myChatRoom; } public synchronized UUID getLastSender() { return lastSender; } public synchronized void setLastSender(UUID lastSender) { this.lastSender = lastSender; } /** * Application entry point. Instantiate the class and initialize the instance. * * @param args the command line arguments. The first argument is used as the configuration file name. */ /** * @param args the command line arguments */ public static void main(String[] args) { // Don't do anything if there is no configuration file name if (args.length == 0) { System.out.println("Configuration file has not been defined"); return; } new ChatMain(args[0]).init(); } /** * Before starting Spiderwiz engine (but after reading program configuration) get the chatter name and chat room. * @return true if both are provided, false if not. */ @Override protected boolean preStart() { myName = getConfig().getProperty("my name"); if (myName == null || myName.isBlank()) { System.out.println("Chatter name has not been defined"); return false; } myChatRoom = getConfig().getProperty("my room"); if (myChatRoom == null || myChatRoom.isBlank()) { System.out.println("Chat room has not been defined"); return false; } System.out.printf("You are chatting as %1$s in room %2$s. Go ahead and type your messages", myName, myChatRoom); System.out.println(); return true; } /** * @return the list of produced objects, in this case Chat is the only one. */ @Override protected String[] getProducedObjects() { return new String[]{Chat.ObjectCode}; } /** * @return the list of consumed objects, in this case Chat is the only one. */ @Override protected String[] getConsumedObjects() { return new String[]{Chat.ObjectCode}; } /** * Add ChatImp implementation class to the object factory list of this application. * @param factoryList */ @Override protected void populateObjectFactory(List<Class<? extends DataObject>> factoryList) { super.populateObjectFactory(factoryList); factoryList.add(ChatImp.class); } /** * Get application parameters that are used for object routing. * @return a ZDictionary object (extension of Map<String, String>) that contains a mapping of "room" to the chat room retrieved * from the configuration file. */ @Override public Map<String, String> getAppParams() { ZDictionary myParams = new ZDictionary(); myParams.put("room", myChatRoom); return myParams; } /** * Process an input line * @param line the line to be broadcast as a chat message. * @return true if processed successfully */ @Override protected boolean processConsoleLine(String line) { try { // If not yet done, instantiate a Chat object and set its 'name' field. if (chat == null) { chat = ChatMain.createTopLevelObject(Chat.class, null); chat.setName(myName); } // If the read line starts with '>' send a private message to the sender of the last received message String destination = null; if (line.startsWith(">")) { if (getLastSender() == null) return true; destination = getLastSender().toString(); line = line.substring(1); } // Set chat message and commit chat.setMessage(line); chat.commit(destination); return true; } catch (NoSuchFieldException | IllegalAccessException ex) { // Theoretically arriving here if the Chat object does not contain ObjectCode static field or the field is not public. // In this case, send a command exception message. sendExceptionMail(ex, "Cannot instantiate Chat class", null, false); return false; } } }
There are two new fields – myChatRoom
(line 20), for storing the chat room name, and lastSender
(line 22) for storing the name of the sender of the last received message so that we can reply personally. These fields also have getters and a setter (lines 29 – 49), because we need to access them from another class (or for synchronization).
The next interesting thing to look at is getInstance()
(line 35). It casts the value returned by Main.getInstance() to its actual type.
In preStart()
(line 73) we load another property from the configuration file – my room
. This determines the name of the chat room used by the application.
As we will see shortly, in this lesson we replace ChatConsume
by ChatImp
, because our implementation class implements both consumer and producer aspects. ChatImp
is registered in populateObjectFactory() (line 112).
The method getAppParams() (line 21), introduced here for the first time, returns a mapping of names to values that is specific to the application instance. Our application maps “room”
to the name of the chat room that it read from the configuration file. We will see its use later.
Finally, in processConsoleLine()
(line 133) we add the “private message” feature. We check if the message that the user wants to send starts with “>”, and if it does and a last sender exists, the message is sent exclusively to that one.
Note the use of commit(destinations) in line 150. This is another version of the commit()
method used until now. The argument destinations
can be a list of stringified application UUIDs concatenated by ‘;’ or null
. In the latter case, the message is broadcast to all the consumers of the object type just like a commit()
without arguments. Here we use it with null
if we want to send the message to the chat room, and with one UUID when we want to send a private message.
Now to ChatImp
:
package org.spiderwiz.tutorial.lesson4; import java.util.Map; import java.util.UUID; import org.spiderwiz.tutorial.objectLib.Chat; /** * Consumer implementation of the Chat class */ public class ChatImp extends Chat{ /** * Called when a chat message is received. Print the sender name followed by a colon, then the message. * Store the sender UUID for private messaging. * @return true to indicate that the event has been handled. */ @Override protected boolean onEvent() { System.out.printf("%1$s: %2$s", getName(), getMessage()); System.out.println(); ChatMain.getInstance().setLastSender(getOriginUUID()); return true; } /** * Restrict message sending to: * 1. applications that are not the current application. * 2. applications that are on the same chat room as the current application. * @param appUUID destination application UUID * @param appName destination application name * @param userID destination user ID * @param remoteAddress remote network address * @param appParams application parameters of the destination application * @return true if the object should be sent to this destination */ @Override protected boolean filterDestination(UUID appUUID, String appName, String userID, String remoteAddress, Map<String, String> appParams ) { return !ChatMain.getInstance().getAppUUID().equals(appUUID) && appParams != null && ChatMain.getInstance().getMyChatRoom().equals(appParams.get("room")); } }
Like in Lesson 3, we override onEvent(). In addition to printing out the received message we also call ChatMain.getInstance().setLastSender()
to save the application UUID of the sender. This is used for private messaging as we mentioned above.
Note that this time we do not compare the UUID of the sender to the current application’s one, because we do the filtering in filterDestination()
as we will see in a moment. Practically there is no difference between doing it either way, but aesthetically we prefer to put all the filtering in one place.
The new thing here is the override of filterDestination(). This method filters outgoing messages so they are destined to the proper destinations. The method is called once for each potential destination, and you have the option to return true
to include it in the sending and false
to exclude it.
The filtering method provides few parameters that can be used to determine the filtering. In our case we use appUUID
to exclude self-messaging, and appParams
to check whether the chat room name of the potential destination is the same as the one loaded from the configuration file by the current application.
The coding work is completed. It remains to define the configuration files. We will test with the following:
[application name]Chat 1 [log folder]/tests/Chat1/Logs [producer-1]websocket=localhost:90/MyHub [my name]FERRANDO [my room]COSI FAN TUTTE
[application name]Chat 2 [log folder]/tests/Chat2/Logs [producer-1]websocket=localhost:90/MyHub [my name]GUGLIELMO
[application name]Chat 3 [log folder]/tests/Chat3/Logs [producer-1]websocket=localhost:90/MyHub [my name]DON ALFONSO [my room]COSI FAN TUTTE
Run it, and you will see that messages sent from FERRANDO are received by DON ALFONSO and vice versa, because both are in COSI FAN TUTTE chat room, but GUGLIELMO, who is not in the room, is excluded.
Then run again and add GUGLIELMO to the room. Try issuing a chat message by FERRANDO, and then let GUGLIELMO type a message that starts with ‘>’, which means a private message to the last sender. Indeed FERRANDO will get it but DON ALFONSO will not.
In this lesson we had the chat room name loaded from the configuration file. Obviously this is not really useful because in real life chatters enter and exit chat rooms dynamically. This is what we will do in the next chapter, which introduces Persistent Data Objects and the Object Hierarchy.
A word about distributed routing
In common implementations of a chat service, users connect to a central system, which manages messaging and distributes messages to chat rooms. Most Internet services, from social networks to online marketing, work like that. We have introduced here a different paradigm – distributed routing. With this paradigm, the destination of each data object is set by the origin of the object, and the object finds its way to its destination through any of the available paths. This is actually the same paradigm as the Internet itself. This concept has many implications that are out of scope for a tutorial but we address them in our blog. See for example The Spiderwiz Vision – Decentralizing the Internet.