Query Objects are a special kind of data objects used for round-trip query and response. The inquirer sets query properties and posts the query. The replier sets response properties on the object it receives and the framework carries the response back to the inquirer.
This is how it works in more detail:
Queries are created by extending the QueryObject class, which by itself is an extension of DataObject. As with basic data objects, the implementation usually involves a base class that is shared in a library between applications, and extensions that may be implemented differently by each application.
At the inquirer side, set query fields and post it by calling post(expires) or post(expires, destinations). The first version broadcasts the query to all the consumers of this query type, the latter posts it to the specified destination(s).
You can handle the response synchronously or asynchronously. To handle synchronously, call waitForReply() after posting the query. This blocks the calling thread until the query is either replied or expires. When the method ends, if the query is replied properly then response properties will be set. Asynchronous response handling is done by overriding onReply(). You may also override onExpire() to do something if the query expires before it is replied.
At the replier side, handle the query by overriding onInquire(). Reply by setting the appropriate properties and return true
when quitting the method.
A query can be closed or open. A closed query handles the first response it gets and then is discarded. An open query can receive multiple responses from one or more repliers and remains active until it expires. To indicate an open or closed query override isOpenQuery().
In this lesson we use a closed query for adding a user login/registration feature to the chat service of the previous lessons. We handle the response synchronously. We also use an open query to get information about all chatters and the rooms they are chatting in. With this query we handle the responses asynchronously.
We are going to throw a new application – User Manager – into our service mesh. It will be responsible for replying login and registration requests, and it will have a list
command for collecting and printing user and room information. Let’s first see the basic query objects that we use. The first is LoginQuery
:
package org.spiderwiz.tutorial.objectLib; import org.spiderwiz.annotation.WizField; import org.spiderwiz.core.QueryObject; import org.spiderwiz.zutils.ZDate; /** * A Query Object used to login an existing user into the system or registering a new one. */ public class LoginQuery extends QueryObject{ /** * Mandatory public static field for all data objects. */ public final static String ObjectCode = "LOGQWRY"; public enum ResultCode {OK, EXISTS, NOT_EXISTS, WRONG_PASSWORD}; @WizField private boolean newUser; // true when a new user is registering, false if an existing user is loggin in @WizField private ResultCode result; // contains the result code of the reply @WizField private String name; // user name @WizField private String password; // user password @WizField private ZDate birthday; // Contains the chatter birth date public boolean isNewUser() { return newUser; } public void setNewUser(boolean newUser) { this.newUser = newUser; } public ResultCode getResult() { return result; } public void setResult(ResultCode result) { this.result = result; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public ZDate getBirthday() { return birthday; } public void setBirthday(ZDate birthday) { this.birthday = birthday; } /** * @return true because we need a reply as soon as possible */ @Override protected boolean isUrgent() { return true; } }
This class is used for both login and registration. Its properties are:
newUser
:true
if this is a registration of a new user,false
if this is a login request of an existing user.result
: anenum
of typeResultCode
that stores the result of the query.name
: user name to login with or to register.password
: password to login with or to supply on registration.birthday
: a date of birth that is supplied on registration and retrieved on login.
The method isUrgent() is overridden to return true
(line 68) because for this kind of query the user expects a prompt reply.
The other mentioned query, used to collect information about active users and their rooms, is ActiveUserQuery
:
package org.spiderwiz.tutorial.objectLib; import org.spiderwiz.annotation.WizField; import org.spiderwiz.core.QueryObject; /** * A Query Object used to get information about a user that is active in a room */ public class ActiveUserQuery extends QueryObject{ /** * Mandatory public static field for all data objects. */ public final static String ObjectCode = "AUSRQWR"; @WizField private String name; // user name @WizField private String room; // the name of the room the user is in public String getName() { return name; } public void setName(String name) { this.name = name; } public String getRoom() { return room; } public void setRoom(String room) { this.room = room; } /** * @return true because this is an open query (that can get responses from multiple parties) */ @Override protected boolean isOpenQuery() { return true; } }
No property is needed when using the object for inquiring. There are two properties set by repliers:
name
: user name.room
: the name of the room the user is active in.
In this case the method isOpenQuery() is overridden to return true
.
With these in place we can start with User Manager:
package org.spiderwiz.tutorial.lesson7.userManager; import java.util.List; import org.spiderwiz.core.DataObject; import org.spiderwiz.tutorial.objectLib.ActiveUserQuery; import org.spiderwiz.tutorial.objectLib.ConsoleMain; import org.spiderwiz.tutorial.objectLib.LoginQuery; import org.spiderwiz.zutils.ZDate; import org.spiderwiz.zutils.ZHashMap; /** * Provides the entry point of the application. Initializes and executes the Spiderwiz framework. */ public class UserManagerMain extends ConsoleMain { private static final String ROOT_DIRECTORY = ""; private static final String CONF_FILENAME = "user-manager.conf"; private static final String APP_NAME = "User Manager"; private static final String APP_VERSION = "Z1.01"; // Version Z1.01: Initial version private static final String LIST = "list"; // list all users currently in any room private final ZHashMap<String, RegisteredUser> users; /** * Class constructor with constant parameters. Call super constructor and create the user map. */ public UserManagerMain() { super(ROOT_DIRECTORY, CONF_FILENAME, APP_NAME, APP_VERSION); users = new ZHashMap<>(); } /** * @return the class instance as UserManagerMain type. */ public static UserManagerMain getInstance() { return (UserManagerMain)ConsoleMain.getInstance(); } /** * 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 UserManagerMain().init(); } /** * Print out usage instructions at start up * @return true */ @Override protected boolean preStart() { System.out.printf("To list all users currently in any room type 'list'.n"); return true; } /** * @return the list of produced object - ActiveUserQuery */ @Override protected String[] getProducedObjects() { return new String[]{ ActiveUserQuery.ObjectCode }; } /** * @return the list of consumed object - LoginQuery */ @Override protected String[] getConsumedObjects() { return new String[]{ LoginQuery.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(LoginQueryReply.class); factoryList.add(ActiveUserQueryInquire.class); } /** * Process an input line * @param line An input line that may contain the 'list' command * @return true if processed successfully */ @Override protected boolean processConsoleLine(String line) { try { if (line.toLowerCase().startsWith(LIST)) listAllUsers(); 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; } } /** * Post an ActiveUserQuery query to get all active users * @throws NoSuchFieldException If any of the referred data objects does not define ObjectCode static field. * @throws IllegalAccessException If ObjectCode is not public. */ private void listAllUsers() throws NoSuchFieldException, IllegalAccessException { ActiveUserQuery query = createQuery(ActiveUserQuery.class); query.post(5 * ZDate.SECOND); } /** * Return the RegisteredUser object associated with the user with the given name * @param name name of the user to look for * @return a RegisteredUser object or null if the user does not exist */ public RegisteredUser getUser(String name) { return users.get(name.toLowerCase()); } /** * If the user does not exist add the given RegisteredUser to the user map. * @param user a RegisteredUser object to insert * @return null if an object for the given user did not exist before and was inserted now, or the user's object if already exists. */ public RegisteredUser addUser(RegisteredUser user) { return users.putIfAbsent(user.getName().toLowerCase(), user); } }
Usually user registration and login requires a database, but we want to keep things simple so we just keep the data in memory. For this purpose we use ZHashMap – the Spiderwiz extension of HashMap that supports synchronization. The keys in the map are user names and the values are RegisteredUser
objects that we will see later. The map is instantiated in the class constructor (line 29).
The application handles one console command – list
, described in preStart() (line 53).
The inquiring side of a query object is its producer while the replying is the consumer. This application is the inquirer of ActiveUserQuery
and the replier of LoginQuery
, so it registers the first in getProducedObjects() (line 62) and the second in getConsumedObjects() (line 72).
The implementation classes are LoginQueryReply
and ActiveUserQueryInquire
. Both are registered in populateObjectFactory() (line 83).
We override processConsoleLine()
(line 95) to parse console lines, and if they contain the list
command we call listAllUsers()
(defined in line 113). In order to achieve its task, the method creates an ActiveUserQuery
query by calling createQuery() and posts it by calling post() with a parameter that indicates expiration time of 5 seconds. Recall that this is an open query that can accept multiple responses and remains active until expiration. Its consumers are the chat applications. Every chat application that has an active user in a chat room replies with the names of the user and the chat room. We will see in a moment how ActiveUserQueryInquire
handles the responses.
The rest of the class code contains two utility methods that access the users
table. The getUser()
method (line 124) gets a RegisteredUser
record by user name and is used for user login. The addUser()
method (line 133) inserts a new record into the table (if one with the same user name does not exist yet) and is used for user registration.
The code of RegisteredUser
is straightforward:
package org.spiderwiz.tutorial.lesson7.userManager; import org.spiderwiz.zutils.ZDate; /** * A class that hold user's name, password and birth date */ public class RegisteredUser { private final String name; // user name private final String password; // user password private final ZDate birthday; // user birth date public RegisteredUser(String name, String password, ZDate birthday) { this.name = name; this.password = password; this.birthday = birthday; } public String getName() { return name; } public String getPassword() { return password; } public ZDate getBirthday() { return birthday; } }
It remains to see the query object implementation classes. LoginQueryReply
is the replier extension of LoginQuery
:
package org.spiderwiz.tutorial.lesson7.userManager; import org.spiderwiz.tutorial.objectLib.LoginQuery; /** * A class that replies LoginQuery queries */ public class LoginQueryReply extends LoginQuery { /** * Examine the query fields and reply accordingly * @return true because all queries are replied. */ @Override protected boolean onInquire() { RegisteredUser user; if (isNewUser()) { // A new user is added if a user with the same name does not exist, otherwise return an error code. user = UserManagerMain.getInstance().addUser( new RegisteredUser(getName(), getPassword(), getBirthday()) ); setResult(user == null ? ResultCode.OK : ResultCode.EXISTS); } else { // If an existing user is loging in check password and return user details. user = UserManagerMain.getInstance().getUser(getName()); if (user == null) setResult(ResultCode.NOT_EXISTS); else if (!user.getPassword().equals(getPassword())) setResult(ResultCode.WRONG_PASSWORD); else { setResult(ResultCode.OK); // Name is returned even though it is known because the name used when registering // may be different in case from the name used for login. setName(user.getName()); setBirthday(user.getBirthday()); } } return true; } }
We override onInquire() to handle a query and reply to it. Recall that this can be either a login or a registration request, depending on the value of the newUser
property (line 17). In the case of a registration request we create a new RegisteredUser
object and call UserManagerMain.addUser()
to add it to the users
table. In the case of successful registration the method returns null
and we set the result
property to OK
. Otherwise a user with the same name already exists and we set it to EXISTS
.
If this is a login request we call UserManagerMain.getUser()
(line 25) to get the user record, then if it exists compare the password given in the query to the registered password. If the user does not exist or the passwords do not match we set an appropriate code in the result
property. If everything is OK we set the result to OK
and set the name
and birthday
properties with the values from the user record (note that the registered user name may differ from the login name by case. That is why we need to return the registered name in the response).
In all cases we return true
to tell the Spiderwiz engine to deliver the response back to the inquirer.
The other query we need to implement is ActiveUserQuery
, of which the application is the inquirer side. Here is ActiveUserQueryInquire
:
package org.spiderwiz.tutorial.lesson7.userManager; import java.util.HashMap; import org.spiderwiz.tutorial.objectLib.ActiveUserQuery; /** * A class that handles ActiveUserQuery from the inquirer side. */ public class ActiveUserQueryInquire extends ActiveUserQuery { private final HashMap<String, Integer> roomCount; // Used to count users per room private int totalUsers = 0; // Used to count total amoun of active users public ActiveUserQueryInquire() { roomCount = new HashMap<>(); } /** * On reply print a message and increase number of users in the room */ @Override protected synchronized void onReply() { System.out.printf("%1$s in room %2$sn", getName(), getRoom()); Integer count = roomCount.get(getRoom()); if (count == null) count = 0; roomCount.put(getRoom(), count + 1); ++totalUsers; } /** * When query expires print totals */ @Override protected synchronized void onExpire() { roomCount.entrySet().forEach((entry) -> { System.out.printf("%1$d chaters in room %2$sn", entry.getValue(), entry.getKey()); }); System.out.printf("Total of %d chatters in all roomsn", totalUsers); } }
As this is an open query and we want to process the responses asynchronously, we need to override onReply() (line 21) and onExpire() (line 34). Recall that each response contains a user name and the name of the room she is chatting in. In onReply()
we print per user information as soon as it arrives and also do general and per room counting. In onExpire()
we print the counting totals.
We are done with User Manager but still need to see its configuration file:
[log folder]/tests/UserManager/Logs [producer-1]ip=localhost;port=10001
Nothing special here, just define a log folder and connect to MyHub
as in the previous lessons.
The other part of the mission is to extend Chat Room Client of Lesson 6 with the following features:
- User name and date of birth are not taken from the configuration file any more.
- A
!register
command starts a procedure that requires the user to type a user name, a password, repeat the password and type date of birth. The passwords need to match and the User Manager must confirm that the name of the user does not already exist. - A
!login
command starts a procedure that requires the user to type a user name and a password and checks against the User Manager that the user exists and the password matches with the one provided at registration. - A
!logout
command logs out a registered or logged in user. - The
!register
and!login
commands cannot work if the user is already logged in. - The
!join
and!leave
commands work as before, except that the user must be logged in before joining a room and a user who is under 18 year old cannot join an adult room. - If Room Manager changes a regular room to an adult room then all users under 18 who are in that room are automatically taken out of the room.
The application responds to ActiveUserQuery
queries by providing the user name and the chat room name that the user is active in. If the user is not active in any room then the query is ignored.
Let’s see how these features are implemented, first in ChatMain
.
package org.spiderwiz.tutorial.lesson7.chat; import java.io.PrintStream; import java.text.ParseException; import java.util.Collection; import java.util.List; import java.util.UUID; import org.spiderwiz.core.DataObject; import org.spiderwiz.tutorial.objectLib.ActiveUserQuery; 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; import org.spiderwiz.tutorial.objectLib.LoginQuery; import org.spiderwiz.zutils.ZDate; /** * 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 static final String JOIN = "!join"; // join a chat room command private static final String LEAVE = "!leave"; // leave current chat room command private static final String LOGIN = "!login"; // start login procedure private static final String LOGOUT = "!logout"; // log the user out private static final String REGISTER = "!register"; // start registration procedure private String myName = null; // The user login name private ZDate birthday = null; // User birth date. Get it when logging in 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 /** * 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(); } /** * 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(); } /** * Print user instructions to the console. * @return true */ @Override protected boolean preStart() { System.out.printf( "Welcome to the dynamic chat system.n" + "To log in type !login.n" + "To log out type !logout.n" + "To register type !register.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 type '>' followed by name of the user you want to send " + "the private message to, followed by ':' for by the message.n" + "To exit type 'exit'.n" ); return true; } /** * @return the list of produced objects - Chatter, Chat, ChatMessage and LoginQuery */ @Override protected String[] getProducedObjects() { return new String[]{ Chatter.ObjectCode, Chat.ObjectCode, ChatMessage.ObjectCode, LoginQuery.ObjectCode }; } /** * @return the list of consumed objects - ChatRoom, Chatter, Chat, ChatMessage and ActiveUserQuery */ @Override protected String[] getConsumedObjects() { return new String[]{ ChatRoom.ObjectCode, Chatter.ObjectCode, Chat.ObjectCode, ChatMessage.ObjectCode, ActiveUserQuery.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); factoryList.add(LoginQuery.class); factoryList.add(ActiveUserQueryReply.class); } /** * 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; case LOGIN: login(); break; case LOGOUT: logout(); break; case REGISTER: register(); break; } return true; } // If the line starts with '>' split the line on the colon (':') and send the test // after the colon as a private message to the user with the name before the colon. if (line.startsWith(">")) { String cmd[] = line.substring(1).split(":"); if (cmd.length < 2) return true; String destination = findUser(cmd[0]); if (destination == 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(cmd[1]); privateMessage.commit(destination); 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; } } public String getMyName() { return myName; } public ZDate getBirthday() { return birthday; } /** * Get user name and password and log in. * @return a PrintStream object. Not needed by the caller but we use it as a trick to save coding. * @throws NoSuchFieldException If LoginQuery does not define ObjectCode static field. * @throws IllegalAccessException If LoginQuery.ObjectCode is not public. */ private PrintStream login() throws NoSuchFieldException, IllegalAccessException { String username; String password; // Check if already logged in if (myName != null) return System.out.printf("You are already logged in as %ss. To log in with another name log out first.n", myName); // Create a login query LoginQuery query = createQuery(LoginQuery.class); while(true) { // Get user name System.out.printf("(type Enter to exit login)n"); username = readConsoleLine("User name:"); if (username.isBlank()) return exitLogin(); do { // Get password password = readConsoleLine("Password:"); if (password.isBlank()) return exitLogin(); // Post the login query query.setNewUser(false); query.setName(username); query.setPassword(password); query.post(5 * ZDate.SECOND); // If the query did not expire check login results, if it did, repeat. if (query.waitForReply()) { switch (query.getResult()) { case OK: myName = query.getName(); birthday = query.getBirthday(); return System.out.printf("Logged in successfully as %s.n", myName); case NOT_EXISTS: System.out.printf("User %s does not exist. Please try again.n", username); break; case WRONG_PASSWORD: System.out.printf("Password did not match, please try again.n"); break; } } else return unavailableUserManager(); } while (query.getResult() == LoginQuery.ResultCode.WRONG_PASSWORD); } } /** * Log out if already logged in */ private void logout() { if (myName == null) { System.out.printf("No user is logged in.n"); return; } leaveRoom(null); System.out.printf("%s is logged out.n", myName); myName = null; birthday = null; } private PrintStream register() throws NoSuchFieldException, IllegalAccessException { String username; String password, password2; // Check if already logged in if (myName != null) return System.out.printf("You are already logged in as %s. To register a new user log out first.n", myName); // Create a registration query LoginQuery query = createQuery(LoginQuery.class); while(true) { // Get user name System.out.printf("(type Enter to exit login)n"); username = readConsoleLine("User name:"); if (username.isBlank()) return exitRegistration(); do { // Get password password = readConsoleLine("Password:"); if (password.isBlank()) return exitRegistration(); // Repeat the password password2 = readConsoleLine("Repeat password:"); if (password2.isBlank()) return exitRegistration(); // Check if passwords are equal if (password.equals(password2)) break; System.out.println("Passwords do not match."); } while (true); // Get and parse birth date do { String s = readConsoleLine("Date of birth (yyyy/mm/dd):"); if (s.isBlank()) return exitRegistration(); try { birthday = ZDate.parseTime(s, "yyyy/MM/dd", null); } catch (ParseException ex) { birthday = null; System.out.printf("Invalid date %s. Please try again.n", s); } } while (birthday == null); // Post the registration query query.setNewUser(true); query.setName(username); query.setPassword(password); query.setBirthday(birthday); query.post(5 * ZDate.SECOND); // If the query did not expire check login results, if it did, repeat. if (query.waitForReply()) { switch (query.getResult()) { case OK: myName = username; return System.out.printf("Registered and logged in successfully as %s.n", myName); case EXISTS: System.out.printf("User %s already exista. Please try again with another name.n", username); break; } } else return unavailableUserManager(); } } /** * Print a message when login existed by typing a blank Enter * @return a PrintStream object. Not needed by the caller but we use it as a trick to save coding. */ private PrintStream exitLogin() { return System.out.printf("Login exitedn"); } /** * Print a message when registration existed by typing a blank Enter * @return a PrintStream object. Not needed by the caller but we use it as a trick to save coding. */ private PrintStream exitRegistration() { return System.out.printf("Registration exitedn"); } /** * Print a message when user management is not available * @return a PrintStream object. Not needed by the caller but we use it as a trick to save coding. */ private PrintStream unavailableUserManager() { return System.out.printf("No user management entity is available to handle this requestn"); } /** * 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 synchronized void joinRoom(String room) throws NoSuchFieldException, IllegalAccessException { // User must be logged in if (myName == null) { System.out.printf("You must log in before joining a room.n"); return; } // Check whether room exists and the user is not already in the room ChatRoomConsume chatRoom = getRootObject().getChild(ChatRoomConsume.class, room); if (chatRoom == null) { System.out.printf("Room %s does not existn", room); return; } if (chatter != null && room.equalsIgnoreCase(chatter.getParent().getObjectID())) return; // Check if a young chatters is trying to join an adult room if (!chatRoom.canJoin(birthday)) { System.out.printf("You cannot join room %s because it is only for adultsn", room); return; } // Leave current room if any leaveRoom(null); // Create a Chatter object for the user that joins the specified room chatter = chatRoom.createChild(Chatter.class, getAppUUID().toString()); chatter.setName(myName); chatter.setBirthday(birthday); 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)) { DataObject removed = chatter.remove(); // leave the room if (removed != null) // can be null if the 'chatter' object has already been removed. removed.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; } } /** * Get current room name, if any * @return if the user is currently in a room return the room name, otherwise return null. */ public synchronized String getCurrentRoom() { return chatter == null ? null : chatter.getParent().getObjectID(); } /** * Find the user whose name equals the parameter * @param name user name to look for * @return If a Chatter object for this user found then return its object ID, which is the UUID of the application * that the given user runs, otherwise return null. * @throws NoSuchFieldException If any of the referred data objects does not define ObjectCode static field. * @throws IllegalAccessException If ObjectCode is not public. */ private String findUser(String name) throws NoSuchFieldException, IllegalAccessException { FindUserFilter filter = new FindUserFilter(name); Collection<Chatter> users = getRootObject().getFilteredChildren(filter); // If the returned collection is not empty return the object ID (which is a UUID) of the first, // otherwise return null. for (Chatter user : users) { return user.getObjectID(); } return null; } }
We will go over the changes from the previous lesson.
In preStart() (line 75) the new commands are added to the printed instructions and the code that accesses the configuration file is removed.
LoginQuery
is added to the list returned by getProducedObjects() (line 97) and ActiveUserQuery
is added to the list returned by getConsumedObjects() (line 110).
LoginQuery
class and the ActiveUserQueryReply
implementation class are registered in populateObjectFactory() (line 125) in addition to the classes from the previous lesson.
The processConsoleLine()
method (line 142) is overridden to handle the new commands – !login
, !logout
and !register
.
The rest of the file contains utility methods that are used to carry on the application tasks, as follows.
The login()
method (line 218) handles the !login
command. It first verifies that the user is not currently logged in. Then it uses createQuery() to create a LoginQuery
object. Afterwards it requests the user to type a user name and a password, which are set in the query object. The newUser
property is set to false
to indicate that this is a login request. The query is posted by calling its post() method, specifying 5 second expiration time.
The query reply is handled synchronously by calling waitForReply() (line 247), which blocks the calling thread until the query is replied or expires. The method returns true
if the query is replied. In this case we check the value of the query’s result
property that can be any of OK
, NOT_EXISTS
or WRONG_PASSWORD
. In the first case the user is logged in, the user name and the date of birth are fetched from the query object and saved, and the method quits. In the second case a message is printed and the method loops to request another user name. In the last case an appropriate message is printed and the method loops to request another password.
If the query expires then waitForReply()
returns false
. This can happen if the User Manager is not running or there is a connection problem. In this case an appropriate message is printed and the method quits.
If at any stage the user types Enter with no text then the method quits.
The logout()
method (line 269) checks if the user is logged in, and if so takes the user out of the room if necessary and sets myName
and birthday
fields to null
.
The register()
method (line 280) is quite similar to login()
. It checks whether the user is not already logged in, creates a LoginQuery
object, requests the user to type a user name, a password, repeat the password and type date of birth, verifies that the two passwords match, sets the query’s newUser
property to true
, sets the other properties and posts the query with 5 second expiration time. It then calls waitForReply() to wait for a reply and processes it. The value of the result
property in this case can be either OK
or EXISTS
(if a user with the same name already exists).
The joinRoom()
method (line 379) is pretty much as in the previous lessons, except that it verifies that the user is logged in before joining a room and that users that want to join adult rooms are at least 18 year old. There is no change in leaveRoom()
(line 419) and the rest of the methods.
Recall that this application needs to reply to ActiveUserQuery
queries. This is done in ActiveUserQueryReply
:
package org.spiderwiz.tutorial.lesson7.chat; import org.spiderwiz.tutorial.objectLib.ActiveUserQuery; /** * Extends ActiveUserQuery for the replying side */ public class ActiveUserQueryReply extends ActiveUserQuery { @Override protected boolean onInquire() { String room = ChatMain.getInstance().getCurrentRoom(); if (room == null) return false; setRoom(room); setName(ChatMain.getInstance().getMyName()); return true; } }
By now you know that query replying involves no more than overriding onInquire(), setting reply properties and returning true
.
There is a small change in ChatRoomConsume
from the previous lesson:
package org.spiderwiz.tutorial.lesson7.chat; import org.spiderwiz.tutorial.objectLib.ChatRoom; import org.spiderwiz.zutils.ZDate; /** * 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; } /** * 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()); } /** * If the room was set to adult and user is not leave the room if user is there * @return true */ @Override protected boolean onEvent() { if (!canJoin(ChatMain.getInstance().getBirthday())) ChatMain.getInstance().leaveRoom(getObjectID()); return true; } /** * Check if a user with the given age can join this room * @param birthday user's birth date * @return true if either this is not an adult room or the user is not under 18 year old. */ public boolean canJoin(ZDate birthday) { return !isAdult() || ZDate.now().diffMonths(birthday) >= 12 * 18; } }
The onEvent() method is overridden (line 36) to check whether the adult
property was changed to true
, and if so check if the user is in this room and is under 18 year old, in which case he is taken out of the room.
The rest of the classes are untouched (except for package name).
Each chat application needs a configuration file that becomes now pretty simple:
[application name]Chat 1 [log folder]/tests/Chat1/Logs [producer-1]websocket=localhost:90/MyHub
[application name]Chat 2 [log folder]/tests/Chat2/Logs [producer-1]websocket=localhost:90/MyHub
[application name]Chat 3 [log folder]/tests/Chat3/Logs [producer-1]websocket=localhost:90/MyHub
To test the entire thing you will need also to run Room Manager from Lesson 5.
What we have now starts to look like a real life service mesh, just without the programming mess that this usually involves. In the next lesson will continue with Query Objects and you will get a twofer – learning about Streaming Queries and Data Object Archiving in the same lesson.