In the previous lesson you were introduced to Query Objects and saw how to easily post a query to the service mesh and get a reply from one or more applications. This was a single reply in the case of a closed query, and possibly multiple replies from few applications in the case of an open query – in both cases each application replied with a single reply object.
Streaming Queries are a bit different because they allow you to post a single query and get back a stream of objects as a response. We will use this mechanism to implement a “show my message history” feature.
Obviously to get message history one needs to keep message history. Normally this would be done with some sort of database or storage files, which has a price tag of development time and performance. We will show here how you can do it in three lines of code and no performance penalty with Data Object Archiving.
Again we will throw a new microservice into our service mesh. We call it Archiver
– an application that listens to all chat messages circulating in the network, archives them and restores them from the archive on user requests.
We start with a new class and modifications in the objectLib
library. For history requests we use ChatHistoryQuery
:
package org.spiderwiz.tutorial.objectLib; import org.spiderwiz.annotation.WizField; import org.spiderwiz.core.QueryObject; import org.spiderwiz.zutils.ZDate; /** * A Query Object used for requesting chat message history. Used as a Streaming Query. */ public class ChatHistoryQuery extends QueryObject{ /** * Mandatory public static field for all data objects. */ public final static String ObjectCode = "HSTRQWR"; @WizField private String username; // the name of the user requesting the history (query field) @WizField private ZDate since; // the starting date of the requested history (query field) @WizField private String room; // the name of the room the message is sent in (reply field) @WizField private ZDate messageDate; // date and time of the message (reply field) @WizField private String message; // message content (reply field) public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public ZDate getSince() { return since; } public void setSince(ZDate since) { this.since = since; } public String getRoom() { return room; } public void setRoom(String room) { this.room = room; } public ZDate getMessageDate() { return messageDate; } public void setMessageDate(ZDate messageDate) { this.messageDate = messageDate; } public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } }
The query properties are username
for identifying the inquiring user and since
for specifying since when history is requested. The other properties – room
, messageDate
and message
are response properties that contain information about one history item. As we are talking about streaming queries, responses will be generated repeatedly until the query is fully replied.
The Chatter
class, known from the previous lessons, has a new boolean stealth
property. The Archiver
acts as a chat user, as it needs to trap all chat messages for archiving. However, we do not want other chatters to be notified when it joins or leaves a room as with regular chatters. Setting stealth
to true
prevents these notifications as we will see below. Here is the updated Chatter
class:
package org.spiderwiz.tutorial.objectLib; import org.spiderwiz.annotation.WizField; import org.spiderwiz.core.DataObject; import org.spiderwiz.zutils.ZDate; /** * A data object that representing 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 @WizField private ZDate birthday; // Contains the chatter birth date @WizField private boolean stealth = false; // True if chatters shall not be notified when this user joins or leave a room public String getName() { return name; } public void setName(String name) { this.name = name; } public ZDate getBirthday() { return birthday; } public void setBirthday(ZDate birthday) { this.birthday = birthday; } public boolean isStealth() { return stealth; } public void setStealth(boolean stealth) { this.stealth = stealth; } /** * @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; } }
Back to the Archiver, let’s see ArchiverMain
:
package org.spiderwiz.tutorial.lesson8.archiver; import java.util.List; import org.spiderwiz.core.DataObject; import org.spiderwiz.tutorial.objectLib.ChatHistoryQuery; 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 ArchiverMain extends ConsoleMain { private static final String ROOT_DIRECTORY = ""; private static final String CONF_FILENAME = "archiver.conf"; private static final String APP_NAME = "Chat Archiver"; private static final String APP_VERSION = "Z1.01"; // Version Z1.01: Initial version static final String MY_USERNAME = "_chat_message_archiver"; // Use this as the "chatter" name when joining a room /** * Class constructor with constant parameters. Call super constructor and create the user map. */ public ArchiverMain() { super(ROOT_DIRECTORY, CONF_FILENAME, APP_NAME, APP_VERSION); } /** * @return the class instance as ArchiverMain type. */ public static ArchiverMain getInstance() { return (ArchiverMain)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 ArchiverMain().init(); } /** * @return the list of produced objects */ @Override protected String[] getProducedObjects() { return new String[]{ Chatter.ObjectCode // To notify when we join a room (we will join every room) }; } /** * @return the list of consumed objects */ @Override protected String[] getConsumedObjects() { return new String[]{ ChatRoom.ObjectCode, // Needs to know about every new room Chatter.ObjectCode, // Needs to know the name of every chatter ChatMessage.ObjectCode, // Listens to every message for archiving. ChatHistoryQuery.ObjectCode // Replier of this query. }; } /** * 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(ChatRoomArchiver.class); factoryList.add(Chatter.class); factoryList.add(ChatMessageArchiver.class); factoryList.add(ChatHistoryQueryReply.class); factoryList.add(ArchivedUser.class); factoryList.add(ArchivedChatItem.class); } /** * Process an input line - nothing to process in our case. * @param line The input line * @return true */ @Override protected boolean processConsoleLine(String line) { return true; } }
Archiver
consumes (line 58) the following object types:
ChatRoom
: because it needs to join every room.Chatter
: because it needs to know the name of every chatter.ChatMessage
: because it needs to archive every message.ChatHistoryQuery
: this is the query object defined in this lesson that is used by chat applications to request chat history.
Archiver
produces (line 48) one object type – Chatter
, which it uses in order to announce itself as a member of each chat room so that the chat applications would route messages to it.
The implementation classes of these classes are registered in populateObjectFactory() (line 72). You will notice that we register two more implementation classes that the application neither produces nor consumes – ArchivedUser
and ArchivedChatItem
. These are used internally for the archiving work as you will see in a moment.
The Archiver
does not do any command line processing, except for exiting on the exit
command, so processConsoleLine()
, which is an abstract method that must be implemented, just returns true
.
Let’s take a look at ChatRoomArchiver
, the implementation extension of ChatRoom
:
package org.spiderwiz.tutorial.lesson8.archiver; import org.spiderwiz.tutorial.objectLib.ChatRoom; import org.spiderwiz.tutorial.objectLib.Chatter; /** * An implementation class of ChatRoom for the archiver. */ public class ChatRoomArchiver extends ChatRoom { /** * Join every room that is created so that it can archive the entire system. */ @Override protected void onNew() { try { Chatter chatter = createChild(Chatter.class, ArchiverMain.getInstance().getAppUUID().toString()); chatter.setName(ArchiverMain.MY_USERNAME); chatter.setStealth(true); // We don't want chatters to see when we join or leave a room. chatter.commit(); System.out.printf("Joined room %sn", getObjectID()); } catch (NoSuchFieldException | IllegalAccessException ex) { // Theoretically arriving here if Chatter does not contain ObjectCode static field or the field is not public. // In this case, send a command exception message. ArchiverMain.getInstance().sendExceptionMail(ex, "Cannot instantiate Chatter class", null, false); } } }
Recall that we consume ChatRoom
because we want to join every room. We do it by overriding onNew(), in which we create a Chatter
child on every chat room that enters our space. Note that “new” in the onNew
context means “new to this application”. This makes a difference when Room Manager is active and creates rooms before Archiver
is launched, in which case the room is not really new, but it appears on the consuming application’s radar as a new object.
The ID of the created Chatter
object is the current application’s UUID as before. The chatter name is set in our case to “_chat_message_archiver”
. We have added a new property to the Chatter
class – stealth
that we set here to true
. We commit the object, print a message and are done.
Now to ChatMessageArchiver
, the implementation extension of ChatMessage
, where we trap every circulated chat message and archive it:
package org.spiderwiz.tutorial.lesson8.archiver; import org.spiderwiz.tutorial.objectLib.ChatMessage; import org.spiderwiz.tutorial.objectLib.Chatter; import org.spiderwiz.zutils.ZDate; /** * Implementation class of ChatMessagee for the archiver. */ public class ChatMessageArchiver extends ChatMessage { /** * Every arriving message is encapsulated in ArchivedChatItem and archived. * @return true */ @Override protected boolean onEvent() { try { ArchivedChatItem item = ArchiverMain.createTopLevelObject(ArchivedUser.class, ((Chatter)getParent()).getName()). createChild(ArchivedChatItem.class, null); item.archive(ZDate.now(), getParent().getParent().getObjectID(), getMessage()); } catch (Exception ex) { ArchiverMain.getInstance().sendExceptionMail(ex, "When trying to archive a chat item", null, false); } return true; } }
Requests for archive retrievals come from specific users, therefore we need to classify the archive by users. We do it in the overriding onEvent() method by creating a top level ArchivedUser
object for the given user and then creating a child ArchivedChatItem
object under it. We use createTopLevelObject() to create the first. This method first checks whether an object with the given ID already exists and creates a new object only if it does not. We then call the internal ArchivedChatItem.archive()
method that we will see in a moment.
As mentioned above, ArchivedUser
and ArchivedChatItem
are neither produced nor consumed but just used internally in this application, therefore they are placed in the project’s package rather than in objectLib
. Here they are:
package org.spiderwiz.tutorial.lesson8.archiver; import org.spiderwiz.core.DataObject; /** * Represents a user whose chat items need to be archived. By itself it has no properties but it serves as the parent of * ArchivedChatItem. */ public class ArchivedUser extends DataObject { /** * Mandatory public static field for all data objects. */ public final static String ObjectCode = "HSTRUSR"; /** * @return null since this is a root object */ @Override protected String getParentCode() { return null; } /** * @return false since this is a persistent object. */ @Override protected boolean isDisposable() { return false; } }
This is an empty class with no properties or methods as it is used just as a hook to classify the archive by users. The implementation of isDisposable() returns false
as you should expect.
package org.spiderwiz.tutorial.lesson8.archiver; import org.spiderwiz.annotation.WizField; import org.spiderwiz.core.DataObject; import org.spiderwiz.zutils.ZDate; /** * Hold one chat history item for archiving and restoration */ public class ArchivedChatItem extends DataObject { /** * Mandatory public static field for all data objects. */ public final static String ObjectCode = "HSTRITM"; @WizField private ZDate messageDate; @WizField private String room; @WizField private String message; public ZDate getMessageDate() { return messageDate; } public void setMessageDate(ZDate messageDate) { this.messageDate = messageDate; } public String getRoom() { return room; } public void setRoom(String room) { this.room = room; } public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } /** * Each restored item is posted as a replyNext() call on the ChatHistoryQuery query. * @param associated the object associated with the restoration, in this case the ChatHistoryQuery that activated it. * @return true */ @Override protected boolean onRestore(Object associated) { ((ChatHistoryQueryReply)associated).replyOneItem(getMessageDate(), getRoom(), getMessage()); return true; } /** * @return the Object Code of ArchivedUser. */ @Override protected String getParentCode() { return ArchivedUser.ObjectCode; } /** * @return true since chat items are disposable. */ @Override protected boolean isDisposable() { return true; } /** * Enable archiving by defining the archive path * @return archive path constructed by "chat/<user name>/<date>/<hour>.arc" */ @Override protected String getArchivePath() { return "chat/#1/#y#m#d/#h"; } /** * Set values in the object and archive it * @param messageDate * @param room * @param message * @throws Exception */ void archive(ZDate messageDate, String room, String message) throws Exception { setMessageDate(messageDate); setRoom(room); setMessage(message); archive(); } }
This is the class that we archive. It is a child of ArchivedUser
(line 59), it is disposable (line 67) and it has three properties – messageDate
, room
and message
. It has an internal method – archive(ZDate messageDate, String room, String message)
(line 87) – that is called from ChatMessageArchiver.onEvent()
as we saw above. The method stores the given values in the object’s properties and calls archive() to archive the object.
The most interesting method that ArchivedChatItem
overrides is getArchivePath() (line 76). Its returned value tells the Spiderwiz framework that objects of this class are archivable and the pattern to use for the archive folder tree structure. In our case we return "chat/#1/#y#m#d/#h"
, which means the following structure:
-
archive
(Archive root folder name, see below)-
chat
(the sub-folder forArchivedChatItem
archive)-
(user-name
#1
means key of one level above the archived item, which is the key ofArchivedUser
, i.e. the user name)-
(date pattern as indicated byyymmdd
#y#m#d
)00.arc
(file name for 12 am, if exists)01.arc
(file name 1 am, if exists)- …
23.arc
(file name for 11 pm, if exists)
-
-
-
The other overriding method is onRestore(). It is called once for each item retrieved from the archive. When it is called the object properties already contain the retrieved values. The method argument is an object that was optionally provided when the restoration procedure was initiated. In this case it is the ChatHistoryQuery
that carries the history request. We will see in a second how it works.
We are ready to wire it all up with ChatHistoryQueryReply
, the implementation class of ChatHistoryQuery
:
package org.spiderwiz.tutorial.lesson8.archiver; import org.spiderwiz.tutorial.objectLib.ChatHistoryQuery; import org.spiderwiz.zutils.ZDate; /** * Implementation class for the replier side of ChatHistoryQuery. */ public class ChatHistoryQueryReply extends ChatHistoryQuery { /** * Reply to the query by activating restoration of the relevant ArchivedChatItem objects * @return true */ @Override protected boolean onInquire() { try { restore(ArchivedChatItem.ObjectCode, this, getSince(), null, getUsername(), null); replyEnd(); } catch (Exception ex) { ArchiverMain.getInstance().sendExceptionMail(ex, "When replying the query", null, false); } return false; // because the query has already been replied in full. } /** * Override the default streaming rate to specify continuous streaming with no delay. * @return 0 */ @Override protected int getStreamingRate() { return 0; } /** * Reply the streaming query with one item. * @param messageDate * @param room * @param message */ void replyOneItem(ZDate messageDate, String room, String message) { try { setMessageDate(messageDate); setRoom(room); setMessage(message); replyNext(); } catch (Exception ex) { ArchiverMain.getInstance().sendExceptionMail(ex, "When replying a query with one chat item", null, false); } } }
We override onInquire() as in Lesson 7 (line 16), but instead of filling in reply properties and returning true
, we start a procedure that generates a stream of objects. Recall that we need to stream chat message history that belongs to username
starting from the time specified by since
(or the full history if since
is null
). This is done by calling the static restore() method (line 18) and associating the current object with the call. The object pops up with each activation of ArchivedChatItem.onRestore()
as we saw above, which calls replyOneItem()
that we see here (line 42). The method sets the object’s response properties for one item and calls replyNext(). This is repeated for each history item included in the query response.
Back to onInquire()
, when all retrieved items are streamed back to the inquirer and the restore()
method finishes, it calls replyEnd() to mark the end of the stream. All done from this side.
Before concluding with the Archiver
note the override of getStreamingRate() (line 31). This method determines the streaming rate of a streaming query, which may require regulation over constrained network connections. In our case we change the default (100 items per second) to zero, which means continuous streaming with no delay.
It remains to see the Archiver
configuration file:
[log folder]/tests/Archiver/Logs [archive folder]/tests/Archiver/Archive [producer-1]ip=localhost;port=10001
There is one property beyond the usual stuff – archive folder
, which defines the root folders for all archives.
Let’s see the changes we did in the chat application. There is a new command that a user can use to request chat history. It has one of the following formats:
!history
– request the full history of chat messages sent by this user.!history <number of hours>h
– request history since <number of hours> ago, e.g.!history 3h
.!history <number of days>d
– request history since <number of days> ago, e.g.!history 1d
.
Here is ChatMain
:
package org.spiderwiz.tutorial.lesson8.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.ChatHistoryQuery; 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; import org.spiderwiz.zutils.ZUtilities; /** * 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 = "Z8.01"; // Version Z8.01: Lesson 8 modifications 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 static final String HISTORY = "!history"; // print my chat history 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 print the history of your own messages, type "!history <hours>h" or "!history <days>d"n" + "(for instance "!history 3h" to see messages since 3 hours ago and "!history 1d" to see messages since 1 day" + " ago).n" + "or type just "!history" to see all your messages.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, ChatHistoryQuery.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); factoryList.add(ChatHistoryQueryInquire.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; case HISTORY: if (myName == null) System.out.println("You must log in before requesting history."); else if (!getHistory(command.length < 2 ? null : command[1])) System.out.println("Invalid command argument"); 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 %s. 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; } /** * Execute !history command * @param arg command argument - <n>H or <n>D * @return true if command has been parsed successfully. * @throws NoSuchFieldException If ChatHistoryQuery does not define ObjectCode static field. * @throws IllegalAccessException If ChatHistoryQuery.ObjectCode is not public. */ private boolean getHistory(String arg) throws NoSuchFieldException, IllegalAccessException { // Parse the argument and calculate time ZDate since = null; if (arg != null) { arg = arg.trim().toLowerCase(); int n = ZUtilities.parseInt(arg.substring(0, arg.length() - 1)); if (n <= 0) return false; int unit; switch (arg.substring(arg.length() - 1)) { case "h": unit = ZDate.HOUR; break; case "d": unit = ZDate.DAY; break; default: return false; } since = ZDate.now().add(-n * unit); } // create a query and post it ChatHistoryQuery query = createQuery(ChatHistoryQuery.class); query.setUsername(myName); query.setSince(since); query.post(5 * ZDate.SECOND); return true; } }
The description of the new command has been added to the instructions printed in preStart() (line 78).
As mentioned above, chat history is requested through the ChatHistoryQuery
query object. This is added to the produced objects (line 110). Its implementation class for the inquirer side is ChatHistoryQueryInquire
, registered in populateObjectFactory() (line 142).
The new command is trapped in processConsoleLine()
(line 175) and is passed to getHistory()
(line 509) for parsing and execution. To execute the query, we create a ChatHistoryQuery
object (line 532), set its properties and call its post() method with 5-second expiration time ( like a single-response query, except that in this case the time is set per reply ﹘ see below).
We will see now how the response, in this case a response stream, is handled by ChatHistoryQueryInquire
:
package org.spiderwiz.tutorial.lesson8.chat; import org.spiderwiz.tutorial.objectLib.ChatHistoryQuery; import org.spiderwiz.zutils.ZDate; /** * Implementing ChatHistoryQuery at the inquirer side. */ public class ChatHistoryQueryInquire extends ChatHistoryQuery { private int messageCount = 0; /** * Print out the information contained in every reply record and increase message count */ @Override protected void onReplyNext() { System.out.printf("%1$s room:%2$s message:%3$sn", getMessageDate().format(ZDate.FULL_DATE), getRoom(), getMessage()); ++messageCount; } @Override protected void onReplyEnd() { System.out.printf( "You have typed " + (messageCount > 0 ? "%1$d" : "no") + " messages" + (getSince() == null ? "" : " since %2$s") +"n", messageCount, getSince() == null ? null : getSince().format(ZDate.FULL_DATE)); } @Override protected void onExpire() { System.out.println("Chat Archiver is not available"); } }
Absolutely straightforward. Instead of overriding onReply()
, the inquirer of a streaming query overrides onReplyNext() and onReplyEnd(). In the first we print each history item as it arrives, in the second we print a summary when the stream ends.
The method onExpire() is activated when the chatting application cannot communicate with the Archiver
. It is overridden (line 30) to print a failure message. Note that in the case of a streaming query the expiration time set by the post()
method is for the first response item, and then on each item the expiration timer is reset for the next item until the end of the stream.
There is a small change in ChatterConsume
:
package org.spiderwiz.tutorial.lesson8.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 (!isStealth() && ChatMain.getInstance().isSameRoom((getParent().getObjectID()))) System.out.printf("%s entered the roomn", getName()); } /** * Notify when a chatter leaves my room. If the chatter is myself leave the room gracefully. * @return true to confirm the removal. */ @Override protected boolean onRemoval() { if (ChatMain.getInstance().getAppUUID().toString().equals(getObjectID())) ChatMain.getInstance().leaveRoom(getParent().getObjectID()); else if (!isStealth() && ChatMain.getInstance().isSameRoom(getParent().getObjectID())) System.out.printf("%s left the roomn", getName()); return true; } }
You apparently remember that we added a stealth
property to the Chatter
class. Here we check it in both onNew() (line 14) and onRemoval() (line 25) to ensure that the Archiver
does not manifest itself when it joins or leaves a chat room.
There is no change in the other classes. Neither there is a need to change anything in the chat application configuration files. Just run everything, including Room Manager and User Manager, and see how much you can do with so little code.
Our lesson is almost finished, but since we are talking about Data Object Archiving we will show you another trick that you can do with it. In the previous lesson we built User Manager – a service that handled user registration and login. As we wanted to keep the example short and simple we did not save user information in a database as this kind of application would usually do. As a result every time when the application shuts down all user data is lost and every user of a chat application needs to repeat the registration procedure – quite annoying even though it is just an exercise.
We have an easy solution without diverting to technologies that are out of the scope of this tutorial. What we are going to do is to use Data Object Archiving for context recovery. This is how it works:
We make (very) few modifications in User Manager. First, we modify LoginQueryReply
to archive itself whenever a new user requests to register. We also override onRestore() to execute restored registration requests. Here is how it looks like:
package org.spiderwiz.tutorial.lesson8.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() { try { 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 = registerUser(); setResult(user == null ? ResultCode.OK : ResultCode.EXISTS); // If registered successfully then archive it. if (user == null) archive(); } 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()); } } } catch (Exception ex) { UserManagerMain.getInstance().sendExceptionMail(ex, "When trying to archive a LogQuery item", null, false); } return true; } /** * @return the path pattern of the registration archive. */ @Override protected String getArchivePath() { return "register/#y#m#d/#h"; } /** * Recover a query that is restored from the archive. * @param associated n/a. contains null. * @return */ @Override protected boolean onRestore(Object associated) { registerUser(); return true; } /** * Add a supposedly new user to the user table * @return the previous user record with this name if exists, if it does not then return null. */ private RegisteredUser registerUser() { return UserManagerMain.getInstance().addUser( new RegisteredUser(getName(), getPassword(), getBirthday()) ); } }
You can see that within onInquire() we call archive() on every successful registration request (line 23). You can also see that we override getArchivePath()
(line 50) to return a path pattern that consists of register
as a sub-folder, under which there is a folder for each day in the pattern yymmdd
, under which there is an archive file for each hour of the day with the default .arc
extension.
We split the code of onInquire()
and move the registration part to the private registerUser()
method, so that we can call it form either onInquire()
or onRestore()
(line 61). That’s it.
The other change is in UserManagerMain
:
package org.spiderwiz.tutorial.lesson8.userManager; import java.util.List; import org.spiderwiz.core.DataObject; import org.spiderwiz.tutorial.lesson7.userManager.*; 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 contains a room management 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; } } /** * Called after calling Main.init(). Restore user registration context * @return true if restoration succeeded. */ @Override protected boolean postInit() { return LoginQuery.restore(LoginQuery.ObjectCode, null, null, null) >= 0; } /** * 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); } }
Here we override postInit()
(line 114) to call restore() in order to activate restoration of the archived LoginQuery
messages. That completes our mission.
The archive folder
property is added to user-manager.conf
:
[log folder]/tests/UserManager/Logs [archive folder]/tests/UserManager/Archive [producer-1]ip=localhost;port=10001
A word about performance
We claimed in the introduction to this lesson that the common archiving mechanism “has a price tag of development time and performance”, and promised to remove this tag with the mechanism presented here. As for development time, we undeniably delivered. It remains to show that our solution is robust in terms of performance and resources. The following points clarify that claim:
- The archived data is stored in sequential files, which are a lot faster than relational databases.
- Unless the name extension of the archived files is
.text
or.txt
(in our case we chose the default.arc
), the files are compressed (with GZip). - The archiving operation uses a sophisticated buffering mechanism that opens files, flushes data and closes them only when the computer is not busy with something else and computing power is available.
- There is never more than one archive file opened at a time.
In the next lesson we will talk about Manual Data Object Reset, a topic that will bring us very close to full coverage of the Spiderwiz Data Object Model.