The Spiderwiz framework is based on the concept of shared Data Objects. We have seen until now two examples – HelloWorld
in Lesson 2 and Chat
in the previous two lessons. Both of them were top-level objects, the second was disposable while the first was not. In this lesson we will demonstrate the use of non-disposable objects that live in a Data Object Tree. The concept of committing and event handling of data object modifications will be extended to the entire object tree.
We continue with the chat service of Lesson 4 and turn the static chat rooms that we had into rooms that can be dynamically created, deleted, joined and left. This is achieved with the following data object architecture:
- The top level objects are of type
ChatRoom
. These will be created dynamically and will persist until deleted. - Another object type,
Chatter
, represents a user in a room.Chatter
objects are children ofChatRoom
objects. They are created when a user joins a chat room and removed when the user leaves the room. - Each
Chatter
hasChatMessage
children that deliver chat messages. UnlikeChatRoom
andChatter
,ChatMessage
objects are disposable, for obvious reasons. - We will also keep the
Chat
object type of Lesson 4 for private messaging (that is done out of the chat room). This type will stay a disposable root object.
As usual, we start with implementing base classes that are placed in the objectLib
package. Here they go:
package org.spiderwiz.tutorial.objectLib; import org.spiderwiz.annotation.WizField; import org.spiderwiz.core.DataObject; /** * ChatRoom class for Spiderwiz tutorial */ public class ChatRoom extends DataObject{ /** * Mandatory public static field for all data objects. */ public final static String ObjectCode = "CHTRM"; @WizField boolean adult = false; // Indicates whether this is an adult only chat room. False by default. public synchronized boolean isAdult() { return adult; } public synchronized void setAdult(boolean adult) { this.adult = adult; } /** * @return null as this is a root object. */ @Override protected String getParentCode() { return null; } /** * @return false because objects of this class are persistent */ @Override protected boolean isDisposable() { return false; } /** * @return false because chat room names are case insensitive. */ @Override protected boolean isCaseSensitive() { return false; } }
Like every data object, the class has a static ObjectCode
field with a preset unique value.
We define only one @WizField
annotated property – its type is boolean
and its name is adult
. The field is used to mark the room as adult only. We will see its use later.
We do not have a field for the room name because it is the key value used when creating a child in the object’s parent. We will see this in a moment.
ChatRoom
objects are top-level objects, therefore getParentCode() returns null
. The objects are persistent, therefore isDisposable() returns false
.
We also want the room names to be case insensitive so we override isCaseSensitive() to return false
(the default is true
).
Next is Chatter
:
package org.spiderwiz.tutorial.objectLib; import org.spiderwiz.annotation.WizField; import org.spiderwiz.core.DataObject; /** * A data object that represents a chat user in a specific chat room */ public class Chatter extends DataObject { /** * Mandatory public static field for all data objects. */ public final static String ObjectCode = "CHTTR"; @WizField private String name; // Contains the name of the chatter public String getName() { return name; } public void setName(String name) { this.name = name; } /** * @return ChatRoom object code */ @Override protected String getParentCode() { return ChatRoom.ObjectCode; } /** * @return false because objects of this class are persistent */ @Override protected boolean isDisposable() { return false; } }
Here we have one @WizField
annotated property of type String
named name
that contains the name of the chatting user. The reason that we do not use the name as the object’s key is because we use the application UUID for that purpose. This will be explained later.
As we have mentioned, Chatter
objects represent a chatter in a specific chat room, so in this case getParentCode() returns ChatRoom.ObjectCode
.
Objects of this class are also persistent so isDisposable() returns false
.
We have arrived to ChatMessage
:
package org.spiderwiz.tutorial.objectLib; import org.spiderwiz.annotation.WizField; import org.spiderwiz.core.DataObject; /** * A data object representing a message sent in chat room */ public class ChatMessage extends DataObject { /** * Mandatory public static field for all data objects. */ public final static String ObjectCode = "CHTMSG"; @WizField private String message; // Contains a chat message public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } /** * @return Chatter object code */ @Override protected String getParentCode() { return Chatter.ObjectCode; } /** * @return true as this object is disposable. */ @Override protected boolean isDisposable() { return true; } @Override protected boolean isUrgent() { return true; } }
This is also straightforward. There is one @WizField
annotated property of type String
named message
that contains the message, getParentCode() returns Chatter.ObjectCode
and isDisposable() in this case returns true
. We also override isUrgent() to return true
.
We use the Chat
class of Lesson 4 as is.
Our library contains now all the base classes that we need and we can continue with the implementation. We keep using the service mesh structure that we had in the previous lessons – a hub (MyHub
) connected to by several client chatting applications. We connect another application to the hub – Room Manager, which handles the creation, deletion and modification of chat rooms. Let’s start with it.
Room Manager is also a command line application that processes console input lines. Here is RoomManagerMain
:
package org.spiderwiz.tutorial.lesson5.roomManager; import java.util.List; import org.spiderwiz.core.DataObject; import org.spiderwiz.tutorial.objectLib.ChatRoom; import org.spiderwiz.tutorial.objectLib.ConsoleMain; /** * Provides the entry point of the application. Initializes and executes the Spiderwiz framework. */ public class RoomManagerMain extends ConsoleMain { private static final String ROOT_DIRECTORY = ""; private static final String CONF_FILENAME = "room-manager.conf"; private static final String APP_NAME = "Room Manager"; private static final String APP_VERSION = "Z1.01"; // Version Z1.01: Initial version private static final String CREATE = "create"; // create chat room command private static final String DELETE = "delete"; // delete chat room command private static final String MODIFY = "modify"; // modify chat room command private static final String RENAME = "rename"; // rename chat room command /** * Class constructor with constant parameters. */ public RoomManagerMain() { super(ROOT_DIRECTORY, CONF_FILENAME, APP_NAME, APP_VERSION); } /** * Application entry point. Instantiate the class and initialize the instance. * * @param args the command line arguments. Not used in this application. */ public static void main(String[] args) { new RoomManagerMain().init(); } /** * Print out usage instructions at start up * @return true */ @Override protected boolean preStart() { System.out.printf( "To create a new room: type 'create <room name>'.n" + "Append a plus sign (+) to the name if you want to create an adult only room.n" + "To delete a room type 'delete <room name>'.n" + "To modify the adult categorization of a room:n" + "Type 'modify <room name>+' if you want to make it an adult room.n" + "Type 'modify <room name>' if you want to remove the adult categorization.n" + "To rename a room type 'rename <room name>=<new name>'.n" + "To exit type 'exit'.n" ); return true; } /** * @return the list of produced objects, in this case ChatRoom is the only one. */ @Override protected String[] getProducedObjects() { return new String[]{ChatRoom.ObjectCode}; } /** * @return the list of consumed objects, in this case we do not consume any so we return an empty list. */ @Override protected String[] getConsumedObjects() { return new String[]{}; } /** * Add ChatRoom 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(ChatRoom.class); } /** * Process an input line * @param line An input line that contains a room management command * @return true if processed successfully */ @Override protected boolean processConsoleLine(String line) { try { // Trim line and split to command / parameter. // Then check if there is a parameter and whether it contains the adult symbol. // If there is not parameter, ignore the command. String command[] = line.trim().split("s+", 2); if (command.length < 2) return true; String name[] = command[1].split("+", -1); boolean adult = name.length > 1; // now split for 'rename' command name = name[0].split("="); String roomName = name[0]; // If exists, get the ChatRoom object ChatRoom room = getRootObject().getChild(ChatRoom.class, roomName); // Switch by command and process it switch (command[0].toLowerCase()) { case CREATE: // Create a new room if does not exist (ignore if it does) if (room == null) { room = createTopLevelObject(ChatRoom.class, roomName); room.setAdult(adult); room.commit(); System.out.printf("Room %1$s(%2$s) createdn", roomName, adult ? "adult" : "unrestricted"); } break; case DELETE: // Delete the room if exists if (room != null) { room.remove(); room.commit(); System.out.printf("Room %1$s deletedn", roomName); } break; case MODIFY: // Modify the 'adult' field if the room exits if (room != null) { room.setAdult(adult); room.commit(); System.out.printf("Room %1$s set to %2$sn", roomName, adult ? "adult" : "unrestricted"); } break; case RENAME: // Rename the room if exists. The rename command is 'rename <old name>=<new name>' if (room != null && name.length > 1) { String newName = name[1]; if (!newName.equalsIgnoreCase(roomName)) { DataObject renamed = room.rename(newName); if (renamed == null) System.out.printf("Cannot rename to already existing room name %sn", newName); else { renamed.commit(); System.out.printf("Room %1$s was renamed %2$sn", roomName, newName); } } } } return true; } catch (NoSuchFieldException | IllegalAccessException ex) { // Theoretically arriving here if any data 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 a data object class", null, false); return false; } } }
In lines 17-20 we define labels for the various chat room commands – create
, delete
, modify
and rename
.
We override preStart() (line 43) to print out user instructions when the application starts.
The application produces only one object – ChatRoom
(line 62) and consumes none (line 70). ChatRoom
is also added to the Object Factory in line 79.
The application tasks are carried out in processConsoleLine()
(line 89). Each input line is parsed and processed accordingly. We switch (line 107) between the following options:
CREATE
(line 108): If a room with the given name does not exist yet, use createTopLevelObject() to create a ChatRoom
object, set the value of the adult
field and commit the object. Also print out a message that a room with the given name has been created.
DELETE
(line 117): If a room with the given name exists, use remove() to delete the object and commit the deleted object. Also print out a message that a room with the given name has been deleted.
MODIFY
(line 125): If a room with the given name exists, set its adult
field as in the parsed command and commit the object. Also print out a message that the adult
field has been set to the required value.
RENAME
(line 133): If a room with the given name exists and the new name differs from the old name, try to rename the object by calling its rename() method. The method returns null
if an object with the new name already exists, so in this case print out an appropriate message. If renaming was done successfully, the method returns a non-null
data object that we commit in order to propagate the renaming action to the consumers of the object. Also print out a message if renaming is successful.
The code is ready to run but we need a configuration file. We use the occasion to demonstrate a hybrid network topology. You probably remember that in Lesson 3 we configured MyHub
to act as a WebSocket server. We will now make it a dual-protocol server by configuring an additional TCP/IP server. Here is the modified configuration:
[log folder]Logs [consumer server-1]websocket;ping-rate=30 [producer server-1]websocket;ping-rate=30 [consumer server-2]port=10001 [hub mode]yes
We connect Room Manager to the newly configured server:
[log folder]/tests/RoomManager/Logs [producer-1]ip=localhost;port=10001
Pay attention that although the TCP/IP server is configured in hub.conf
as consumer server-2
, we still configure the client as producer-1
. There is no connection between the serial number of the server configuration to that of the client. The numbers are arbitrary and their sole purpose is to differentiate multiple entries in the same configuration file.
Everything is ready. You can run the application and play with it, but of course it doesn’t have much practical value as long as there are no chatters in the rooms. So let’s see the other application of this lesson – Dynamic Chat Room Client.
The new chat client has the following features:
- Users type
!join room-name
to join a chat room. - Users type
!leave
to leave their chat room. - Users type a message that does not start with ‘!’ or ‘>’ to send it to all other members of the room.
- Users type ‘>’ followed by a message to send the message privately to the sender of the last message they have received even if the sender is not in the same room.
- When a user joins a room all other room members get a message “user-name entered the room”.
- When a user leaves a room all other room members get a message “user-name left the room”.
- Users that are not configured as “adult” cannot join an adult chat room.
- When Room Manager deletes a room all users that are in that room leave the room automatically and get an appropriate message.
- When Room Manager modifies a room from non-adult to adult, all users that are in that room and are not configured as adults leave the room automatically and get an appropriate message.
- When a user that is configured as “adult” is in an adult room, and the user configuration changes (e.g. by SpiderAdmin) to non-adult, the change takes the user out of the room.
- When Room Manager renames a room all users that are in the room are notified of the change but stay in the room.
- When Room Manager terminates, gracefully or not, all rooms are deleted and users in the rooms are notified.
- When a chatter application terminates while in a room, gracefully or not, all other members of the room get notified that the user left the room.
If this looks like a long list, you will soon see how easy it is to implement all these with Spiderwiz. So we start with ChatMain
:
package org.spiderwiz.tutorial.lesson5.chat; import java.util.List; import java.util.UUID; import org.spiderwiz.admin.xml.OpResultsEx; import org.spiderwiz.core.DataObject; import org.spiderwiz.tutorial.objectLib.Chat; import org.spiderwiz.tutorial.objectLib.ChatMessage; import org.spiderwiz.tutorial.objectLib.ChatRoom; import org.spiderwiz.tutorial.objectLib.Chatter; import org.spiderwiz.tutorial.objectLib.ConsoleMain; /** * 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 = "Dynamic Chat Room Client"; private static final String APP_VERSION = "Z1.01"; // Version Z1.01: Initial version private static final String JOIN = "!join"; // join a chat room command private static final String LEAVE = "!leave"; // leave current chat room command private String myName; // Get this from the configuration file private Chatter chatter = null; // A Chatter object representing the user chatting in a specific room private ChatMessage message = null; // A ChatMessage object for committing chat messages private Chat privateMessage = null; // A Chat object for committing private 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 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, * then print user instructions to the console. * @return true if the name is 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; } System.out.printf( "Hello %s. Welcome to the dynamic chat system.n" + "To join a chat room or change your current chat room, type "!join <room name>".n" + "To leave your chat room, type "!leave".n" + "To send a message in the current chat room just type the message " + "(a message cannot start with either '>' or '!').n" + "To send a private message to the sender of the last message you have received " + "type '>' followed by the message.n" + "To exit type 'exit'.n", myName ); return true; } /** * @return the list of produced objects - Chatter, Chat and ChatMessage */ @Override protected String[] getProducedObjects() { return new String[]{ Chatter.ObjectCode, Chat.ObjectCode, ChatMessage.ObjectCode }; } /** * @return the list of consumed objects - ChatRoom, Chatter, Chat and ChatMessage */ @Override protected String[] getConsumedObjects() { return new String[]{ ChatRoom.ObjectCode, Chatter.ObjectCode, Chat.ObjectCode, ChatMessage.ObjectCode }; } /** * Add implementation classes to the object factory list of this application. * @param factoryList */ @Override protected void populateObjectFactory(List<Class<? extends DataObject>> factoryList) { super.populateObjectFactory(factoryList); factoryList.add(ChatRoomConsume.class); factoryList.add(ChatterConsume.class); factoryList.add(ChatConsume.class); factoryList.add(ChatMessageImp.class); } /** * @return true if application is configured as an adult chatter */ public boolean isAdult() { return getConfig().isPropertySet("adult"); } /** * Join a room after checking that the room exists, its adult setting matches user configuration, * and the user is not already there. If the user is in another room leave that room first. * @param room the new room name * @throws NoSuchFieldException If any of the created data objects does not define ObjectCode static field. * @throws IllegalAccessException If ObjectCode is not public. */ private void joinRoom(String room) throws NoSuchFieldException, IllegalAccessException { ChatRoom chatRoom = getRootObject().getChild(ChatRoom.class, room); if (chatRoom == null) { System.out.printf("Room %s does not existn", room); return; } if (chatter != null && room.equalsIgnoreCase(chatter.getParent().getObjectID())) return; if (chatRoom.isAdult() && !isAdult()) { System.out.printf("You cannot join room %s because it is only for adultsn", room); return; } leaveRoom(null); chatter = chatRoom.createChild(Chatter.class, getAppUUID().toString()); chatter.setName(myName); chatter.commit(); // let everybody know I joined the room // Prepare a ChatMessage object for sending messages from now on message = chatter.createChild(ChatMessage.class, null); System.out.printf("Joined room %sn", room); } /** * If the user is in the given room, leave it. * @param room the name of the room to be checked if the user is there. If null, leave the current room without checking */ public synchronized void leaveRoom(String room) { if (isSameRoom(room)) { chatter.remove(); // leave the room chatter.commit(); // let everybody know I did System.out.printf("Left room %sn", chatter.getParent().getObjectID()); chatter = null; message = null; } } /** * Check if the user is in the same room as the parameter * @param room the name of the room to be checked if the user is there. If null, return true * @return true if the user in 'room' or 'room' is null */ public synchronized boolean isSameRoom(String room) { return chatter != null && (room == null || room.equalsIgnoreCase(chatter.getParent().getObjectID())); } /** * Check whether the user running the application whose UUID is given is on the same room as us. This is done by getting * our current ChatRoom object and see if it has a child with that ID. * @param appUUID Application UUID to check * @return true if the application is in the same room as us. */ public synchronized boolean isMemberOfMyRoom(UUID appUUID) { try { if (chatter == null) return false; ChatRoom chatRoom = (ChatRoom)chatter.getParent(); return chatRoom != null && chatRoom.getChild(Chatter.class, appUUID.toString()) != null; } catch (NoSuchFieldException | IllegalAccessException ex) { // Theoretically arriving here if a data 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 a data object class", null, false); return false; } } /** * Called when application configuration has been changed. If the user were changed to "not adult" and she is on * a room defined as adult only then take the user out of the room. * @return the value returned by the super method. */ @Override public OpResultsEx reloadConfig() { OpResultsEx opResult = super.reloadConfig(); if (chatter != null) ((ChatRoomConsume)chatter.getParent()).reloadConfig(); return opResult; } /** * 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 the line starts with '!' this is a command line if (line.startsWith("!")) { String command[] = line.trim().split("s+", 2); switch (command[0].toLowerCase()) { case JOIN: // if room name is provided join the room if (command.length > 1) joinRoom(command[1]); break; case LEAVE: // If the user is in a room, leave it leaveRoom(null); break; } return true; } // If the line starts with '>' send a private message to the sender of the last received message if (line.startsWith(">")) { if (getLastSender() == null) return true; // If didn't do yet, create an orphan Chatter object for sending private messages, // then prepare a ChatMessage object for sending private messages if (privateMessage == null) { privateMessage = createTopLevelObject(Chat.class, null); privateMessage.setName(myName); } // Set message and commit to destination privateMessage.setMessage(line.substring(1)); privateMessage.commit(getLastSender().toString()); return true; } // If this is a normal message, make sure the user is in a room and broadcast the message if (message != null) { message.setMessage(line); message.commit(); } return true; } catch (NoSuchFieldException | IllegalAccessException ex) { // Theoretically arriving here if a data 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 a data object class", null, false); return false; } } }
Here too ChatMain
extends ConsoleMain
. The user command labels are defined in lines 20-21. We then define myName
to hold the user name, a Chatter
object that represents the chatter, a ChatMessage
object that is used to send messages in the room, a Chat
object that is used to send private messages and a lastSender
property to hold the UUID of the last sender for private messaging (in the next lesson we will demonstrate more flexible private messaging).
In preStart() (line 75) we load the chatter name from the configuration file and print out usage instructions.
The produced data objects are Chatter
, Chat
and ChatMessage
(line 99). These are also consumed, in addition to ChatRoom
(line 111).
The implementation objects that we will see soon are ChatRoomConsume
, ChatterConsume
, ChatConsume
and ChatMessageImp
. They are registered in line 125.
The function isAdult()
(line 136) returns the adult
property as defined in the configuration file.
The interesting stuff starts on line 147 with few utility methods. The first is joinRoom()
that is called when a user joins a chat room. It gets a room name as a parameter and performs the following:
- Call getRootObject().getChild(ChatRoom.class, room) to get the desired
ChatRoom
object. Anull
response means that the room does not exist, in which case we print a message and quit. - If class field
chatter
, which is an object of typeChatter
, is notnull
, it means that the user is already in a room. In this case call chatter.getParent() to get the object’s parent, which is aChatRoom
object that refers to the room the user is in. Then call getObjectID() on the object to get the room name and compare it to the name of the room the user wants to join. If they are equal, do nothing. - Get
isAdult()
property of theChatRoom
object. If it istrue
, check theadult
property in the application’s configuration. If the room is for adults and the user is not then print an appropriate message and quit. - Call the utility method
leaveRoom()
to leave the room the user is now in, if there is any. - Call createChild() to create a
Chatter
object as a child of the desiredChatRoom
object. The method returns the createdChatter
object. - Set user name on the created object.
- Commit the object to propagate to other chatters that this user is now in the room.
- Call createChild() on the
chatter
object to create aChatMessage
object that will be used for chatting in the room. - Print out an appropriate message.
As we have just mentioned, we join a user to a room by creating a Chatter
object as a child of the appropriate ChatRoom
object. Note that the key used in the creation is the UUID of the application that the user runs (line 161). This facilitates easy routing of messages to appropriate rooms as we will shortly see.
The next utility method to discuss is leaveRoom()
(line 173). It gets a parameter room
that is null
if users shall leave any room that they are now in, or a room name if they shall leave only if they are currently in that room. The code is straightforward – call isSameRoom()
to determine whether there is a need to leave the room, and if so call remove() on the Chatter
object that is currently a child of a ChatRoom
object, commit the removed object to propagate the removal to other users, print out a message and set chatter
and message
fields to null
, as they are no longer applicable.
The function isSameRoom()
(line 188) returns true
if either its argument is null
or it equals the name of the room the user is now in.
The last utility method here is isMemberOfMyRoom()
(line 199). Its parameter is the UUID of an application that needs to be checked whether its user is in the same room as the user of this application. This is done as follows:
- If the
chatter
field isnull
then the user is not in any room so returnfalse
. - Call getParent() on the
chatter
object to get theChatRoom
object pertaining to the room the user is now in. - Call getChild() to get the object’s child with a key value equal to the stringified UUID specified as the method parameter. If the returned value is not
null
, the user identified by that UUID is in the room.
We will use the occasion to demonstrate the use of the overriding reloadConfig() method (line 220). It is called when application configuration is changed, usually by SpiderAdmin. After the mandatory call to the super
method, we use the chatter
field to check whether the user is in a room, and if so we call reloadConfig()
on its ChatRoom
parent. You will see below what happens next.
The class concludes with processConsoleLine()
(line 233). It is straightforward but we will describe it nevertheless:
- Determine whether the line contains a command (starts with ‘!’). If it is then parse it.
- If the command is
!join
calljoinRoom()
. - If the command is
!leave
callleaveRoom()
. - If the line contains a private message (starts with ‘>’) then proceed as in Lesson 4.
- If the line contains a normal message then determine whether the user is in a room by checking that
message
is notnull
. - If
message
is notnull
then it contains aChatMessage
object that is a child of the currentChatter
object. In this case set itsmessage
property to the message and commit the object.
Let’s see the data object implementation classes. First ChatRoomConsume
:
package org.spiderwiz.tutorial.lesson5.chat; import org.spiderwiz.tutorial.objectLib.ChatRoom; /** * Consumer implementation of the ChatRoom class */ public class ChatRoomConsume extends ChatRoom { /** * If the chat room has been removed and the user is there, leave the room * @return true to confirm the removal. */ @Override protected boolean onRemoval() { ChatMain.getInstance().leaveRoom(getObjectID()); return true; } /** * If the room was set to adult and user is not configured as such, leave the room if user is there * @return true */ @Override protected boolean onEvent() { if (isAdult() && !ChatMain.getInstance().isAdult()) ChatMain.getInstance().leaveRoom(getObjectID()); return true; } /** * Notify on room name change if the user is in the renamed room * @param oldID */ @Override protected void onRename(String oldID) { if (ChatMain.getInstance().isSameRoom(getObjectID())) System.out.printf("Your room %1$s was renamed %2$sn", oldID, getObjectID()); } /** * Called when application configuration has been changed. If the user were changed to "not adult" and the room is an adult room * then onEvent() would take the user out of the room. */ public void reloadConfig() { onEvent(); } }
Now that you know that all the hard work is done by Spiderwiz, you are apparently not surprised to see how simple the code is. This is what it does:
Override onRemoval() (line 15) to get users out of a deleted room.
Override onEvent() (line 25) to get non-adult users out of a room that became adult.
Override onRename() (line 36) to notify users that are in a renamed room about the name change.
The last method, reloadConfig()
(line 45), is called from a ChatMain
method of the same name when the application configuration is changed. It calls onEvent()
on the same class. The effect is that users whose configuration was changed from adult to non-adult are thrown out of adult rooms.
ChatterConsume
is even simpler:
package org.spiderwiz.tutorial.lesson5.chat; import org.spiderwiz.tutorial.objectLib.Chatter; /** * Consumer implementation of Chatter data object. Notify when a chatter enters or leaves my room. */ public class ChatterConsume extends Chatter { /** * Notify when a chatter enters my room. */ @Override protected void onNew() { // The room is the parent of this object if (ChatMain.getInstance().isSameRoom((getParent().getObjectID()))) System.out.printf("%s entered the roomn", getName()); } /** * Notify when a chatter leaves my room * @return true to confirm the removal. */ @Override protected boolean onRemoval() { // The room is the parent of this object if (ChatMain.getInstance().isSameRoom(getParent().getObjectID())) System.out.printf("%s left the roomn", getName()); return true; } }
This class overrides onNew() (line 14) to print a message when a new user joins the room the user is currently in, and overrides onRemoval() (line 25) to print a message when a user leaves that room.
Not much more work in ChatMessageImp
:
package org.spiderwiz.tutorial.lesson5.chat; import java.util.Map; import java.util.UUID; import org.spiderwiz.tutorial.objectLib.ChatMessage; import org.spiderwiz.tutorial.objectLib.Chatter; /** * Consumer implementation of the ChatMessage class */ public class ChatMessageImp extends ChatMessage { /** * Called when a chat message is received. Check if the message has been sent in the room we are in, * Then print the sender name and the message. * Store the sender UUID for private messaging. * @return true to indicate that the event has been handled. */ @Override protected boolean onEvent() { if (ChatMain.getInstance().isSameRoom(getParent().getParent().getObjectID())) { System.out.printf("%1$s: %2$sn", ((Chatter)getParent()).getName(), getMessage()); 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 user * @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) && ChatMain.getInstance().isMemberOfMyRoom(appUUID); } }
In onEvent() (line 19) we check whether the user is in the same room as the sender of the message (although routing is done at the sender side, a wrong message may slip in due to synchronization issues). If she is, we print the message and save the sender’s UUID for private messaging.
In filterDestination() (line 39), which is called when the user is the sender, we check whether this is not a self message and the user in the destination UUID is in the same room as the sender.
The last class we have to look at is ChatConsume
, used to process an incoming private message. This is the simplest of all:
package org.spiderwiz.tutorial.lesson5.chat; import org.spiderwiz.tutorial.objectLib.Chat; /** * Consumer implementation of the Chat class */ public class ChatConsume extends Chat{ /** * Called when a private message is received. Print the sender name followed by a colon, then the message. * @return true to indicate that the event has been handled. */ @Override protected boolean onEvent() { System.out.printf("%1$s: %2$sn", getName(), getMessage()); return true; } }
Here we need only to override onEvent() and print the message. No filtering is needed because a private message is sent out of the room with a direct destination in the commit() method.
We are done. As you can see, the code we had to type, excluding the comments, is not much longer than the feature list we outlined above. In fact some features, such as that when an application terminates then all the objects it created are removed from the object tree, are built into the Spiderwiz engine with no need for extra coding.
To run and test the code we need configuration files. Here are the files for three clients – Chat1
, Chat2
and Chat3
:
[application name]Chat 1 [log folder]/tests/Chat1/Logs [producer-1]websocket=localhost:90/MyHub [my name]FERRANDO
[application name]Chat 2 [log folder]/tests/Chat2/Logs [producer-1]websocket=localhost:90/MyHub [my name]GUGLIELMO
[log folder]/tests/Chat3/Logs [producer-1]websocket=localhost:90/MyHub [my name]DON ALFONSO [adult]yes
The buddies from Cosi Fan Tutte star here too. The only adult one is DON ALFONSO. If you know the opera, you would not wonder why.
Note that although Room Manager connects to My Hub on TCP/IP, the chat applications connect as WebSocket clients as before. This is a demonstration of a hybrid network topology that we mentioned before.
Go ahead, play with the applications and try to test all their features. See how programming is a joy when it is easy as a Lego toy.
In the next lesson we will continue with the Data Object Tree and learn how to easily traverse it to collect information and perform actions.